From 6c5b4d9595a0d92d11cec46314b419779a39b9ad Mon Sep 17 00:00:00 2001 From: Du Peng Date: Fri, 2 Jun 2023 18:33:07 +0200 Subject: [PATCH 01/63] (bug) OpenAPI accounting gateway_object_id missing error --- CHANGELOG.md | 2 ++ app/models/invoice.rb | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e10f82e..188c8c221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +- Fix a bug: OpenAPI accounting gateway_object_id missing error + ## v6.0.6 2023 May 4 - Fix a bug: invalid duration for machine/spaces reservations in statistics, when using slots of not 1 hour diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 2365559d8..b607fd08a 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -175,8 +175,8 @@ class Invoice < PaymentDocument if paid_by_card? { payment_mean: mean, - gateway_object_id: payment_gateway_object.gateway_object_id, - gateway_object_type: payment_gateway_object.gateway_object_type + gateway_object_id: payment_gateway_object&.gateway_object_id, + gateway_object_type: payment_gateway_object&.gateway_object_type } end when :wallet From 4fdbd4fcf15cde79efd2ecdaf39d1811b005f879 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Thu, 8 Jun 2023 10:50:00 +0200 Subject: [PATCH 02/63] (bug) unable to modify the price of prepaid pack --- CHANGELOG.md | 1 + .../machines/configure-packs-button.tsx | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 188c8c221..4dcd8b2c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog Fab-manager - Fix a bug: OpenAPI accounting gateway_object_id missing error +- Fix a bug: unable to modify the price of prepaid pack ## v6.0.6 2023 May 4 diff --git a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx index de1126bad..0f2eb7320 100644 --- a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx @@ -115,17 +115,17 @@ export const ConfigurePacksButton: React.FC = ({ pack itemId={p.id} itemType={t('app.admin.configure_packs_button.pack')} destroy={PrepaidPackAPI.destroy}/> - - {packData && } - )} + + {packData && } + {packs?.length === 0 && {t('app.admin.configure_packs_button.no_packs')}} } From 91fa1fcd85e85934839b97f8ec4f995e3bfe23c6 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Thu, 15 Jun 2023 19:08:17 +0200 Subject: [PATCH 03/63] (bug) notification type missing --- CHANGELOG.md | 1 + db/seeds/notification_types.rb | 106 +++++++++++++++++++++++++-------- 2 files changed, 82 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dcd8b2c4..feb19ba0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Fix a bug: OpenAPI accounting gateway_object_id missing error - Fix a bug: unable to modify the price of prepaid pack +- Fix a bug: notification type missing ## v6.0.6 2023 May 4 diff --git a/db/seeds/notification_types.rb b/db/seeds/notification_types.rb index 8dbe8dc68..a181bd344 100644 --- a/db/seeds/notification_types.rb +++ b/db/seeds/notification_types.rb @@ -1,33 +1,89 @@ # frozen_string_literal: true -unless NotificationType.find_by(name: 'notify_member_training_authorization_expired') - NotificationType.create!( - name: 'notify_member_training_authorization_expired', - category: 'trainings', - is_configurable: false - ) -end +NOTIFICATIONS_TYPES = [ + { name: 'notify_admin_when_project_published', category: 'projects', is_configurable: true }, + { name: 'notify_project_collaborator_to_valid', category: 'projects', is_configurable: false }, + { name: 'notify_project_author_when_collaborator_valid', category: 'projects', is_configurable: true }, + { name: 'notify_user_training_valid', category: 'trainings', is_configurable: false }, + { name: 'notify_member_subscribed_plan', category: 'subscriptions', is_configurable: false }, + { name: 'notify_member_create_reservation', category: 'agenda', is_configurable: false }, + { name: 'notify_member_subscribed_plan_is_changed', category: 'deprecated', is_configurable: false }, + { name: 'notify_admin_member_create_reservation', category: 'agenda', is_configurable: true }, + { name: 'notify_member_slot_is_modified', category: 'agenda', is_configurable: false }, + { name: 'notify_admin_slot_is_modified', category: 'agenda', is_configurable: true }, -unless NotificationType.find_by(name: 'notify_member_training_invalidated') - NotificationType.create!( - name: 'notify_member_training_invalidated', - category: 'trainings', - is_configurable: false - ) -end + { name: 'notify_admin_when_user_is_created', category: 'users_accounts', is_configurable: true }, + { name: 'notify_admin_subscribed_plan', category: 'subscriptions', is_configurable: true }, + { name: 'notify_user_when_invoice_ready', category: 'payments', is_configurable: true }, + { name: 'notify_member_subscription_will_expire_in_7_days', category: 'subscriptions', is_configurable: false }, + { name: 'notify_member_subscription_is_expired', category: 'subscriptions', is_configurable: false }, + { name: 'notify_admin_subscription_will_expire_in_7_days', category: 'subscriptions', is_configurable: true }, + { name: 'notify_admin_subscription_is_expired', category: 'subscriptions', is_configurable: true }, + { name: 'notify_admin_subscription_canceled', category: 'subscriptions', is_configurable: true }, + { name: 'notify_member_subscription_canceled', category: 'subscriptions', is_configurable: false }, + { name: 'notify_user_when_avoir_ready', category: 'wallet', is_configurable: false }, -unless NotificationType.find_by(name: 'notify_admin_order_is_paid') - NotificationType.create!( - name: 'notify_admin_order_is_paid', - category: 'shop', - is_configurable: true - ) -end + { name: 'notify_member_slot_is_canceled', category: 'agenda', is_configurable: false }, + { name: 'notify_admin_slot_is_canceled', category: 'agenda', is_configurable: true }, + { name: 'notify_partner_subscribed_plan', category: 'subscriptions', is_configurable: false }, + { name: 'notify_member_subscription_extended', category: 'subscriptions', is_configurable: false }, + { name: 'notify_admin_subscription_extended', category: 'subscriptions', is_configurable: true }, + { name: 'notify_admin_user_group_changed', category: 'users_accounts', is_configurable: true }, + { name: 'notify_user_user_group_changed', category: 'users_accounts', is_configurable: false }, + { name: 'notify_admin_when_user_is_imported', category: 'users_accounts', is_configurable: true }, + { name: 'notify_user_profile_complete', category: 'users_accounts', is_configurable: false }, + { name: 'notify_user_auth_migration', category: 'user', is_configurable: false }, + + { name: 'notify_admin_user_merged', category: 'users_accounts', is_configurable: true }, + { name: 'notify_admin_profile_complete', category: 'users_accounts', is_configurable: true }, + { name: 'notify_admin_abuse_reported', category: 'projects', is_configurable: true }, + { name: 'notify_admin_invoicing_changed', category: 'deprecated', is_configurable: false }, + { name: 'notify_user_wallet_is_credited', category: 'wallet', is_configurable: false }, + { name: 'notify_admin_user_wallet_is_credited', category: 'wallet', is_configurable: true }, + { name: 'notify_admin_export_complete', category: 'exports', is_configurable: false }, + { name: 'notify_member_about_coupon', category: 'agenda', is_configurable: false }, + { name: 'notify_member_reservation_reminder', category: 'agenda', is_configurable: false }, + + { name: 'notify_admin_free_disk_space', category: 'app_management', is_configurable: false }, + { name: 'notify_admin_close_period_reminder', category: 'accountings', is_configurable: true }, + { name: 'notify_admin_archive_complete', category: 'accountings', is_configurable: true }, + { name: 'notify_privacy_policy_changed', category: 'app_management', is_configurable: false }, + { name: 'notify_admin_import_complete', category: 'app_management', is_configurable: false }, + { name: 'notify_admin_refund_created', category: 'wallet', is_configurable: true }, + { name: 'notify_admins_role_update', category: 'users_accounts', is_configurable: true }, + { name: 'notify_user_role_update', category: 'users_accounts', is_configurable: false }, + { name: 'notify_admin_objects_stripe_sync', category: 'payments', is_configurable: false }, + { name: 'notify_user_when_payment_schedule_ready', category: 'payments', is_configurable: false }, + + { name: 'notify_admin_payment_schedule_failed', category: 'payments', is_configurable: true }, + { name: 'notify_member_payment_schedule_failed', category: 'payments', is_configurable: false }, + { name: 'notify_admin_payment_schedule_check_deadline', category: 'payments', is_configurable: true }, + { name: 'notify_admin_payment_schedule_transfer_deadline', category: 'payments', is_configurable: true }, + { name: 'notify_admin_payment_schedule_error', category: 'payments', is_configurable: true }, + { name: 'notify_member_payment_schedule_error', category: 'payments', is_configurable: false }, + { name: 'notify_admin_payment_schedule_gateway_canceled', category: 'payments', is_configurable: true }, + { name: 'notify_member_payment_schedule_gateway_canceled', category: 'payments', is_configurable: false }, + { name: 'notify_admin_user_supporting_document_files_created', category: 'supporting_documents', is_configurable: true }, + { name: 'notify_admin_user_supporting_document_files_updated', category: 'supporting_documents', is_configurable: true }, + + { name: 'notify_user_is_validated', category: 'users_accounts', is_configurable: false }, + { name: 'notify_user_is_invalidated', category: 'users_accounts', is_configurable: false }, + { name: 'notify_user_supporting_document_refusal', category: 'supporting_documents', is_configurable: false }, + { name: 'notify_admin_user_supporting_document_refusal', category: 'supporting_documents', is_configurable: true }, + { name: 'notify_user_order_is_ready', category: 'shop', is_configurable: false }, + { name: 'notify_user_order_is_canceled', category: 'shop', is_configurable: false }, + { name: 'notify_user_order_is_refunded', category: 'shop', is_configurable: false }, + { name: 'notify_admin_low_stock_threshold', category: 'shop', is_configurable: true }, + { name: 'notify_admin_training_auto_cancelled', category: 'trainings', is_configurable: true }, + { name: 'notify_member_training_auto_cancelled', category: 'trainings', is_configurable: false } +].freeze + +NOTIFICATIONS_TYPES.each do |notification_type| + next if NotificationType.find_by(name: notification_type[:name]) -unless NotificationType.find_by(name: 'notify_member_reservation_limit_reached') NotificationType.create!( - name: 'notify_member_reservation_limit_reached', - category: 'agenda', - is_configurable: false + name: notification_type[:name], + category: notification_type[:category], + is_configurable: notification_type[:is_configurable] ) end From 75259773d5098fcc80cdbfa41375d2594a950769 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 19 Jun 2023 21:01:01 +0200 Subject: [PATCH 04/63] (bug) check object = nil --- app/models/footprintable.rb | 2 +- .../api/payment_schedules/_payment_schedule.json.jbuilder | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/footprintable.rb b/app/models/footprintable.rb index 18f17d1e5..33dc4774d 100644 --- a/app/models/footprintable.rb +++ b/app/models/footprintable.rb @@ -20,7 +20,7 @@ class Footprintable < ApplicationRecord return false unless persisted? reload - footprint_children.map(&:check_footprint).all? && !chained_element.corrupted? + footprint_children.map(&:check_footprint).all? && chained_element && !chained_element.corrupted? end # @return [ChainedElement] diff --git a/app/views/api/payment_schedules/_payment_schedule.json.jbuilder b/app/views/api/payment_schedules/_payment_schedule.json.jbuilder index 8875f10c0..73b5ecbbc 100644 --- a/app/views/api/payment_schedules/_payment_schedule.json.jbuilder +++ b/app/views/api/payment_schedules/_payment_schedule.json.jbuilder @@ -14,8 +14,8 @@ if payment_schedule.operator_profile end end json.main_object do - json.type payment_schedule.main_object.object_type - json.id payment_schedule.main_object.object_id + json.type payment_schedule.main_object&.object_type + json.id payment_schedule.main_object&.object_id end if payment_schedule.gateway_subscription # this attribute is used to known which gateway should we interact with, in the front-end From 48ea7509cfda57c9e77a53ac86ffb697262da4aa Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 19 Jun 2023 21:09:26 +0200 Subject: [PATCH 05/63] (bug) Incorrect amount calculation when paying monthly subcription with a wallet --- CHANGELOG.md | 1 + lib/pay_zen/service.rb | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index feb19ba0d..9ba0521e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Fix a bug: OpenAPI accounting gateway_object_id missing error - Fix a bug: unable to modify the price of prepaid pack - Fix a bug: notification type missing +- Fix critical bug: Incorrect amount calculation when paying monthly subcription with a wallet for PayZen ## v6.0.6 2023 May 4 diff --git a/lib/pay_zen/service.rb b/lib/pay_zen/service.rb index 2b5f096ce..f3b15be56 100644 --- a/lib/pay_zen/service.rb +++ b/lib/pay_zen/service.rb @@ -25,8 +25,15 @@ class PayZen::Service < Payment::Service order_id: order_id } unless first_item.details['adjustment']&.zero? && first_item.details['other_items']&.zero? - params[:initial_amount] = payzen_amount(first_item.amount) - params[:initial_amount_number] = 1 + initial_amount = first_item.amount + initial_amount -= payment_schedule.wallet_amount if payment_schedule.wallet_amount + if initial_amount.zero? + params[:effect_date] = (first_item.due_date + 1.month).iso8601 + params[:rrule] = rrule(payment_schedule, -1) + else + params[:initial_amount] = payzen_amount(initial_amount) + params[:initial_amount_number] = 1 + end end pz_subscription = client.create_subscription(**params) @@ -123,16 +130,21 @@ class PayZen::Service < Payment::Service private - def rrule(payment_schedule) + def rrule(payment_schedule, offset = 0) count = payment_schedule.payment_schedule_items.count - "RRULE:FREQ=MONTHLY;COUNT=#{count}" + "RRULE:FREQ=MONTHLY;COUNT=#{count + offset}" end # check if the given transaction matches the given PaymentScheduleItem def transaction_matches?(transaction, payment_schedule_item) transaction_date = Time.zone.parse(transaction['creationDate']).to_date - transaction['amount'] == payment_schedule_item.amount && + amount = payment_schedule_item.amount + if !payment_schedule_item.details['adjustment']&.zero? && payment_schedule_item.payment_schedule.wallet_amount + amount -= payment_schedule_item.payment_schedule.wallet_amount + end + + transaction['amount'] == amount && transaction_date >= payment_schedule_item.due_date.to_date && transaction_date <= payment_schedule_item.due_date.to_date + 7.days end From 77035d81e291a429f41f0a07fd479d6a5155dff2 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 20 Jun 2023 15:27:23 +0200 Subject: [PATCH 06/63] Version 6.0.7 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba0521e5..53cfd1622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +## v6.0.7 2023 June 20 + - Fix a bug: OpenAPI accounting gateway_object_id missing error - Fix a bug: unable to modify the price of prepaid pack - Fix a bug: notification type missing diff --git a/package.json b/package.json index 38cad9cdb..1805174ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "6.0.6", + "version": "6.0.7", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "keywords": [ "fablab", From 11a079b3e26fc9c2798b257cabaad4b514384836 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Thu, 22 Jun 2023 11:59:52 +0200 Subject: [PATCH 07/63] (bug) unable to refresh machine/space/training calender after pay an reservation --- CHANGELOG.md | 2 ++ .../src/javascript/controllers/machines.js.erb | 17 +++-------------- .../src/javascript/controllers/spaces.js.erb | 17 +++-------------- .../src/javascript/controllers/trainings.js.erb | 17 +++-------------- 4 files changed, 11 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53cfd1622..4bd02d555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +- Fix a bug: unable to refresh machine/space/training calender after pay an reservation + ## v6.0.7 2023 June 20 - Fix a bug: OpenAPI accounting gateway_object_id missing error diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index 9d8556865..984eaf665 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -692,20 +692,9 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran * Refetch all events from the API and re-populate the calendar with the resulting slots */ const refreshCalendar = function () { - const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView'); - return Availability.machine({ - machineId: $scope.machine.id, - member_id: $scope.ctrl.member.id, - start: view.start, - end: view.end, - timezone: Fablab.timezone - }, function (slots) { - uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents'); - return $scope.eventSources.splice(0, 1, { - events: slots, - textColor: 'black' - } - ); + $scope.eventSources.splice(0, 1, { + url: `/api/availabilities/machines/${$transition$.params().id}?member_id=${$scope.ctrl.member.id}`, + textColor: 'black' }); } diff --git a/app/frontend/src/javascript/controllers/spaces.js.erb b/app/frontend/src/javascript/controllers/spaces.js.erb index d4bded6da..379c17123 100644 --- a/app/frontend/src/javascript/controllers/spaces.js.erb +++ b/app/frontend/src/javascript/controllers/spaces.js.erb @@ -609,20 +609,9 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi * Refetch all events from the API and re-populate the calendar with the resulting slots */ const refreshCalendar = function () { - const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView'); - return Availability.spaces({ - spaceId: $scope.space.id, - member_id: $scope.ctrl.member.id, - start: view.start, - end: view.end, - timezone: Fablab.timezone - }, function (spaces) { - uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents'); - return $scope.eventSources.splice(0, 1, { - events: spaces, - textColor: 'black' - } - ); + $scope.eventSources.splice(0, 1, { + url: `/api/availabilities/spaces/${$transition$.params().id}?member_id=${$scope.ctrl.member.id}`, + textColor: 'black' }); }; diff --git a/app/frontend/src/javascript/controllers/trainings.js.erb b/app/frontend/src/javascript/controllers/trainings.js.erb index 0fa0a18a8..fe96a2e77 100644 --- a/app/frontend/src/javascript/controllers/trainings.js.erb +++ b/app/frontend/src/javascript/controllers/trainings.js.erb @@ -385,20 +385,9 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra * Refetch all events from the API and re-populate the calendar with the resulting slots */ const refreshCalendar = function () { - const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView'); - const id = $transition$.params().id === 'all' ? $transition$.params().id : $scope.training.id; - Availability.trainings({ - trainingId: id, - member_id: $scope.ctrl.member.id, - start: view.start, - end: view.end, - timezone: Fablab.timezone - }, function (trainings) { - uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents'); - $scope.eventSources.splice(0, 1, { - events: trainings, - textColor: 'black' - }); + $scope.eventSources.splice(0, 1, { + url: `/api/availabilities/trainings/${$transition$.params().id}`, + textColor: 'black' }); } From 8217bb1a59e811f4eff7734e82e3ae19b72be43b Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Thu, 29 Jun 2023 08:47:42 +0200 Subject: [PATCH 08/63] adds project categories --- Gemfile.lock | 5 +- .../api/project_categories_controller.rb | 45 ++++++ app/controllers/api/projects_controller.rb | 2 +- .../src/javascript/api/project-category.ts | 25 ++++ .../javascript/controllers/admin/projects.js | 50 ++++++- .../src/javascript/controllers/projects.js | 42 ++++-- .../src/javascript/models/project-category.ts | 5 + app/frontend/src/javascript/models/setting.ts | 8 +- app/frontend/src/javascript/router.js | 17 ++- .../javascript/services/project_category.js | 11 ++ .../templates/admin/projects/index.html | 5 +- .../admin/projects/project_categories.html | 38 +++++ .../templates/admin/projects/settings.html | 24 +++ app/frontend/templates/projects/_form.html | 17 +++ app/frontend/templates/projects/index.html | 4 + app/frontend/templates/projects/show.html | 22 ++- app/helpers/settings_helper.rb | 2 + app/models/project.rb | 3 + app/models/project_category.rb | 6 + app/models/projects_project_category.rb | 4 + app/policies/project_category_policy.rb | 16 ++ app/policies/setting_policy.rb | 2 +- app/services/project_service.rb | 1 + .../project_categories/index.json.jbuilder | 5 + app/views/api/projects/_indexed.json.jbuilder | 1 + app/views/api/projects/show.json.jbuilder | 5 + config/locales/app.admin.en.yml | 7 + config/locales/app.admin.fr.yml | 7 + config/locales/en.yml | 2 + config/locales/fr.yml | 2 + config/routes.rb | 1 + ...0230626122844_create_project_categories.rb | 9 ++ ...2947_create_projects_project_categories.rb | 12 ++ db/seeds/settings.rb | 3 + db/structure.sql | 141 +++++++++++++++++- test/fixtures/history_values.yml | 7 + test/fixtures/project_categories.yml | 12 ++ test/fixtures/projects_project_categories.yml | 7 + test/fixtures/settings.yml | 12 ++ test/frontend/__fixtures__/settings.ts | 12 ++ test/integration/project_categories_test.rb | 68 +++++++++ test/models/project_category_test.rb | 11 ++ test/models/project_test.rb | 15 ++ 43 files changed, 654 insertions(+), 39 deletions(-) create mode 100644 app/controllers/api/project_categories_controller.rb create mode 100644 app/frontend/src/javascript/api/project-category.ts create mode 100644 app/frontend/src/javascript/models/project-category.ts create mode 100644 app/frontend/src/javascript/services/project_category.js create mode 100644 app/frontend/templates/admin/projects/project_categories.html create mode 100644 app/models/project_category.rb create mode 100644 app/models/projects_project_category.rb create mode 100644 app/policies/project_category_policy.rb create mode 100644 app/views/api/project_categories/index.json.jbuilder create mode 100644 db/migrate/20230626122844_create_project_categories.rb create mode 100644 db/migrate/20230626122947_create_projects_project_categories.rb create mode 100644 test/fixtures/project_categories.yml create mode 100644 test/fixtures/projects_project_categories.yml create mode 100644 test/integration/project_categories_test.rb create mode 100644 test/models/project_category_test.rb create mode 100644 test/models/project_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index 4606b128b..75449b5b4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,7 @@ GEM rails (>= 4.1) ast (2.4.2) attr_required (1.0.1) - awesome_print (1.8.0) + awesome_print (1.9.2) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) @@ -269,6 +269,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.3-x86_64-darwin) + racc (~> 1.4) nokogiri (1.14.3-x86_64-linux) racc (~> 1.4) oauth2 (1.4.4) @@ -524,6 +526,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-21 x86_64-linux DEPENDENCIES diff --git a/app/controllers/api/project_categories_controller.rb b/app/controllers/api/project_categories_controller.rb new file mode 100644 index 000000000..36cd0e59f --- /dev/null +++ b/app/controllers/api/project_categories_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# API Controller for resources of type ProjectCategory +class API::ProjectCategoriesController < ApplicationController + before_action :set_project_category, only: %i[update destroy] + before_action :authenticate_user!, only: %i[create update destroy] + def index + @project_categories = ProjectCategory.all + end + + def create + authorize ProjectCategory + @project_category = ProjectCategory.new(project_category_params) + if @project_category.save + render json: @project_category, status: :created + else + render json: @project_category.errors, status: :unprocessable_entity + end + end + + def update + authorize ProjectCategory + if @project_category.update(project_category_params) + render json: @project_category, status: :ok + else + render json: @project_category.errors, status: :unprocessable_entity + end + end + + def destroy + authorize ProjectCategory + @project_category.destroy + head :no_content + end + + private + + def set_project_category + @project_category = ProjectCategory.find(params[:id]) + end + + def project_category_params + params.require(:project_category).permit(:name) + end +end diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index dc1cf7699..9bde77ed2 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -69,7 +69,7 @@ class API::ProjectsController < API::APIController def project_params params.require(:project).permit(:name, :description, :tags, :machine_ids, :component_ids, :theme_ids, :licence_id, :status_id, :state, - user_ids: [], machine_ids: [], component_ids: [], theme_ids: [], + user_ids: [], machine_ids: [], component_ids: [], theme_ids: [], project_category_ids: [], project_image_attributes: [:attachment], project_caos_attributes: %i[id attachment _destroy], project_steps_attributes: [ diff --git a/app/frontend/src/javascript/api/project-category.ts b/app/frontend/src/javascript/api/project-category.ts new file mode 100644 index 000000000..cd723f525 --- /dev/null +++ b/app/frontend/src/javascript/api/project-category.ts @@ -0,0 +1,25 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; +import { ProjectCategory } from '../models/project-category'; + +export default class ProjectCategoryAPI { + static async index (): Promise> { + const res: AxiosResponse> = await apiClient.get('/api/project_categories'); + return res?.data; + } + + static async create (newProjectCategory: ProjectCategory): Promise { + const res: AxiosResponse = await apiClient.post('/api/project_categories', { project_category: newProjectCategory }); + return res?.data; + } + + static async update (updatedProjectCategory: ProjectCategory): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/project_categories/${updatedProjectCategory.id}`, { project_category: updatedProjectCategory }); + return res?.data; + } + + static async destroy (projectCategoryId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/project_categories/${projectCategoryId}`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/controllers/admin/projects.js b/app/frontend/src/javascript/controllers/admin/projects.js index c184a67f9..4f25dd2ae 100644 --- a/app/frontend/src/javascript/controllers/admin/projects.js +++ b/app/frontend/src/javascript/controllers/admin/projects.js @@ -12,8 +12,8 @@ */ 'use strict'; -Application.Controllers.controller('AdminProjectsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise', '_t', 'Member', 'uiTourService', 'settingsPromise', 'growl', - function ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise, _t, Member, uiTourService, settingsPromise, growl) { +Application.Controllers.controller('AdminProjectsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'ProjectCategory', 'componentsPromise', 'licencesPromise', 'themesPromise', 'projectCategoriesPromise', '_t', 'Member', 'uiTourService', 'settingsPromise', 'growl', + function ($scope, $state, Component, Licence, Theme, ProjectCategory, componentsPromise, licencesPromise, themesPromise, projectCategoriesPromise, _t, Member, uiTourService, settingsPromise, growl) { // Materials list (plastic, wood ...) $scope.components = componentsPromise; @@ -23,6 +23,9 @@ Application.Controllers.controller('AdminProjectsController', ['$scope', '$state // Themes list (cooking, sport ...) $scope.themes = themesPromise; + // Project categories list (generic categorization) + $scope.projectCategories = projectCategoriesPromise; + // Application settings $scope.allSettings = settingsPromise; @@ -115,6 +118,49 @@ Application.Controllers.controller('AdminProjectsController', ['$scope', '$state } }; + /** + * Saves a new project category / Update an existing project category to the server (form validation callback) + * @param data {Object} project category name + * @param [data] {number} project category id, in case of update + */ + $scope.saveProjectCategory = function (data, id) { + if (id != null) { + return ProjectCategory.update({ id }, data); + } else { + return ProjectCategory.save(data, resp => $scope.projectCategories[$scope.projectCategories.length - 1].id = resp.id); + } + }; + + /** + * Deletes the project category at the specified index + * @param index {number} project category index in the $scope.projectCategories array + */ + $scope.removeProjectCategory = function (index) { + ProjectCategory.delete($scope.projectCategories[index]); + return $scope.projectCategories.splice(index, 1); + }; + + /** + * Creates a new empty entry in the $scope.projectCategories array + */ + $scope.addProjectCategory = function () { + $scope.inserted = { name: '' }; + $scope.projectCategories.push($scope.inserted); + }; + + /** + * Removes the newly inserted but not saved project category / Cancel the current project category modification + * @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ + * @param index {number} project category index in the $scope.projectCategories array + */ + $scope.cancelProjectCategory = function (rowform, index) { + if ($scope.projectCategories[index].id != null) { + rowform.$cancel(); + } else { + $scope.projectCategories.splice(index, 1); + } + }; + /** * Saves a new licence / Update an existing licence to the server (form validation callback) * @param data {Object} licence name and description diff --git a/app/frontend/src/javascript/controllers/projects.js b/app/frontend/src/javascript/controllers/projects.js index 6aa7d8112..cf6aa6f11 100644 --- a/app/frontend/src/javascript/controllers/projects.js +++ b/app/frontend/src/javascript/controllers/projects.js @@ -29,6 +29,7 @@ * - $scope.themes = [{Theme}] * - $scope.licences = [{Licence}] * - $scope.allowedExtensions = [{String}] + * - $scope.projectCategoriesWording = [{String}] * - $scope.submited(content) * - $scope.cancel() * - $scope.addFile() @@ -43,7 +44,7 @@ * - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ] */ class ProjectsController { - constructor ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t) { + constructor ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t) { // remove codeview from summernote editor $scope.summernoteOptsProject = angular.copy($rootScope.summernoteOpts); $scope.summernoteOptsProject.toolbar[6][1].splice(1, 1); @@ -78,6 +79,16 @@ class ProjectsController { }); }); + // Retrieve the list of themes from the server + ProjectCategory.query().$promise.then(function (data) { + $scope.projectCategories = data.map(function (d) { + return ({ + id: d.id, + name: d.name + }); + }); + }); + // Retrieve the list of licences from the server Licence.query().$promise.then(function (data) { $scope.licences = data.map(function (d) { @@ -104,6 +115,8 @@ class ProjectsController { // List of extensions allowed for CAD attachements upload $scope.allowedExtensions = allowedExtensions.setting.value.split(' '); + $scope.projectCategoriesWording = projectCategoriesWording.setting.value; + /** * For use with ngUpload (https://github.com/twilson63/ngUpload). * Intended to be the callback when an upload is done: any raised error will be stacked in the @@ -281,8 +294,8 @@ class ProjectsController { /** * Controller used on projects listing page */ -Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', 'settingsPromise', 'openLabActive', - function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive) { +Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'projectCategoriesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', 'settingsPromise', 'openLabActive', + function ($scope, $state, Project, machinesPromise, themesPromise, projectCategoriesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive) { /* PRIVATE STATIC CONSTANTS */ // Number of projects added to the page when the user clicks on 'load more projects' @@ -294,6 +307,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P // Fab-manager's instance ID in the openLab network $scope.openlabAppId = settingsPromise.openlab_app_id; + $scope.projectCategoriesFilterPlaceholder = settingsPromise.project_categories_filter_placeholder; + // Is openLab enabled on the instance? $scope.openlab = { projectsActive: openLabActive.isPresent, @@ -319,6 +334,9 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P // list of themes / used for filtering $scope.themes = themesPromise; + // list of projectCategories / used for filtering + $scope.projectCategories = projectCategoriesPromise; + // list of components / used for filtering $scope.components = componentsPromise; @@ -420,6 +438,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P updateUrlParam('component_id', search.component_id); updateUrlParam('machine_id', search.machine_id); updateUrlParam('status_id', search.status_id); + updateUrlParam('project_category_id', search.project_category_id); return true; }; @@ -496,8 +515,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P /** * Controller used in the project creation page */ -Application.Controllers.controller('NewProjectController', ['$rootScope', '$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', 'Status', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t', - function ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) { +Application.Controllers.controller('NewProjectController', ['$rootScope', '$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'ProjectCategory', 'Licence', 'Status', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', 'projectCategoriesWording', '_t', + function ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, CSRF, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t) { CSRF.setMetaTags(); // API URL where the form will be posted @@ -529,15 +548,15 @@ Application.Controllers.controller('NewProjectController', ['$rootScope', '$scop }; // Using the ProjectsController - return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t); + return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t); } ]); /** * Controller used in the project edition page */ -Application.Controllers.controller('EditProjectController', ['$rootScope', '$scope', '$state', '$transition$', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', 'Status', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t', - function ($rootScope, $scope, $state, $transition$, Project, Machine, Member, Component, Theme, Licence, Status, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) { +Application.Controllers.controller('EditProjectController', ['$rootScope', '$scope', '$state', '$transition$', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'ProjectCategory', 'Licence', 'Status', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', 'projectCategoriesWording', '_t', + function ($rootScope, $scope, $state, $transition$, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t) { /* PUBLIC SCOPE */ // API URL where the form will be posted @@ -583,7 +602,7 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco } // Using the ProjectsController - return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t); + return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t); }; // !!! MUST BE CALLED AT THE END of the controller @@ -594,14 +613,15 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco /** * Controller used in the public project's details page */ -Application.Controllers.controller('ShowProjectController', ['$scope', '$state', 'projectPromise', 'shortnamePromise', '$location', '$uibModal', 'dialogs', '_t', - function ($scope, $state, projectPromise, shortnamePromise, $location, $uibModal, dialogs, _t) { +Application.Controllers.controller('ShowProjectController', ['$scope', '$state', 'projectPromise', 'shortnamePromise', 'projectCategoriesWording', '$location', '$uibModal', 'dialogs', '_t', + function ($scope, $state, projectPromise, shortnamePromise, projectCategoriesWording, $location, $uibModal, dialogs, _t) { /* PUBLIC SCOPE */ // Store the project's details $scope.project = projectPromise; $scope.projectUrl = $location.absUrl(); $scope.disqusShortname = shortnamePromise.setting.value; + $scope.projectCategoriesWording = projectCategoriesWording.setting.value; /** * Test if the provided user has the edition rights on the current project diff --git a/app/frontend/src/javascript/models/project-category.ts b/app/frontend/src/javascript/models/project-category.ts new file mode 100644 index 000000000..83a0c2ab1 --- /dev/null +++ b/app/frontend/src/javascript/models/project-category.ts @@ -0,0 +1,5 @@ +// Type model used in ProjectSettings and its child components +export interface ProjectCategory { + name: string, + id?: number, +} diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 27f84db14..c3ed7bc24 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -198,7 +198,9 @@ export const fabHubSettings = [ export const projectsSettings = [ 'allowed_cad_extensions', 'allowed_cad_mime_types', - 'disqus_shortname' + 'disqus_shortname', + 'project_categories_filter_placeholder', + 'project_categories_wording' ] as const; export const prepaidPacksSettings = [ @@ -221,7 +223,7 @@ export const pricingSettings = [ 'extended_prices_in_same_day' ] as const; -export const poymentSettings = [ +export const paymentSettings = [ 'payment_gateway' ] as const; @@ -291,7 +293,7 @@ export const allSettings = [ ...registrationSettings, ...adminSettings, ...pricingSettings, - ...poymentSettings, + ...paymentSettings, ...displaySettings, ...storeSettings, ...trainingsSettings, diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 26ce00a93..897226a2c 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -301,8 +301,9 @@ angular.module('application.router', ['ui.router']) themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default']" }).$promise; }], - openLabActive: ['Setting', function (Setting) { return Setting.isPresent({ name: 'openlab_app_secret' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default', 'project_categories_filter_placeholder']" }).$promise; }], + openLabActive: ['Setting', function (Setting) { return Setting.isPresent({ name: 'openlab_app_secret' }).$promise; }], + projectCategoriesPromise: ['ProjectCategory', function (ProjectCategory) { return ProjectCategory.query().$promise; }] } }) .state('app.logged.projects_new', { @@ -314,7 +315,8 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }] + allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }], + projectCategoriesWording: ['Setting', function (Setting) { return Setting.get({ name: 'project_categories_wording' }).$promise; }] } }) .state('app.public.projects_show', { @@ -327,7 +329,8 @@ angular.module('application.router', ['ui.router']) }, resolve: { projectPromise: ['$transition$', 'Project', function ($transition$, Project) { return Project.get({ id: $transition$.params().id }).$promise; }], - shortnamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'disqus_shortname' }).$promise; }] + shortnamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'disqus_shortname' }).$promise; }], + projectCategoriesWording: ['Setting', function (Setting) { return Setting.get({ name: 'project_categories_wording' }).$promise; }] } }) .state('app.logged.projects_edit', { @@ -340,7 +343,8 @@ angular.module('application.router', ['ui.router']) }, resolve: { projectPromise: ['$transition$', 'Project', function ($transition$, Project) { return Project.get({ id: $transition$.params().id }).$promise; }], - allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }] + allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }], + projectCategoriesWording: ['Setting', function (Setting) { return Setting.get({ name: 'project_categories_wording' }).$promise; }] } }) @@ -735,10 +739,11 @@ angular.module('application.router', ['ui.router']) componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], licencesPromise: ['Licence', function (Licence) { return Licence.query().$promise; }], themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], + projectCategoriesPromise: ['ProjectCategory', function (ProjectCategory) { return ProjectCategory.query().$promise; }], settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'disqus_shortname', 'allowed_cad_extensions', " + - "'allowed_cad_mime_types', 'openlab_app_id', 'openlab_app_secret', 'openlab_default']" + "'allowed_cad_mime_types', 'openlab_app_id', 'openlab_app_secret', 'openlab_default', 'project_categories_filter_placeholder']" }).$promise; }] } diff --git a/app/frontend/src/javascript/services/project_category.js b/app/frontend/src/javascript/services/project_category.js new file mode 100644 index 000000000..079636210 --- /dev/null +++ b/app/frontend/src/javascript/services/project_category.js @@ -0,0 +1,11 @@ +'use strict'; + +Application.Services.factory('ProjectCategory', ['$resource', function ($resource) { + return $resource('/api/project_categories/:id', + { id: '@id' }, { + update: { + method: 'PUT' + } + } + ); +}]); diff --git a/app/frontend/templates/admin/projects/index.html b/app/frontend/templates/admin/projects/index.html index 46265ae15..c1cf88997 100644 --- a/app/frontend/templates/admin/projects/index.html +++ b/app/frontend/templates/admin/projects/index.html @@ -42,7 +42,10 @@ - + + + + diff --git a/app/frontend/templates/admin/projects/project_categories.html b/app/frontend/templates/admin/projects/project_categories.html new file mode 100644 index 000000000..36667205b --- /dev/null +++ b/app/frontend/templates/admin/projects/project_categories.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + +
{{ 'app.admin.project_categories.name' }}
+ + {{ projectCategory.name }} + + + +
+ + +
+
+ + +
+
diff --git a/app/frontend/templates/admin/projects/settings.html b/app/frontend/templates/admin/projects/settings.html index 73bd51897..4f3479872 100644 --- a/app/frontend/templates/admin/projects/settings.html +++ b/app/frontend/templates/admin/projects/settings.html @@ -95,3 +95,27 @@ + +
+
+ {{ 'app.admin.projects.settings.project_categories' }} +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
\ No newline at end of file diff --git a/app/frontend/templates/projects/_form.html b/app/frontend/templates/projects/_form.html index fb4a9ff69..fccac499c 100644 --- a/app/frontend/templates/projects/_form.html +++ b/app/frontend/templates/projects/_form.html @@ -279,6 +279,23 @@ +
+
+

{{ projectCategoriesWording }}

+
+
+ + + + + + + + + + +
+
diff --git a/app/frontend/templates/projects/index.html b/app/frontend/templates/projects/index.html index 22d8f8143..d92fa82ea 100644 --- a/app/frontend/templates/projects/index.html +++ b/app/frontend/templates/projects/index.html @@ -61,6 +61,10 @@ + + diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index 5c0aa93ab..ecc3666a4 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -174,12 +174,24 @@ -
+
+
+

{{ projectCategoriesWording }}

+
- -
+
    +
  • + {{projectCategory.name}} +
  • +
+
+ +
+ + +
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 1520bd44d..5688198e0 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -196,6 +196,8 @@ module SettingsHelper events_banner_cta_active events_banner_cta_label events_banner_cta_url + project_categories_filter_placeholder + project_categories_wording ].freeze end # rubocop:enable Metrics/ModuleLength diff --git a/app/models/project.rb b/app/models/project.rb index 238353e96..004fa62db 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -41,6 +41,8 @@ class Project < ApplicationRecord accepts_nested_attributes_for :project_steps, allow_destroy: true has_many :abuses, as: :signaled, dependent: :destroy, class_name: 'Abuse' + has_many :projects_project_categories, dependent: :destroy + has_many :project_categories, through: :projects_project_categories # validations validates :author, :name, presence: true @@ -68,6 +70,7 @@ class Project < ApplicationRecord scope :with_component, ->(component_ids) { joins(:projects_components).where(projects_components: { component_id: component_ids }) } scope :with_space, ->(spaces_ids) { joins(:projects_spaces).where(projects_spaces: { space_id: spaces_ids }) } scope :with_status, ->(statuses_ids) { where(status_id: statuses_ids) } + scope :with_project_category, ->(project_category_ids) { joins(:projects_project_categories).where(projects_project_categories: { project_category_id: project_category_ids }) } pg_search_scope :search, against: :search_vector, using: { diff --git a/app/models/project_category.rb b/app/models/project_category.rb new file mode 100644 index 000000000..20c0a3f45 --- /dev/null +++ b/app/models/project_category.rb @@ -0,0 +1,6 @@ +class ProjectCategory < ApplicationRecord + validates :name, presence: true + + has_many :projects_project_categories, dependent: :destroy + has_many :projects, through: :projects_project_categories +end diff --git a/app/models/projects_project_category.rb b/app/models/projects_project_category.rb new file mode 100644 index 000000000..8d2cb3f92 --- /dev/null +++ b/app/models/projects_project_category.rb @@ -0,0 +1,4 @@ +class ProjectsProjectCategory < ApplicationRecord + belongs_to :project + belongs_to :project_category +end diff --git a/app/policies/project_category_policy.rb b/app/policies/project_category_policy.rb new file mode 100644 index 000000000..33472a19e --- /dev/null +++ b/app/policies/project_category_policy.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Check if user is an admin to allow create, update and destroy project_category +class ProjectCategoryPolicy < ApplicationPolicy + def create? + user.admin? + end + + def update? + create? + end + + def destroy? + create? + end +end diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 1593beb55..d277f7973 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -46,7 +46,7 @@ class SettingPolicy < ApplicationPolicy external_id machines_banner_active machines_banner_text machines_banner_cta_active machines_banner_cta_label machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label - events_banner_cta_url] + events_banner_cta_url project_categories_filter_placeholder project_categories_wording] end ## diff --git a/app/services/project_service.rb b/app/services/project_service.rb index 823d9365d..d551a0b19 100644 --- a/app/services/project_service.rb +++ b/app/services/project_service.rb @@ -21,6 +21,7 @@ class ProjectService records = records.with_machine(query_params['machine_id']) if query_params['machine_id'].present? records = records.with_component(query_params['component_id']) if query_params['component_id'].present? records = records.with_theme(query_params['theme_id']) if query_params['theme_id'].present? + records = records.with_project_category(query_params['project_category_id']) if query_params['project_category_id'].present? records = records.with_space(query_params['space_id']) if query_params['space_id'].present? records = records.with_status(query_params['status_id']) if query_params['status_id'].present? records = if query_params['q'].present? diff --git a/app/views/api/project_categories/index.json.jbuilder b/app/views/api/project_categories/index.json.jbuilder new file mode 100644 index 000000000..23380c6d0 --- /dev/null +++ b/app/views/api/project_categories/index.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +json.array!(@project_categories) do |project_category| + json.extract! project_category, :id, :name +end diff --git a/app/views/api/projects/_indexed.json.jbuilder b/app/views/api/projects/_indexed.json.jbuilder index afdfa520d..326126429 100644 --- a/app/views/api/projects/_indexed.json.jbuilder +++ b/app/views/api/projects/_indexed.json.jbuilder @@ -7,6 +7,7 @@ json.user_ids project.user_ids json.machine_ids project.machine_ids json.theme_ids project.theme_ids json.component_ids project.component_ids +json.project_category_ids project.project_category_ids json.tags project.tags json.name project.name json.description project.description diff --git a/app/views/api/projects/show.json.jbuilder b/app/views/api/projects/show.json.jbuilder index e13618f81..0c76e9edb 100644 --- a/app/views/api/projects/show.json.jbuilder +++ b/app/views/api/projects/show.json.jbuilder @@ -39,6 +39,11 @@ json.themes @project.themes do |t| json.id t.id json.name t.name end +json.project_category_ids @project.project_category_ids +json.project_categories @project.project_categories do |t| + json.id t.id + json.name t.name +end json.user_ids @project.user_ids json.project_users @project.project_users do |pu| json.id pu.user.id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 8714ac56d..89ec56e12 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -415,6 +415,8 @@ en: add_a_material: "Add a material" themes: "Themes" add_a_new_theme: "Add a new theme" + project_categories: "Categories" + add_a_new_project_category: "Add a new category" licences: "Licences" statuses: "Statuses" description: "Description" @@ -445,6 +447,9 @@ en: open_lab_app_secret: "Secret" openlab_default_info_html: "In the projects gallery, visitors can switch between two views: all shared projects from the whole OpenLab network, or only the projects documented in your Fab Lab.
Here, you can choose which view is shown by default." default_to_openlab: "Display OpenLab by default" + project_categories: Categories + project_categories: + name: "Name" projects_setting: add: "Add" actions_controls: "Actions" @@ -1773,6 +1778,8 @@ en: extended_prices_in_same_day: "Extended prices in the same day" public_registrations: "Public registrations" show_username_in_admin_list: "Show the username in the list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index e7082ef58..eca8d0fe5 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -415,6 +415,8 @@ fr: add_a_material: "Ajouter un matériau" themes: "Thématiques" add_a_new_theme: "Ajouter une nouvelle thématique" + project_categories: "Catégories" + add_a_new_project_category: "Ajouter une nouvelle catégorie" licences: "Licences" statuses: "Statuts" description: "Description" @@ -445,6 +447,9 @@ fr: open_lab_app_secret: "Secret" openlab_default_info_html: "Dans la galerie de projets, les visiteurs peuvent choisir entre deux vues : tous les projets de l'ensemble du réseau OpenLab, ou uniquement les projets documentés dans votre Fab Lab.
Ici, vous pouvez choisir quelle vue est affichée par défaut." default_to_openlab: "Afficher OpenLab par défaut" + project_categories: Catégories + project_categories: + name: "Nom" projects_setting: add: "Ajouter" actions_controls: "Actions" @@ -1773,6 +1778,8 @@ fr: extended_prices_in_same_day: "Prix étendus le même jour" public_registrations: "Inscriptions publiques" show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste" + project_categories_filter_placeholder: "Texte du filtre par catégories de la galerie de projets" + project_categories_wording: "Mot utilisé en remplacement du mot \"Catégories\" sur les pages publiques" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" diff --git a/config/locales/en.yml b/config/locales/en.yml index 3bd7aae37..1b390ca66 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -697,6 +697,8 @@ en: trainings_authorization_validity_duration: "Trainings validity period duration" trainings_invalidation_rule: "Trainings automatic invalidation" trainings_invalidation_rule_period: "Grace period before invalidating a training" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" #statuses of projects statuses: new: "New" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5331975c3..3ccf791de 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -697,6 +697,8 @@ fr: trainings_authorization_validity_duration: "Durée de la période de validité des formations" trainings_invalidation_rule: "Invalidation automatique des formations" trainings_invalidation_rule_period: "Période de grâce avant d'invalider une formation" + project_categories_filter_placeholder: "Texte du filtre par catégories de la galerie de projets" + project_categories_wording: "Mot utilisé en remplacement du mot \"Catégories\" sur les pages publiques" #statuses of projects statuses: new: "Nouveau" diff --git a/config/routes.rb b/config/routes.rb index 4f9872af2..a813bcc5f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,7 @@ Rails.application.routes.draw do resources :themes resources :licences resources :statuses + resources :project_categories resources :admins, only: %i[index create destroy] resources :settings, only: %i[show update index], param: :name do patch '/bulk_update', action: 'bulk_update', on: :collection diff --git a/db/migrate/20230626122844_create_project_categories.rb b/db/migrate/20230626122844_create_project_categories.rb new file mode 100644 index 000000000..2c4a21ca4 --- /dev/null +++ b/db/migrate/20230626122844_create_project_categories.rb @@ -0,0 +1,9 @@ +class CreateProjectCategories < ActiveRecord::Migration[7.0] + def change + create_table :project_categories do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20230626122947_create_projects_project_categories.rb b/db/migrate/20230626122947_create_projects_project_categories.rb new file mode 100644 index 000000000..2465af743 --- /dev/null +++ b/db/migrate/20230626122947_create_projects_project_categories.rb @@ -0,0 +1,12 @@ +class CreateProjectsProjectCategories < ActiveRecord::Migration[7.0] + def change + create_table :projects_project_categories do |t| + t.belongs_to :project, foreign_key: true, null: false + t.belongs_to :project_category, foreign_key: true, null: false + + t.timestamps + end + + add_index :projects_project_categories, [:project_id, :project_category_id], unique: true, name: :idx_projects_project_categories + end +end diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 4df47c23c..a77f7bf57 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -728,3 +728,6 @@ Setting.set('accounting_Error_code', 'ERROR') unless Setting.find_by(name: 'acco Setting.set('accounting_Error_label', 'Erroneous invoices to refund') unless Setting.find_by(name: 'accounting_Error_label').try(:value) Setting.set('external_id', false) unless Setting.find_by(name: 'external_id').try(:value) + +Setting.set('project_categories_filter_placeholder', 'Toutes les catégories') unless Setting.find_by(name: 'project_categories_filter_placeholder').try(:value) +Setting.set('project_categories_wording', 'Catégories') unless Setting.find_by(name: 'project_categories_wording').try(:value) diff --git a/db/structure.sql b/db/structure.sql index 0f648a913..a9aa1f96d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9,13 +9,6 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; --- --- Name: public; Type: SCHEMA; Schema: -; Owner: - --- - --- *not* creating schema, since initdb creates it - - -- -- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: - -- @@ -2725,6 +2718,37 @@ CREATE SEQUENCE public.profiles_id_seq ALTER SEQUENCE public.profiles_id_seq OWNED BY public.profiles.id; +-- +-- Name: project_categories; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.project_categories ( + id bigint NOT NULL, + name character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: project_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.project_categories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: project_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.project_categories_id_seq OWNED BY public.project_categories.id; + + -- -- Name: project_steps; Type: TABLE; Schema: public; Owner: - -- @@ -2893,6 +2917,38 @@ CREATE SEQUENCE public.projects_machines_id_seq ALTER SEQUENCE public.projects_machines_id_seq OWNED BY public.projects_machines.id; +-- +-- Name: projects_project_categories; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projects_project_categories ( + id bigint NOT NULL, + project_id bigint NOT NULL, + project_category_id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: projects_project_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.projects_project_categories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: projects_project_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.projects_project_categories_id_seq OWNED BY public.projects_project_categories.id; + + -- -- Name: projects_spaces; Type: TABLE; Schema: public; Owner: - -- @@ -4746,6 +4802,13 @@ ALTER TABLE ONLY public.profile_custom_fields ALTER COLUMN id SET DEFAULT nextva ALTER TABLE ONLY public.profiles ALTER COLUMN id SET DEFAULT nextval('public.profiles_id_seq'::regclass); +-- +-- Name: project_categories id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.project_categories ALTER COLUMN id SET DEFAULT nextval('public.project_categories_id_seq'::regclass); + + -- -- Name: project_steps id; Type: DEFAULT; Schema: public; Owner: - -- @@ -4781,6 +4844,13 @@ ALTER TABLE ONLY public.projects_components ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY public.projects_machines ALTER COLUMN id SET DEFAULT nextval('public.projects_machines_id_seq'::regclass); +-- +-- Name: projects_project_categories id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects_project_categories ALTER COLUMN id SET DEFAULT nextval('public.projects_project_categories_id_seq'::regclass); + + -- -- Name: projects_spaces id; Type: DEFAULT; Schema: public; Owner: - -- @@ -5646,6 +5716,14 @@ ALTER TABLE ONLY public.profiles ADD CONSTRAINT profiles_pkey PRIMARY KEY (id); +-- +-- Name: project_categories project_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.project_categories + ADD CONSTRAINT project_categories_pkey PRIMARY KEY (id); + + -- -- Name: project_steps project_steps_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -5686,6 +5764,14 @@ ALTER TABLE ONLY public.projects ADD CONSTRAINT projects_pkey PRIMARY KEY (id); +-- +-- Name: projects_project_categories projects_project_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects_project_categories + ADD CONSTRAINT projects_project_categories_pkey PRIMARY KEY (id); + + -- -- Name: projects_spaces projects_spaces_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -5998,6 +6084,13 @@ ALTER TABLE ONLY public.wallets ADD CONSTRAINT wallets_pkey PRIMARY KEY (id); +-- +-- Name: idx_projects_project_categories; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_projects_project_categories ON public.projects_project_categories USING btree (project_id, project_category_id); + + -- -- Name: index_abuses_on_signaled_type_and_signaled_id; Type: INDEX; Schema: public; Owner: - -- @@ -6894,6 +6987,20 @@ CREATE UNIQUE INDEX index_projects_on_slug ON public.projects USING btree (slug) CREATE INDEX index_projects_on_status_id ON public.projects USING btree (status_id); +-- +-- Name: index_projects_project_categories_on_project_category_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_projects_project_categories_on_project_category_id ON public.projects_project_categories USING btree (project_category_id); + + +-- +-- Name: index_projects_project_categories_on_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_projects_project_categories_on_project_id ON public.projects_project_categories USING btree (project_id); + + -- -- Name: index_projects_spaces_on_project_id; Type: INDEX; Schema: public; Owner: - -- @@ -8057,6 +8164,14 @@ ALTER TABLE ONLY public.projects ADD CONSTRAINT fk_rails_b4a83cd9b3 FOREIGN KEY (status_id) REFERENCES public.statuses(id); +-- +-- Name: projects_project_categories fk_rails_ba4a985e85; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects_project_categories + ADD CONSTRAINT fk_rails_ba4a985e85 FOREIGN KEY (project_id) REFERENCES public.projects(id); + + -- -- Name: statistic_profiles fk_rails_bba64e5eb9; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8209,6 +8324,14 @@ ALTER TABLE ONLY public.event_price_categories ADD CONSTRAINT fk_rails_dcd2787d07 FOREIGN KEY (event_id) REFERENCES public.events(id); +-- +-- Name: projects_project_categories fk_rails_de9f22810e; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects_project_categories + ADD CONSTRAINT fk_rails_de9f22810e FOREIGN KEY (project_category_id) REFERENCES public.project_categories(id); + + -- -- Name: cart_item_coupons fk_rails_e1cb402fac; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8693,6 +8816,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230324095639'), ('20230328094807'), ('20230328094808'), -('20230328094809'); +('20230328094809'), +('20230626122844'), +('20230626122947'); diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 4ce437af6..5f23e32b4 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -852,3 +852,10 @@ history_value_100: updated_at: 2023-04-05 09:16:08.000511500 Z invoicing_profile_id: 1 +history_value_101: + id: 101 + setting_id: 100 + value: 'Toutes les catégories' + created_at: 2023-04-05 09:16:08.000511500 Z + updated_at: 2023-04-05 09:16:08.000511500 Z + invoicing_profile_id: 1 diff --git a/test/fixtures/project_categories.yml b/test/fixtures/project_categories.yml new file mode 100644 index 000000000..d0216e095 --- /dev/null +++ b/test/fixtures/project_categories.yml @@ -0,0 +1,12 @@ + +project_category_1: + id: 1 + name: Module 1 + created_at: 2023-06-26 15:39:08.259759000 Z + updated_at: 2016-06-26 15:39:08.259759000 Z + +project_category_2: + id: 2 + name: Module 2 + created_at: 2016-06-26 15:39:08.265840000 Z + updated_at: 2016-06-26 15:39:08.265840000 Z diff --git a/test/fixtures/projects_project_categories.yml b/test/fixtures/projects_project_categories.yml new file mode 100644 index 000000000..6dac837e3 --- /dev/null +++ b/test/fixtures/projects_project_categories.yml @@ -0,0 +1,7 @@ + +projects_project_category_1: + id: 1 + project_id: 1 + project_category_id: 1 + created_at: 2023-06-26 15:39:08.259759000 Z + updated_at: 2016-06-26 15:39:08.259759000 Z diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index eb21fa7f8..7936991a4 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -586,3 +586,15 @@ setting_99: name: home_css created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + +setting_100: + id: 100 + name: project_categories_filter_placeholder + created_at: 2023-04-05 09:16:08.000511500 Z + updated_at: 2023-04-05 09:16:08.000511500 Z + +setting_101: + id: 101 + name: project_categories_wording + created_at: 2023-04-05 09:16:08.000511500 Z + updated_at: 2023-04-05 09:16:08.000511500 Z diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 03dd134be..8a7cbf5ae 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -825,6 +825,18 @@ export const settings: Array = [ value: 'https://www.sleede.com/', last_update: '2022-12-23T14:39:12+0100', localized: 'Url' + }, + { + name: 'project_categories_filter_placeholder', + value: 'Toutes les catégories', + last_update: '2022-12-23T14:39:12+0100', + localized: 'Placeholder for categories filter in project gallery' + }, + { + name: 'project_categories_wording', + value: 'Catégories', + last_update: '2022-12-23T14:39:12+0100', + localized: 'Project categories overridden name' } ]; diff --git a/test/integration/project_categories_test.rb b/test/integration/project_categories_test.rb new file mode 100644 index 000000000..dc2872e26 --- /dev/null +++ b/test/integration/project_categories_test.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProjectCategoriesTest < ActionDispatch::IntegrationTest + def setup + @admin = User.find_by(username: 'admin') + login_as(@admin, scope: :user) + end + + test 'create a project_category' do + post '/api/project_categories', + params: { + name: 'Module de fou' + }.to_json, + headers: default_headers + + # Check response format & project_category + assert_equal 201, response.status, response.body + assert_match Mime[:json].to_s, response.content_type + + # Check the correct project_category was created + res = json_response(response.body) + project_category = ProjectCategory.where(id: res[:id]).first + assert_not_nil project_category, 'project_category was not created in database' + + assert_equal 'Module de fou', res[:name] + end + + test 'update a project_category' do + patch '/api/project_categories/1', + params: { + name: 'Nouveau nom' + }.to_json, + headers: default_headers + + # Check response format & project_category + assert_equal 200, response.status, response.body + assert_match Mime[:json].to_s, response.content_type + + # Check the project_category was updated + res = json_response(response.body) + assert_equal 1, res[:id] + assert_equal 'Nouveau nom', res[:name] + end + + test 'list all project_categories' do + get '/api/project_categories' + + # Check response format & project_category + assert_equal 200, response.status, response.body + assert_match Mime[:json].to_s, response.content_type + + # Check the list items are ok + project_categories = json_response(response.body) + assert_equal ProjectCategory.count, project_categories.count + end + + test 'delete a project_category' do + project_category = ProjectCategory.create!(name: 'Gone too soon') + delete "/api/project_categories/#{project_category.id}" + assert_response :success + assert_empty response.body + assert_raise ActiveRecord::RecordNotFound do + project_category.reload + end + end +end diff --git a/test/models/project_category_test.rb b/test/models/project_category_test.rb new file mode 100644 index 000000000..59a705c80 --- /dev/null +++ b/test/models/project_category_test.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProjectCategoryTest < ActiveSupport::TestCase + test 'fixtures are valid' do + ProjectCategory.find_each do |project_category| + assert project_category.valid? + end + end +end diff --git a/test/models/project_test.rb b/test/models/project_test.rb new file mode 100644 index 000000000..452ce3806 --- /dev/null +++ b/test/models/project_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProjectTest < ActiveSupport::TestCase + test 'fixtures are valid' do + Project.find_each do |project| + assert project.valid? + end + end + + test 'relation project_categories' do + assert_equal [project_categories(:project_category_1)], projects(:project_1).project_categories + end +end From 97c652f785e816580c5f629bc5783aa9582ef7cd Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Thu, 29 Jun 2023 14:32:06 +0200 Subject: [PATCH 09/63] filter projects list by member --- Gemfile.lock | 3 ++ .../src/javascript/controllers/projects.js | 45 +++++++++++++++++-- app/frontend/src/javascript/models/setting.ts | 3 +- app/frontend/src/javascript/router.js | 2 +- .../templates/admin/projects/settings.html | 15 +++++++ app/frontend/templates/projects/index.html | 9 ++++ app/helpers/settings_helper.rb | 1 + app/policies/setting_policy.rb | 2 +- app/services/project_service.rb | 8 ++++ app/views/application/index.html.erb | 3 ++ config/locales/app.admin.en.yml | 2 + config/locales/app.admin.fr.yml | 2 + config/locales/app.public.en.yml | 1 + config/locales/app.public.fr.yml | 1 + config/locales/en.yml | 1 + config/locales/fr.yml | 1 + db/seeds/settings.rb | 2 + test/fixtures/history_values.yml | 7 +++ test/fixtures/settings.yml | 6 +++ test/frontend/__fixtures__/settings.ts | 6 +++ 20 files changed, 114 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4606b128b..3f76d9e08 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,6 +269,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.3-x86_64-darwin) + racc (~> 1.4) nokogiri (1.14.3-x86_64-linux) racc (~> 1.4) oauth2 (1.4.4) @@ -524,6 +526,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-21 x86_64-linux DEPENDENCIES diff --git a/app/frontend/src/javascript/controllers/projects.js b/app/frontend/src/javascript/controllers/projects.js index 6aa7d8112..f09615e07 100644 --- a/app/frontend/src/javascript/controllers/projects.js +++ b/app/frontend/src/javascript/controllers/projects.js @@ -281,8 +281,8 @@ class ProjectsController { /** * Controller used on projects listing page */ -Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', 'settingsPromise', 'openLabActive', - function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive) { +Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', 'settingsPromise', 'openLabActive', 'Member', 'Diacritics', + function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive, Member, Diacritics) { /* PRIVATE STATIC CONSTANTS */ // Number of projects added to the page when the user clicks on 'load more projects' @@ -294,12 +294,18 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P // Fab-manager's instance ID in the openLab network $scope.openlabAppId = settingsPromise.openlab_app_id; + $scope.memberFilterPresence = settingsPromise.projects_list_member_filter_presence !== 'false'; + // Is openLab enabled on the instance? $scope.openlab = { projectsActive: openLabActive.isPresent, searchOverWholeNetwork: settingsPromise.openlab_default === 'true' }; + if (!$scope.memberFilterPresence) { + $location.$$search.member_id = ''; + } + // default search parameters $scope.search = { q: ($location.$$search.q || ''), @@ -307,7 +313,24 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P machine_id: (parseInt($location.$$search.machine_id) || undefined), component_id: (parseInt($location.$$search.component_id) || undefined), theme_id: (parseInt($location.$$search.theme_id) || undefined), - status_id: (parseInt($location.$$search.status_id) || undefined) + status_id: (parseInt($location.$$search.status_id) || undefined), + member_id: (parseInt($location.$$search.member_id) || undefined) + }; + + $scope.autoCompleteMemberName = function (nameLookup) { + if (!nameLookup) { + return; + } + $scope.isLoadingMembers = true; + const asciiName = Diacritics.remove(nameLookup); + + const q = { query: asciiName }; + + Member.search(q, function (users) { + $scope.matchingMembers = users; + $scope.isLoadingMembers = false; + } + , function (error) { console.error(error); }); }; // list of projects to display @@ -361,6 +384,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $scope.search.component_id = undefined; $scope.search.theme_id = undefined; $scope.search.status_id = undefined; + $scope.search.member_id = undefined; $scope.$apply(); $scope.setUrlQueryParams($scope.search); $scope.triggerSearch(); @@ -420,6 +444,16 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P updateUrlParam('component_id', search.component_id); updateUrlParam('machine_id', search.machine_id); updateUrlParam('status_id', search.status_id); + updateUrlParam('member_id', search.member_id); + return true; + }; + + $scope.setSearchMemberId = function (searchMember) { + if (searchMember) { + $scope.search.member_id = searchMember.id; + } else { + $scope.search.member_id = undefined; + } return true; }; @@ -450,6 +484,11 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P } else { $scope.openlab.searchOverWholeNetwork = $scope.openlab.projectsActive; } + if ($location.$$search.member_id && $scope.memberFilterPresence) { + Member.get({ id: $location.$$search.member_id }, function (member) { + $scope.searchMember = member; + }); + } return $scope.triggerSearch(); }; diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 27f84db14..37ffd2fdb 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -198,7 +198,8 @@ export const fabHubSettings = [ export const projectsSettings = [ 'allowed_cad_extensions', 'allowed_cad_mime_types', - 'disqus_shortname' + 'disqus_shortname', + 'projects_list_member_filter_presence' ] as const; export const prepaidPacksSettings = [ diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 26ce00a93..a400a3323 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -301,7 +301,7 @@ angular.module('application.router', ['ui.router']) themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default']" }).$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default', 'projects_list_member_filter_presence']" }).$promise; }], openLabActive: ['Setting', function (Setting) { return Setting.isPresent({ name: 'openlab_app_secret' }).$promise; }] } }) diff --git a/app/frontend/templates/admin/projects/settings.html b/app/frontend/templates/admin/projects/settings.html index 73bd51897..e7757928f 100644 --- a/app/frontend/templates/admin/projects/settings.html +++ b/app/frontend/templates/admin/projects/settings.html @@ -95,3 +95,18 @@ + +
+
+ {{ 'app.admin.projects.settings.filters' }} +
+
+
+ +
+
+
diff --git a/app/frontend/templates/projects/index.html b/app/frontend/templates/projects/index.html index 22d8f8143..1ee31bddf 100644 --- a/app/frontend/templates/projects/index.html +++ b/app/frontend/templates/projects/index.html @@ -65,6 +65,15 @@ + + + + + + + + + diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 1520bd44d..5dc8cea10 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -196,6 +196,7 @@ module SettingsHelper events_banner_cta_active events_banner_cta_label events_banner_cta_url + projects_list_member_filter_presence ].freeze end # rubocop:enable Metrics/ModuleLength diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 1593beb55..1e11dfe90 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -46,7 +46,7 @@ class SettingPolicy < ApplicationPolicy external_id machines_banner_active machines_banner_text machines_banner_cta_active machines_banner_cta_label machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label - events_banner_cta_url] + events_banner_cta_url projects_list_member_filter_presence] end ## diff --git a/app/services/project_service.rb b/app/services/project_service.rb index 823d9365d..3e01d5c16 100644 --- a/app/services/project_service.rb +++ b/app/services/project_service.rb @@ -23,6 +23,14 @@ class ProjectService records = records.with_theme(query_params['theme_id']) if query_params['theme_id'].present? records = records.with_space(query_params['space_id']) if query_params['space_id'].present? records = records.with_status(query_params['status_id']) if query_params['status_id'].present? + + if query_params['member_id'].present? + member = User.find(query_params['member_id']) + if member + records = records.where(id: Project.user_projects(member.statistic_profile.id)).or(Project.where(id: Project.collaborations(member.id))) + end + end + records = if query_params['q'].present? records.search(query_params['q']) else diff --git a/app/views/application/index.html.erb b/app/views/application/index.html.erb index 159ec0737..87396cdcf 100644 --- a/app/views/application/index.html.erb +++ b/app/views/application/index.html.erb @@ -61,6 +61,9 @@ buttons: <%= I18n.t('app.shared.buttons').to_json.html_safe %>, messages: <%= I18n.t('app.shared.messages').to_json.html_safe %> } + }, + date: { + month_names: <%= I18n.t('date.month_names').to_json.html_safe %> } }; Fablab.weekStartingDay = <%= Date.parse(Rails.application.secrets.week_starting_day).strftime('%w') %>; diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 8714ac56d..8d211c2b0 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -445,6 +445,7 @@ en: open_lab_app_secret: "Secret" openlab_default_info_html: "In the projects gallery, visitors can switch between two views: all shared projects from the whole OpenLab network, or only the projects documented in your Fab Lab.
Here, you can choose which view is shown by default." default_to_openlab: "Display OpenLab by default" + filters: Projects list filters projects_setting: add: "Add" actions_controls: "Actions" @@ -1773,6 +1774,7 @@ en: extended_prices_in_same_day: "Extended prices in the same day" public_registrations: "Public registrations" show_username_in_admin_list: "Show the username in the list" + projects_list_member_filter_presence: "Presence of member filter on projects list" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index e7082ef58..003ab199c 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -445,6 +445,7 @@ fr: open_lab_app_secret: "Secret" openlab_default_info_html: "Dans la galerie de projets, les visiteurs peuvent choisir entre deux vues : tous les projets de l'ensemble du réseau OpenLab, ou uniquement les projets documentés dans votre Fab Lab.
Ici, vous pouvez choisir quelle vue est affichée par défaut." default_to_openlab: "Afficher OpenLab par défaut" + filters: Filtres de la vue liste projects_setting: add: "Ajouter" actions_controls: "Actions" @@ -1773,6 +1774,7 @@ fr: extended_prices_in_same_day: "Prix étendus le même jour" public_registrations: "Inscriptions publiques" show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste" + projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 7dfc61f69..389775687 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -183,6 +183,7 @@ en: all_materials: "All materials" load_next_projects: "Load next projects" rough_draft: "Rough draft" + filter_by_member: "Filter by member" status_filter: all_statuses: "All statuses" select_status: "Select a status" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 09a07a116..82a6b7f15 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -183,6 +183,7 @@ fr: all_materials: "Tous les matériaux" load_next_projects: "Charger les projets suivants" rough_draft: "Brouillon" + filter_by_member: "Filter par membre" status_filter: all_statuses: "Tous les statuts" select_status: "Sélectionnez un statut" diff --git a/config/locales/en.yml b/config/locales/en.yml index 3bd7aae37..7c3443d36 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -697,6 +697,7 @@ en: trainings_authorization_validity_duration: "Trainings validity period duration" trainings_invalidation_rule: "Trainings automatic invalidation" trainings_invalidation_rule_period: "Grace period before invalidating a training" + projects_list_member_filter_presence: "Presence of member filter on projects list" #statuses of projects statuses: new: "New" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5331975c3..af049d102 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -697,6 +697,7 @@ fr: trainings_authorization_validity_duration: "Durée de la période de validité des formations" trainings_invalidation_rule: "Invalidation automatique des formations" trainings_invalidation_rule_period: "Période de grâce avant d'invalider une formation" + projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" #statuses of projects statuses: new: "Nouveau" diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 4df47c23c..28beafcda 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -728,3 +728,5 @@ Setting.set('accounting_Error_code', 'ERROR') unless Setting.find_by(name: 'acco Setting.set('accounting_Error_label', 'Erroneous invoices to refund') unless Setting.find_by(name: 'accounting_Error_label').try(:value) Setting.set('external_id', false) unless Setting.find_by(name: 'external_id').try(:value) + +Setting.set('projects_list_member_filter_presence', false) unless Setting.find_by(name: 'projects_list_member_filter_presence') diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 4ce437af6..0ba0f4645 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -852,3 +852,10 @@ history_value_100: updated_at: 2023-04-05 09:16:08.000511500 Z invoicing_profile_id: 1 +history_value_103: + id: 103 + setting_id: 102 + value: 'false' + created_at: 2023-04-05 09:16:08.000511500 Z + updated_at: 2023-04-05 09:16:08.000511500 Z + invoicing_profile_id: 1 \ No newline at end of file diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index eb21fa7f8..7b26448ac 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -586,3 +586,9 @@ setting_99: name: home_css created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + +setting_102: + id: 102 + name: projects_list_member_filter_presence + created_at: 2023-04-05 09:16:08.000511500 Z + updated_at: 2023-04-05 09:16:08.000511500 Z diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 03dd134be..246191ea4 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -825,6 +825,12 @@ export const settings: Array = [ value: 'https://www.sleede.com/', last_update: '2022-12-23T14:39:12+0100', localized: 'Url' + }, + { + name: 'projects_list_member_filter_presence', + value: 'false', + last_update: '2022-12-23T14:39:12+0100', + localized: 'Projects list member filter presence' } ]; From 82823dd4cc55c525e86bad7a146449f3d403451d Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Thu, 29 Jun 2023 16:37:16 +0200 Subject: [PATCH 10/63] download project to markdown file --- Gemfile | 2 + Gemfile.lock | 8 ++- app/controllers/api/projects_controller.rb | 6 ++ app/frontend/templates/projects/show.html | 15 ++-- app/policies/project_policy.rb | 4 ++ app/services/project_to_markdown.rb | 83 ++++++++++++++++++++++ config/locales/app.public.en.yml | 1 + config/locales/app.public.fr.yml | 1 + config/locales/app.shared.en.yml | 3 + config/locales/app.shared.fr.yml | 3 + config/routes.rb | 1 + 11 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 app/services/project_to_markdown.rb diff --git a/Gemfile b/Gemfile index 822c4dd0b..a63b12a8a 100644 --- a/Gemfile +++ b/Gemfile @@ -149,3 +149,5 @@ gem 'acts_as_list' # Error reporting gem 'sentry-rails' gem 'sentry-ruby' + +gem "reverse_markdown" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 4606b128b..cd14fbf34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -82,7 +82,7 @@ GEM rails (>= 4.1) ast (2.4.2) attr_required (1.0.1) - awesome_print (1.8.0) + awesome_print (1.9.2) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) @@ -269,6 +269,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.3-x86_64-darwin) + racc (~> 1.4) nokogiri (1.14.3-x86_64-linux) racc (~> 1.4) oauth2 (1.4.4) @@ -396,6 +398,8 @@ GEM responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) + reverse_markdown (2.1.1) + nokogiri rexml (3.2.5) rolify (5.3.0) rubocop (1.31.2) @@ -524,6 +528,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-21 x86_64-linux DEPENDENCIES @@ -587,6 +592,7 @@ DEPENDENCIES redis-session-store repost responders (~> 3.0) + reverse_markdown rolify rubocop (~> 1.31) rubocop-rails diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index dc1cf7699..a2ea6de2f 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -18,6 +18,12 @@ class API::ProjectsController < API::APIController @project = Project.friendly.find(params[:id]) end + def markdown + @project = Project.friendly.find(params[:id]) + authorize @project + send_data ProjectToMarkdown.new(@project).call, filename: "#{@project.name.parameterize}-#{@project.id}.md", disposition: 'attachment', type: 'text/markdown' + end + def create @project = Project.new(project_params.merge(author_statistic_profile_id: current_user.statistic_profile.id)) if @project.save diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index 5c0aa93ab..109b69e1c 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -174,12 +174,17 @@ -
+ - -
+
+ +
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 8daad8143..96033e0cb 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -19,6 +19,10 @@ class ProjectPolicy < ApplicationPolicy user.admin? or record.author.user_id == user.id or record.users.include?(user) end + def markdown? + user.admin? or record.author.user_id == user.id or record.users.include?(user) + end + def destroy? user.admin? or record.author.user_id == user.id end diff --git a/app/services/project_to_markdown.rb b/app/services/project_to_markdown.rb new file mode 100644 index 000000000..0dfe81914 --- /dev/null +++ b/app/services/project_to_markdown.rb @@ -0,0 +1,83 @@ +class ProjectToMarkdown + attr_reader :project + + def initialize(project) + @project = project + end + + def call + md = [] + + md << "# #{project.name}" + + md << "![#{I18n.t('app.shared.project.illustration')}](#{full_url(project.project_image.attachment.url)})" if project.project_image + + md << ReverseMarkdown.convert(project.description.to_s) + + project_steps = project.project_steps.order(:step_nb) + + if project_steps.present? + md << "## #{I18n.t('app.shared.project.steps')}" + + project_steps.each do |project_step| + md << "### #{I18n.t('app.shared.project.step_N').gsub('{INDEX}', project_step.step_nb.to_s)} : #{project_step.title}" + md << ReverseMarkdown.convert(project_step.description.to_s) + + project_step.project_step_images.each_with_index do |image, i| + md << "![#{I18n.t('app.shared.project.step_image')} #{i+1}](#{full_url(project.project_image.attachment.url)})" + end + end + end + + if project.themes.present? + md << "## #{I18n.t('app.shared.project.themes')}" + md << project.themes.map(&:name).join(', ') + end + + if project.project_caos.present? + md << "## #{I18n.t('app.shared.project.CAD_files')}" + project.project_caos.each do |cao| + md << "![#{cao.attachment_identifier}](#{full_url(cao.attachment_url)})" + end + end + + md << "## #{I18n.t('app.shared.project.status')}" + md << project.status.name + + if project.machines.present? + md << "## #{I18n.t('app.shared.project.employed_machines')}" + md << project.machines.map(&:name).join(', ') + end + + if project.components.present? + md << "## #{I18n.t('app.shared.project.employed_materials')}" + md << project.components.map(&:name).join(', ') + end + + if project.project_users.present? + md << "## #{I18n.t('app.shared.project.collaborators')}" + md << project.project_users.map { |pu| pu.user.profile.full_name }.join(', ') + end + + if project.licence.present? + md << "## #{I18n.t('app.shared.project.licence')}" + md << project.licence.name + end + + if project.tags.present? + md << "## #{I18n.t('app.shared.project.tags')}" + md << project.tags + end + + + md = md.reject { |line| line.blank? } + + md.join("\n\n") + end + + private + + def full_url(path) + "#{Rails.application.routes.url_helpers.root_url[...-1]}#{path}" + end +end \ No newline at end of file diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 7dfc61f69..be378bcd5 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -216,6 +216,7 @@ en: report: "Report" do_you_really_want_to_delete_this_project: "Do you really want to delete this project?" status: "Status" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "The machines" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 09a07a116..5d2004658 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -216,6 +216,7 @@ fr: report: "Signaler" do_you_really_want_to_delete_this_project: "Êtes-vous sur de vouloir supprimer ce projet ?" status: "Statut" + markdown_file: "Fichier Markdown" #list of machines machines_list: the_fablab_s_machines: "Les machines" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 423e3a6f9..127ae2ed1 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -131,6 +131,7 @@ en: illustration: "Visual" add_an_illustration: "Add an illustration" CAD_file: "CAD file" + CAD_files: "CAD files" allowed_extensions: "Allowed extensions:" add_a_new_file: "Add a new file" description: "Description" @@ -138,6 +139,7 @@ en: steps: "Steps" step_N: "Step {INDEX}" step_title: "Step title" + step_image: "Image" add_a_picture: "Add a picture" change_the_picture: "Change the picture" delete_the_step: "Delete the step" @@ -150,6 +152,7 @@ en: employed_machines: "Employed machines" collaborators: "Collaborators" creative_commons_licences: "Creative Commons licences" + licence: "Licence" themes: "Themes" tags: "Tags" save_as_draft: "Save as draft" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index e03e762b3..40816e060 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -131,6 +131,7 @@ fr: illustration: "Illustration" add_an_illustration: "Ajouter un visuel" CAD_file: "Fichier CAO" + CAD_files: "Fichiers CAO" allowed_extensions: "Extensions autorisées :" add_a_new_file: "Ajouter un nouveau fichier" description: "Description" @@ -138,6 +139,7 @@ fr: steps: "Étapes" step_N: "Étape {INDEX}" step_title: "Titre de l'étape" + step_image: "Image" add_a_picture: "Ajouter une image" change_the_picture: "Modifier l'image" delete_the_step: "Supprimer l'étape" @@ -150,6 +152,7 @@ fr: employed_machines: "Machines utilisées" collaborators: "Les collaborateurs" creative_commons_licences: "Licences Creative Commons" + licence: "Licence" themes: "Thématiques" tags: "Étiquettes" save_as_draft: "Enregistrer comme brouillon" diff --git a/config/routes.rb b/config/routes.rb index 4f9872af2..f0e0c1087 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -38,6 +38,7 @@ Rails.application.routes.draw do get :last_published get :search end + get :markdown, on: :member end resources :openlab_projects, only: :index resources :machines From 50ed3c9ed2af3770c9b70c1026d7539b6310cb97 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Fri, 30 Jun 2023 11:15:37 +0200 Subject: [PATCH 11/63] projects to markdown in zip --- Gemfile | 4 ++-- app/controllers/api/projects_controller.rb | 19 ++++++++++++---- .../src/javascript/controllers/projects.js | 5 +++++ app/frontend/templates/projects/index.html | 6 +++++ app/frontend/templates/projects/show.html | 4 ++-- app/models/project_step.rb | 2 ++ app/policies/project_policy.rb | 6 ++--- app/services/project_service.rb | 13 ++++++----- app/services/project_to_markdown.rb | 15 ++++++++----- app/services/projects_archive.rb | 22 +++++++++++++++++++ config/locales/app.public.en.yml | 1 + config/locales/app.public.fr.yml | 1 + config/locales/app.shared.en.yml | 1 + config/locales/app.shared.fr.yml | 1 + test/fixtures/project_steps.yml | 2 ++ test/fixtures/projects.yml | 1 + test/integration/projects_test.rb | 17 ++++++++++++++ test/services/project_to_markdown_test.rb | 21 ++++++++++++++++++ 18 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 app/services/projects_archive.rb create mode 100644 test/integration/projects_test.rb create mode 100644 test/services/project_to_markdown_test.rb diff --git a/Gemfile b/Gemfile index a63b12a8a..bbe94468b 100644 --- a/Gemfile +++ b/Gemfile @@ -30,6 +30,7 @@ group :development, :test do # comment over to use visual debugger (eg. RubyMine), uncomment to use manual debugging # gem 'byebug' gem 'dotenv-rails' + gem 'pry' end group :development do @@ -43,7 +44,6 @@ group :development do # Preview mail in the browser gem 'listen', '~> 3.0.5' gem 'overcommit' - gem 'pry' gem 'rb-readline' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'railroady' @@ -150,4 +150,4 @@ gem 'acts_as_list' gem 'sentry-rails' gem 'sentry-ruby' -gem "reverse_markdown" \ No newline at end of file +gem "reverse_markdown" diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index a2ea6de2f..e333ab64a 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -59,12 +59,23 @@ class API::ProjectsController < API::APIController def search service = ProjectService.new - res = service.search(params, current_user) + paginate = request.format.zip? ? false : true + res = service.search(params, current_user, paginate: paginate) + render json: res, status: :unprocessable_entity and return if res[:error] - @total = res[:total] - @projects = res[:projects] - render :index + respond_to do |format| + format.json do + @total = res[:total] + @projects = res[:projects] + render :index + end + format.zip do + head :forbidden unless current_user.admin? || current_user.manager? + + send_data ProjectsArchive.new(res[:projects]).call, filename: "projets.zip", disposition: 'attachment', type: 'application/zip' + end + end end private diff --git a/app/frontend/src/javascript/controllers/projects.js b/app/frontend/src/javascript/controllers/projects.js index 6aa7d8112..6c06f6365 100644 --- a/app/frontend/src/javascript/controllers/projects.js +++ b/app/frontend/src/javascript/controllers/projects.js @@ -332,6 +332,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $scope.triggerSearch(); }; + $scope.zipUrl = '/api/projects/search.zip'; + /** * Callback triggered when the button "search from the whole network" is toggled */ @@ -420,6 +422,9 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P updateUrlParam('component_id', search.component_id); updateUrlParam('machine_id', search.machine_id); updateUrlParam('status_id', search.status_id); + + $scope.zipUrl = '/api/projects/search.zip?' + new URLSearchParams({ search: JSON.stringify($location.search()) }).toString(); + return true; }; diff --git a/app/frontend/templates/projects/index.html b/app/frontend/templates/projects/index.html index 22d8f8143..57bd02ecc 100644 --- a/app/frontend/templates/projects/index.html +++ b/app/frontend/templates/projects/index.html @@ -67,6 +67,12 @@ + +
diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index 109b69e1c..f628c1c1f 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -174,8 +174,8 @@
-
- + diff --git a/app/models/project_step.rb b/app/models/project_step.rb index 0324c54f2..b9b76a248 100644 --- a/app/models/project_step.rb +++ b/app/models/project_step.rb @@ -5,4 +5,6 @@ class ProjectStep < ApplicationRecord belongs_to :project, touch: true has_many :project_step_images, as: :viewable, dependent: :destroy accepts_nested_attributes_for :project_step_images, allow_destroy: true, reject_if: :all_blank + + default_scope -> { order(:step_nb) } end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 96033e0cb..8da49c747 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -16,14 +16,14 @@ class ProjectPolicy < ApplicationPolicy end def update? - user.admin? or record.author.user_id == user.id or record.users.include?(user) + user.admin? || record.author.user_id == user.id || record.users.include?(user) end def markdown? - user.admin? or record.author.user_id == user.id or record.users.include?(user) + user.admin? || user.manager? || record.author.user_id == user.id || record.users.include?(user) end def destroy? - user.admin? or record.author.user_id == user.id + user.admin? || record.author.user_id == user.id end end diff --git a/app/services/project_service.rb b/app/services/project_service.rb index 823d9365d..6b7bddecd 100644 --- a/app/services/project_service.rb +++ b/app/services/project_service.rb @@ -2,17 +2,17 @@ # Provides methods for Project class ProjectService - def search(params, current_user) + def search(params, current_user, paginate: true) connection = ActiveRecord::Base.connection return { error: 'invalid adapter' } unless connection.instance_values['config'][:adapter] == 'postgresql' - search_from_postgre(params, current_user) + search_from_postgre(params, current_user, paginate: paginate) end private - def search_from_postgre(params, current_user) - query_params = JSON.parse(params[:search]) + def search_from_postgre(params, current_user, paginate: true) + query_params = JSON.parse(params[:search] || "{}") records = Project.published_or_drafts(current_user&.statistic_profile&.id) records = Project.user_projects(current_user&.statistic_profile&.id) if query_params['from'] == 'mine' @@ -29,6 +29,9 @@ class ProjectService records.order(created_at: :desc) end - { total: records.count, projects: records.includes(:users, :project_image).page(params[:page]) } + records = records.includes(:users, :project_image) + records = records.page(params[:page]) if paginate + + { total: records.count, projects: records } end end diff --git a/app/services/project_to_markdown.rb b/app/services/project_to_markdown.rb index 0dfe81914..7f3521ff0 100644 --- a/app/services/project_to_markdown.rb +++ b/app/services/project_to_markdown.rb @@ -14,7 +14,7 @@ class ProjectToMarkdown md << ReverseMarkdown.convert(project.description.to_s) - project_steps = project.project_steps.order(:step_nb) + project_steps = project.project_steps if project_steps.present? md << "## #{I18n.t('app.shared.project.steps')}" @@ -29,6 +29,9 @@ class ProjectToMarkdown end end + md << "## #{I18n.t('app.shared.project.author')}" + md << project.author&.user&.profile&.full_name + if project.themes.present? md << "## #{I18n.t('app.shared.project.themes')}" md << project.themes.map(&:name).join(', ') @@ -41,8 +44,10 @@ class ProjectToMarkdown end end - md << "## #{I18n.t('app.shared.project.status')}" - md << project.status.name + if project.status + md << "## #{I18n.t('app.shared.project.status')}" + md << project.status.name + end if project.machines.present? md << "## #{I18n.t('app.shared.project.employed_machines')}" @@ -54,9 +59,9 @@ class ProjectToMarkdown md << project.components.map(&:name).join(', ') end - if project.project_users.present? + if project.users.present? md << "## #{I18n.t('app.shared.project.collaborators')}" - md << project.project_users.map { |pu| pu.user.profile.full_name }.join(', ') + md << project.users.map { |u| u.profile.full_name }.join(', ') end if project.licence.present? diff --git a/app/services/projects_archive.rb b/app/services/projects_archive.rb new file mode 100644 index 000000000..009ea661f --- /dev/null +++ b/app/services/projects_archive.rb @@ -0,0 +1,22 @@ +class ProjectsArchive + attr_reader :projects + + def initialize(projects) + @projects = projects + end + + def call + stringio = Zip::OutputStream.write_buffer do |zio| + projects.includes(:project_image, :themes, + :project_caos, :status, :machines, + :components, :licence, + project_steps: :project_step_images, + author: { user: :profile }, + users: :profile).find_each do |project| + zio.put_next_entry("#{project.name.parameterize}-#{project.id}.md") + zio.write ProjectToMarkdown.new(project).call + end + end + stringio.string + end +end \ No newline at end of file diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index be378bcd5..8c2a0cd1a 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -183,6 +183,7 @@ en: all_materials: "All materials" load_next_projects: "Load next projects" rough_draft: "Rough draft" + download_archive: Download status_filter: all_statuses: "All statuses" select_status: "Select a status" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 5d2004658..7c8ce8cc8 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -183,6 +183,7 @@ fr: all_materials: "Tous les matériaux" load_next_projects: "Charger les projets suivants" rough_draft: "Brouillon" + download_archive: Télécharger status_filter: all_statuses: "Tous les statuts" select_status: "Sélectionnez un statut" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 127ae2ed1..bfe88a627 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -151,6 +151,7 @@ en: employed_materials: "Employed materials" employed_machines: "Employed machines" collaborators: "Collaborators" + author: Author creative_commons_licences: "Creative Commons licences" licence: "Licence" themes: "Themes" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 40816e060..2c655d255 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -151,6 +151,7 @@ fr: employed_materials: "Matériaux utilisés" employed_machines: "Machines utilisées" collaborators: "Les collaborateurs" + author: Auteur creative_commons_licences: "Licences Creative Commons" licence: "Licence" themes: "Thématiques" diff --git a/test/fixtures/project_steps.yml b/test/fixtures/project_steps.yml index cc2516cba..96d44b4fb 100644 --- a/test/fixtures/project_steps.yml +++ b/test/fixtures/project_steps.yml @@ -7,6 +7,7 @@ project_step_1: created_at: 2016-04-04 15:39:08.259759000 Z updated_at: 2016-04-04 15:39:08.259759000 Z title: Le manche + step_nb: 1 project_step_2: id: 2 @@ -16,3 +17,4 @@ project_step_2: created_at: 2016-04-04 15:39:08.265840000 Z updated_at: 2016-04-04 15:39:08.265840000 Z title: La presse + step_nb: 2 \ No newline at end of file diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml index 07e635e93..cec0d59b7 100644 --- a/test/fixtures/projects.yml +++ b/test/fixtures/projects.yml @@ -12,3 +12,4 @@ project_1: state: published slug: presse-puree published_at: 2016-04-04 15:39:08.267614000 Z + status_id: 1 \ No newline at end of file diff --git a/test/integration/projects_test.rb b/test/integration/projects_test.rb new file mode 100644 index 000000000..60477000f --- /dev/null +++ b/test/integration/projects_test.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProjectsTest < ActionDispatch::IntegrationTest + def setup + @admin = User.find_by(username: 'admin') + login_as(@admin, scope: :user) + end + + test 'download markdown file' do + get "/api/projects/1/markdown" + + assert_response :success + assert_equal "text/markdown", response.content_type + end +end \ No newline at end of file diff --git a/test/services/project_to_markdown_test.rb b/test/services/project_to_markdown_test.rb new file mode 100644 index 000000000..388f615f1 --- /dev/null +++ b/test/services/project_to_markdown_test.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ProjectToMarkdownTest < ActiveSupport::TestCase + test "ProjectToMarkdown is working" do + project = projects(:project_1) + service = ProjectToMarkdown.new(project) + + markdown_str = nil + + assert_nothing_raised do + markdown_str = service.call + end + + assert_includes markdown_str, project.name + project.project_steps.each do |project_step| + assert_includes markdown_str, project_step.title + end + end +end From af69ba67df31cf5f138c473187173fc27c02861d Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Fri, 30 Jun 2023 11:26:26 +0200 Subject: [PATCH 12/63] wip --- app/frontend/src/javascript/controllers/projects.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/frontend/src/javascript/controllers/projects.js b/app/frontend/src/javascript/controllers/projects.js index cf6aa6f11..dfc7e7457 100644 --- a/app/frontend/src/javascript/controllers/projects.js +++ b/app/frontend/src/javascript/controllers/projects.js @@ -322,7 +322,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P machine_id: (parseInt($location.$$search.machine_id) || undefined), component_id: (parseInt($location.$$search.component_id) || undefined), theme_id: (parseInt($location.$$search.theme_id) || undefined), - status_id: (parseInt($location.$$search.status_id) || undefined) + status_id: (parseInt($location.$$search.status_id) || undefined), + project_category_id: (parseInt($location.$$search.project_category_id) || undefined) }; // list of projects to display @@ -379,6 +380,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $scope.search.component_id = undefined; $scope.search.theme_id = undefined; $scope.search.status_id = undefined; + $scope.search.project_category_id = undefined; $scope.$apply(); $scope.setUrlQueryParams($scope.search); $scope.triggerSearch(); From 94b0b062bc5ac89b9a742c882ee20ac861fa35eb Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Mon, 3 Jul 2023 07:57:54 +0200 Subject: [PATCH 13/63] fix --- app/frontend/src/javascript/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 897226a2c..03ac8423b 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -743,7 +743,7 @@ angular.module('application.router', ['ui.router']) settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'disqus_shortname', 'allowed_cad_extensions', " + - "'allowed_cad_mime_types', 'openlab_app_id', 'openlab_app_secret', 'openlab_default', 'project_categories_filter_placeholder']" + "'allowed_cad_mime_types', 'openlab_app_id', 'openlab_app_secret', 'openlab_default', 'project_categories_filter_placeholder', 'project_categories_wording']" }).$promise; }] } From bc284e7813a78d4699fc9d2348676d7ce89d33b3 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Mon, 3 Jul 2023 11:42:29 +0200 Subject: [PATCH 14/63] filter projects by date --- .../src/javascript/controllers/projects.js | 20 +++++++++++-- app/frontend/src/javascript/models/setting.ts | 3 +- app/frontend/src/javascript/router.js | 2 +- .../templates/admin/projects/settings.html | 7 +++++ app/frontend/templates/projects/index.html | 28 ++++++++++++++++++- app/helpers/settings_helper.rb | 1 + app/policies/setting_policy.rb | 2 +- app/services/project_service.rb | 6 ++++ app/views/application/index.html.erb | 3 -- config/locales/app.admin.en.yml | 1 + config/locales/app.admin.fr.yml | 1 + config/locales/app.public.en.yml | 3 ++ config/locales/app.public.fr.yml | 3 ++ config/locales/en.yml | 1 + config/locales/fr.yml | 1 + db/seeds/settings.rb | 1 + test/fixtures/history_values.yml | 8 ++++++ test/fixtures/settings.yml | 6 ++++ test/frontend/__fixtures__/settings.ts | 6 ++++ 19 files changed, 94 insertions(+), 9 deletions(-) diff --git a/app/frontend/src/javascript/controllers/projects.js b/app/frontend/src/javascript/controllers/projects.js index f09615e07..da3e1eb7c 100644 --- a/app/frontend/src/javascript/controllers/projects.js +++ b/app/frontend/src/javascript/controllers/projects.js @@ -294,7 +294,9 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P // Fab-manager's instance ID in the openLab network $scope.openlabAppId = settingsPromise.openlab_app_id; + // settings of optional filters $scope.memberFilterPresence = settingsPromise.projects_list_member_filter_presence !== 'false'; + $scope.dateFiltersPresence = settingsPromise.projects_list_date_filters_presence !== 'false'; // Is openLab enabled on the instance? $scope.openlab = { @@ -306,6 +308,9 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $location.$$search.member_id = ''; } + fromDate = $location.$$search.from_date ? new Date($location.$$search.from_date) : undefined; + toDate = $location.$$search.to_date ? new Date($location.$$search.to_date) : undefined; + // default search parameters $scope.search = { q: ($location.$$search.q || ''), @@ -314,7 +319,9 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P component_id: (parseInt($location.$$search.component_id) || undefined), theme_id: (parseInt($location.$$search.theme_id) || undefined), status_id: (parseInt($location.$$search.status_id) || undefined), - member_id: (parseInt($location.$$search.member_id) || undefined) + member_id: (parseInt($location.$$search.member_id) || undefined), + from_date: fromDate, + to_date: toDate }; $scope.autoCompleteMemberName = function (nameLookup) { @@ -385,6 +392,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $scope.search.theme_id = undefined; $scope.search.status_id = undefined; $scope.search.member_id = undefined; + $scope.search.from_date = undefined; + $scope.search.to_date = undefined; $scope.$apply(); $scope.setUrlQueryParams($scope.search); $scope.triggerSearch(); @@ -413,7 +422,10 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P } else { updateUrlParam('whole_network', 'f'); $scope.projectsPagination = new paginationService.Instance(Project, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreCallback, 'search'); - Project.search({ search: $scope.search, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) { + const fromDate = $scope.search.from_date ? $scope.search.from_date.toLocaleDateString() : undefined; + const toDate = $scope.search.to_date ? $scope.search.to_date.toLocaleDateString() : undefined; + const searchParams = Object.assign({}, $scope.search, { from_date: fromDate, to_date: toDate }); + Project.search({ search: searchParams, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) { $scope.projectsPagination.totalCount = projectsPromise.meta.total; $scope.projects = projectsPromise.projects; }); @@ -445,6 +457,10 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P updateUrlParam('machine_id', search.machine_id); updateUrlParam('status_id', search.status_id); updateUrlParam('member_id', search.member_id); + const fromDate = search.from_date ? search.from_date.toDateString() : undefined; + updateUrlParam('from_date', fromDate); + const toDate = search.to_date ? search.to_date.toDateString() : undefined; + updateUrlParam('to_date', toDate); return true; }; diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 37ffd2fdb..72a2afae1 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -199,7 +199,8 @@ export const projectsSettings = [ 'allowed_cad_extensions', 'allowed_cad_mime_types', 'disqus_shortname', - 'projects_list_member_filter_presence' + 'projects_list_member_filter_presence', + 'projects_list_date_filters_presence' ] as const; export const prepaidPacksSettings = [ diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index a400a3323..eb06912d8 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -301,7 +301,7 @@ angular.module('application.router', ['ui.router']) themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }], componentsPromise: ['Component', function (Component) { return Component.query().$promise; }], machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default', 'projects_list_member_filter_presence']" }).$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default', 'projects_list_member_filter_presence', 'projects_list_date_filters_presence']" }).$promise; }], openLabActive: ['Setting', function (Setting) { return Setting.isPresent({ name: 'openlab_app_secret' }).$promise; }] } }) diff --git a/app/frontend/templates/admin/projects/settings.html b/app/frontend/templates/admin/projects/settings.html index e7757928f..df37133fc 100644 --- a/app/frontend/templates/admin/projects/settings.html +++ b/app/frontend/templates/admin/projects/settings.html @@ -108,5 +108,12 @@ on-error="onError" class-name="'m-l'">
+
+ +
diff --git a/app/frontend/templates/projects/index.html b/app/frontend/templates/projects/index.html index 1ee31bddf..e58a6eb22 100644 --- a/app/frontend/templates/projects/index.html +++ b/app/frontend/templates/projects/index.html @@ -22,7 +22,7 @@
-

Filter

+

{{ 'app.public.projects_list.filter' }}

{{ 'app.public.projects_list.reset_all_filters' | translate }}
@@ -74,6 +74,32 @@ + + + +
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 5dc8cea10..42e922522 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -197,6 +197,7 @@ module SettingsHelper events_banner_cta_label events_banner_cta_url projects_list_member_filter_presence + projects_list_date_filters_presence ].freeze end # rubocop:enable Metrics/ModuleLength diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 1e11dfe90..d5763f70e 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -46,7 +46,7 @@ class SettingPolicy < ApplicationPolicy external_id machines_banner_active machines_banner_text machines_banner_cta_active machines_banner_cta_label machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label - events_banner_cta_url projects_list_member_filter_presence] + events_banner_cta_url projects_list_member_filter_presence projects_list_date_filters_presence] end ## diff --git a/app/services/project_service.rb b/app/services/project_service.rb index 3e01d5c16..7acf89a86 100644 --- a/app/services/project_service.rb +++ b/app/services/project_service.rb @@ -31,6 +31,12 @@ class ProjectService end end + created_from = Time.zone.parse(query_params['from_date']).beginning_of_day if query_params['from_date'].present? + created_to = Time.zone.parse(query_params['to_date']).end_of_day if query_params['to_date'].present? + if created_from || created_to + records = records.where(created_at: created_from..created_to) + end + records = if query_params['q'].present? records.search(query_params['q']) else diff --git a/app/views/application/index.html.erb b/app/views/application/index.html.erb index 87396cdcf..159ec0737 100644 --- a/app/views/application/index.html.erb +++ b/app/views/application/index.html.erb @@ -61,9 +61,6 @@ buttons: <%= I18n.t('app.shared.buttons').to_json.html_safe %>, messages: <%= I18n.t('app.shared.messages').to_json.html_safe %> } - }, - date: { - month_names: <%= I18n.t('date.month_names').to_json.html_safe %> } }; Fablab.weekStartingDay = <%= Date.parse(Rails.application.secrets.week_starting_day).strftime('%w') %>; diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 8d211c2b0..558a9c6d5 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1775,6 +1775,7 @@ en: public_registrations: "Public registrations" show_username_in_admin_list: "Show the username in the list" projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of date filters on projects list" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 003ab199c..4713e50ea 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -1775,6 +1775,7 @@ fr: public_registrations: "Inscriptions publiques" show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste" projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" + projects_list_date_filters_presence: "Présence des filtres par date dans la vue liste des projets" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 389775687..4afd8a93c 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -167,6 +167,7 @@ en: full_price: "Full price: " #projects gallery projects_list: + filter: Filter the_fablab_projects: "The projects" add_a_project: "Add a project" network_search: "Fab-manager network" @@ -184,6 +185,8 @@ en: load_next_projects: "Load next projects" rough_draft: "Rough draft" filter_by_member: "Filter by member" + created_from: Created from + created_to: Created to status_filter: all_statuses: "All statuses" select_status: "Select a status" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 82a6b7f15..9fe6efb2e 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -167,6 +167,7 @@ fr: full_price: "Plein tarif : " #projects gallery projects_list: + filter: Filtrer the_fablab_projects: "Les projets" add_a_project: "Ajouter un projet" network_search: "Réseau Fab-Manager" @@ -184,6 +185,8 @@ fr: load_next_projects: "Charger les projets suivants" rough_draft: "Brouillon" filter_by_member: "Filter par membre" + created_from: Créés à partir du + created_to: Créés jusqu'au status_filter: all_statuses: "Tous les statuts" select_status: "Sélectionnez un statut" diff --git a/config/locales/en.yml b/config/locales/en.yml index 7c3443d36..0bb5308ae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -698,6 +698,7 @@ en: trainings_invalidation_rule: "Trainings automatic invalidation" trainings_invalidation_rule_period: "Grace period before invalidating a training" projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of dates filter on projects list" #statuses of projects statuses: new: "New" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index af049d102..8a7830d3f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -698,6 +698,7 @@ fr: trainings_invalidation_rule: "Invalidation automatique des formations" trainings_invalidation_rule_period: "Période de grâce avant d'invalider une formation" projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" + projects_list_date_filters_presence: "Présence des filtres par date dans la vue liste des projets" #statuses of projects statuses: new: "Nouveau" diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 28beafcda..2f770b440 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -730,3 +730,4 @@ Setting.set('accounting_Error_label', 'Erroneous invoices to refund') unless Set Setting.set('external_id', false) unless Setting.find_by(name: 'external_id').try(:value) Setting.set('projects_list_member_filter_presence', false) unless Setting.find_by(name: 'projects_list_member_filter_presence') +Setting.set('projects_list_date_filters_presence', false) unless Setting.find_by(name: 'projects_list_date_filters_presence') diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 0ba0f4645..867a6f606 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -858,4 +858,12 @@ history_value_103: value: 'false' created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + invoicing_profile_id: 1 + +history_value_104: + id: 104 + setting_id: 103 + value: 'false' + created_at: 2023-04-05 09:16:08.000511500 Z + updated_at: 2023-04-05 09:16:08.000511500 Z invoicing_profile_id: 1 \ No newline at end of file diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index 7b26448ac..0897fa5b3 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -592,3 +592,9 @@ setting_102: name: projects_list_member_filter_presence created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + +setting_103: + id: 103 + name: projects_list_date_filters_presence + created_at: 2023-04-05 09:16:08.000511500 Z + updated_at: 2023-04-05 09:16:08.000511500 Z diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 246191ea4..dbe7b7786 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -831,6 +831,12 @@ export const settings: Array = [ value: 'false', last_update: '2022-12-23T14:39:12+0100', localized: 'Projects list member filter presence' + }, + { + name: 'projects_list_date_filters_presence', + value: 'false', + last_update: '2022-12-23T14:39:12+0100', + localized: 'Projects list date filters presence' } ]; From b3970da1c44ee71962ab9ef407402788d4008ef7 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Mon, 3 Jul 2023 16:31:45 +0200 Subject: [PATCH 15/63] displays "my orders" link only if store module is active --- app/frontend/templates/shared/header.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/templates/shared/header.html.erb b/app/frontend/templates/shared/header.html.erb index 7dbc6dc96..f644d1654 100644 --- a/app/frontend/templates/shared/header.html.erb +++ b/app/frontend/templates/shared/header.html.erb @@ -48,7 +48,7 @@
  • {{ 'app.public.common.my_events' }}
  • {{ 'app.public.common.my_invoices' }}
  • {{ 'app.public.common.my_payment_schedules' }}
  • -
  • {{ 'app.public.common.my_orders' }}
  • +
  • {{ 'app.public.common.my_orders' }}
  • {{ 'app.public.common.my_wallet' }}
  • {{ 'app.public.common.help' }}
  • From e50c90b30d93ae54163c10ef4818a218877313b4 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 3 Jul 2023 17:43:27 +0200 Subject: [PATCH 16/63] (bug) Accouning Line in duplicate --- CHANGELOG.md | 2 ++ Gemfile.lock | 3 +++ app/services/accounting/accounting_service.rb | 16 ++++++++++++---- app/workers/accounting_worker.rb | 4 ---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd02d555..b01f784dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog Fab-manager - Fix a bug: unable to refresh machine/space/training calender after pay an reservation +- Fix a bug: Accouning Line in duplicate +- [TODO DEPLOY] `rails fablab:setup:build_accounting_lines` ## v6.0.7 2023 June 20 diff --git a/Gemfile.lock b/Gemfile.lock index 4606b128b..5cb4eaf82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -269,6 +269,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.3-x86_64-darwin) + racc (~> 1.4) nokogiri (1.14.3-x86_64-linux) racc (~> 1.4) oauth2 (1.4.4) @@ -524,6 +526,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-20 x86_64-linux DEPENDENCIES diff --git a/app/services/accounting/accounting_service.rb b/app/services/accounting/accounting_service.rb index ee224065e..1ecfb4b28 100644 --- a/app/services/accounting/accounting_service.rb +++ b/app/services/accounting/accounting_service.rb @@ -22,11 +22,19 @@ class Accounting::AccountingService lines = [] processed = [] invoices.find_each do |i| - Rails.logger.debug { "processing invoice #{i.id}..." } unless Rails.env.test? - lines.concat(generate_lines(i)) - processed.push(i.id) + Rails.logger.debug { "[AccountLine] processing invoice #{i.id}..." } unless Rails.env.test? + if i.main_item.nil? + Rails.logger.error { "[AccountLine] invoice #{i.id} main_item is nil" } unless Rails.env.test? + else + lines.concat(generate_lines(i)) + processed.push(i.id) + end + end + ActiveRecord::Base.transaction do + ids = invoices.map(&:id) + AccountingLine.where(invoice_id: ids).delete_all + AccountingLine.create!(lines) end - AccountingLine.create!(lines) processed end diff --git a/app/workers/accounting_worker.rb b/app/workers/accounting_worker.rb index e2cf5f448..71b5a443a 100644 --- a/app/workers/accounting_worker.rb +++ b/app/workers/accounting_worker.rb @@ -27,8 +27,6 @@ class AccountingWorker end def invoices(invoices_ids) - # clean - AccountingLine.where(invoice_id: invoices_ids).delete_all # build service = Accounting::AccountingService.new invoices = Invoice.where(id: invoices_ids) @@ -37,8 +35,6 @@ class AccountingWorker end def all - # clean - AccountingLine.delete_all # build service = Accounting::AccountingService.new ids = service.build_from_invoices(Invoice.all) From d4313a1efcf1d4447860ab4e57e755ca4b9a5516 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 3 Jul 2023 17:45:59 +0200 Subject: [PATCH 17/63] (doc) update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b01f784dd..c00ddb437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Fix a bug: unable to refresh machine/space/training calender after pay an reservation - Fix a bug: Accouning Line in duplicate +- Fix a bug: displays "my orders" link only if store module is active - [TODO DEPLOY] `rails fablab:setup:build_accounting_lines` ## v6.0.7 2023 June 20 From 878051d2b14402eb8983c888506cda21050ce188 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 3 Jul 2023 18:18:00 +0200 Subject: [PATCH 18/63] (doc) update changelogs --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c00ddb437..a8b648577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog Fab-manager +- Improved projects list filter - Fix a bug: unable to refresh machine/space/training calender after pay an reservation - Fix a bug: Accouning Line in duplicate - Fix a bug: displays "my orders" link only if store module is active From 212e044a6b025611c737507038aac35942928be9 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 3 Jul 2023 18:39:02 +0200 Subject: [PATCH 19/63] (i18n) updated translations --- config/locales/app.admin.de.yml | 10 ++++++++++ config/locales/app.admin.es.yml | 10 ++++++++++ config/locales/app.admin.fr.yml | 18 +++++++++--------- config/locales/app.admin.it.yml | 10 ++++++++++ config/locales/app.admin.no.yml | 10 ++++++++++ config/locales/app.admin.pt.yml | 10 ++++++++++ config/locales/app.admin.zu.yml | 10 ++++++++++ config/locales/app.public.de.yml | 6 ++++++ config/locales/app.public.es.yml | 6 ++++++ config/locales/app.public.fr.yml | 12 ++++++------ config/locales/app.public.it.yml | 6 ++++++ config/locales/app.public.no.yml | 6 ++++++ config/locales/app.public.pt.yml | 6 ++++++ config/locales/app.public.zu.yml | 6 ++++++ config/locales/app.shared.de.yml | 4 ++++ config/locales/app.shared.es.yml | 4 ++++ config/locales/app.shared.fr.yml | 4 ++-- config/locales/app.shared.it.yml | 4 ++++ config/locales/app.shared.no.yml | 4 ++++ config/locales/app.shared.pt.yml | 4 ++++ config/locales/app.shared.zu.yml | 4 ++++ config/locales/de.yml | 4 ++++ config/locales/es.yml | 4 ++++ config/locales/fr.yml | 8 ++++---- config/locales/it.yml | 4 ++++ config/locales/no.yml | 4 ++++ config/locales/pt.yml | 4 ++++ config/locales/zu.yml | 4 ++++ 28 files changed, 165 insertions(+), 21 deletions(-) diff --git a/config/locales/app.admin.de.yml b/config/locales/app.admin.de.yml index 1c7c20384..fa60516d2 100644 --- a/config/locales/app.admin.de.yml +++ b/config/locales/app.admin.de.yml @@ -415,6 +415,8 @@ de: add_a_material: "Materialien hinfügen" themes: "Themen" add_a_new_theme: "Neues Thema hinzufügen" + project_categories: "Categories" + add_a_new_project_category: "Add a new category" licences: "Lizenzen" statuses: "Statuses" description: "Beschreibung" @@ -445,6 +447,10 @@ de: open_lab_app_secret: "Geheimnis" openlab_default_info_html: "In der Projektgalerie können Besucher zwischen zwei Ansichten wechseln: alle gemeinsam geteilten Projekte des OpenLab-Netzwerkes oder nur die in Ihrem FabLab dokumentierten Projekte.
    Hier können Sie die standardmäßig angezeigte Ansicht auswählen." default_to_openlab: "OpenLab standardmäßig anzeigen" + filters: Projects list filters + project_categories: Categories + project_categories: + name: "Name" projects_setting: add: "Hinzufügen" actions_controls: "Actions" @@ -1773,6 +1779,10 @@ de: extended_prices_in_same_day: "Erweiterte Preise am selben Tag" public_registrations: "Öffentliche Registrierungen" show_username_in_admin_list: "Show the username in the list" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of date filters on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" overlapping_options: training_reservations: "Schulungen" machine_reservations: "Maschinen" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index aba85bcc3..d230e753b 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -415,6 +415,8 @@ es: add_a_material: "Añadir un material" themes: "Temas" add_a_new_theme: "Añadir un nuevo tema" + project_categories: "Categories" + add_a_new_project_category: "Add a new category" licences: "Licencias" statuses: "Statuses" description: "Descripción" @@ -445,6 +447,10 @@ es: open_lab_app_secret: "Secret" openlab_default_info_html: "In the projects gallery, visitors can switch between two views: all shared projects from the whole OpenLab network, or only the projects documented in your Fab Lab.
    Here, you can choose which view is shown by default." default_to_openlab: "Display OpenLab by default" + filters: Projects list filters + project_categories: Categories + project_categories: + name: "Name" projects_setting: add: "Add" actions_controls: "Actions" @@ -1773,6 +1779,10 @@ es: extended_prices_in_same_day: "Extended prices in the same day" public_registrations: "Public registrations" show_username_in_admin_list: "Show the username in the list" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of date filters on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index f01a165e5..4bc83e3aa 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -415,8 +415,8 @@ fr: add_a_material: "Ajouter un matériau" themes: "Thématiques" add_a_new_theme: "Ajouter une nouvelle thématique" - project_categories: "Catégories" - add_a_new_project_category: "Ajouter une nouvelle catégorie" + project_categories: "Categories" + add_a_new_project_category: "Add a new category" licences: "Licences" statuses: "Statuts" description: "Description" @@ -447,10 +447,10 @@ fr: open_lab_app_secret: "Secret" openlab_default_info_html: "Dans la galerie de projets, les visiteurs peuvent choisir entre deux vues : tous les projets de l'ensemble du réseau OpenLab, ou uniquement les projets documentés dans votre Fab Lab.
    Ici, vous pouvez choisir quelle vue est affichée par défaut." default_to_openlab: "Afficher OpenLab par défaut" - filters: Filtres de la vue liste - project_categories: Catégories + filters: Projects list filters + project_categories: Categories project_categories: - name: "Nom" + name: "Name" projects_setting: add: "Ajouter" actions_controls: "Actions" @@ -1779,10 +1779,10 @@ fr: extended_prices_in_same_day: "Prix étendus le même jour" public_registrations: "Inscriptions publiques" show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste" - projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" - projects_list_date_filters_presence: "Présence des filtres par date dans la vue liste des projets" - project_categories_filter_placeholder: "Texte du filtre par catégories de la galerie de projets" - project_categories_wording: "Mot utilisé en remplacement du mot \"Catégories\" sur les pages publiques" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of date filters on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" diff --git a/config/locales/app.admin.it.yml b/config/locales/app.admin.it.yml index 4a62c96b2..102b4ef2f 100644 --- a/config/locales/app.admin.it.yml +++ b/config/locales/app.admin.it.yml @@ -415,6 +415,8 @@ it: add_a_material: "Aggiungi un materiale" themes: "Temi" add_a_new_theme: "Aggiungi un nuovo tema" + project_categories: "Categories" + add_a_new_project_category: "Add a new category" licences: "Licenze" statuses: "Status" description: "Descrizione" @@ -445,6 +447,10 @@ it: open_lab_app_secret: "Segreto" openlab_default_info_html: "Nella galleria di progetti, i visitatori possono scegliere tra due viste: tutti i progetti condivisi da tutta la rete di OpenLab, o solo i progetti documentati nel tuo Fab Lab.
    Qui, puoi scegliere quale vista è mostrata per impostazione predefinita." default_to_openlab: "Visualizza OpenLab per impostazione predefinita" + filters: Projects list filters + project_categories: Categories + project_categories: + name: "Name" projects_setting: add: "Aggiungi" actions_controls: "Azioni" @@ -1773,6 +1779,10 @@ it: extended_prices_in_same_day: "Prezzi estesi nello stesso giorno" public_registrations: "Registri pubblici" show_username_in_admin_list: "Mostra il nome utente nella lista" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of date filters on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" overlapping_options: training_reservations: "Abilitazioni" machine_reservations: "Macchine" diff --git a/config/locales/app.admin.no.yml b/config/locales/app.admin.no.yml index 352d90f9d..67b76790e 100644 --- a/config/locales/app.admin.no.yml +++ b/config/locales/app.admin.no.yml @@ -415,6 +415,8 @@ add_a_material: "Legg til et materiale" themes: "Temaer" add_a_new_theme: "Legge til et nytt tema" + project_categories: "Categories" + add_a_new_project_category: "Add a new category" licences: "Lisenser" statuses: "Statuses" description: "Beskrivelse" @@ -445,6 +447,10 @@ open_lab_app_secret: "Hemmelighet" openlab_default_info_html: "I prosjektgalleriet kan besøkende bytte mellom to visninger: alle delte projetter fra hele OpenLab-nettverket. eller bare prosjektene som er dokumentert i din Fab Lab.
    Her kan du velge hvilken visning som standard." default_to_openlab: "Vis OpenLab som standard" + filters: Projects list filters + project_categories: Categories + project_categories: + name: "Name" projects_setting: add: "Add" actions_controls: "Actions" @@ -1773,6 +1779,10 @@ extended_prices_in_same_day: "Extended prices in the same day" public_registrations: "Public registrations" show_username_in_admin_list: "Show the username in the list" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of date filters on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index c3b88f936..05eb8c93b 100644 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -415,6 +415,8 @@ pt: add_a_material: "Adicionar material" themes: "Temas" add_a_new_theme: "Adicionar um novo tema" + project_categories: "Categories" + add_a_new_project_category: "Add a new category" licences: "Licenças" statuses: "Statuses" description: "Descrição" @@ -445,6 +447,10 @@ pt: open_lab_app_secret: "Senha" openlab_default_info_html: "Na galeria de projetos, os visitantes podem alternar entre duas visualizações: todos os projetos compartilhados de toda a rede OpenLab, ou apenas os projetos documentados no seu Fab Lab.
    Aqui, você pode escolher qual exibição é mostrada por padrão." default_to_openlab: "Mostrar OpenLab por padrão" + filters: Projects list filters + project_categories: Categories + project_categories: + name: "Name" projects_setting: add: "Add" actions_controls: "Actions" @@ -1773,6 +1779,10 @@ pt: extended_prices_in_same_day: "Preços estendidos no mesmo dia" public_registrations: "Inscrições públicas" show_username_in_admin_list: "Mostrar o nome de usuário na lista" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of date filters on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" overlapping_options: training_reservations: "Treinamentos" machine_reservations: "Máquinas" diff --git a/config/locales/app.admin.zu.yml b/config/locales/app.admin.zu.yml index 61a96210a..9977ec4cb 100644 --- a/config/locales/app.admin.zu.yml +++ b/config/locales/app.admin.zu.yml @@ -415,6 +415,8 @@ zu: add_a_material: "crwdns24306:0crwdne24306:0" themes: "crwdns24308:0crwdne24308:0" add_a_new_theme: "crwdns24310:0crwdne24310:0" + project_categories: "crwdns37617:0crwdne37617:0" + add_a_new_project_category: "crwdns37619:0crwdne37619:0" licences: "crwdns24312:0crwdne24312:0" statuses: "crwdns36893:0crwdne36893:0" description: "crwdns24314:0crwdne24314:0" @@ -445,6 +447,10 @@ zu: open_lab_app_secret: "crwdns24362:0crwdne24362:0" openlab_default_info_html: "crwdns37609:0crwdne37609:0" default_to_openlab: "crwdns24366:0crwdne24366:0" + filters: crwdns37621:0crwdne37621:0 + project_categories: crwdns37623:0crwdne37623:0 + project_categories: + name: "crwdns37625:0crwdne37625:0" projects_setting: add: "crwdns36895:0crwdne36895:0" actions_controls: "crwdns36897:0crwdne36897:0" @@ -1773,6 +1779,10 @@ zu: extended_prices_in_same_day: "crwdns26752:0crwdne26752:0" public_registrations: "crwdns26754:0crwdne26754:0" show_username_in_admin_list: "crwdns26756:0crwdne26756:0" + projects_list_member_filter_presence: "crwdns37627:0crwdne37627:0" + projects_list_date_filters_presence: "crwdns37629:0crwdne37629:0" + project_categories_filter_placeholder: "crwdns37631:0crwdne37631:0" + project_categories_wording: "crwdns37633:0crwdne37633:0" overlapping_options: training_reservations: "crwdns26758:0crwdne26758:0" machine_reservations: "crwdns26760:0crwdne26760:0" diff --git a/config/locales/app.public.de.yml b/config/locales/app.public.de.yml index 01ea569e1..b8f0a9d2b 100644 --- a/config/locales/app.public.de.yml +++ b/config/locales/app.public.de.yml @@ -167,6 +167,7 @@ de: full_price: "Voller Preis: " #projects gallery projects_list: + filter: Filter the_fablab_projects: "The projects" add_a_project: "Projekt hinzufügen" network_search: "Fab-manager network" @@ -183,6 +184,10 @@ de: all_materials: "Alle Materialien" load_next_projects: "Nächste Projekte laden" rough_draft: "Grober Entwurf" + filter_by_member: "Filter by member" + created_from: Created from + created_to: Created to + download_archive: Download status_filter: all_statuses: "All statuses" select_status: "Select a status" @@ -216,6 +221,7 @@ de: report: "Melden" do_you_really_want_to_delete_this_project: "Wollen Sie dieses Projekt wirklich löschen?" status: "Status" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "The machines" diff --git a/config/locales/app.public.es.yml b/config/locales/app.public.es.yml index b6b78c9b0..d884d4321 100644 --- a/config/locales/app.public.es.yml +++ b/config/locales/app.public.es.yml @@ -167,6 +167,7 @@ es: full_price: "Full price: " #projects gallery projects_list: + filter: Filter the_fablab_projects: "The projects" add_a_project: "Añadir un proyecto" network_search: "Fab-manager network" @@ -183,6 +184,10 @@ es: all_materials: "Todo el material" load_next_projects: "Cargar más proyectos" rough_draft: "Borrador" + filter_by_member: "Filter by member" + created_from: Created from + created_to: Created to + download_archive: Download status_filter: all_statuses: "All statuses" select_status: "Select a status" @@ -216,6 +221,7 @@ es: report: "Reportar" do_you_really_want_to_delete_this_project: "¿Está seguro de querer eliminar este proyecto?" status: "Status" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "The machines" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index bff82e15c..26d8ac301 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -167,7 +167,7 @@ fr: full_price: "Plein tarif : " #projects gallery projects_list: - filter: Filtrer + filter: Filter the_fablab_projects: "Les projets" add_a_project: "Ajouter un projet" network_search: "Réseau Fab-Manager" @@ -184,10 +184,10 @@ fr: all_materials: "Tous les matériaux" load_next_projects: "Charger les projets suivants" rough_draft: "Brouillon" - filter_by_member: "Filter par membre" - created_from: Créés à partir du - created_to: Créés jusqu'au - download_archive: Télécharger + filter_by_member: "Filter by member" + created_from: Created from + created_to: Created to + download_archive: Download status_filter: all_statuses: "Tous les statuts" select_status: "Sélectionnez un statut" @@ -221,7 +221,7 @@ fr: report: "Signaler" do_you_really_want_to_delete_this_project: "Êtes-vous sur de vouloir supprimer ce projet ?" status: "Statut" - markdown_file: "Fichier Markdown" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "Les machines" diff --git a/config/locales/app.public.it.yml b/config/locales/app.public.it.yml index 963c6a577..3b48a4ebb 100644 --- a/config/locales/app.public.it.yml +++ b/config/locales/app.public.it.yml @@ -167,6 +167,7 @@ it: full_price: "Prezzo intero: " #projects gallery projects_list: + filter: Filter the_fablab_projects: "Progetti" add_a_project: "Aggiungi un progetto" network_search: "Fab-manager network" @@ -183,6 +184,10 @@ it: all_materials: "Tutti i materiali" load_next_projects: "Carica i progetti successivi" rough_draft: "Bozza preliminare" + filter_by_member: "Filter by member" + created_from: Created from + created_to: Created to + download_archive: Download status_filter: all_statuses: "Tutti gli stati" select_status: "Seleziona uno status" @@ -216,6 +221,7 @@ it: report: "Segnalazione" do_you_really_want_to_delete_this_project: "Vuoi davvero eliminare questo progetto?" status: "Stato" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "Le macchine" diff --git a/config/locales/app.public.no.yml b/config/locales/app.public.no.yml index aa9d6a1dc..b1753896c 100644 --- a/config/locales/app.public.no.yml +++ b/config/locales/app.public.no.yml @@ -167,6 +167,7 @@ full_price: "Full pris: " #projects gallery projects_list: + filter: Filter the_fablab_projects: "The projects" add_a_project: "Legg til et prosjekt" network_search: "Fab-manager network" @@ -183,6 +184,10 @@ all_materials: "Alle materialer" load_next_projects: "Last neste prosjekt" rough_draft: "Tidlig utkast" + filter_by_member: "Filter by member" + created_from: Created from + created_to: Created to + download_archive: Download status_filter: all_statuses: "All statuses" select_status: "Select a status" @@ -216,6 +221,7 @@ report: "Rapporter" do_you_really_want_to_delete_this_project: "Vil du virkelig slette dette prosjektet?" status: "Status" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "The machines" diff --git a/config/locales/app.public.pt.yml b/config/locales/app.public.pt.yml index 77f61af48..3fcaaec49 100644 --- a/config/locales/app.public.pt.yml +++ b/config/locales/app.public.pt.yml @@ -167,6 +167,7 @@ pt: full_price: "Valor inteira: " #projects gallery projects_list: + filter: Filter the_fablab_projects: "Os projetos" add_a_project: "Adicionar projeto" network_search: "Rede Fab-manager" @@ -183,6 +184,10 @@ pt: all_materials: "Todos os materiais" load_next_projects: "Carregar próximos projetos" rough_draft: "Rascunho" + filter_by_member: "Filter by member" + created_from: Created from + created_to: Created to + download_archive: Download status_filter: all_statuses: "Todos os status" select_status: "Selecione um status" @@ -216,6 +221,7 @@ pt: report: "Enviar" do_you_really_want_to_delete_this_project: "Você quer realmente deletar esse projeto?" status: "Status" + markdown_file: "Markdown file" #list of machines machines_list: the_fablab_s_machines: "As máquinas" diff --git a/config/locales/app.public.zu.yml b/config/locales/app.public.zu.yml index 3c30ca692..417ead98d 100644 --- a/config/locales/app.public.zu.yml +++ b/config/locales/app.public.zu.yml @@ -167,6 +167,7 @@ zu: full_price: "crwdns28058:0crwdne28058:0" #projects gallery projects_list: + filter: crwdns37635:0crwdne37635:0 the_fablab_projects: "crwdns36237:0crwdne36237:0" add_a_project: "crwdns28062:0crwdne28062:0" network_search: "crwdns37071:0crwdne37071:0" @@ -183,6 +184,10 @@ zu: all_materials: "crwdns28088:0crwdne28088:0" load_next_projects: "crwdns28090:0crwdne28090:0" rough_draft: "crwdns28092:0crwdne28092:0" + filter_by_member: "crwdns37637:0crwdne37637:0" + created_from: crwdns37639:0crwdne37639:0 + created_to: crwdns37641:0crwdne37641:0 + download_archive: crwdns37643:0crwdne37643:0 status_filter: all_statuses: "crwdns37073:0crwdne37073:0" select_status: "crwdns37075:0crwdne37075:0" @@ -216,6 +221,7 @@ zu: report: "crwdns28144:0crwdne28144:0" do_you_really_want_to_delete_this_project: "crwdns28146:0crwdne28146:0" status: "crwdns37077:0crwdne37077:0" + markdown_file: "crwdns37645:0crwdne37645:0" #list of machines machines_list: the_fablab_s_machines: "crwdns36239:0crwdne36239:0" diff --git a/config/locales/app.shared.de.yml b/config/locales/app.shared.de.yml index 70ecece02..2e5b7333e 100644 --- a/config/locales/app.shared.de.yml +++ b/config/locales/app.shared.de.yml @@ -131,6 +131,7 @@ de: illustration: "Ansicht" add_an_illustration: "Illustration hinzufügen" CAD_file: "CAD-Datei" + CAD_files: "CAD files" allowed_extensions: "Zugelassene Dateitypen:" add_a_new_file: "Neue Datei hinzufügen" description: "Beschreibung" @@ -138,6 +139,7 @@ de: steps: "Schritte" step_N: "Schritt {INDEX}" step_title: "Titel des Schrits" + step_image: "Image" add_a_picture: "Ein Bild hinzufügen" change_the_picture: "Bild ändern" delete_the_step: "Diesen Schritt löschen" @@ -149,7 +151,9 @@ de: employed_materials: "Verwendetes Material" employed_machines: "Verwendete Maschinen" collaborators: "Mitarbeitende" + author: Author creative_commons_licences: "Creative Commons-Lizenzen" + licence: "Licence" themes: "Themen" tags: "Stichwörter" save_as_draft: "Als Entwurf speichern" diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index da12eb735..f2d54ba74 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -131,6 +131,7 @@ es: illustration: "Ilustración" add_an_illustration: "Añadir una ilustración" CAD_file: "Fichero CAD" + CAD_files: "CAD files" allowed_extensions: "Extensiones permitidas:" add_a_new_file: "Añadir un nuevo archivo" description: "Description" @@ -138,6 +139,7 @@ es: steps: "Pasos" step_N: "Step {INDEX}" step_title: "Título de los pasos" + step_image: "Image" add_a_picture: "Añadir imagen" change_the_picture: "Cambiar imagen" delete_the_step: "Eliminar el paso" @@ -149,7 +151,9 @@ es: employed_materials: "Material empleados" employed_machines: "Máquinas empleadas" collaborators: "Collaborators" + author: Author creative_commons_licences: "Licencias Creative Commons" + licence: "Licence" themes: "Themes" tags: "Tags" save_as_draft: "Save as draft" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 2c655d255..8bf660c5d 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -131,7 +131,7 @@ fr: illustration: "Illustration" add_an_illustration: "Ajouter un visuel" CAD_file: "Fichier CAO" - CAD_files: "Fichiers CAO" + CAD_files: "CAD files" allowed_extensions: "Extensions autorisées :" add_a_new_file: "Ajouter un nouveau fichier" description: "Description" @@ -151,7 +151,7 @@ fr: employed_materials: "Matériaux utilisés" employed_machines: "Machines utilisées" collaborators: "Les collaborateurs" - author: Auteur + author: Author creative_commons_licences: "Licences Creative Commons" licence: "Licence" themes: "Thématiques" diff --git a/config/locales/app.shared.it.yml b/config/locales/app.shared.it.yml index f136a1376..0186856bb 100644 --- a/config/locales/app.shared.it.yml +++ b/config/locales/app.shared.it.yml @@ -131,6 +131,7 @@ it: illustration: "Illustrazione" add_an_illustration: "Aggiungi un'illustrazione" CAD_file: "File CAD" + CAD_files: "CAD files" allowed_extensions: "Estensioni consentite:" add_a_new_file: "Aggiungi nuovo file" description: "Descrizione" @@ -138,6 +139,7 @@ it: steps: "Passaggi" step_N: "Passaggio {INDEX}" step_title: "Titolo del passaggio" + step_image: "Image" add_a_picture: "Aggiungi un'immagine" change_the_picture: "Cambia immagine" delete_the_step: "Elimina il passaggio" @@ -149,7 +151,9 @@ it: employed_materials: "Materiali impiegati" employed_machines: "Macchine impiegate" collaborators: "Collaboratori" + author: Author creative_commons_licences: "Licenze Creative Commons" + licence: "Licence" themes: "Temi" tags: "Etichette" save_as_draft: "Salva come bozza" diff --git a/config/locales/app.shared.no.yml b/config/locales/app.shared.no.yml index ce8e6acbd..da0d91a74 100644 --- a/config/locales/app.shared.no.yml +++ b/config/locales/app.shared.no.yml @@ -131,6 +131,7 @@ illustration: "Bilde" add_an_illustration: "Legg til en illustrasjon" CAD_file: "CAD-filer" + CAD_files: "CAD files" allowed_extensions: "Tillatte filtyper:" add_a_new_file: "Legg til ny fil" description: "Beskrivelse" @@ -138,6 +139,7 @@ steps: "Skritt" step_N: "Trinn {INDEX}" step_title: "Tittel på steg" + step_image: "Image" add_a_picture: "Legg til bilde" change_the_picture: "Endre bilde" delete_the_step: "Slett trinnet" @@ -149,7 +151,9 @@ employed_materials: "Materialer brukt" employed_machines: "Maskiner brukt" collaborators: "Samarbeidspartnere" + author: Author creative_commons_licences: "Creative Commons lisenser" + licence: "Licence" themes: "Temaer" tags: "Etiketter" save_as_draft: "Lagre som utkast" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index 7ca328679..de9c0ad2d 100644 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -131,6 +131,7 @@ pt: illustration: "Foto" add_an_illustration: "Adicionar foto" CAD_file: "Arquivo CAD" + CAD_files: "CAD files" allowed_extensions: "Extensões permitidas:" add_a_new_file: "Adicionar novo arquivo" description: "Descrição" @@ -138,6 +139,7 @@ pt: steps: "Passos" step_N: "Passo {INDEX}" step_title: "Passo Título" + step_image: "Image" add_a_picture: "Adicionar imagem" change_the_picture: "Alterar imagem" delete_the_step: "Deletar este passo" @@ -149,7 +151,9 @@ pt: employed_materials: "Materiais utilizados" employed_machines: "Máquinas utilizadas" collaborators: "Colaboradores" + author: Author creative_commons_licences: "Licença Creative Commons" + licence: "Licence" themes: "Temas" tags: "Tags" save_as_draft: "Salvar como rascunho" diff --git a/config/locales/app.shared.zu.yml b/config/locales/app.shared.zu.yml index 0f2f6cc5b..fc1b8764d 100644 --- a/config/locales/app.shared.zu.yml +++ b/config/locales/app.shared.zu.yml @@ -131,6 +131,7 @@ zu: illustration: "crwdns28728:0crwdne28728:0" add_an_illustration: "crwdns28730:0crwdne28730:0" CAD_file: "crwdns28732:0crwdne28732:0" + CAD_files: "crwdns37647:0crwdne37647:0" allowed_extensions: "crwdns28734:0crwdne28734:0" add_a_new_file: "crwdns28736:0crwdne28736:0" description: "crwdns28738:0crwdne28738:0" @@ -138,6 +139,7 @@ zu: steps: "crwdns28742:0crwdne28742:0" step_N: "crwdns28744:0{INDEX}crwdne28744:0" step_title: "crwdns28746:0crwdne28746:0" + step_image: "crwdns37649:0crwdne37649:0" add_a_picture: "crwdns28748:0crwdne28748:0" change_the_picture: "crwdns28750:0crwdne28750:0" delete_the_step: "crwdns28752:0crwdne28752:0" @@ -149,7 +151,9 @@ zu: employed_materials: "crwdns28764:0crwdne28764:0" employed_machines: "crwdns28766:0crwdne28766:0" collaborators: "crwdns28768:0crwdne28768:0" + author: crwdns37651:0crwdne37651:0 creative_commons_licences: "crwdns28770:0crwdne28770:0" + licence: "crwdns37653:0crwdne37653:0" themes: "crwdns28772:0crwdne28772:0" tags: "crwdns28774:0crwdne28774:0" save_as_draft: "crwdns28776:0crwdne28776:0" diff --git a/config/locales/de.yml b/config/locales/de.yml index f372552c4..63482dae1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -697,6 +697,10 @@ de: trainings_authorization_validity_duration: "Trainings validity period duration" trainings_invalidation_rule: "Trainings automatic invalidation" trainings_invalidation_rule_period: "Grace period before invalidating a training" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of dates filter on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" #statuses of projects statuses: new: "Neu" diff --git a/config/locales/es.yml b/config/locales/es.yml index 869b6de38..5221aef5a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -697,6 +697,10 @@ es: trainings_authorization_validity_duration: "Trainings validity period duration" trainings_invalidation_rule: "Trainings automatic invalidation" trainings_invalidation_rule_period: "Grace period before invalidating a training" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of dates filter on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" #statuses of projects statuses: new: "New" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 2b77a17e2..6d8a4588f 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -697,10 +697,10 @@ fr: trainings_authorization_validity_duration: "Durée de la période de validité des formations" trainings_invalidation_rule: "Invalidation automatique des formations" trainings_invalidation_rule_period: "Période de grâce avant d'invalider une formation" - projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" - projects_list_date_filters_presence: "Présence des filtres par date dans la vue liste des projets" - project_categories_filter_placeholder: "Texte du filtre par catégories de la galerie de projets" - project_categories_wording: "Mot utilisé en remplacement du mot \"Catégories\" sur les pages publiques" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of dates filter on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" #statuses of projects statuses: new: "Nouveau" diff --git a/config/locales/it.yml b/config/locales/it.yml index bb8eda00a..9a2ba4665 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -697,6 +697,10 @@ it: trainings_authorization_validity_duration: "Durata del periodo di validità delle abilitazioni" trainings_invalidation_rule: "Annullamento automatico delle abilitazioni" trainings_invalidation_rule_period: "Periodo di tolleranza prima di invalidare un'abilitazione" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of dates filter on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" #statuses of projects statuses: new: "Nuovo" diff --git a/config/locales/no.yml b/config/locales/no.yml index 17dd1da76..3d42226a0 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -697,6 +697,10 @@ trainings_authorization_validity_duration: "Trainings validity period duration" trainings_invalidation_rule: "Trainings automatic invalidation" trainings_invalidation_rule_period: "Grace period before invalidating a training" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of dates filter on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" #statuses of projects statuses: new: "New" diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 08b3bf79b..2aced98a9 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -697,6 +697,10 @@ pt: trainings_authorization_validity_duration: "Trainings validity period duration" trainings_invalidation_rule: "Trainings automatic invalidation" trainings_invalidation_rule_period: "Grace period before invalidating a training" + projects_list_member_filter_presence: "Presence of member filter on projects list" + projects_list_date_filters_presence: "Presence of dates filter on projects list" + project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" + project_categories_wording: "Wording used to replace \"Categories\" on public pages" #statuses of projects statuses: new: "Novo" diff --git a/config/locales/zu.yml b/config/locales/zu.yml index ae7cc63dd..1388b87ae 100644 --- a/config/locales/zu.yml +++ b/config/locales/zu.yml @@ -697,6 +697,10 @@ zu: trainings_authorization_validity_duration: "crwdns37105:0crwdne37105:0" trainings_invalidation_rule: "crwdns37107:0crwdne37107:0" trainings_invalidation_rule_period: "crwdns37109:0crwdne37109:0" + projects_list_member_filter_presence: "crwdns37655:0crwdne37655:0" + projects_list_date_filters_presence: "crwdns37657:0crwdne37657:0" + project_categories_filter_placeholder: "crwdns37659:0crwdne37659:0" + project_categories_wording: "crwdns37661:0crwdne37661:0" #statuses of projects statuses: new: "crwdns37111:0crwdne37111:0" From 734608f31c9a9d8f2992116275f99866e7abdc47 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 3 Jul 2023 18:40:29 +0200 Subject: [PATCH 20/63] Version 6.0.8 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b648577..ec5c8cf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog Fab-manager +## v6.0.8 2023 July 03 + - Improved projects list filter - Fix a bug: unable to refresh machine/space/training calender after pay an reservation - Fix a bug: Accouning Line in duplicate diff --git a/package.json b/package.json index 1805174ea..3d86e5e37 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "6.0.7", + "version": "6.0.8", "description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.", "keywords": [ "fablab", From 7d10132953b3556c6913939209f8260f0edcca33 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Fri, 31 Mar 2023 14:44:37 +0200 Subject: [PATCH 21/63] (feat) add Family account setting --- Gemfile.lock | 1 + app/frontend/src/javascript/models/setting.ts | 3 ++- app/frontend/templates/admin/settings/compte.html | 11 +++++++++++ app/helpers/settings_helper.rb | 1 + app/policies/setting_policy.rb | 2 +- config/locales/app.admin.en.yml | 3 +++ config/locales/app.admin.fr.yml | 3 +++ config/locales/en.yml | 1 + db/seeds/settings.rb | 1 + test/fixtures/history_values.yml | 10 ++++++++++ test/fixtures/settings.yml | 5 +++++ test/frontend/__fixtures__/settings.ts | 6 ++++++ 12 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cd14fbf34..6c6c012ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -528,6 +528,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-20 x86_64-darwin-21 x86_64-linux diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index ef4f535f1..e075ae1e3 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -178,7 +178,8 @@ export const accountSettings = [ 'external_id', 'user_change_group', 'user_validation_required', - 'user_validation_required_list' + 'user_validation_required_list', + 'family_account' ] as const; export const analyticsSettings = [ diff --git a/app/frontend/templates/admin/settings/compte.html b/app/frontend/templates/admin/settings/compte.html index 0ed115706..3935b9c98 100644 --- a/app/frontend/templates/admin/settings/compte.html +++ b/app/frontend/templates/admin/settings/compte.html @@ -51,6 +51,17 @@ +
    +

    {{ 'app.admin.settings.family_account' }}

    +

    +
    + +
    +

    {{ 'app.admin.settings.captcha' }}

    diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 0f268a7e9..8e60fe4e1 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -167,6 +167,7 @@ module SettingsHelper user_validation_required user_validation_required_list show_username_in_admin_list + family_account store_module store_withdrawal_instructions store_hidden diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 1a74868be..7c8001bef 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -47,7 +47,7 @@ class SettingPolicy < ApplicationPolicy machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label events_banner_cta_url projects_list_member_filter_presence projects_list_date_filters_presence - project_categories_filter_placeholder project_categories_wording] + project_categories_filter_placeholder project_categories_wording family_account] end ## diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index c91c20d25..dae87a978 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1783,6 +1783,9 @@ en: projects_list_date_filters_presence: "Presence of date filters on projects list" project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" project_categories_wording: "Wording used to replace \"Categories\" on public pages" + family_account: "family account" + family_account_info_html: "By activating this option, you offer your members the possibility to add their child(ren) to their own account. You can also request proof if you wish to validate them." + enable_family_account: "Enable the Family Account option" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 4bc83e3aa..d3c4941a1 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -1783,6 +1783,9 @@ fr: projects_list_date_filters_presence: "Presence of date filters on projects list" project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" project_categories_wording: "Wording used to replace \"Categories\" on public pages" + family_account: "Compte famille" + family_account_info_html: "En activant cette option, vous offrez à vos membres la possibilité d'ajouter sur leur propre compte leur(s) enfants. Vous pouvez aussi demander un justificatif si vous souhaitez les valider." + enable_family_account: "Activer l'option Compte Famille" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" diff --git a/config/locales/en.yml b/config/locales/en.yml index 248a18e50..cec01d3e2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -701,6 +701,7 @@ en: projects_list_date_filters_presence: "Presence of dates filter on projects list" project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" project_categories_wording: "Wording used to replace \"Categories\" on public pages" + family_account: "Family account" #statuses of projects statuses: new: "New" diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 7e455834e..92d932a82 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -733,3 +733,4 @@ Setting.set('projects_list_member_filter_presence', false) unless Setting.find_b Setting.set('projects_list_date_filters_presence', false) unless Setting.find_by(name: 'projects_list_date_filters_presence') Setting.set('project_categories_filter_placeholder', 'Toutes les catégories') unless Setting.find_by(name: 'project_categories_filter_placeholder').try(:value) Setting.set('project_categories_wording', 'Catégories') unless Setting.find_by(name: 'project_categories_wording').try(:value) +Setting.set('family_account', false) unless Setting.find_by(name: 'family_account').try(:value) diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index e8ac8f534..645e05f9a 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -853,6 +853,7 @@ history_value_100: invoicing_profile_id: 1 history_value_101: + id: 101 setting_id: 100 value: 'Toutes les catégories' created_at: 2023-04-05 09:16:08.000511500 Z @@ -860,6 +861,7 @@ history_value_101: invoicing_profile_id: 1 history_value_102: + id: 102 setting_id: 101 value: 'Catégories' created_at: 2023-04-05 09:16:08.000511500 Z @@ -880,4 +882,12 @@ history_value_104: value: 'false' created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + +history_value_105: + id: 105 + setting_id: 104 + value: 'false' + created_at: '2023-03-31 14:38:40.000421' + updated_at: '2023-03-31 14:38:40.000421' + footprint: invoicing_profile_id: 1 diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index e0ce32209..8e8326214 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -610,3 +610,8 @@ setting_103: name: projects_list_date_filters_presence created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + +setting_104: + name: family_account + created_at: 2023-03-31 14:38:40.000421500 Z + updated_at: 2023-03-31 14:38:40.000421500 Z diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 262b47f67..824e16533 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -849,6 +849,12 @@ export const settings: Array = [ value: 'Catégories', last_update: '2022-12-23T14:39:12+0100', localized: 'Project categories overridden name' + }, + { + name: 'family_account', + value: 'false', + last_update: '2023-03-31T14:39:12+0100', + localized: 'Family account' } ]; From 5365cbdaba9531bba01304803a498c0ecd1882e1 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 3 Apr 2023 18:23:49 +0200 Subject: [PATCH 22/63] (wip) add/edit user children --- app/controllers/api/children_controller.rb | 52 +++++++++++ app/frontend/src/javascript/api/child.ts | 31 +++++++ .../components/family-account/child-form.tsx | 53 ++++++++++++ .../components/family-account/child-item.tsx | 38 ++++++++ .../components/family-account/child-modal.tsx | 69 +++++++++++++++ .../family-account/children-list.tsx | 86 +++++++++++++++++++ .../src/javascript/controllers/children.js | 23 +++++ app/frontend/src/javascript/models/child.ts | 16 ++++ app/frontend/src/javascript/router.js | 16 +++- .../templates/dashboard/children.html | 11 +++ app/frontend/templates/dashboard/nav.html | 1 + app/models/child.rb | 14 +++ app/models/user.rb | 3 + app/policies/child_policy.rb | 31 +++++++ app/views/api/children/_child.json.jbuilder | 3 + app/views/api/children/create.json.jbuilder | 3 + app/views/api/children/index.json.jbuilder | 5 ++ app/views/api/children/update.json.jbuilder | 3 + config/locales/app.public.en.yml | 3 + config/locales/app.public.fr.yml | 16 ++++ config/routes.rb | 2 + db/migrate/20230331132506_create_children.rb | 17 ++++ db/schema.rb | 17 +++- 23 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 app/controllers/api/children_controller.rb create mode 100644 app/frontend/src/javascript/api/child.ts create mode 100644 app/frontend/src/javascript/components/family-account/child-form.tsx create mode 100644 app/frontend/src/javascript/components/family-account/child-item.tsx create mode 100644 app/frontend/src/javascript/components/family-account/child-modal.tsx create mode 100644 app/frontend/src/javascript/components/family-account/children-list.tsx create mode 100644 app/frontend/src/javascript/controllers/children.js create mode 100644 app/frontend/src/javascript/models/child.ts create mode 100644 app/frontend/templates/dashboard/children.html create mode 100644 app/models/child.rb create mode 100644 app/policies/child_policy.rb create mode 100644 app/views/api/children/_child.json.jbuilder create mode 100644 app/views/api/children/create.json.jbuilder create mode 100644 app/views/api/children/index.json.jbuilder create mode 100644 app/views/api/children/update.json.jbuilder create mode 100644 db/migrate/20230331132506_create_children.rb diff --git a/app/controllers/api/children_controller.rb b/app/controllers/api/children_controller.rb new file mode 100644 index 000000000..7c5a1003a --- /dev/null +++ b/app/controllers/api/children_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# API Controller for resources of type Child +# Children are used to provide a way to manage multiple users in the family account +class API::ChildrenController < API::ApiController + before_action :authenticate_user! + before_action :set_child, only: %i[show update destroy] + + def index + @children = policy_scope(Child) + end + + def show + authorize @child + end + + def create + @child = Child.new(child_params) + authorize @child + if @child.save + render status: :created + else + render json: @child.errors.full_messages, status: :unprocessable_entity + end + end + + def update + authorize @child + + if @child.update(child_params) + render status: :ok + else + render json: @child.errors.full_messages, status: :unprocessable_entity + end + end + + def destroy + authorize @child + @child.destroy + head :no_content + end + + private + + def set_child + @child = Child.find(params[:id]) + end + + def child_params + params.require(:child).permit(:first_name, :last_name, :email, :phone, :birthday, :user_id) + end +end diff --git a/app/frontend/src/javascript/api/child.ts b/app/frontend/src/javascript/api/child.ts new file mode 100644 index 000000000..1ff948219 --- /dev/null +++ b/app/frontend/src/javascript/api/child.ts @@ -0,0 +1,31 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; +import { Child, ChildIndexFilter } from '../models/child'; +import ApiLib from '../lib/api'; + +export default class ChildAPI { + static async index (filters: ChildIndexFilter): Promise> { + const res: AxiosResponse> = await apiClient.get(`/api/children${ApiLib.filtersToQuery(filters)}`); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/children/${id}`); + return res?.data; + } + + static async create (child: Child): Promise { + const res: AxiosResponse = await apiClient.post('/api/children', { child }); + return res?.data; + } + + static async update (child: Child): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/children/${child.id}`, { child }); + return res?.data; + } + + static async destroy (childId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/children/${childId}`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/components/family-account/child-form.tsx b/app/frontend/src/javascript/components/family-account/child-form.tsx new file mode 100644 index 000000000..fee281548 --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/child-form.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { Child } from '../../models/child'; +import { TDateISODate } from '../../typings/date-iso'; +import { FormInput } from '../form/form-input'; + +interface ChildFormProps { + child: Child; + onChange: (field: string, value: string | TDateISODate) => void; +} + +/** + * A form for creating or editing a child. + */ +export const ChildForm: React.FC = ({ child, onChange }) => { + const { t } = useTranslation('public'); + + const { register, formState } = useForm({ + defaultValues: child + }); + + /** + * Handle the change of a child form field + */ + const handleChange = (event: React.ChangeEvent): void => { + onChange(event.target.id, event.target.value); + }; + + return ( +
    +
    + {t('app.public.child_form.child_form_info')} +
    +
    + + + +
    + ); +}; diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx new file mode 100644 index 000000000..747d8212c --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Child } from '../../models/child'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; + +interface ChildItemProps { + child: Child; + onEdit: (child: Child) => void; + onDelete: (child: Child) => void; +} + +/** + * A child item. + */ +export const ChildItem: React.FC = ({ child, onEdit, onDelete }) => { + const { t } = useTranslation('public'); + + return ( +
    +
    +
    {t('app.public.child_item.last_name')}
    +
    {child.last_name}
    +
    +
    +
    {t('app.public.child_item.first_name')}
    +
    {child.first_name}
    +
    +
    +
    {t('app.public.child_item.birthday')}
    +
    {child.birthday}
    +
    +
    + } onClick={() => onEdit(child)} className="edit-button" /> + } onClick={() => onDelete(child)} className="delete-button" /> +
    +
    + ); +}; diff --git a/app/frontend/src/javascript/components/family-account/child-modal.tsx b/app/frontend/src/javascript/components/family-account/child-modal.tsx new file mode 100644 index 000000000..03f1486c4 --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/child-modal.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FabModal, ModalSize } from '../base/fab-modal'; +import { Child } from '../../models/child'; +import { TDateISODate } from '../../typings/date-iso'; +import ChildAPI from '../../api/child'; +import { ChildForm } from './child-form'; + +interface ChildModalProps { + child?: Child; + isOpen: boolean; + toggleModal: () => void; +} + +/** + * A modal for creating or editing a child. + */ +export const ChildModal: React.FC = ({ child, isOpen, toggleModal }) => { + const { t } = useTranslation('public'); + const [data, setData] = useState(child); + console.log(child, data); + + /** + * Save the child to the API + */ + const handleSaveChild = async (): Promise => { + try { + if (child?.id) { + await ChildAPI.update(data); + } else { + await ChildAPI.create(data); + } + toggleModal(); + } catch (error) { + console.error(error); + } + }; + + /** + * Check if the form is valid to save the child + */ + const isPreventedSaveChild = (): boolean => { + return !data?.first_name || !data?.last_name; + }; + + /** + * Handle the change of a child form field + */ + const handleChildChanged = (field: string, value: string | TDateISODate): void => { + setData({ + ...data, + [field]: value + }); + }; + + return ( + + + + ); +}; diff --git a/app/frontend/src/javascript/components/family-account/children-list.tsx b/app/frontend/src/javascript/components/family-account/children-list.tsx new file mode 100644 index 000000000..c737e2d98 --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/children-list.tsx @@ -0,0 +1,86 @@ +import React, { useState, useEffect } from 'react'; +import { react2angular } from 'react2angular'; +import { Child } from '../../models/child'; +// import { ChildListItem } from './child-list-item'; +import ChildAPI from '../../api/child'; +import { User } from '../../models/user'; +import { useTranslation } from 'react-i18next'; +import { Loader } from '../base/loader'; +import { IApplication } from '../../models/application'; +import { ChildModal } from './child-modal'; +import { ChildItem } from './child-item'; +import { FabButton } from '../base/fab-button'; + +declare const Application: IApplication; + +interface ChildrenListProps { + currentUser: User; +} + +/** + * A list of children belonging to the current user. + */ +export const ChildrenList: React.FC = ({ currentUser }) => { + const { t } = useTranslation('public'); + + const [children, setChildren] = useState>([]); + const [isOpenChildModal, setIsOpenChildModal] = useState(false); + const [child, setChild] = useState(); + + useEffect(() => { + ChildAPI.index({ user_id: currentUser.id }).then(setChildren); + }, [currentUser]); + + /** + * Open the add child modal + */ + const addChild = () => { + setIsOpenChildModal(true); + setChild({ user_id: currentUser.id } as Child); + }; + + /** + * Open the edit child modal + */ + const editChild = (child: Child) => { + setIsOpenChildModal(true); + setChild(child); + }; + + /** + * Delete a child + */ + const deleteChild = (child: Child) => { + ChildAPI.destroy(child.id).then(() => { + setChildren(children.filter(c => c.id !== child.id)); + }); + }; + + return ( +
    +
    +

    {t('app.public.children_list.heading')}

    + + {t('app.public.children_list.add_child')} + +
    + +
    + {children.map(child => ( + + ))} +
    + setIsOpenChildModal(false)} /> +
    + ); +}; + +const ChildrenListWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser'])); diff --git a/app/frontend/src/javascript/controllers/children.js b/app/frontend/src/javascript/controllers/children.js new file mode 100644 index 000000000..1fc180294 --- /dev/null +++ b/app/frontend/src/javascript/controllers/children.js @@ -0,0 +1,23 @@ +'use strict'; + +Application.Controllers.controller('ChildrenController', ['$scope', 'memberPromise', 'growl', + function ($scope, memberPromise, growl) { + // Current user's profile + $scope.user = memberPromise; + + /** + * Callback used to display a error message + */ + $scope.onError = function (message) { + console.error(message); + growl.error(message); + }; + + /** + * Callback used to display a success message + */ + $scope.onSuccess = function (message) { + growl.success(message); + }; + } +]); diff --git a/app/frontend/src/javascript/models/child.ts b/app/frontend/src/javascript/models/child.ts new file mode 100644 index 000000000..cea24bf1d --- /dev/null +++ b/app/frontend/src/javascript/models/child.ts @@ -0,0 +1,16 @@ +import { TDateISODate } from '../typings/date-iso'; +import { ApiFilter } from './api'; + +export interface ChildIndexFilter extends ApiFilter { + user_id: number, +} + +export interface Child { + id?: number, + last_name: string, + first_name: string, + email?: string, + phone?: string, + birthday: TDateISODate, + user_id: number +} diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 5adfd6e40..8c8db892d 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -28,9 +28,9 @@ angular.module('application.router', ['ui.router']) logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }], sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }], modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['machines_module', 'spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module', 'public_agenda_module', 'store_module']" }).$promise; }], - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations', 'store_hidden']" }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations', 'store_hidden', 'family_account']" }).$promise; }] }, - onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) { + onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'settingsPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, settingsPromise, CSRF) { // Retrieve Anti-CSRF tokens from cookies CSRF.setMetaTags(); // Application logo @@ -47,6 +47,9 @@ angular.module('application.router', ['ui.router']) publicAgenda: (modulesPromise.public_agenda_module === 'true'), statistics: (modulesPromise.statistics_module === 'true') }; + $rootScope.settings = { + familyAccount: (settingsPromise.family_account === 'true') + }; }] }) .state('app.public', { @@ -151,6 +154,15 @@ angular.module('application.router', ['ui.router']) } } }) + .state('app.logged.dashboard.children', { + url: '/children', + views: { + 'main@': { + templateUrl: '/dashboard/children.html', + controller: 'ChildrenController' + } + } + }) .state('app.logged.dashboard.settings', { url: '/settings', views: { diff --git a/app/frontend/templates/dashboard/children.html b/app/frontend/templates/dashboard/children.html new file mode 100644 index 000000000..8f5c2c17d --- /dev/null +++ b/app/frontend/templates/dashboard/children.html @@ -0,0 +1,11 @@ +
    +
    +
    + +
    + +
    + + + +
    diff --git a/app/frontend/templates/dashboard/nav.html b/app/frontend/templates/dashboard/nav.html index 8da97d01a..f0dc4bd10 100644 --- a/app/frontend/templates/dashboard/nav.html +++ b/app/frontend/templates/dashboard/nav.html @@ -11,6 +11,7 @@

    {{ 'app.public.common.dashboard' }}

    ); diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx index 747d8212c..44ca25aea 100644 --- a/app/frontend/src/javascript/components/family-account/child-item.tsx +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Child } from '../../models/child'; import { useTranslation } from 'react-i18next'; import { FabButton } from '../base/fab-button'; +import FormatLib from '../../lib/format'; interface ChildItemProps { child: Child; @@ -27,7 +28,7 @@ export const ChildItem: React.FC = ({ child, onEdit, onDelete })
    {t('app.public.child_item.birthday')}
    -
    {child.birthday}
    +
    {FormatLib.date(child.birthday)}
    } onClick={() => onEdit(child)} className="edit-button" /> diff --git a/app/frontend/src/javascript/components/family-account/child-modal.tsx b/app/frontend/src/javascript/components/family-account/child-modal.tsx index 03f1486c4..baa9af408 100644 --- a/app/frontend/src/javascript/components/family-account/child-modal.tsx +++ b/app/frontend/src/javascript/components/family-account/child-modal.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FabModal, ModalSize } from '../base/fab-modal'; import { Child } from '../../models/child'; @@ -11,15 +11,20 @@ interface ChildModalProps { child?: Child; isOpen: boolean; toggleModal: () => void; + onSuccess: (child: Child) => void; + onError: (error: string) => void; } /** * A modal for creating or editing a child. */ -export const ChildModal: React.FC = ({ child, isOpen, toggleModal }) => { +export const ChildModal: React.FC = ({ child, isOpen, toggleModal, onSuccess, onError }) => { const { t } = useTranslation('public'); const [data, setData] = useState(child); - console.log(child, data); + + useEffect(() => { + setData(child); + }, [child]); /** * Save the child to the API @@ -32,18 +37,12 @@ export const ChildModal: React.FC = ({ child, isOpen, toggleMod await ChildAPI.create(data); } toggleModal(); + onSuccess(data); } catch (error) { - console.error(error); + onError(error); } }; - /** - * Check if the form is valid to save the child - */ - const isPreventedSaveChild = (): boolean => { - return !data?.first_name || !data?.last_name; - }; - /** * Handle the change of a child form field */ @@ -60,10 +59,9 @@ export const ChildModal: React.FC = ({ child, isOpen, toggleMod isOpen={isOpen} toggleModal={toggleModal} closeButton={true} - confirmButton={t('app.public.child_modal.save')} - onConfirm={handleSaveChild} - preventConfirm={isPreventedSaveChild()}> - + confirmButton={false} + onConfirm={handleSaveChild} > + ); }; diff --git a/app/frontend/src/javascript/components/family-account/children-list.tsx b/app/frontend/src/javascript/components/family-account/children-list.tsx index c737e2d98..287f69802 100644 --- a/app/frontend/src/javascript/components/family-account/children-list.tsx +++ b/app/frontend/src/javascript/components/family-account/children-list.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; import { react2angular } from 'react2angular'; import { Child } from '../../models/child'; -// import { ChildListItem } from './child-list-item'; import ChildAPI from '../../api/child'; import { User } from '../../models/user'; import { useTranslation } from 'react-i18next'; @@ -15,12 +14,14 @@ declare const Application: IApplication; interface ChildrenListProps { currentUser: User; + onSuccess: (error: string) => void; + onError: (error: string) => void; } /** * A list of children belonging to the current user. */ -export const ChildrenList: React.FC = ({ currentUser }) => { +export const ChildrenList: React.FC = ({ currentUser, onError }) => { const { t } = useTranslation('public'); const [children, setChildren] = useState>([]); @@ -52,10 +53,17 @@ export const ChildrenList: React.FC = ({ currentUser }) => { */ const deleteChild = (child: Child) => { ChildAPI.destroy(child.id).then(() => { - setChildren(children.filter(c => c.id !== child.id)); + ChildAPI.index({ user_id: currentUser.id }).then(setChildren); }); }; + /** + * Handle save child success from the API + */ + const handleSaveChildSuccess = () => { + ChildAPI.index({ user_id: currentUser.id }).then(setChildren); + }; + return (
    @@ -70,7 +78,7 @@ export const ChildrenList: React.FC = ({ currentUser }) => { ))}
    - setIsOpenChildModal(false)} /> + setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} />
    ); }; @@ -83,4 +91,4 @@ const ChildrenListWrapper: React.FC = (props) => { ); }; -Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser'])); +Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser', 'onSuccess', 'onError'])); diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index d4a4331e9..b19d42ccc 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -22,13 +22,14 @@ type FormInputProps = FormComponent & Ab onChange?: (event: React.ChangeEvent) => void, nullable?: boolean, ariaLabel?: string, - maxLength?: number + maxLength?: number, + max?: number | string, } /** * This component is a template for an input component to use within React Hook Form */ -export const FormInput = ({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength }: FormInputProps) => { +export const FormInput = ({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength, max }: FormInputProps) => { const [characterCount, setCharacterCount] = useState(0); /** @@ -100,7 +101,8 @@ export const FormInput = ({ id, re disabled={typeof disabled === 'function' ? disabled(id) : disabled} placeholder={placeholder} accept={accept} - maxLength={maxLength} /> + maxLength={maxLength} + max={max} /> {(type === 'file' && placeholder) && {placeholder}} {maxLength && {characterCount} / {maxLength}} {addOn && addOnAction && } diff --git a/app/models/child.rb b/app/models/child.rb index 49ebdaf9e..93332bde8 100644 --- a/app/models/child.rb +++ b/app/models/child.rb @@ -6,9 +6,11 @@ class Child < ApplicationRecord validates :first_name, presence: true validates :last_name, presence: true + validates :email, presence: true, format: { with: Devise.email_regexp } validate :validate_age + # birthday should less than 18 years ago def validate_age - errors.add(:birthday, 'You should be over 18 years old.') if birthday.blank? && birthday < 18.years.ago + errors.add(:birthday, I18n.t('.errors.messages.birthday_less_than_18_years_ago')) if birthday.blank? || birthday > 18.years.ago end end diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 1ec5f80f6..dee4e8ced 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -484,6 +484,22 @@ en: start_typing: "Start typing..." children_list: heading: "My children" + add_child: "Add a child" + child_modal: + edit_child: "Edit child" + new_child: "New child" + child_form: + child_form_info: "Note that you can only add your children under 18 years old. Supporting documents are requested by your administrator, they will be useful to validate your child's account and authorize the reservation of events." + first_name: "First name" + last_name: "Last name" + birthday: "Birthday" + email: "Email" + phone: "Phone" + save: "Save" + child_item: + first_name: "First name of the child" + last_name: "Last name of the child" + birthday: "Birthday" tour: conclusion: title: "Thank you for your attention" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index f430d82b1..781ee06d2 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -488,11 +488,14 @@ fr: child_modal: edit_child: "Modifier un enfant" new_child: "Ajouter un enfant" - save: "Enregistrer" child_form: child_form_info: "Notez que vous ne pouvez ajouter que vos enfants de moins de 18 ans. Des pièces justificatives sont demandés par votre administrateur, elles lui seront utiles pour valider le compte de votre enfant et ainsi autoriser la réservation d'événements." first_name: "Prénom" last_name: "Nom" + birthday: "Date de naissance" + email: "Courriel" + phone: "Téléphone" + save: "Enregistrer" child_item: first_name: "Prénom de l'enfant" last_name: "Nom de l'enfant" diff --git a/config/locales/en.yml b/config/locales/en.yml index cec01d3e2..6cdd8c50f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -49,6 +49,7 @@ en: gateway_amount_too_large: "Payments above %{AMOUNT} are not supported. Please order directly at the reception." product_in_use: "This product have already been ordered" slug_already_used: "is already used" + birthday_less_than_18_years_ago: "Birthday must be under 18 years ago" coupon: code_format_error: "only caps letters, numbers, and dashes are allowed" apipie: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6d8a4588f..17b01ef07 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -49,6 +49,7 @@ fr: gateway_amount_too_large: "Les paiements supérieurs à %{AMOUNT} ne sont pas pris en charge. Merci de passer commande directement à l'accueil." product_in_use: "Ce produit a déjà été commandé" slug_already_used: "est déjà utilisée" + birthday_less_than_18_years_ago: "l'age devez avoir au moins 18 ans." coupon: code_format_error: "seules les majuscules, chiffres et tirets sont autorisés" apipie: From 9373f507060fb603734f45fe0178f3ab7a6726ca Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 9 May 2023 18:54:16 +0200 Subject: [PATCH 24/63] (wip) event reservation naminative --- Gemfile.lock | 1 - app/controllers/api/events_controller.rb | 2 +- .../components/events/event-form.tsx | 5 + .../src/javascript/controllers/events.js.erb | 51 ++++- app/frontend/src/javascript/models/event.ts | 3 +- app/frontend/templates/events/show.html | 16 +- app/models/booking_user.rb | 9 + app/models/reservation.rb | 3 + app/views/api/events/_event.json.jbuilder | 2 +- config/locales/app.admin.en.yml | 2 + config/locales/app.admin.fr.yml | 2 + ...9121907_add_booking_nominative_to_event.rb | 8 + .../20230509161557_create_booking_users.rb | 15 ++ db/structure.sql | 208 +++++++++++++++--- .../components/events/event-form.test.tsx | 1 + 15 files changed, 287 insertions(+), 41 deletions(-) create mode 100644 app/models/booking_user.rb create mode 100644 db/migrate/20230509121907_add_booking_nominative_to_event.rb create mode 100644 db/migrate/20230509161557_create_booking_users.rb diff --git a/Gemfile.lock b/Gemfile.lock index 6c6c012ae..1d1f8f54d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -529,7 +529,6 @@ GEM PLATFORMS x86_64-darwin-20 - x86_64-darwin-21 x86_64-linux DEPENDENCIES diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index ce4b42bd9..1b8abdc77 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -96,7 +96,7 @@ class API::EventsController < API::APIController # handle general properties event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time, :amount, :nb_total_places, :availability_id, :all_day, :recurrence, - :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, + :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :booking_nominative, event_theme_ids: [], event_image_attributes: %i[id attachment], event_files_attributes: %i[id attachment _destroy], diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index e8a89c29f..033dff894 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -290,6 +290,11 @@ export const EventForm: React.FC = ({ action, event, onError, on label={t('app.admin.event_form.seats_available')} type="number" tooltip={t('app.admin.event_form.seats_help')} /> + u.booked_id === $scope.ctrl.member.id && u.booked_type === 'User')) { + return true; + } + } + return false; + }; + /** * Callback to call when the number of tickets to book changes in the current booking */ - $scope.changeNbPlaces = function () { + $scope.changeNbPlaces = function (priceType) { // compute the total remaining places let remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces; for (let ticket in $scope.reserve.tickets) { @@ -247,6 +260,22 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } } + const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length; + const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; + if (nbReservePlaces > nbBookingUsers) { + _.times(nbReservePlaces - nbBookingUsers, () => { + if (!hasMemberInBookingUsers()) { + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, booked_id: $scope.ctrl.member.id, booked_type: 'User', name: $scope.ctrl.member.name }); + } else { + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); + } + }); + } else { + _.times(nbBookingUsers - nbReservePlaces, () => { + $scope.reserve.bookingUsers[priceType].pop(); + }); + } + // recompute the total price return $scope.computeEventAmount(); }; @@ -638,7 +667,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' reservable_type: 'Event', slots_reservations_attributes: [], nb_reserve_places: reserve.nbReservePlaces, - tickets_attributes: [] + tickets_attributes: [], + booking_users_attributes: [] }; reservation.slots_reservations_attributes.push({ @@ -656,6 +686,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } } + if (event.booking_nominative) { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + reservation.booking_users_attributes.push(user); + } + } + console.log(reservation); + } + return { reservation }; }; @@ -688,11 +727,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' tickets: {}, toReserve: false, amountTotal: 0, - totalSeats: 0 + totalSeats: 0, + bookingUsers: { + normal: [], + }, }; for (let evt_px_cat of Array.from($scope.event.event_price_categories_attributes)) { $scope.reserve.nbPlaces[evt_px_cat.id] = __range__(0, $scope.event.nb_free_places, true); + $scope.reserve.bookingUsers[evt_px_cat.id] = []; $scope.reserve.tickets[evt_px_cat.id] = 0; } diff --git a/app/frontend/src/javascript/models/event.ts b/app/frontend/src/javascript/models/event.ts index 5cca1d9e8..52bdc1496 100644 --- a/app/frontend/src/javascript/models/event.ts +++ b/app/frontend/src/javascript/models/event.ts @@ -63,7 +63,8 @@ export interface Event { }>, recurrence: RecurrenceOption, recurrence_end_at: Date, - advanced_accounting_attributes?: AdvancedAccounting + advanced_accounting_attributes?: AdvancedAccounting, + booking_nominative: boolean, } export interface EventDecoration { diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index ec362a3d5..c8d86cd02 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -116,16 +116,28 @@
    - {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
    +
    +
    + + +
    +
    - {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
    +
    +
    + + +
    +
    diff --git a/app/models/booking_user.rb b/app/models/booking_user.rb new file mode 100644 index 000000000..78c6fe6b5 --- /dev/null +++ b/app/models/booking_user.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# BookingUser is a class for save the booking info of reservation +# booked can be a User or a Child (polymorphic) +class BookingUser < ApplicationRecord + belongs_to :reservation + belongs_to :booked, polymorphic: true + belongs_to :event_price_category +end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index d0472354b..9b9d0ff4f 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -23,6 +23,9 @@ class Reservation < ApplicationRecord has_many :prepaid_pack_reservations, dependent: :destroy + has_many :booking_users, dependent: :destroy + accepts_nested_attributes_for :booking_users, allow_destroy: true + validates :reservable_id, :reservable_type, presence: true validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) } validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) } diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder index 09a19e0c0..11be0748e 100644 --- a/app/views/api/events/_event.json.jbuilder +++ b/app/views/api/events/_event.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true -json.extract! event, :id, :title, :description +json.extract! event, :id, :title, :description, :booking_nominative if event.event_image json.event_image_attributes do json.id event.event_image.id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index dae87a978..74d26c121 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -139,6 +139,8 @@ en: event_themes: "Event themes" age_range: "Age range" add_price: "Add a price" + booking_nominative: "Nominative booking" + booking_nominative_help: "If you check this option, the members will have to enter the names of the participants when booking." save: "Save" create_success: "The event was created successfully" events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index d3c4941a1..34bbaed54 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -139,6 +139,8 @@ fr: event_themes: "Thèmes de l'événement" age_range: "Tranche d'âge" add_price: "Ajouter un tarif" + booking_nominative: "Réservation nominative" + booking_nominative_help: "Si cette option est activée, les réservations seront nominatives. Les participants devront s'identifier pour réserver." save: "Enregistrer" create_success: "L'événement a bien été créé" events_updated: "{COUNT, plural, one {}=1{Un événement à été} other{{COUNT} événements ont été}} mis à jour avec succès" diff --git a/db/migrate/20230509121907_add_booking_nominative_to_event.rb b/db/migrate/20230509121907_add_booking_nominative_to_event.rb new file mode 100644 index 000000000..1a709170d --- /dev/null +++ b/db/migrate/20230509121907_add_booking_nominative_to_event.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# add booking_nominative to event +class AddBookingNominativeToEvent < ActiveRecord::Migration[7.0] + def change + add_column :events, :booking_nominative, :boolean, default: false + end +end diff --git a/db/migrate/20230509161557_create_booking_users.rb b/db/migrate/20230509161557_create_booking_users.rb new file mode 100644 index 000000000..f1ba65728 --- /dev/null +++ b/db/migrate/20230509161557_create_booking_users.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# create booking_users table +class CreateBookingUsers < ActiveRecord::Migration[7.0] + def change + create_table :booking_users do |t| + t.string :name + t.belongs_to :reservation, foreign_key: true + t.references :booked, polymorphic: true + t.references :event_price_category, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index a9aa1f96d..54505685e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -108,8 +108,8 @@ SET default_tablespace = ''; CREATE TABLE public.abuses ( id integer NOT NULL, - signaled_id integer, signaled_type character varying, + signaled_id integer, first_name character varying, last_name character varying, email character varying, @@ -229,8 +229,8 @@ CREATE TABLE public.addresses ( locality character varying, country character varying, postal_code character varying, - placeable_id integer, placeable_type character varying, + placeable_id integer, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -339,8 +339,8 @@ CREATE TABLE public.ar_internal_metadata ( CREATE TABLE public.assets ( id integer NOT NULL, - viewable_id integer, viewable_type character varying, + viewable_id integer, attachment character varying, type character varying, created_at timestamp without time zone, @@ -513,6 +513,41 @@ CREATE SEQUENCE public.availability_tags_id_seq ALTER SEQUENCE public.availability_tags_id_seq OWNED BY public.availability_tags.id; +-- +-- Name: booking_users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.booking_users ( + id bigint NOT NULL, + name character varying, + reservation_id bigint, + booked_type character varying, + booked_id bigint, + event_price_category_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: booking_users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.booking_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: booking_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.booking_users_id_seq OWNED BY public.booking_users.id; + + -- -- Name: cart_item_coupons; Type: TABLE; Schema: public; Owner: - -- @@ -885,6 +920,42 @@ CREATE SEQUENCE public.chained_elements_id_seq ALTER SEQUENCE public.chained_elements_id_seq OWNED BY public.chained_elements.id; +-- +-- Name: children; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.children ( + id bigint NOT NULL, + user_id bigint, + first_name character varying, + last_name character varying, + birthday date, + phone character varying, + email character varying, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: children_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.children_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: children_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.children_id_seq OWNED BY public.children.id; + + -- -- Name: components; Type: TABLE; Schema: public; Owner: - -- @@ -958,8 +1029,8 @@ ALTER SEQUENCE public.coupons_id_seq OWNED BY public.coupons.id; CREATE TABLE public.credits ( id integer NOT NULL, - creditable_id integer, creditable_type character varying, + creditable_id integer, plan_id integer, hours integer, created_at timestamp without time zone, @@ -1129,7 +1200,8 @@ CREATE TABLE public.events ( recurrence_id integer, age_range_id integer, category_id integer, - deleted_at timestamp without time zone + deleted_at timestamp without time zone, + booking_nominative boolean DEFAULT false ); @@ -1755,15 +1827,15 @@ ALTER SEQUENCE public.notification_types_id_seq OWNED BY public.notification_typ CREATE TABLE public.notifications ( id integer NOT NULL, receiver_id integer, - attached_object_id integer, attached_object_type character varying, + attached_object_id integer, notification_type_id integer, is_read boolean DEFAULT false, created_at timestamp without time zone, updated_at timestamp without time zone, receiver_type character varying, is_send boolean DEFAULT false, - meta_data jsonb DEFAULT '{}'::jsonb + meta_data jsonb DEFAULT '"{}"'::jsonb ); @@ -2491,8 +2563,8 @@ CREATE TABLE public.prices ( id integer NOT NULL, group_id integer, plan_id integer, - priceable_id integer, priceable_type character varying, + priceable_id integer, amount integer, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, @@ -3018,8 +3090,8 @@ CREATE TABLE public.reservations ( message text, created_at timestamp without time zone, updated_at timestamp without time zone, - reservable_id integer, reservable_type character varying, + reservable_id integer, nb_reserve_places integer, statistic_profile_id integer ); @@ -3051,8 +3123,8 @@ ALTER SEQUENCE public.reservations_id_seq OWNED BY public.reservations.id; CREATE TABLE public.roles ( id integer NOT NULL, name character varying, - resource_id integer, resource_type character varying, + resource_id integer, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -4158,8 +4230,8 @@ CREATE TABLE public.users ( is_allow_newsletter boolean, current_sign_in_ip inet, last_sign_in_ip inet, - mapped_from_sso character varying, - validated_at timestamp without time zone + validated_at timestamp without time zone, + mapped_from_sso character varying ); @@ -4368,6 +4440,13 @@ ALTER TABLE ONLY public.availabilities ALTER COLUMN id SET DEFAULT nextval('publ ALTER TABLE ONLY public.availability_tags ALTER COLUMN id SET DEFAULT nextval('public.availability_tags_id_seq'::regclass); +-- +-- Name: booking_users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users ALTER COLUMN id SET DEFAULT nextval('public.booking_users_id_seq'::regclass); + + -- -- Name: cart_item_coupons id; Type: DEFAULT; Schema: public; Owner: - -- @@ -4445,6 +4524,13 @@ ALTER TABLE ONLY public.categories ALTER COLUMN id SET DEFAULT nextval('public.c ALTER TABLE ONLY public.chained_elements ALTER COLUMN id SET DEFAULT nextval('public.chained_elements_id_seq'::regclass); +-- +-- Name: children id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.children ALTER COLUMN id SET DEFAULT nextval('public.children_id_seq'::regclass); + + -- -- Name: components id; Type: DEFAULT; Schema: public; Owner: - -- @@ -5220,6 +5306,14 @@ ALTER TABLE ONLY public.availability_tags ADD CONSTRAINT availability_tags_pkey PRIMARY KEY (id); +-- +-- Name: booking_users booking_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users + ADD CONSTRAINT booking_users_pkey PRIMARY KEY (id); + + -- -- Name: cart_item_coupons cart_item_coupons_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -5308,6 +5402,14 @@ ALTER TABLE ONLY public.chained_elements ADD CONSTRAINT chained_elements_pkey PRIMARY KEY (id); +-- +-- Name: children children_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.children + ADD CONSTRAINT children_pkey PRIMARY KEY (id); + + -- -- Name: components components_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -5804,6 +5906,14 @@ ALTER TABLE ONLY public.roles ADD CONSTRAINT roles_pkey PRIMARY KEY (id); +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + -- -- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -6154,6 +6264,27 @@ CREATE INDEX index_availability_tags_on_availability_id ON public.availability_t CREATE INDEX index_availability_tags_on_tag_id ON public.availability_tags USING btree (tag_id); +-- +-- Name: index_booking_users_on_booked; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_booking_users_on_booked ON public.booking_users USING btree (booked_type, booked_id); + + +-- +-- Name: index_booking_users_on_event_price_category_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_booking_users_on_event_price_category_id ON public.booking_users USING btree (event_price_category_id); + + +-- +-- Name: index_booking_users_on_reservation_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_booking_users_on_reservation_id ON public.booking_users USING btree (reservation_id); + + -- -- Name: index_cart_item_coupons_on_coupon_id; Type: INDEX; Schema: public; Owner: - -- @@ -6336,6 +6467,13 @@ CREATE UNIQUE INDEX index_categories_on_slug ON public.categories USING btree (s CREATE INDEX index_chained_elements_on_element ON public.chained_elements USING btree (element_type, element_id); +-- +-- Name: index_children_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_children_on_user_id ON public.children USING btree (user_id); + + -- -- Name: index_coupons_on_code; Type: INDEX; Schema: public; Owner: - -- @@ -7484,21 +7622,6 @@ CREATE INDEX proof_of_identity_type_id_and_proof_of_identity_refusal_id ON publi CREATE UNIQUE INDEX unique_not_null_external_id ON public.invoicing_profiles USING btree (external_id) WHERE (external_id IS NOT NULL); --- --- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version); - - --- --- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: - --- - -CREATE RULE accounting_periods_del_protect AS - ON DELETE TO public.accounting_periods DO INSTEAD NOTHING; - - -- -- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: - -- @@ -7732,6 +7855,14 @@ ALTER TABLE ONLY public.subscriptions ADD CONSTRAINT fk_rails_358a71f738 FOREIGN KEY (statistic_profile_id) REFERENCES public.statistic_profiles(id); +-- +-- Name: booking_users fk_rails_38ad1ae7e8; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users + ADD CONSTRAINT fk_rails_38ad1ae7e8 FOREIGN KEY (reservation_id) REFERENCES public.reservations(id); + + -- -- Name: invoices fk_rails_40d78f8cf6; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8132,6 +8263,14 @@ ALTER TABLE ONLY public.cart_item_coupons ADD CONSTRAINT fk_rails_a44bac1e45 FOREIGN KEY (operator_profile_id) REFERENCES public.invoicing_profiles(id); +-- +-- Name: children fk_rails_a51d7cfb22; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.children + ADD CONSTRAINT fk_rails_a51d7cfb22 FOREIGN KEY (user_id) REFERENCES public.users(id); + + -- -- Name: projects_themes fk_rails_b021a22658; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8380,6 +8519,14 @@ ALTER TABLE ONLY public.projects ADD CONSTRAINT fk_rails_e812590204 FOREIGN KEY (author_statistic_profile_id) REFERENCES public.statistic_profiles(id); +-- +-- Name: booking_users fk_rails_e88263229e; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users + ADD CONSTRAINT fk_rails_e88263229e FOREIGN KEY (event_price_category_id) REFERENCES public.event_price_categories(id); + + -- -- Name: user_tags fk_rails_ea0382482a; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8491,7 +8638,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20140605125131'), ('20140605142133'), ('20140605151442'), -('20140606133116'), ('20140609092700'), ('20140609092827'), ('20140610153123'), @@ -8560,14 +8706,12 @@ INSERT INTO "schema_migrations" (version) VALUES ('20150507075620'), ('20150512123546'), ('20150520132030'), -('20150520133409'), ('20150526130729'), ('20150527153312'), ('20150529113555'), ('20150601125944'), ('20150603104502'), ('20150603104658'), -('20150603133050'), ('20150604081757'), ('20150604131525'), ('20150608142234'), @@ -8649,7 +8793,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20160905142700'), ('20160906094739'), ('20160906094847'), -('20160906145713'), ('20160915105234'), ('20161123104604'), ('20170109085345'), @@ -8817,6 +8960,9 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230328094807'), ('20230328094808'), ('20230328094809'), +('20230331132506'), +('20230509121907'), +('20230509161557'); ('20230626122844'), ('20230626122947'); diff --git a/test/frontend/components/events/event-form.test.tsx b/test/frontend/components/events/event-form.test.tsx index 7b3d53b6c..49f50ce79 100644 --- a/test/frontend/components/events/event-form.test.tsx +++ b/test/frontend/components/events/event-form.test.tsx @@ -27,6 +27,7 @@ describe('EventForm', () => { expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.seats_available/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.standard_rate/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.event_form.booking_nominative/)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_price/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_a_new_file/ })).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.advanced_accounting_form.code/)).toBeInTheDocument(); From 177748169acec970a185a4c29ee32eaf9b644c5a Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 10 May 2023 18:47:13 +0200 Subject: [PATCH 25/63] (wip) save booking user for event nominatif --- .../src/javascript/controllers/events.js.erb | 11 ++- app/frontend/templates/events/show.html | 10 +-- app/models/cart_item.rb | 5 ++ app/models/cart_item/event_reservation.rb | 13 +++ .../event_reservation_booking_user.rb | 10 +++ app/services/cart_service.rb | 3 +- config/locales/app.public.en.yml | 1 + config/locales/app.public.fr.yml | 1 + ...rt_item_event_reservation_booking_users.rb | 15 ++++ db/structure.sql | 88 +++++++++++++++++++ 10 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 app/models/cart_item.rb create mode 100644 app/models/cart_item/event_reservation_booking_user.rb create mode 100644 db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 63e2bd25d..335f97580 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -264,11 +264,14 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; if (nbReservePlaces > nbBookingUsers) { _.times(nbReservePlaces - nbBookingUsers, () => { + /* if (!hasMemberInBookingUsers()) { $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, booked_id: $scope.ctrl.member.id, booked_type: 'User', name: $scope.ctrl.member.name }); } else { $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); } + */ + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); }); } else { _.times(nbBookingUsers - nbReservePlaces, () => { @@ -689,10 +692,14 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' if (event.booking_nominative) { for (const key of Object.keys($scope.reserve.bookingUsers)) { for (const user of $scope.reserve.bookingUsers[key]) { - reservation.booking_users_attributes.push(user); + reservation.booking_users_attributes.push({ + event_price_category_id: user.event_price_category_id, + name: user.name, + booked_id: user.booked_id, + booked_type: user.booked_type + }); } } - console.log(reservation); } return { reservation }; diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index c8d86cd02..a75de228b 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -119,9 +119,9 @@ {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
    -
    +
    - +
    @@ -132,9 +132,9 @@ {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
    -
    +
    - +
    @@ -169,7 +169,7 @@
    {{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}
    {{ 'app.public.events_show.full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:reservation.nb_reserve_places} }}
    -
    +
    {{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:ticket.booked} }}
    diff --git a/app/models/cart_item.rb b/app/models/cart_item.rb new file mode 100644 index 000000000..4397e866e --- /dev/null +++ b/app/models/cart_item.rb @@ -0,0 +1,5 @@ +module CartItem + def self.table_name_prefix + "cart_item_" + end +end diff --git a/app/models/cart_item/event_reservation.rb b/app/models/cart_item/event_reservation.rb index 9ae7be11a..cbb90a934 100644 --- a/app/models/cart_item/event_reservation.rb +++ b/app/models/cart_item/event_reservation.rb @@ -13,6 +13,11 @@ class CartItem::EventReservation < CartItem::Reservation foreign_type: 'cart_item_type', as: :cart_item accepts_nested_attributes_for :cart_item_reservation_slots + has_many :cart_item_event_reservation_booking_users, class_name: 'CartItem::EventReservationBookingUser', dependent: :destroy, + inverse_of: :cart_item_event_reservation, + foreign_key: 'cart_item_event_reservation_id' + accepts_nested_attributes_for :cart_item_event_reservation_booking_users + belongs_to :operator_profile, class_name: 'InvoicingProfile' belongs_to :customer_profile, class_name: 'InvoicingProfile' @@ -63,6 +68,14 @@ class CartItem::EventReservation < CartItem::Reservation booked: t.booked } end, + booking_users_attributes: cart_item_event_reservation_booking_users.map do |b| + { + event_price_category_id: b.event_price_category_id, + booked_type: b.booked_type, + booked_id: b.booked_id, + name: b.name + } + end, nb_reserve_places: normal_tickets, statistic_profile_id: StatisticProfile.find_by(user: customer).id ) diff --git a/app/models/cart_item/event_reservation_booking_user.rb b/app/models/cart_item/event_reservation_booking_user.rb new file mode 100644 index 000000000..82df9ac9c --- /dev/null +++ b/app/models/cart_item/event_reservation_booking_user.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# A relation table between a pending event reservation and reservation users for this event +class CartItem::EventReservationBookingUser < ApplicationRecord + self.table_name = 'cart_item_event_reservation_booking_users' + + belongs_to :cart_item_event_reservation, class_name: 'CartItem::EventReservation', inverse_of: :cart_item_event_reservation_booking_users + belongs_to :event_price_category, inverse_of: :cart_item_event_reservation_tickets + belongs_to :booked, polymorphic: true +end diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index 6c6588e4b..52210c40d 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -171,7 +171,8 @@ class CartService event: reservable, cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes], normal_tickets: cart_item[:nb_reserve_places], - cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {}) + cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {}, + cart_item_event_reservation_booking_users_attributes: cart_item[:booking_users_attributes] || {}) when Space CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile, operator_profile: @operator.invoicing_profile, diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index dee4e8ced..6d7f2b66d 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -363,6 +363,7 @@ en: view_event_list: "View events to come" share_on_facebook: "Share on Facebook" share_on_twitter: "Share on Twitter" + last_name_and_first_name: "Last name and first name" #public calendar calendar: calendar: "Calendar" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 781ee06d2..f7ddb0f08 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -363,6 +363,7 @@ fr: view_event_list: "Voir les événements à venir" share_on_facebook: "Partager sur Facebook" share_on_twitter: "Partager sur Twitter" + last_name_and_first_name: "Nom et prénom" #public calendar calendar: calendar: "Calendrier" diff --git a/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb new file mode 100644 index 000000000..bbaae5626 --- /dev/null +++ b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# A relation table between a pending event reservation and reservation users for this event +class CreateCartItemEventReservationBookingUsers < ActiveRecord::Migration[7.0] + def change + create_table :cart_item_event_reservation_booking_users do |t| + t.string :name + t.belongs_to :cart_item_event_reservation, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_cart_item_event_reservation' } + t.references :event_price_category, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_event_price_category' } + t.references :booked, polymorphic: true + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 54505685e..b77f0a07c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -581,6 +581,41 @@ CREATE SEQUENCE public.cart_item_coupons_id_seq ALTER SEQUENCE public.cart_item_coupons_id_seq OWNED BY public.cart_item_coupons.id; +-- +-- Name: cart_item_event_reservation_booking_users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.cart_item_event_reservation_booking_users ( + id bigint NOT NULL, + name character varying, + cart_item_event_reservation_id bigint, + event_price_category_id bigint, + booked_type character varying, + booked_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: cart_item_event_reservation_booking_users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.cart_item_event_reservation_booking_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: cart_item_event_reservation_booking_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.cart_item_event_reservation_booking_users_id_seq OWNED BY public.cart_item_event_reservation_booking_users.id; + + -- -- Name: cart_item_event_reservation_tickets; Type: TABLE; Schema: public; Owner: - -- @@ -4454,6 +4489,13 @@ ALTER TABLE ONLY public.booking_users ALTER COLUMN id SET DEFAULT nextval('publi ALTER TABLE ONLY public.cart_item_coupons ALTER COLUMN id SET DEFAULT nextval('public.cart_item_coupons_id_seq'::regclass); +-- +-- Name: cart_item_event_reservation_booking_users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users ALTER COLUMN id SET DEFAULT nextval('public.cart_item_event_reservation_booking_users_id_seq'::regclass); + + -- -- Name: cart_item_event_reservation_tickets id; Type: DEFAULT; Schema: public; Owner: - -- @@ -5322,6 +5364,14 @@ ALTER TABLE ONLY public.cart_item_coupons ADD CONSTRAINT cart_item_coupons_pkey PRIMARY KEY (id); +-- +-- Name: cart_item_event_reservation_booking_users cart_item_event_reservation_booking_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users + ADD CONSTRAINT cart_item_event_reservation_booking_users_pkey PRIMARY KEY (id); + + -- -- Name: cart_item_event_reservation_tickets cart_item_event_reservation_tickets_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -6285,6 +6335,20 @@ CREATE INDEX index_booking_users_on_event_price_category_id ON public.booking_us CREATE INDEX index_booking_users_on_reservation_id ON public.booking_users USING btree (reservation_id); +-- +-- Name: index_cart_item_booking_users_on_cart_item_event_reservation; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_cart_item_booking_users_on_cart_item_event_reservation ON public.cart_item_event_reservation_booking_users USING btree (cart_item_event_reservation_id); + + +-- +-- Name: index_cart_item_booking_users_on_event_price_category; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_cart_item_booking_users_on_event_price_category ON public.cart_item_event_reservation_booking_users USING btree (event_price_category_id); + + -- -- Name: index_cart_item_coupons_on_coupon_id; Type: INDEX; Schema: public; Owner: - -- @@ -6306,6 +6370,13 @@ CREATE INDEX index_cart_item_coupons_on_customer_profile_id ON public.cart_item_ CREATE INDEX index_cart_item_coupons_on_operator_profile_id ON public.cart_item_coupons USING btree (operator_profile_id); +-- +-- Name: index_cart_item_event_reservation_booking_users_on_booked; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_cart_item_event_reservation_booking_users_on_booked ON public.cart_item_event_reservation_booking_users USING btree (booked_type, booked_id); + + -- -- Name: index_cart_item_event_reservations_on_customer_profile_id; Type: INDEX; Schema: public; Owner: - -- @@ -7655,6 +7726,14 @@ ALTER TABLE ONLY public.payment_schedules ADD CONSTRAINT fk_rails_00308dc223 FOREIGN KEY (wallet_transaction_id) REFERENCES public.wallet_transactions(id); +-- +-- Name: cart_item_event_reservation_booking_users fk_rails_0964335a37; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users + ADD CONSTRAINT fk_rails_0964335a37 FOREIGN KEY (event_price_category_id) REFERENCES public.event_price_categories(id); + + -- -- Name: cart_item_free_extensions fk_rails_0d11862969; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -7927,6 +8006,14 @@ ALTER TABLE ONLY public.chained_elements ADD CONSTRAINT fk_rails_4fad806cca FOREIGN KEY (previous_id) REFERENCES public.chained_elements(id); +-- +-- Name: cart_item_event_reservation_booking_users fk_rails_5206c6ca4a; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users + ADD CONSTRAINT fk_rails_5206c6ca4a FOREIGN KEY (cart_item_event_reservation_id) REFERENCES public.cart_item_event_reservations(id); + + -- -- Name: cart_item_event_reservation_tickets fk_rails_5307e8aab8; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8963,6 +9050,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230331132506'), ('20230509121907'), ('20230509161557'); +('20230510141305'); ('20230626122844'), ('20230626122947'); From 07b47d79563ac6e77f31de2948059aaf7316226e Mon Sep 17 00:00:00 2001 From: Du Peng Date: Thu, 11 May 2023 11:22:23 +0200 Subject: [PATCH 26/63] (wip) add event type --- app/controllers/api/events_controller.rb | 2 +- .../components/events/event-form.tsx | 26 ++++++++++++++----- .../src/javascript/controllers/events.js.erb | 2 +- app/frontend/src/javascript/models/event.ts | 3 ++- app/frontend/templates/events/show.html | 4 +-- app/models/event.rb | 2 ++ app/views/api/events/_event.json.jbuilder | 2 +- config/locales/app.admin.en.yml | 7 +++-- config/locales/app.admin.fr.yml | 7 +++-- ...9121907_add_booking_nominative_to_event.rb | 8 ------ ...rt_item_event_reservation_booking_users.rb | 3 ++- .../20230511081018_add_event_type_to_event.rb | 10 +++++++ db/structure.sql | 4 ++- test/fixtures/history_values.yml | 1 - .../components/events/event-form.test.tsx | 2 +- test/integration/events/as_admin_test.rb | 1 + 16 files changed, 55 insertions(+), 29 deletions(-) delete mode 100644 db/migrate/20230509121907_add_booking_nominative_to_event.rb create mode 100644 db/migrate/20230511081018_add_event_type_to_event.rb diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index 1b8abdc77..4d05f6b56 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -96,7 +96,7 @@ class API::EventsController < API::APIController # handle general properties event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time, :amount, :nb_total_places, :availability_id, :all_day, :recurrence, - :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :booking_nominative, + :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :event_type, event_theme_ids: [], event_image_attributes: %i[id attachment], event_files_attributes: %i[id attachment _destroy], diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 033dff894..0a2ce1d8c 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form'; -import { Event, EventDecoration, EventPriceCategoryAttributes, RecurrenceOption } from '../../models/event'; +import { Event, EventDecoration, EventPriceCategoryAttributes, RecurrenceOption, EventType } from '../../models/event'; import EventAPI from '../../api/event'; import { useTranslation } from 'react-i18next'; import { FormInput } from '../form/form-input'; @@ -40,7 +40,7 @@ interface EventFormProps { * Form to edit or create events */ export const EventForm: React.FC = ({ action, event, onError, onSuccess }) => { - const { handleSubmit, register, control, setValue, formState } = useForm({ defaultValues: { ...event } }); + const { handleSubmit, register, control, setValue, formState } = useForm({ defaultValues: Object.assign({ event_type: 'standard' }, event) }); const output = useWatch({ control }); const { fields, append, remove } = useFieldArray({ control, name: 'event_price_categories_attributes' }); @@ -168,6 +168,17 @@ export const EventForm: React.FC = ({ action, event, onError, on ]; }; + /** + * This method provides event type options + */ + const buildEventTypeOptions = (): Array> => { + return [ + { label: t('app.admin.event_form.event_types.standard'), value: 'standard' }, + { label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' }, + { label: t('app.admin.event_form.event_types.family'), value: 'family' } + ]; + }; + return (
    @@ -203,6 +214,12 @@ export const EventForm: React.FC = ({ action, event, onError, on label={t('app.admin.event_form.description')} limit={null} heading bulletList blockquote link video image /> + = ({ action, event, onError, on label={t('app.admin.event_form.seats_available')} type="number" tooltip={t('app.admin.event_form.seats_help')} /> - {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
    -
    +
    @@ -132,7 +132,7 @@ {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
    -
    +
    diff --git a/app/models/event.rb b/app/models/event.rb index 60bb3f70b..96ab48199 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -33,6 +33,8 @@ class Event < ApplicationRecord has_many :cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :destroy + validates :event_type, inclusion: { in: %w[standard nominative family] }, presence: true + attr_accessor :recurrence, :recurrence_end_at before_save :update_nb_free_places diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder index 11be0748e..885a29d28 100644 --- a/app/views/api/events/_event.json.jbuilder +++ b/app/views/api/events/_event.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true -json.extract! event, :id, :title, :description, :booking_nominative +json.extract! event, :id, :title, :description, :event_type if event.event_image json.event_image_attributes do json.id event.event_image.id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 74d26c121..eca3e8e05 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -139,8 +139,6 @@ en: event_themes: "Event themes" age_range: "Age range" add_price: "Add a price" - booking_nominative: "Nominative booking" - booking_nominative_help: "If you check this option, the members will have to enter the names of the participants when booking." save: "Save" create_success: "The event was created successfully" events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated" @@ -153,6 +151,11 @@ en: every_week: "Every week" every_month: "Every month" every_year: "Every year" + event_type: "Event type" + event_types: + standard: "Event standard" + nominative: "Event nominative" + family: "Event family" plan_form: ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" tab_settings: "Settings" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 34bbaed54..f477117d5 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -139,8 +139,6 @@ fr: event_themes: "Thèmes de l'événement" age_range: "Tranche d'âge" add_price: "Ajouter un tarif" - booking_nominative: "Réservation nominative" - booking_nominative_help: "Si cette option est activée, les réservations seront nominatives. Les participants devront s'identifier pour réserver." save: "Enregistrer" create_success: "L'événement a bien été créé" events_updated: "{COUNT, plural, one {}=1{Un événement à été} other{{COUNT} événements ont été}} mis à jour avec succès" @@ -153,6 +151,11 @@ fr: every_week: "Chaque semaine" every_month: "Chaque mois" every_year: "Chaque année" + event_type: "Type d'événement" + event_types: + standard: "Evénement standard" + nominative: "Evénement nominatif" + family: "Evénement famille" plan_form: ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement" tab_settings: "Paramètres" diff --git a/db/migrate/20230509121907_add_booking_nominative_to_event.rb b/db/migrate/20230509121907_add_booking_nominative_to_event.rb deleted file mode 100644 index 1a709170d..000000000 --- a/db/migrate/20230509121907_add_booking_nominative_to_event.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# add booking_nominative to event -class AddBookingNominativeToEvent < ActiveRecord::Migration[7.0] - def change - add_column :events, :booking_nominative, :boolean, default: false - end -end diff --git a/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb index bbaae5626..e5d1a3465 100644 --- a/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb +++ b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb @@ -5,7 +5,8 @@ class CreateCartItemEventReservationBookingUsers < ActiveRecord::Migration[7.0] def change create_table :cart_item_event_reservation_booking_users do |t| t.string :name - t.belongs_to :cart_item_event_reservation, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_cart_item_event_reservation' } + t.belongs_to :cart_item_event_reservation, foreign_key: true, + index: { name: 'index_cart_item_booking_users_on_cart_item_event_reservation' } t.references :event_price_category, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_event_price_category' } t.references :booked, polymorphic: true diff --git a/db/migrate/20230511081018_add_event_type_to_event.rb b/db/migrate/20230511081018_add_event_type_to_event.rb new file mode 100644 index 000000000..4f1f832a9 --- /dev/null +++ b/db/migrate/20230511081018_add_event_type_to_event.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Add event_type to event model, to be able to create standard/nominative/family events +class AddEventTypeToEvent < ActiveRecord::Migration[7.0] + def change + add_column :events, :event_type, :string, default: 'standard' + Event.reset_column_information + Event.update_all(event_type: 'standard') + end +end diff --git a/db/structure.sql b/db/structure.sql index b77f0a07c..d1215001f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1236,7 +1236,7 @@ CREATE TABLE public.events ( age_range_id integer, category_id integer, deleted_at timestamp without time zone, - booking_nominative boolean DEFAULT false + event_type character varying DEFAULT 'standard'::character varying ); @@ -9051,6 +9051,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230509121907'), ('20230509161557'); ('20230510141305'); +('20230511080650'), +('20230511081018'); ('20230626122844'), ('20230626122947'); diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 645e05f9a..5738b7399 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -889,5 +889,4 @@ history_value_105: value: 'false' created_at: '2023-03-31 14:38:40.000421' updated_at: '2023-03-31 14:38:40.000421' - footprint: invoicing_profile_id: 1 diff --git a/test/frontend/components/events/event-form.test.tsx b/test/frontend/components/events/event-form.test.tsx index 49f50ce79..219542a23 100644 --- a/test/frontend/components/events/event-form.test.tsx +++ b/test/frontend/components/events/event-form.test.tsx @@ -15,6 +15,7 @@ describe('EventForm', () => { expect(screen.getByLabelText(/app.admin.event_form.title/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.matching_visual/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.description/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.event_form.event_type/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.event_category/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.event_themes/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.age_range/)).toBeInTheDocument(); @@ -27,7 +28,6 @@ describe('EventForm', () => { expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.seats_available/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.standard_rate/)).toBeInTheDocument(); - expect(screen.getByLabelText(/app.admin.event_form.booking_nominative/)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_price/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_a_new_file/ })).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.advanced_accounting_form.code/)).toBeInTheDocument(); diff --git a/test/integration/events/as_admin_test.rb b/test/integration/events/as_admin_test.rb index e4d721ed4..5d59eef64 100644 --- a/test/integration/events/as_admin_test.rb +++ b/test/integration/events/as_admin_test.rb @@ -22,6 +22,7 @@ class Events::AsAdminTest < ActionDispatch::IntegrationTest end_date: 1.week.from_now.utc, end_time: 1.week.from_now.utc.change(hour: 20), category_id: Category.first.id, + event_type: 'standard', amount: 0 } }.to_json, From e85e2d7b472ba3cf11fecd3ed9359d5a6a873b9c Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 15 May 2023 16:42:01 +0200 Subject: [PATCH 27/63] (wip) pay family and nominative event --- app/controllers/api/children_controller.rb | 5 +- .../components/events/event-form.tsx | 13 +- .../src/javascript/controllers/events.js.erb | 164 ++++++++++++++---- app/frontend/src/javascript/services/child.js | 11 ++ app/frontend/templates/events/show.html | 34 +++- app/policies/child_policy.rb | 7 - 6 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 app/frontend/src/javascript/services/child.js diff --git a/app/controllers/api/children_controller.rb b/app/controllers/api/children_controller.rb index f923f2bb3..618d859fb 100644 --- a/app/controllers/api/children_controller.rb +++ b/app/controllers/api/children_controller.rb @@ -7,7 +7,10 @@ class API::ChildrenController < API::APIController before_action :set_child, only: %i[show update destroy] def index - @children = policy_scope(Child) + authorize Child + user_id = current_user.id + user_id = params[:user_id] if current_user.privileged? && params[:user_id] + @children = Child.where(user_id: user_id) end def show diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 0a2ce1d8c..5ed6a7036 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -54,6 +54,7 @@ export const EventForm: React.FC = ({ action, event, onError, on const [isOpenRecurrentModal, setIsOpenRecurrentModal] = useState(false); const [updatingEvent, setUpdatingEvent] = useState(null); const [isActiveAccounting, setIsActiveAccounting] = useState(false); + const [isActiveFamilyAccount, setIsActiveFamilyAccount] = useState(false); useEffect(() => { EventCategoryAPI.index() @@ -69,6 +70,7 @@ export const EventForm: React.FC = ({ action, event, onError, on .then(data => setPriceCategoriesOptions(data.map(c => decorationToOption(c)))) .catch(onError); SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).catch(onError); + SettingAPI.get('family_account').then(res => setIsActiveFamilyAccount(res.value === 'true')).catch(onError); }, []); useEffect(() => { @@ -172,11 +174,14 @@ export const EventForm: React.FC = ({ action, event, onError, on * This method provides event type options */ const buildEventTypeOptions = (): Array> => { - return [ - { label: t('app.admin.event_form.event_types.standard'), value: 'standard' }, - { label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' }, - { label: t('app.admin.event_form.event_types.family'), value: 'family' } + const options = [ + { label: t('app.admin.event_form.event_types.standard'), value: 'standard' as EventType }, + { label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' as EventType } ]; + if (isActiveFamilyAccount) { + options.push({ label: t('app.admin.event_form.event_types.family'), value: 'family' as EventType }); + } + return options; }; return ( diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 32e266bfe..5782994be 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -136,8 +136,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve } ]); -Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'SlotsReservation', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment', - function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment) { +Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'SlotsReservation', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment', 'Child', + function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment, Child) { /* PUBLIC SCOPE */ // reservations for the currently shown event @@ -150,6 +150,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.ctrl = { member: {} }; + // children for the member + $scope.children = []; + // parameters for a new reservation $scope.reserve = { nbPlaces: { @@ -226,22 +229,12 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }); }; - const hasMemberInBookingUsers = function () { - const keys = Object.keys($scope.reserve.bookingUsers); - for (const key of keys) { - if ($scope.reserve.bookingUsers[key].find(u => u.booked_id === $scope.ctrl.member.id && u.booked_type === 'User')) { - return true; - } - } - return false; - }; - /** * Callback to call when the number of tickets to book changes in the current booking */ $scope.changeNbPlaces = function (priceType) { // compute the total remaining places - let remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces; + let remain = ($scope.event.event_type === 'family' ? ($scope.children.length + 1) : $scope.event.nb_free_places) - $scope.reserve.nbReservePlaces; for (let ticket in $scope.reserve.tickets) { remain -= $scope.reserve.tickets[ticket]; } @@ -260,36 +253,41 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } } - const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length; - const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; - if (nbReservePlaces > nbBookingUsers) { - _.times(nbReservePlaces - nbBookingUsers, () => { - /* - if (!hasMemberInBookingUsers()) { - $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, booked_id: $scope.ctrl.member.id, booked_type: 'User', name: $scope.ctrl.member.name }); - } else { - $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); - } - */ - $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); - }); - } else { - _.times(nbBookingUsers - nbReservePlaces, () => { - $scope.reserve.bookingUsers[priceType].pop(); - }); + if ($scope.event.event_type === 'nominative' || $scope.event.event_type === 'family') { + const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length; + const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; + if (nbReservePlaces > nbBookingUsers) { + _.times(nbReservePlaces - nbBookingUsers, () => { + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, bookedUsers: buildBookedUsersOptions() }); + }); + } else { + _.times(nbBookingUsers - nbReservePlaces, () => { + $scope.reserve.bookingUsers[priceType].pop(); + }); + } } // recompute the total price return $scope.computeEventAmount(); }; + $scope.changeBookedUser = function () { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + user.bookedUsers = buildBookedUsersOptions(user.booked); + } + } + } + /** * Callback to reset the current reservation parameters * @param e {Object} see https://docs.angularjs.org/guide/expression#-event- */ $scope.cancelReserve = function (e) { e.preventDefault(); - return resetEventReserve(); + resetEventReserve(); + updateNbReservePlaces(); + return; }; $scope.isUserValidatedByType = () => { @@ -354,6 +352,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; getReservations($scope.event.id, 'Event', $scope.ctrl.member.id); + getChildren($scope.ctrl.member.id).then(() => { + updateNbReservePlaces(); + }); }); } }; @@ -615,6 +616,31 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' growl.error(message); }; + /** + * Checks if the reservation of current event is valid + */ + $scope.reservationIsValid = () => { + if ($scope.event.event_type === 'nominative') { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + if (!_.trim(user.name)) { + return false; + } + } + } + } + if ($scope.event.event_type === 'family') { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + if (!user.booked) { + return false; + } + } + } + } + return true; + } + /* PRIVATE SCOPE */ /** @@ -634,6 +660,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' // get the current user's reservations into $scope.reservations if ($scope.currentUser) { getReservations($scope.event.id, 'Event', $scope.currentUser.id); + getChildren($scope.currentUser.id).then(function (children) { + updateNbReservePlaces(); + }); } // watch when a coupon is applied to re-compute the total price @@ -658,6 +687,72 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }).$promise.then(function (reservations) { $scope.reservations = reservations; }); }; + /** + * Retrieve the children for the user + * @param user_id {number} the user's id (current or managed) + */ + const getChildren = function (user_id) { + return Child.query({ + user_id + }).$promise.then(function (children) { + $scope.children = children; + return $scope.children; + }); + }; + + /** + * Update the number of places reserved by the current user + */ + const hasBookedUser = function (userKey) { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + if (user.booked && user.booked.key === userKey) { + return true; + } + } + } + return false; + }; + + /** + * Build the list of options for the select box of the booked users + * @param booked {object} the booked user + */ + const buildBookedUsersOptions = function (booked) { + const options = []; + const userKey = `user_${$scope.ctrl.member.id}`; + if ((booked && booked.key === userKey) || !hasBookedUser(userKey)) { + options.push({ key: userKey, name: $scope.ctrl.member.name, type: 'User', id: $scope.ctrl.member.id }); + } + for (const child of $scope.children) { + const key = `child_${child.id}`; + if ((booked && booked.key === key) || !hasBookedUser(key)) { + options.push({ + key, + name: child.first_name + ' ' + child.last_name, + id: child.id, + type: 'Child' + }); + } + } + return options; + }; + + /** + * update number of places available for each price category for the family event + */ + const updateNbReservePlaces = function () { + if ($scope.event.event_type === 'family') { + const maxPlaces = $scope.children.length + 1; + if ($scope.event.nb_free_places > maxPlaces) { + $scope.reserve.nbPlaces.normal = __range__(0, maxPlaces, true); + for (let evt_px_cat of Array.from($scope.event.event_price_categories_attributes)) { + $scope.reserve.nbPlaces[evt_px_cat.id] = __range__(0, maxPlaces, true); + } + } + } + }; + /** * Create a hash map implementing the Reservation specs * @param reserve {Object} Reservation parameters (places...) @@ -694,9 +789,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' for (const user of $scope.reserve.bookingUsers[key]) { reservation.booking_users_attributes.push({ event_price_category_id: user.event_price_category_id, - name: user.name, - booked_id: user.booked_id, - booked_type: user.booked_type + name: user.booked ? user.booked.name : _.trim(user.name), + booked_id: user.booked ? user.booked.id : undefined, + booked_type: user.booked ? user.booked.type : undefined, }); } } @@ -865,6 +960,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.reservations.push(reservation); }); resetEventReserve(); + updateNbReservePlaces(); $scope.reserveSuccess = true; $scope.coupon.applied = null; if ($scope.currentUser.role === 'admin') { diff --git a/app/frontend/src/javascript/services/child.js b/app/frontend/src/javascript/services/child.js new file mode 100644 index 000000000..07d0d916c --- /dev/null +++ b/app/frontend/src/javascript/services/child.js @@ -0,0 +1,11 @@ +'use strict'; + +Application.Services.factory('Child', ['$resource', function ($resource) { + return $resource('/api/children/:id', + { id: '@id' }, { + update: { + method: 'PUT' + } + } + ); +}]); diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index 1da00b830..72cd5aa07 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -122,7 +122,20 @@
    - + +
    +
    +
    +
    + +
    @@ -135,7 +148,20 @@
    - + +
    +
    +
    +
    + +
    @@ -202,11 +228,11 @@
    -
    @@ -174,6 +180,12 @@ {{ 'app.shared.cart.child_validation_required_alert' }}

    + +

    + + {{ 'app.shared.cart.child_birthday_must_be_under_18_years_ago_alert' }} +

    +
    diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index be26b070d..8872fea25 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -374,6 +374,7 @@ en: no_tags: "No tags" user_validation_required_alert: "Warning!
    Your administrator must validate your account. Then, you'll then be able to access all the booking features." child_validation_required_alert: "Warning!
    Your administrator must validate your child account. Then, you'll then be able to book the event." + child_birthday_must_be_under_18_years_ago_alert: "Warning!
    Your child must be under 18 years ago. Then, you'll then be able to book the event." # feature-tour modal tour: previous: "Previous" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 2338a1e1e..573f1cfe5 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -374,6 +374,7 @@ fr: no_tags: "Aucune étiquette" user_validation_required_alert: "Attention !
    Votre administrateur doit valider votre compte. Vous pourrez alors accéder à l'ensemble des fonctionnalités de réservation." child_validation_required_alert: "Attention !
    Votre administrateur doit valider votre compte enfant. Vous pourrez alors réserver l'événement." + child_birthday_must_be_under_18_years_ago_alert: "Attention !
    La date de naissance de l'enfant doit être inférieure à 18 ans. Vous pourrez alors réserver l'événement." #feature-tour modal tour: previous: "Précédent" From 78d8b3bfe6d71b0e9c89762d57b101691273ef84 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 29 May 2023 15:15:32 +0200 Subject: [PATCH 40/63] (feat) show event reservation booking users in events of member dashbaord --- app/frontend/templates/dashboard/events.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/frontend/templates/dashboard/events.html b/app/frontend/templates/dashboard/events.html index 1783502d7..9af91bd8f 100644 --- a/app/frontend/templates/dashboard/events.html +++ b/app/frontend/templates/dashboard/events.html @@ -26,12 +26,20 @@ translate-values="{NUMBER: r.nb_reserve_places}"> {{ 'app.logged.dashboard.events.NUMBER_normal_places_reserved' }} + +
    + {{bu.name}} +

    {{ 'app.logged.dashboard.events.NUMBER_of_NAME_places_reserved' }} + +
    + {{bu.name}} +
    From e7bac208a790d8f55b98eda8c8b160668f13dc1c Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 29 May 2023 19:32:51 +0200 Subject: [PATCH 41/63] (feat) child's supporting document file created/updated notification --- app/services/child_service.rb | 27 ++++++++++++++++++- ...rting_document_files_created.json.jbuilder | 3 +++ ...rting_document_files_updated.json.jbuilder | 5 ++++ ...supporting_document_files_created.html.erb | 18 +++++++++++++ ...supporting_document_files_updated.html.erb | 19 +++++++++++++ config/locales/app.logged.en.yml | 2 ++ config/locales/app.logged.fr.yml | 2 ++ config/locales/en.yml | 4 +++ config/locales/fr.yml | 4 +++ config/locales/mails.en.yml | 10 +++++++ config/locales/mails.fr.yml | 10 +++++++ db/seeds/notification_types.rb | 16 +++++++++++ 12 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder create mode 100644 app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder create mode 100644 app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb create mode 100644 app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb diff --git a/app/services/child_service.rb b/app/services/child_service.rb index e589a9f54..45ebd1a4c 100644 --- a/app/services/child_service.rb +++ b/app/services/child_service.rb @@ -7,13 +7,38 @@ class ChildService NotificationCenter.call type: 'notify_admin_child_created', receiver: User.admins_and_managers, attached_object: child + all_files_are_upload = true + SupportingDocumentType.where(document_type: 'Child').each do |sdt| + file = sdt.supporting_document_files.find_by(supportable: child) + all_files_are_upload = false if file.nil? || file.attachment_identifier.nil? + end + if all_files_are_upload + NotificationCenter.call type: 'notify_admin_user_child_supporting_document_files_created', + receiver: User.admins_and_managers, + attached_object: child + end + return true end false end def self.update(child, child_params) - child.update(child_params) + if child.update(child_params) + all_files_are_upload = true + SupportingDocumentType.where(document_type: 'Child').each do |sdt| + file = sdt.supporting_document_files.find_by(supportable: child) + all_files_are_upload = false if file.nil? || file.attachment_identifier.nil? + end + if all_files_are_upload + NotificationCenter.call type: 'notify_admin_user_child_supporting_document_files_updated', + receiver: User.admins_and_managers, + attached_object: child + end + + return true + end + false end def self.validate(child, is_valid) diff --git a/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder new file mode 100644 index 000000000..4a16e4502 --- /dev/null +++ b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder @@ -0,0 +1,3 @@ +json.title notification.notification_type +json.description t('.supporting_document_files_uploaded', + NAME: notification.attached_object&.full_name || t('api.notifications.deleted_user')) diff --git a/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder new file mode 100644 index 000000000..5b97b34ea --- /dev/null +++ b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +json.title notification.notification_type +json.description t('.supporting_document_files_uploaded', + NAME: notification.attached_object&.full_name || t('api.notifications.deleted_user')) diff --git a/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb new file mode 100644 index 000000000..239915785 --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb @@ -0,0 +1,18 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

    + <%= t('.body.supporting_document_files_uploaded_below', + NAME: @attached_object&.full_name || t('api.notifications.deleted_user')) %> +

    +
      +<% if @attached_object %> + <% SupportingDocumentType.where(document_type: 'Child').each do |type| %> +
    • <%= type.name %>
    • + <% end %> +<% end %> +
    +<% if Setting.get('child_validation_required') %> +

    + <%= t('.body.validate_child') %> +

    +<% end %> diff --git a/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb new file mode 100644 index 000000000..00355c465 --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb @@ -0,0 +1,19 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

    + <%= t('.body.child_update_supporting_document_file', + NAME: @attached_object&.full_name || t('api.notifications.deleted_user')) %> +

    +
      +<% if @attached_object %> + <% SupportingDocumentType.where(document_type: 'Child').each do |type| %> +
    • <%= type.name %>
    • + <% end %> +<% end %> +
    + +<% if Setting.get('child_validation_required') %> +

    + <%= t('.body.validate_child') %> +

    +<% end %> diff --git a/config/locales/app.logged.en.yml b/config/locales/app.logged.en.yml index 69afbfbd7..f284fbc5d 100644 --- a/config/locales/app.logged.en.yml +++ b/config/locales/app.logged.en.yml @@ -295,6 +295,8 @@ en: notify_admin_user_child_supporting_document_refusal: "A supporting document of child has been rejected" notify_admin_user_supporting_document_files_created: "A user has uploaded a supporting document" notify_admin_user_supporting_document_files_updated: "A user has updated a supporting document" + notify_admin_user_child_supporting_document_files_created: "A child has uploaded a supporting document" + notify_admin_user_child_supporting_document_files_updated: "A child has updated a supporting document" notify_admin_member_create_reservation: "A member books a reservation" notify_admin_slot_is_modified: "A reservation slot has been modified" notify_admin_slot_is_canceled: "A reservation has been cancelled" diff --git a/config/locales/app.logged.fr.yml b/config/locales/app.logged.fr.yml index 0dc9c03f0..24550f9d1 100644 --- a/config/locales/app.logged.fr.yml +++ b/config/locales/app.logged.fr.yml @@ -295,6 +295,8 @@ fr: notify_admin_user_child_supporting_document_refusal: "Un justificatif de l'enfant a été refusé" notify_admin_user_supporting_document_files_created: "Un utilisateur a téléversé un justificatif" notify_admin_user_supporting_document_files_updated: "Un utilisateur a mis à jour un justificatif" + notify_admin_user_child_supporting_document_files_created: "Un enfant a téléversé un justificatif" + notify_admin_user_child_supporting_document_files_updated: "Un enfant a mis à jour un justificatif" notify_admin_member_create_reservation: "Un membre fait une réservation" notify_admin_slot_is_modified: "Un créneau de réservation a été modifié" notify_admin_slot_is_canceled: "Une réservation a été annulée" diff --git a/config/locales/en.yml b/config/locales/en.yml index ff00e33d7..7646770b6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -443,6 +443,10 @@ en: supporting_document_files_uploaded: "Supporting document uploaded by member %{NAME}." notify_admin_user_supporting_document_files_updated: supporting_document_files_uploaded: "Supporting document changed by member %{NAME}." + notify_admin_user_child_supporting_document_files_created: + supporting_document_files_uploaded: "Child's %{NAME} supporting document uploaded." + notify_admin_user_child_supporting_document_files_updated: + supporting_document_files_uploaded: "Supporting document changed by child %{NAME}." notify_user_is_validated: account_validated: "Your account is valid." notify_user_is_invalidated: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 03d0aa8e5..ec3c61adc 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -443,6 +443,10 @@ fr: supporting_document_files_uploaded: "Le membre %{NAME} a téléversé un nouveau justificatif." notify_admin_user_supporting_document_files_updated: supporting_document_files_uploaded: "Le membre %{NAME} a modifié un justificatif." + notify_admin_user_child_supporting_document_files_created: + supporting_document_files_uploaded: "L'enfant %{NAME} a téléversé un nouveau justificatif." + notify_admin_user_child_supporting_document_files_updated: + supporting_document_files_uploaded: "L'enfant %{NAME} a modifié un justificatif." notify_user_is_validated: account_validated: "Votre compte est valide." notify_user_is_invalidated: diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index bf2c74d02..0d6ca6a84 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -393,6 +393,16 @@ en: body: user_update_supporting_document_file: "Member %{NAME} has modified the supporting documents below:" validate_user: "Please validate this account" + notify_admin_user_child_supporting_document_files_created: + subject: "Supporting documents of child uploaded by a member" + body: + supporting_document_files_uploaded_below: "Child %{NAME} has uploaded the following supporting documents:" + validate_child: "Please validate this child account" + notify_admin_user_child_supporting_document_files_updated: + subject: "Child's supporting documents have changed" + body: + child_update_supporting_document_file: "Child %{NAME} has modified the supporting documents below:" + validate_child: "Please validate this child account" notify_user_is_validated: subject: "Account validated" body: diff --git a/config/locales/mails.fr.yml b/config/locales/mails.fr.yml index 69e10ab97..60bfe3be1 100644 --- a/config/locales/mails.fr.yml +++ b/config/locales/mails.fr.yml @@ -393,6 +393,16 @@ fr: body: user_update_supporting_document_file: "Le membre %{NAME} a modifié le justificatif ci-dessous :" validate_user: "Veuillez valider son compte" + notify_admin_user_child_supporting_document_files_created: + subject: "Justificatif de l'enfant téléversé par un membre" + body: + supporting_document_files_uploaded_below: "L'enfant %{NAME} a téléversé le justificatif suivant :" + validate_child: "Veuillez valider son compte enfant" + notify_admin_user_child_supporting_document_files_updated: + subject: "Le justificatif d'un enfant a changé" + body: + child_update_supporting_document_file: "L'enfant %{NAME} a modifié le justificatif ci-dessous :" + validate_child: "Veuillez valider son compte enfant" notify_user_is_validated: subject: "Compte validé" body: diff --git a/db/seeds/notification_types.rb b/db/seeds/notification_types.rb index 635b6cd6f..2ef8f21ae 100644 --- a/db/seeds/notification_types.rb +++ b/db/seeds/notification_types.rb @@ -127,3 +127,19 @@ unless NotificationType.find_by(name: 'notify_user_child_is_invalidated') is_configurable: false ) end + +unless NotificationType.find_by(name: 'notify_admin_user_child_supporting_document_files_updated') + NotificationType.create!( + name: 'notify_admin_user_child_supporting_document_files_updated', + category: 'supporting_documents', + is_configurable: true + ) +end + +unless NotificationType.find_by(name: 'notify_admin_user_child_supporting_document_files_created') + NotificationType.create!( + name: 'notify_admin_user_child_supporting_document_files_created', + category: 'supporting_documents', + is_configurable: true + ) +end From f64e76a4631c9af20870817115dc4d7e45c68f45 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 30 May 2023 10:43:27 +0200 Subject: [PATCH 42/63] (bug) child validated_at type --- app/frontend/src/javascript/models/child.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend/src/javascript/models/child.ts b/app/frontend/src/javascript/models/child.ts index bc00f1fb1..b9fcaca9a 100644 --- a/app/frontend/src/javascript/models/child.ts +++ b/app/frontend/src/javascript/models/child.ts @@ -1,4 +1,4 @@ -import { TDateISODate } from '../typings/date-iso'; +import { TDateISODate, TDateISO } from '../typings/date-iso'; import { ApiFilter } from './api'; export interface ChildIndexFilter extends ApiFilter { @@ -13,7 +13,7 @@ export interface Child { phone?: string, birthday: TDateISODate, user_id: number, - validated_at?: TDateISODate, + validated_at?: TDateISO, supporting_document_files_attributes?: Array<{ id?: number, supportable_id?: number, From d4b2b16425210ac764b75e5d8935862da0aa1469 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 31 May 2023 11:29:10 +0200 Subject: [PATCH 43/63] (ui) integration --- .../components/family-account/child-form.tsx | 167 ++++++++++-------- .../components/family-account/child-item.tsx | 31 ++-- ...ildren-list.tsx => children-dashboard.tsx} | 28 +-- .../components/form/form-file-upload.tsx | 7 +- .../supporting-documents-types-list.tsx | 59 +++---- .../components/user/members-list-item.tsx | 102 +++++++++++ .../components/user/members-list.tsx | 37 ++++ app/frontend/src/javascript/models/member.ts | 47 +++++ app/frontend/src/stylesheets/application.scss | 5 +- .../modules/base/edit-destroy-buttons.scss | 3 + .../stylesheets/modules/base/fab-modal.scss | 1 + .../modules/family-account/child-form.scss | 43 +++++ .../modules/family-account/child-item.scss | 62 ++++++- .../family-account/children-dashboard.scss | 20 +++ .../modules/form/form-file-upload.scss | 2 + .../src/stylesheets/modules/members.scss | 105 +++++++++++ .../supporting-documents-types-list.scss | 35 ++-- .../modules/user/user-validation.scss | 4 + .../templates/admin/members/edit.html | 2 +- .../templates/admin/members/members.html | 47 +---- .../templates/dashboard/children.html | 2 +- app/frontend/templates/events/show.html | 19 +- .../templates/shared/_member_select.html | 9 +- config/locales/app.admin.en.yml | 10 +- config/locales/app.public.en.yml | 18 +- config/locales/app.public.fr.yml | 2 +- config/locales/app.shared.en.yml | 9 +- 27 files changed, 654 insertions(+), 222 deletions(-) rename app/frontend/src/javascript/components/family-account/{children-list.tsx => children-dashboard.tsx} (76%) create mode 100644 app/frontend/src/javascript/components/user/members-list-item.tsx create mode 100644 app/frontend/src/javascript/components/user/members-list.tsx create mode 100644 app/frontend/src/javascript/models/member.ts create mode 100644 app/frontend/src/stylesheets/modules/family-account/child-form.scss create mode 100644 app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss diff --git a/app/frontend/src/javascript/components/family-account/child-form.tsx b/app/frontend/src/javascript/components/family-account/child-form.tsx index 02234f827..bc7ab29c1 100644 --- a/app/frontend/src/javascript/components/family-account/child-form.tsx +++ b/app/frontend/src/javascript/components/family-account/child-form.tsx @@ -10,6 +10,7 @@ import { FileType } from '../../models/file'; import { SupportingDocumentType } from '../../models/supporting-document-type'; import { User } from '../../models/user'; import { SupportingDocumentsRefusalModal } from '../supporting-documents/supporting-documents-refusal-modal'; +import { FabAlert } from '../base/fab-alert'; interface ChildFormProps { child: Child; @@ -65,92 +66,114 @@ export const ChildForm: React.FC = ({ child, onSubmit, supportin return (
    {!isPrivileged() && -
    - {t('app.public.child_form.child_form_info')} -
    + +

    {t('app.public.child_form.child_form_info')}

    +
    }
    - - - moment(value).isAfter(moment().subtract(18, 'year')) }} - formState={formState} - label={t('app.public.child_form.birthday')} - type="date" - max={moment().format('YYYY-MM-DD')} - min={moment().subtract(18, 'year').format('YYYY-MM-DD')} - /> - +
    + + +
    +
    + moment(value).isAfter(moment().subtract(18, 'year')) }} + formState={formState} + label={t('app.public.child_form.birthday')} + type="date" + max={moment().format('YYYY-MM-DD')} + min={moment().subtract(18, 'year').format('YYYY-MM-DD')} + /> + +
    - {output.supporting_document_files_attributes.map((sf, index) => { - if (isPrivileged()) { + + {!isPrivileged() && <> +

    {t('app.public.child_form.supporting_documents')}

    + {output.supporting_document_files_attributes.map((sf, index) => { return ( -
    -
    {getSupportingDocumentsTypeName(sf.supporting_document_type_id)}
    - {sf.attachment_url && ( - - {sf.attachment} - - - )} - {!sf.attachment_url && ( -
    {t('app.public.child_form.to_complete')}
    - )} -
    + ); - } - return ( - - ); - })} + })} + }
    - + {t('app.public.child_form.save')} - {isPrivileged() && -
    - {t('app.public.child_form.refuse_documents')} - -
    - }
    + + {isPrivileged() && <> +

    {t('app.public.child_form.supporting_documents')}

    +
    + {output.supporting_document_files_attributes.map((sf, index) => { + return ( +
    + {getSupportingDocumentsTypeName(sf.supporting_document_type_id)} + {sf.attachment_url && ( +
    +

    {sf.attachment}

    + + + +
    + )} + {!sf.attachment_url && ( +
    +

    {t('app.public.child_form.to_complete')}

    +
    + )} +
    + ); + })} +
    + } + + {isPrivileged() && <> + +

    {t('app.public.child_form.refuse_documents_info')}

    +
    +
    + {t('app.public.child_form.refuse_documents')} + +
    + }
    ); diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx index 1f0134808..62de02e9e 100644 --- a/app/frontend/src/javascript/components/family-account/child-item.tsx +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -5,9 +5,11 @@ import { FabButton } from '../base/fab-button'; import FormatLib from '../../lib/format'; import { DeleteChildModal } from './delete-child-modal'; import ChildAPI from '../../api/child'; +import { PencilSimple, Trash, UserSquare } from 'phosphor-react'; interface ChildItemProps { child: Child; + size: 'sm' | 'lg'; onEdit: (child: Child) => void; onDelete: (error: string) => void; onError: (error: string) => void; @@ -16,7 +18,7 @@ interface ChildItemProps { /** * A child item. */ -export const ChildItem: React.FC = ({ child, onEdit, onDelete, onError }) => { +export const ChildItem: React.FC = ({ child, size, onEdit, onDelete, onError }) => { const { t } = useTranslation('public'); const [isOpenDeleteChildModal, setIsOpenDeleteChildModal] = React.useState(false); @@ -38,22 +40,29 @@ export const ChildItem: React.FC = ({ child, onEdit, onDelete, o }; return ( -
    -
    +
    +
    + +
    +
    {t('app.public.child_item.last_name')} -
    {child.last_name}
    +

    {child.last_name}

    -
    +
    {t('app.public.child_item.first_name')} -
    {child.first_name}
    +

    {child.first_name}

    -
    +
    {t('app.public.child_item.birthday')} -
    {FormatLib.date(child.birthday)}
    +

    {FormatLib.date(child.birthday)}

    -
    - } onClick={() => onEdit(child)} className="edit-button" /> - } onClick={toggleDeleteChildModal} className="delete-button" /> +
    + onEdit(child)} className="edit-btn"> + + + + +
    diff --git a/app/frontend/src/javascript/components/family-account/children-list.tsx b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx similarity index 76% rename from app/frontend/src/javascript/components/family-account/children-list.tsx rename to app/frontend/src/javascript/components/family-account/children-dashboard.tsx index 875555ddd..30d950068 100644 --- a/app/frontend/src/javascript/components/family-account/children-list.tsx +++ b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx @@ -14,9 +14,10 @@ import SupportingDocumentTypeAPI from '../../api/supporting-document-type'; declare const Application: IApplication; -interface ChildrenListProps { +interface ChildrenDashboardProps { user: User; operator: User; + adminPanel?: boolean; onSuccess: (error: string) => void; onError: (error: string) => void; } @@ -24,7 +25,7 @@ interface ChildrenListProps { /** * A list of children belonging to the current user. */ -export const ChildrenList: React.FC = ({ user, operator, onError, onSuccess }) => { +export const ChildrenDashboard: React.FC = ({ user, operator, adminPanel, onError, onSuccess }) => { const { t } = useTranslation('public'); const [children, setChildren] = useState>([]); @@ -92,19 +93,24 @@ export const ChildrenList: React.FC = ({ user, operator, onEr }; return ( -
    +
    -

    {t('app.public.children_list.heading')}

    + {adminPanel + ?

    {t('app.public.children_dashboard.heading')}

    + :

    {t('app.public.children_dashboard.member_heading')}

    + } {!isPrivileged() && ( - - {t('app.public.children_list.add_child')} - +
    + + {t('app.public.children_dashboard.add_child')} + +
    )}
    {children.map(child => ( - + ))}
    setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} /> @@ -112,12 +118,12 @@ export const ChildrenList: React.FC = ({ user, operator, onEr ); }; -const ChildrenListWrapper: React.FC = (props) => { +const ChildrenDashboardWrapper: React.FC = (props) => { return ( - + ); }; -Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['user', 'operator', 'onSuccess', 'onError'])); +Application.Components.component('childrenDashboard', react2angular(ChildrenDashboardWrapper, ['user', 'operator', 'adminPanel', 'onSuccess', 'onError'])); diff --git a/app/frontend/src/javascript/components/form/form-file-upload.tsx b/app/frontend/src/javascript/components/form/form-file-upload.tsx index 18d25fbae..22b58a252 100644 --- a/app/frontend/src/javascript/components/form/form-file-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-file-upload.tsx @@ -75,9 +75,10 @@ export const FormFileUpload = ({ id, label, re return (
    - {hasFile() && ( - {file.attachment_name} - )} + {hasFile() + ? {file.attachment_name} + : {t('app.shared.form_file_upload.placeholder')} + }
    {file?.id && file?.attachment_url && ( {getGroupsNames(poit.group_ids)} {poit.name} -
    +
    - + - +
    @@ -292,38 +293,26 @@ const SupportingDocumentsTypesList: React.FC onSuccess={onDestroySuccess} onError={onError}/> - - - - - - - - - {supportingDocumentsTypes.map(poit => { - return ( - - - - - ); - })} - -
    - - {t('app.admin.settings.account.supporting_documents_types_list.name')} - - -
    {poit.name} -
    - - - - - - -
    -
    +
    + {supportingDocumentsTypes.map(poit => { + return ( +
    +
    +

    {poit.name}

    +
    + + + + + + +
    +
    +
    + ); + })} +
    + {!hasTypes() && (

    diff --git a/app/frontend/src/javascript/components/user/members-list-item.tsx b/app/frontend/src/javascript/components/user/members-list-item.tsx new file mode 100644 index 000000000..19041b4e9 --- /dev/null +++ b/app/frontend/src/javascript/components/user/members-list-item.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Member } from '../../models/member'; +import { Child } from '../../models/child'; +import ChildAPI from '../../api/child'; +import { FabButton } from '../base/fab-button'; +import { CaretDown, User, Users } from 'phosphor-react'; +import { ChildItem } from '../family-account/child-item'; +import { EditDestroyButtons } from '../base/edit-destroy-buttons'; + +interface MembersListItemProps { + member: Member, + onError: (message: string) => void, + onSuccess: (message: string) => void +} + +/** + * Members list + */ +export const MembersListItem: React.FC = ({ member, onError, onSuccess }) => { + const { t } = useTranslation('admin'); + + const [children, setChildren] = useState>([]); + const [childrenList, setChildrenList] = useState(false); + + useEffect(() => { + ChildAPI.index({ user_id: member.id }).then(setChildren); + }, [member]); + + /** + * Redirect to the given user edition page + */ + const toMemberEdit = (memberId: number): void => { + window.location.href = `/#!/admin/members/${memberId}/edit`; + }; + + return ( +

    +
    +
    + {(children.length > 0) + ? + : + } +
    + {(children.length > 0) && + setChildrenList(!childrenList)} className={`toggle ${childrenList ? 'open' : ''}`}> + + + } +
    + +
    +
    +
    + {t('app.admin.members_list_item.surname')} +

    {member.profile.last_name}

    +
    +
    + {t('app.admin.members_list_item.first_name')} +

    {member.profile.first_name}

    +
    +
    + {t('app.admin.members_list_item.phone')} +

    {member.profile.phone || '---'}

    +
    +
    + {t('app.admin.members_list_item.email')} +

    {member.email}

    +
    +
    + {t('app.admin.members_list_item.group')} +

    {member.group.name}

    +
    +
    + {t('app.admin.members_list_item.subscription')} +

    {member.subscribed_plan?.name || '---'}

    +
    +
    + +
    + {/* TODO: */} + toMemberEdit(member.id)} + onDeleteSuccess={() => onSuccess} + itemId={member.id} + itemType={t('app.admin.members_list_item.item_type')} + destroy={() => new Promise(() => console.log(`Delete member ${member.id}`))} /> +
    +
    + + { (children.length > 0) && +
    +
    + {children.map(child => ( + console.log('edit child')} onDelete={() => console.log('delete child')} onError={onError} /> + ))} +
    + } +
    + ); +}; diff --git a/app/frontend/src/javascript/components/user/members-list.tsx b/app/frontend/src/javascript/components/user/members-list.tsx new file mode 100644 index 000000000..939db4632 --- /dev/null +++ b/app/frontend/src/javascript/components/user/members-list.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { IApplication } from '../../models/application'; +import { Loader } from '../base/loader'; +import { react2angular } from 'react2angular'; +import { Member } from '../../models/member'; +import { MembersListItem } from './members-list-item'; + +declare const Application: IApplication; + +interface MembersListProps { + members: Member[], + onError: (message: string) => void, + onSuccess: (message: string) => void +} + +/** + * Members list + */ +export const MembersList: React.FC = ({ members, onError, onSuccess }) => { + return ( +
    + {members.map(member => ( + + ))} +
    + ); +}; + +const MembersListWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess'])); diff --git a/app/frontend/src/javascript/models/member.ts b/app/frontend/src/javascript/models/member.ts new file mode 100644 index 000000000..b163c3814 --- /dev/null +++ b/app/frontend/src/javascript/models/member.ts @@ -0,0 +1,47 @@ +import { TDateISO } from '../typings/date-iso'; + +export interface Member { + maxMembers: number + id: number + username: string + email: string + profile: { + first_name: string + last_name: string + phone: string + } + need_completion?: boolean + group: { + name: string + } + subscribed_plan?: Plan + validated_at: TDateISO +} + +interface Plan { + id: number + base_name: string + name: string + amount: number + interval: string + interval_count: number + training_credit_nb: number + training_credits: [ + { + training_id: number + }, + { + training_id: number + } + ] + machine_credits: [ + { + machine_id: number + hours: number + }, + { + machine_id: number + hours: number + } + ] +} diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index e9416affb..cf9ff38aa 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -52,6 +52,9 @@ @import "modules/events/event-form"; @import "modules/events/update-recurrent-modal"; @import "modules/events/events-settings.scss"; +@import "modules/family-account/child-form"; +@import "modules/family-account/child-item"; +@import "modules/family-account/children-dashboard"; @import "modules/form/abstract-form-item"; @import "modules/form/form-input"; @import "modules/form/form-multi-file-upload"; @@ -181,8 +184,6 @@ @import "modules/tour"; @import "modules/wallet-info"; -@import "modules/family-account/child-item"; - @import "app.responsive"; @import "overrides"; diff --git a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss index 6adaece60..2fa61210a 100644 --- a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss +++ b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss @@ -1,6 +1,9 @@ .edit-destroy-buttons { + width: fit-content; + flex-shrink: 0; border-radius: var(--border-radius-sm); overflow: hidden; + button { @include btn; border-radius: 0; diff --git a/app/frontend/src/stylesheets/modules/base/fab-modal.scss b/app/frontend/src/stylesheets/modules/base/fab-modal.scss index fb34b42c8..777b29999 100644 --- a/app/frontend/src/stylesheets/modules/base/fab-modal.scss +++ b/app/frontend/src/stylesheets/modules/base/fab-modal.scss @@ -30,6 +30,7 @@ animation: 0.3s ease-out slideInFromTop; position: relative; top: 90px; + max-width: 100vw; margin: auto; opacity: 1; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); diff --git a/app/frontend/src/stylesheets/modules/family-account/child-form.scss b/app/frontend/src/stylesheets/modules/family-account/child-form.scss new file mode 100644 index 000000000..b1ed219d4 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/family-account/child-form.scss @@ -0,0 +1,43 @@ +.child-form { + .grp { + display: flex; + flex-direction: column; + @media (min-width: 640px) {flex-direction: row; } + + .form-item:first-child { margin-right: 2.4rem; } + } + + hr { width: 100%; } + .actions { + align-self: flex-end; + } + + .document-list { + margin-bottom: 1.6rem; + display: flex; + flex-direction: column; + gap: 1.6rem; + + &-item { + display: flex; + flex-direction: column; + gap: 0.8rem; + .type { + @include text-sm; + } + .file, + .missing { + padding: 0.8rem 0.8rem 0.8rem 1.6rem; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + p { margin: 0; } + } + .missing { + background-color: var(--gray-soft-light); + } + } + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/family-account/child-item.scss b/app/frontend/src/stylesheets/modules/family-account/child-item.scss index 40706e12b..f505a73bd 100644 --- a/app/frontend/src/stylesheets/modules/family-account/child-item.scss +++ b/app/frontend/src/stylesheets/modules/family-account/child-item.scss @@ -1,12 +1,62 @@ .child-item { width: 100%; display: grid; - grid-template-rows: repeat(3, min-content); - grid-template-columns: 1fr 1fr; + grid-template-columns: min-content 1fr; + align-items: flex-start; gap: 1.6rem 2.4rem; - align-items: center; - padding: 1.6rem; - border: 1px solid var(--gray-soft-dark); - border-radius: var(--border-radius); background-color: var(--gray-soft-lightest); + &.lg { + padding: 1.6rem; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + } + &.sm { + .actions button { + height: 3rem !important; + min-height: auto; + } + } + + & > div:not(.actions) { + display: flex; + flex-direction: column; + span { + @include text-xs; + color: var(--gray-hard-light); + } + } + p { + margin: 0; + @include text-base(600); + } + &.sm p { + @include text-sm(500); + } + + .status { + grid-row: 1/5; + align-self: stretch; + display: flex; + align-items: center; + } + &.is-validated .status svg { + color: var(--success-dark); + } + + .actions { + align-self: center; + justify-self: flex-end; + } + + @media (min-width: 768px) { + grid-template-columns: min-content repeat(3, 1fr); + .status { grid-row: auto; } + .actions { + grid-column-end: -1; + display: flex; + } + } + @media (min-width: 1024px) { + grid-template-columns: min-content repeat(3, 1fr) max-content; + } } diff --git a/app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss b/app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss new file mode 100644 index 000000000..bed5aae61 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss @@ -0,0 +1,20 @@ +.children-dashboard { + max-width: 1600px; + margin: 0 auto; + padding-bottom: 6rem; + @include grid-col(12); + gap: 3.2rem; + align-items: flex-start; + + header { + @include header(); + padding-bottom: 0; + grid-column: 2 / -2; + } + .children-list { + grid-column: 2 / -2; + display: flex; + flex-direction: column; + gap: 1.6rem; + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/form/form-file-upload.scss b/app/frontend/src/stylesheets/modules/form/form-file-upload.scss index 5707bbc45..b1bad6758 100644 --- a/app/frontend/src/stylesheets/modules/form/form-file-upload.scss +++ b/app/frontend/src/stylesheets/modules/form/form-file-upload.scss @@ -13,6 +13,8 @@ margin-bottom: 1.6rem; } + .placeholder { color: var(--gray-soft-darkest); } + .actions { margin-left: auto; display: flex; diff --git a/app/frontend/src/stylesheets/modules/members.scss b/app/frontend/src/stylesheets/modules/members.scss index 34a9209f9..dc30f4c6f 100644 --- a/app/frontend/src/stylesheets/modules/members.scss +++ b/app/frontend/src/stylesheets/modules/members.scss @@ -1,4 +1,109 @@ .promote-member img { width: 16px; height: 21px; +} + +.members-list { + width: 100%; + margin: 2.4rem 0; + display: flex; + flex-direction: column; + gap: 2.4rem; + + &-item { + width: 100%; + padding: 1.6rem; + display: grid; + grid-template-columns: 48px 1fr; + gap: 1.6rem 2.4rem; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + background-color: var(--gray-soft-lightest); + &.is-validated .left-col .status svg { color: var(--success-dark); } + &.is-incomplet .left-col .status svg { color: var(--alert); } + + .left-col { + grid-row: span 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + .status { + display: flex; + align-items: center; + } + + .toggle { + height: fit-content; + background-color: var(--gray-soft); + border: none; + svg { transition: transform 0.5s ease-in-out; } + &.open svg { transform: rotate(-180deg); } + } + } + + .member { + display: flex; + flex-direction: column; + gap: 2.4rem; + &-infos { + flex: 1; + display: grid; + gap: 1.6rem; + + & > div:not(.actions) { + display: flex; + flex-direction: column; + span { + @include text-xs; + color: var(--gray-hard-light); + } + } + p { + margin: 0; + @include text-base(600); + line-height: 1.5; + } + + } + &-actions { + align-self: flex-end; + } + } + + .member-children { + max-height: 0; + display: flex; + flex-direction: column; + gap: 1.6rem; + overflow-y: hidden; + transition: max-height 0.5s ease-in-out; + &.open { + max-height: 17rem; + overflow-y: auto; + } + + hr { margin: 0; } + .child-item:last-of-type { padding-bottom: 0; } + } + + @media (min-width: 768px) { + .member-infos { + grid-template-columns: repeat(2, 1fr); + } + } + @media (min-width: 1024px) { + .member { + flex-direction: row; + &-actions { + align-self: center; + } + } + } + @media (min-width: 1220px) { + .member-infos { + grid-template-columns: repeat(3, 1fr); + } + } + } } \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss index b9fca3534..2448c0dc3 100644 --- a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss +++ b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss @@ -37,6 +37,7 @@ } .title { + margin-bottom: 1.6rem; display: flex; flex-direction: row; justify-content: space-between; @@ -54,22 +55,26 @@ } } - table { - thead > tr { - th.group-name, - th.name { - width: 40% - } - th.actions { - width: 20%; - } - } + .document-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min-content, 50rem)); + gap: 1.6rem; - tbody { - .buttons { - .edit-btn { - margin-right: 5px; - } + &-item { + display: flex; + flex-direction: column; + gap: 0.8rem; + .type { + @include text-sm; + } + .file { + padding: 0.8rem 0.8rem 0.8rem 1.6rem; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + p { margin: 0; } } } } diff --git a/app/frontend/src/stylesheets/modules/user/user-validation.scss b/app/frontend/src/stylesheets/modules/user/user-validation.scss index 2c160f181..3c9e1b2b6 100644 --- a/app/frontend/src/stylesheets/modules/user/user-validation.scss +++ b/app/frontend/src/stylesheets/modules/user/user-validation.scss @@ -9,3 +9,7 @@ vertical-align: middle; } } +.child-validation { + margin: 0 0 2rem; + text-align: center; +} \ No newline at end of file diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index 8dbbffcdb..752bffe3a 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -63,7 +63,7 @@ - + diff --git a/app/frontend/templates/admin/members/members.html b/app/frontend/templates/admin/members/members.html index b0be69229..36d1e43a5 100644 --- a/app/frontend/templates/admin/members/members.html +++ b/app/frontend/templates/admin/members/members.html @@ -17,11 +17,12 @@
    +
    - -
    + - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {{ 'app.admin.members.username' | translate }} {{ 'app.admin.members.surname' | translate }} {{ 'app.admin.members.first_name' | translate }}
    - - {{ m.username }}{{ m.profile.last_name }}{{ m.profile.first_name }} -
    - - - {{ 'app.shared.user_admin.incomplete_profile' }} -
    -
    + +
    diff --git a/app/frontend/templates/dashboard/children.html b/app/frontend/templates/dashboard/children.html index 9f81ef21c..022cfa3a0 100644 --- a/app/frontend/templates/dashboard/children.html +++ b/app/frontend/templates/dashboard/children.html @@ -7,5 +7,5 @@
    - +
    diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index 0b4aeb048..cf44f2333 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -49,7 +49,7 @@
    -
    +
    {{event.event_files_attributes.length}}

    {{ 'app.public.events_show.downloadable_documents' }}

    @@ -72,8 +72,11 @@
    +
    + {{ 'app.public.events_show.event_type.nominative' }} + {{ 'app.public.events_show.event_type.family' }} +
    -
    {{event.category.name}}
    {{theme.name}} @@ -136,17 +139,17 @@ class="form-control"> - -

    + + {{ 'app.shared.cart.child_validation_required_alert' }} -

    +
    - -

    + + {{ 'app.shared.cart.child_birthday_must_be_under_18_years_ago_alert' }} -

    +
    diff --git a/app/frontend/templates/shared/_member_select.html b/app/frontend/templates/shared/_member_select.html index b508c96fe..730f5de4a 100644 --- a/app/frontend/templates/shared/_member_select.html +++ b/app/frontend/templates/shared/_member_select.html @@ -12,8 +12,11 @@ {{member}} -
    - {{ 'app.shared.member_select.member_not_validated' }} -
    + + + + {{ 'app.shared.member_select.member_not_validated' }} + +
    diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 140eb587e..27332ca33 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1195,6 +1195,14 @@ en: member_filter_all: "All" member_filter_not_confirmed: "Unconfirmed" member_filter_inactive_for_3_years: "Inactive for 3 years" + members_list_item: + item_type: "member" + surname: "Surname" + first_name: "First name" + phone: "Phone" + email: "Email" + group: "Group" + subscription: "Subscription" #add a member members_new: add_a_member: "Add a member" @@ -1895,7 +1903,7 @@ en: no_groups_info: "Supporting documents are necessarily applied to groups.
    If you do not have any group yet, you can create one from the \"Users/Groups\" page (button on the right)." create_groups: "Create groups" supporting_documents_type_title: "Supporting documents requests" - add_type: "New supporting documents request" + add_type: "Add new document" group_name: "Group" name: "Supporting documents" no_types: "You do not have any supporting documents requests.
    Make sure you have created at least one group in order to add a request." diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 6cd2b7157..99d6d0f84 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -318,6 +318,9 @@ en: event_description: "Event description" downloadable_documents: "Downloadable documents" information_and_booking: "Information and booking" + event_type: + family: "Family event" + nominative: "Nominative event" dates: "Dates" beginning: "Beginning:" ending: "Ending:" @@ -483,25 +486,28 @@ en: member_select: select_a_member: "Select a member" start_typing: "Start typing..." - children_list: - heading: "My children" + children_dashboard: + heading: "Children" + member_heading: "My Children" add_child: "Add a child" child_modal: edit_child: "Edit child" new_child: "New child" child_form: - child_form_info: "Note that you can only add your children under 18 years old. Supporting documents are requested by your administrator, they will be useful to validate your child's account and authorize the reservation of events." + child_form_info: "Please note that you can only add a child under the age of 18. Supporting documents are requested by your administrator, they will be useful to validate your child's account and authorize the reservation of events." first_name: "First name" last_name: "Last name" birthday: "Birthday" email: "Email" phone: "Phone" save: "Save" + supporting_documents: "Supporting documents" to_complete: "To complete" - refuse_documents: "Refusing the documents" + refuse_documents_info: "You can refuse a selection of documents by clicking on the following button." + refuse_documents: "Refuse documents" child_item: - first_name: "First name of the child" - last_name: "Last name of the child" + first_name: "Child first name" + last_name: "Child last name" birthday: "Birthday" deleted: "The child has been deleted." unable_to_delete: "Unable to delete the child." diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 3ec715aba..4f89f4df1 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -483,7 +483,7 @@ fr: member_select: select_a_member: "Sélectionnez un membre" start_typing: "Commencez à écrire..." - children_list: + children_dashboard: heading: "Mes enfants" add_child: "Ajouter un enfant" child_modal: diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 8872fea25..e2ff2a219 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -169,7 +169,7 @@ en: member_select: select_a_member: "Select a member" start_typing: "Start typing..." - member_not_validated: "Warning:
    The member was not validated." + member_not_validated: "This member has not yet been validated." #payment modal abstract_payment_modal: online_payment: "Online payment" @@ -372,9 +372,9 @@ en: slot_tags: "Slot tags" user_tags: "User tags" no_tags: "No tags" - user_validation_required_alert: "Warning!
    Your administrator must validate your account. Then, you'll then be able to access all the booking features." - child_validation_required_alert: "Warning!
    Your administrator must validate your child account. Then, you'll then be able to book the event." - child_birthday_must_be_under_18_years_ago_alert: "Warning!
    Your child must be under 18 years ago. Then, you'll then be able to book the event." + user_validation_required_alert: "Your administrator must validate your account. Then, you'll then be able to access all the booking features." + child_validation_required_alert: "Your administrator must validate your child account. Then, you'll then be able to book the event." + child_birthday_must_be_under_18_years_ago_alert: "Your child must be under 18. Then, you'll then be able to book the event." # feature-tour modal tour: previous: "Previous" @@ -447,6 +447,7 @@ en: select_all: "Select all" unselect_all: "Unselect all" form_file_upload: + placeholder: "Add a file" browse: "Browse" edit: "Edit" form_image_upload: From e392cc1803ae6f16af22b8caee6676886210fc07 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 31 May 2023 14:55:09 +0200 Subject: [PATCH 44/63] (ui) restor deleted table's style --- .../modules/base/edit-destroy-buttons.scss | 2 +- .../supporting-documents-types-list.scss | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss index 2fa61210a..949b4b028 100644 --- a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss +++ b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss @@ -1,5 +1,5 @@ .edit-destroy-buttons { - width: fit-content; + width: max-content; flex-shrink: 0; border-radius: var(--border-radius-sm); overflow: hidden; diff --git a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss index 2448c0dc3..30837f400 100644 --- a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss +++ b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss @@ -55,6 +55,18 @@ } } + table { + thead > tr { + th.group-name, + th.name { + width: 40% + } + th.actions { + width: 20%; + } + } + } + .document-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(min-content, 50rem)); @@ -83,4 +95,4 @@ text-align: center; } } -} +} \ No newline at end of file From 050ae9be6714fd4408ecfa51e23af0f16e1c4cab Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 31 May 2023 15:18:39 +0200 Subject: [PATCH 45/63] (ui) fix gap --- app/frontend/src/stylesheets/modules/members.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend/src/stylesheets/modules/members.scss b/app/frontend/src/stylesheets/modules/members.scss index dc30f4c6f..b54e92a35 100644 --- a/app/frontend/src/stylesheets/modules/members.scss +++ b/app/frontend/src/stylesheets/modules/members.scss @@ -15,7 +15,7 @@ padding: 1.6rem; display: grid; grid-template-columns: 48px 1fr; - gap: 1.6rem 2.4rem; + gap: 0 2.4rem; border: 1px solid var(--gray-soft-dark); border-radius: var(--border-radius); background-color: var(--gray-soft-lightest); @@ -83,7 +83,7 @@ overflow-y: auto; } - hr { margin: 0; } + hr { margin: 1.6rem 0 0; } .child-item:last-of-type { padding-bottom: 0; } } From a4d4107cb18a551c9328e76ff5482fe65618880a Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 31 May 2023 14:42:12 +0200 Subject: [PATCH 46/63] (i18n) update translation --- config/locales/app.public.fr.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 4f89f4df1..9a4c93af9 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -318,6 +318,9 @@ fr: event_description: "Description de l’événement" downloadable_documents: "Documents à télécharger" information_and_booking: "Informations et réservation" + event_type: + family: "Evénement famille" + nominative: "Evénement nominatif" dates: "Dates" beginning: "Début :" ending: "Fin :" @@ -497,8 +500,10 @@ fr: email: "Courriel" phone: "Téléphone" save: "Enregistrer" + supporting_documents: "Documents justificatifs" to_complete: "À compléter" refuse_documents: "Refuser les documents" + refuse_documents_info: "Vous pouvez refuser une sélection de documents en cliquant sur le bouton suivant." child_item: first_name: "Prénom de l'enfant" last_name: "Nom de l'enfant" From 26f1c939cd4e88008fe653c6fe3b7dd291adad61 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 31 May 2023 18:20:14 +0200 Subject: [PATCH 47/63] (feat) edit/delete child in members list --- app/controllers/api/children_controller.rb | 2 +- .../components/family-account/child-item.tsx | 7 ++- .../components/family-account/child-modal.tsx | 4 +- .../family-account/children-dashboard.tsx | 4 +- .../components/user/members-list-item.tsx | 41 ++++++------- .../components/user/members-list.tsx | 60 +++++++++++++++++-- .../javascript/controllers/admin/members.js | 42 ++++++++++++- app/frontend/src/javascript/models/member.ts | 2 + .../templates/admin/members/members.html | 4 +- app/views/api/members/list.json.jbuilder | 12 ++++ 10 files changed, 141 insertions(+), 37 deletions(-) diff --git a/app/controllers/api/children_controller.rb b/app/controllers/api/children_controller.rb index bbd1d8e04..ac1d98b78 100644 --- a/app/controllers/api/children_controller.rb +++ b/app/controllers/api/children_controller.rb @@ -10,7 +10,7 @@ class API::ChildrenController < API::APIController authorize Child user_id = current_user.id user_id = params[:user_id] if current_user.privileged? && params[:user_id] - @children = Child.where(user_id: user_id) + @children = Child.where(user_id: user_id).includes(:supporting_document_files).order(:created_at) end def show diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx index 62de02e9e..4192d2c04 100644 --- a/app/frontend/src/javascript/components/family-account/child-item.tsx +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -11,7 +11,7 @@ interface ChildItemProps { child: Child; size: 'sm' | 'lg'; onEdit: (child: Child) => void; - onDelete: (error: string) => void; + onDelete: (child: Child, error: string) => void; onError: (error: string) => void; } @@ -33,8 +33,9 @@ export const ChildItem: React.FC = ({ child, size, onEdit, onDel const deleteChild = () => { ChildAPI.destroy(child.id).then(() => { toggleDeleteChildModal(); - onDelete(t('app.public.child_item.deleted')); - }).catch(() => { + onDelete(child, t('app.public.child_item.deleted')); + }).catch((e) => { + console.error(e); onError(t('app.public.child_item.unable_to_delete')); }); }; diff --git a/app/frontend/src/javascript/components/family-account/child-modal.tsx b/app/frontend/src/javascript/components/family-account/child-modal.tsx index acc75e9f4..f950e207e 100644 --- a/app/frontend/src/javascript/components/family-account/child-modal.tsx +++ b/app/frontend/src/javascript/components/family-account/child-modal.tsx @@ -13,7 +13,7 @@ interface ChildModalProps { operator: User; isOpen: boolean; toggleModal: () => void; - onSuccess: (msg: string) => void; + onSuccess: (child: Child, msg: string) => void; onError: (error: string) => void; supportingDocumentsTypes: Array; } @@ -35,7 +35,7 @@ export const ChildModal: React.FC = ({ child, isOpen, toggleMod await ChildAPI.create(data); } toggleModal(); - onSuccess(''); + onSuccess(data, ''); } catch (error) { onError(error); } diff --git a/app/frontend/src/javascript/components/family-account/children-dashboard.tsx b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx index 30d950068..c9f856d58 100644 --- a/app/frontend/src/javascript/components/family-account/children-dashboard.tsx +++ b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx @@ -70,7 +70,7 @@ export const ChildrenDashboard: React.FC = ({ user, oper /** * Delete a child */ - const handleDeleteChildSuccess = (msg: string) => { + const handleDeleteChildSuccess = (_child: Child, msg: string) => { ChildAPI.index({ user_id: user.id }).then(setChildren); onSuccess(msg); }; @@ -78,7 +78,7 @@ export const ChildrenDashboard: React.FC = ({ user, oper /** * Handle save child success from the API */ - const handleSaveChildSuccess = (msg: string) => { + const handleSaveChildSuccess = (_data: Child, msg: string) => { ChildAPI.index({ user_id: user.id }).then(setChildren); if (msg) { onSuccess(msg); diff --git a/app/frontend/src/javascript/components/user/members-list-item.tsx b/app/frontend/src/javascript/components/user/members-list-item.tsx index 19041b4e9..4d0bea4f0 100644 --- a/app/frontend/src/javascript/components/user/members-list-item.tsx +++ b/app/frontend/src/javascript/components/user/members-list-item.tsx @@ -1,32 +1,28 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Member } from '../../models/member'; import { Child } from '../../models/child'; -import ChildAPI from '../../api/child'; import { FabButton } from '../base/fab-button'; -import { CaretDown, User, Users } from 'phosphor-react'; +import { CaretDown, User, Users, PencilSimple, Trash } from 'phosphor-react'; import { ChildItem } from '../family-account/child-item'; -import { EditDestroyButtons } from '../base/edit-destroy-buttons'; interface MembersListItemProps { member: Member, onError: (message: string) => void, onSuccess: (message: string) => void + onEditChild: (child: Child) => void; + onDeleteChild: (child: Child, error: string) => void; + onDeleteMember: (memberId: number) => void; } /** * Members list */ -export const MembersListItem: React.FC = ({ member, onError, onSuccess }) => { +export const MembersListItem: React.FC = ({ member, onError, onEditChild, onDeleteChild, onDeleteMember }) => { const { t } = useTranslation('admin'); - const [children, setChildren] = useState>([]); const [childrenList, setChildrenList] = useState(false); - useEffect(() => { - ChildAPI.index({ user_id: member.id }).then(setChildren); - }, [member]); - /** * Redirect to the given user edition page */ @@ -38,12 +34,12 @@ export const MembersListItem: React.FC = ({ member, onErro
    - {(children.length > 0) + {(member.children.length > 0) ? : }
    - {(children.length > 0) && + {(member.children.length > 0) && setChildrenList(!childrenList)} className={`toggle ${childrenList ? 'open' : ''}`}> @@ -78,22 +74,21 @@ export const MembersListItem: React.FC = ({ member, onErro
    -
    - {/* TODO: */} - toMemberEdit(member.id)} - onDeleteSuccess={() => onSuccess} - itemId={member.id} - itemType={t('app.admin.members_list_item.item_type')} - destroy={() => new Promise(() => console.log(`Delete member ${member.id}`))} /> +
    + toMemberEdit(member.id)} className="edit-btn"> + + + onDeleteMember(member.id)} className="delete-btn"> + +
    - { (children.length > 0) && + { (member.children.length > 0) &&

    - {children.map(child => ( - console.log('edit child')} onDelete={() => console.log('delete child')} onError={onError} /> + {member.children.map((child: Child) => ( + ))}
    } diff --git a/app/frontend/src/javascript/components/user/members-list.tsx b/app/frontend/src/javascript/components/user/members-list.tsx index 939db4632..ad911c0a0 100644 --- a/app/frontend/src/javascript/components/user/members-list.tsx +++ b/app/frontend/src/javascript/components/user/members-list.tsx @@ -1,27 +1,79 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { IApplication } from '../../models/application'; import { Loader } from '../base/loader'; import { react2angular } from 'react2angular'; import { Member } from '../../models/member'; import { MembersListItem } from './members-list-item'; +import { SupportingDocumentType } from '../../models/supporting-document-type'; +import SupportingDocumentTypeAPI from '../../api/supporting-document-type'; +import { Child } from '../../models/child'; +import { ChildModal } from '../family-account/child-modal'; +import { User } from '../../models/user'; declare const Application: IApplication; interface MembersListProps { members: Member[], + operator: User, onError: (message: string) => void, onSuccess: (message: string) => void + onDeleteMember: (memberId: number) => void; + onDeletedChild: (memberId: number, childId: number) => void; + onUpdatedChild: (memberId: number, child: Child) => void; } /** * Members list */ -export const MembersList: React.FC = ({ members, onError, onSuccess }) => { +export const MembersList: React.FC = ({ members, onError, onSuccess, operator, onDeleteMember, onDeletedChild, onUpdatedChild }) => { + const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState>([]); + const [child, setChild] = useState(); + const [isOpenChildModal, setIsOpenChildModal] = useState(false); + + useEffect(() => { + SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => { + setSupportingDocumentsTypes(tData); + }); + }, []); + + /** + * Open the edit child modal + */ + const editChild = (child: Child) => { + setIsOpenChildModal(true); + setChild({ + ...child, + supporting_document_files_attributes: supportingDocumentsTypes.map(t => { + const file = child.supporting_document_files_attributes.find(f => f.supporting_document_type_id === t.id); + return file || { supporting_document_type_id: t.id }; + }) + } as Child); + }; + + /** + * Delete a child + */ + const handleDeleteChildSuccess = (c: Child, msg: string) => { + onDeletedChild(c.user_id, c.id); + onSuccess(msg); + }; + + /** + * Handle save child success from the API + */ + const handleSaveChildSuccess = (c: Child, msg: string) => { + onUpdatedChild(c.user_id, c); + if (msg) { + onSuccess(msg); + } + }; + return (
    {members.map(member => ( - + ))} + setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} />
    ); }; @@ -34,4 +86,4 @@ const MembersListWrapper: React.FC = (props) => { ); }; -Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess'])); +Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess', 'operator', 'onDeleteMember', 'onDeletedChild', 'onUpdatedChild'])); diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index e26a3da48..e7b388827 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -291,7 +291,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', Member.delete( { id: memberId }, function () { - $scope.members.splice(findItemIdxById($scope.members, memberId), 1); + $scope.members = _.filter($scope.members, function (m) { return m.id !== memberId; }); return growl.success(_t('app.admin.members.member_successfully_deleted')); }, function (error) { @@ -303,6 +303,32 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', ); }; + $scope.onDeletedChild = function (memberId, childId) { + $scope.members = $scope.members.map(function (member) { + if (member.id === memberId) { + member.children = _.filter(member.children, function (c) { return c.id !== childId; }); + return member; + } + return member; + }); + }; + + $scope.onUpdatedChild = function (memberId, child) { + $scope.members = $scope.members.map(function (member) { + if (member.id === memberId) { + member.children = member.children.map(function (c) { + if (c.id === child.id) { + return child; + } + return c; + }); + console.log(member.children); + return member; + } + return member; + }); + }; + /** * Ask for confirmation then delete the specified administrator * @param admins {Array} full list of administrators @@ -588,6 +614,20 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', } }; + /** + * Callback triggered in case of error + */ + $scope.onError = (message) => { + growl.error(message); + }; + + /** + * Callback triggered in case of success + */ + $scope.onSuccess = (message) => { + growl.success(message); + }; + /* PRIVATE SCOPE */ /** diff --git a/app/frontend/src/javascript/models/member.ts b/app/frontend/src/javascript/models/member.ts index b163c3814..3c9e8cad2 100644 --- a/app/frontend/src/javascript/models/member.ts +++ b/app/frontend/src/javascript/models/member.ts @@ -1,4 +1,5 @@ import { TDateISO } from '../typings/date-iso'; +import { Child } from './child'; export interface Member { maxMembers: number @@ -16,6 +17,7 @@ export interface Member { } subscribed_plan?: Plan validated_at: TDateISO + children: Child[] } interface Plan { diff --git a/app/frontend/templates/admin/members/members.html b/app/frontend/templates/admin/members/members.html index 36d1e43a5..22fe77590 100644 --- a/app/frontend/templates/admin/members/members.html +++ b/app/frontend/templates/admin/members/members.html @@ -35,7 +35,9 @@
    - +
    + +
    diff --git a/app/views/api/members/list.json.jbuilder b/app/views/api/members/list.json.jbuilder index 38dd1b1a2..97840a4b8 100644 --- a/app/views/api/members/list.json.jbuilder +++ b/app/views/api/members/list.json.jbuilder @@ -18,4 +18,16 @@ json.array!(@members) do |member| end end json.validated_at member.validated_at + json.children member.children.order(:created_at) do |child| + json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id, :validated_at + json.supporting_document_files_attributes child.supporting_document_files do |f| + json.id f.id + json.supportable_id f.supportable_id + json.supportable_type f.supportable_type + json.supporting_document_type_id f.supporting_document_type_id + json.attachment f.attachment.file&.filename + json.attachment_name f.attachment_identifier + json.attachment_url f.attachment_identifier ? "/api/supporting_document_files/#{f.id}/download" : nil + end + end end From b7bcce75f3b4ebd52c2881ef36b64dfc12b2c943 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 31 May 2023 18:26:08 +0200 Subject: [PATCH 48/63] (bug) hide member and child select if admin dont select a member before reserve event --- app/frontend/templates/events/show.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index cf44f2333..66f217f80 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -128,7 +128,7 @@
    -
    +
    Date: Tue, 13 Jun 2023 11:38:06 +0200 Subject: [PATCH 49/63] (wip) add pre-registration to event --- app/controllers/api/events_controller.rb | 1 + .../javascript/components/events/event-form.tsx | 14 ++++++++++++++ app/frontend/src/javascript/models/event.ts | 2 ++ app/frontend/templates/events/show.html | 4 ++-- app/policies/local_payment_policy.rb | 3 ++- app/views/api/events/_event.json.jbuilder | 2 +- config/locales/app.admin.en.yml | 3 +++ config/locales/app.admin.fr.yml | 3 +++ ...20230612123250_add_pre_registration_to_event.rb | 9 +++++++++ db/structure.sql | 7 +++++-- 10 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20230612123250_add_pre_registration_to_event.rb diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index 4d05f6b56..781e170ba 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -97,6 +97,7 @@ class API::EventsController < API::APIController event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time, :amount, :nb_total_places, :availability_id, :all_day, :recurrence, :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :event_type, + :pre_registration, :pre_registration_end_date, event_theme_ids: [], event_image_attributes: %i[id attachment], event_files_attributes: %i[id attachment _destroy], diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 5ed6a7036..0d8c0b81b 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -55,6 +55,7 @@ export const EventForm: React.FC = ({ action, event, onError, on const [updatingEvent, setUpdatingEvent] = useState(null); const [isActiveAccounting, setIsActiveAccounting] = useState(false); const [isActiveFamilyAccount, setIsActiveFamilyAccount] = useState(false); + const [isAcitvePreRegistration, setIsActivePreRegistration] = useState(event?.pre_registration); useEffect(() => { EventCategoryAPI.index() @@ -241,6 +242,19 @@ export const EventForm: React.FC = ({ action, event, onError, on formState={formState} options={ageRangeOptions} label={t('app.admin.event_form.age_range')} />} + + {isAcitvePreRegistration && + + }
    diff --git a/app/frontend/src/javascript/models/event.ts b/app/frontend/src/javascript/models/event.ts index 892ca95dc..7f72b1974 100644 --- a/app/frontend/src/javascript/models/event.ts +++ b/app/frontend/src/javascript/models/event.ts @@ -66,6 +66,8 @@ export interface Event { recurrence_end_at: Date, advanced_accounting_attributes?: AdvancedAccounting, event_type: EventType, + pre_registration?: boolean, + pre_registration_end_date?: TDateISODate | Date, } export interface EventDecoration { diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index 66f217f80..6a3c9652e 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -255,11 +255,11 @@
    -