From f81cbc72fa850c0c45e6d99d7f116bad1d9602d3 Mon Sep 17 00:00:00 2001 From: vincent Date: Fri, 1 Jul 2022 09:30:45 +0200 Subject: [PATCH 001/141] Increase About page title's size --- app/frontend/src/stylesheets/app.components.scss | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/frontend/src/stylesheets/app.components.scss b/app/frontend/src/stylesheets/app.components.scss index 7e332674c..c3334eda1 100644 --- a/app/frontend/src/stylesheets/app.components.scss +++ b/app/frontend/src/stylesheets/app.components.scss @@ -408,9 +408,10 @@ .about-title, .about-title p { margin: 0; font-size: rem-calc(50); - line-height: rem-calc(48); + line-height: rem-calc(56); color: #fff; font-weight: 900; + text-align: left; } .about-title-aside { @@ -418,14 +419,6 @@ font-size: rem-calc(18); } - .about-title, - .about-title p - { - font-size: rem-calc(18); - line-height: rem-calc(30); - text-align: left; - } - &.ng-hide { opacity: 0; From eb1c54d8f3873f2101144ebe6c579fb0894f34be Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 4 Jul 2022 10:48:42 +0200 Subject: [PATCH 002/141] updated diagrams --- app/models/slot.rb | 11 +- doc/class-diagram.svg | 27790 +++++++++++++++++++++------------------- doc/database.svg | 24893 ++++++++++++++++++++--------------- 3 files changed, 28964 insertions(+), 23730 deletions(-) diff --git a/app/models/slot.rb b/app/models/slot.rb index 20f458be5..180c4231b 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true -# Time range, slicing an Availability. -# Its duration is defined by globally by Setting.get('slot_duration') but can be overridden per availability -# During a slot a Reservation is possible -# Only reserved slots are persisted in DB, others are instantiated on the fly +# A Time range. +# Slots have two functions: +# - slicing an Availability +# > Slots duration are defined globally by Setting.get('slot_duration') but can be overridden per availability. +# > These slots are not persisted in database and instantiated on the fly, if needed. +# - hold detailed data about a Reservation. +# > Persisted slots (in DB) represents booked slots and stores data about a time range that have been reserved. class Slot < ApplicationRecord include NotifyWith::NotificationAttachedObject diff --git a/doc/class-diagram.svg b/doc/class-diagram.svg index 60658d114..54c693e9c 100644 --- a/doc/class-diagram.svg +++ b/doc/class-diagram.svg @@ -1,20921 +1,21913 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + - - + + - - + + - - + + - - + + PlanFile - - AuthProvider + + + - - - + + - - + + plan_file - - auth_provider + + - - + + - - DatabaseProvider + + - - - + + string - - + + + - - providable + + - - - + + attachment - - - + + - - + + string - - DatabaseProvider + + + - - + + - - + + type - - + + - - + + integer - - ProjectStepImage + + + - - - + + - - + + viewable_id - - project_step_images + + - - + + string - - ProjectStep + + + - - - + + - - + + viewable_type - - project_steps + + + - - + + + - - + + - - + + PlanFile - - text + + - - - + + - - + + - - description + + - - + + ProjectImage - - integer + + + - - - + + - - + + project_image - - step_nb + + - - + + - - string + + - - - + + string - - + + + - - title + + - - - + + attachment - - - + + - - + + string - - ProjectStep + + + - - + + - - + + type - - + + - - + + integer - - ProjectUser + + + - - - + + - - + + viewable_id - - project_users + + - - + + string - - + + + - - + + - - boolean + + viewable_type - - - + + + - - + + + - - is_valid + + - - + + ProjectImage - - string + + - - - + + - - + + - - valid_token + + - - - + + StatisticGraph - - - + + + - - + + - - ProjectUser + + statistic_graph - - + + - - + + StatisticIndex - - + + + - - + + - - Import + + statistic_index - - - + + - - + + - - imports + + - - + + string - - + + + - - + + - - string + + chart_type - - - + + - - + + integer - - attachment + + + - - + + - - string + + limit - - - + + + - - + + + - - category + + - - + + StatisticGraph - - text + + - - - + + - - + + - - results + + - - + + Credit - - string + + + - - - + + - - + + credit - - update_field + + - - - + + Credit - - - + + + - - + + - - Import + + space_credit - - + + - - + + Credit - - + + + - - Invoice + + - - - + + training_credit - - + + - - avoir + + UsersCredit - - + + + - - Invoice + + - - - + + users_credits - - + + - - invoice + + - - + + - - InvoiceItem + + integer - - - + + + - - + + - - invoice_items + + hours_used - - + + + - - Invoice + + + - - - + + - - + + UsersCredit - - invoices + + - - + + - - InvoicingProfile + + - - - + + - - + + SpaceImage - - invoicing_profile + + + - - + + - - Invoice + + space_image - - - + + - - + + - - operated_invoices + + - - + + string - - InvoicingProfile + + + - - - + + - - + + attachment - - operator_profile + + - - + + string - - PaymentGatewayObject + + + - - - + + - - + + type - - payment_gateway_object + + - - + + integer - - PaymentScheduleItem + + + - - - + + - - + + viewable_id - - payment_schedule_item + + - - + + string - - StatisticProfile + + + - - - + + - - + + viewable_type - - statistic_profile + + + - - + + + - - WalletTransaction + + - - - + + SpaceImage - - + + - - wallet_transaction + + - - + + - - + + - - + + Availability - - datetime + + + - - - + + - - + + availabilities - - avoir_date + + - - + + AvailabilityTag - - text + + + - - - + + - - + + availability_tags - - description + + - - + + UserTag - - string + + + - - - + + - - + + user_tags - - environment + + - - + + User - - string + + + - - - + + - - + + users - - footprint + + - - + + - - integer + + - - - + + string - - + + + - - invoiced_id + + - - + + name - - string + + + - - - + + + - - + + - - invoiced_type + + Tag - - + + - - integer + + - - - + + - - + + - - operator_profile_id + + Price - - + + + - - string + + - - - + + machines_prices - - + + - - payment_method + + Space - - + + + - - string + + - - - + + priceable - - + + - - reference + + Price - - + + + - - boolean + + - - - + + prices - - + + - - subscription_to_expire + + Price - - + + + - - integer + + - - - + + spaces_prices - - + + - - total + + - - + + - - string + + integer - - - + + + - - + + - - type + + amount - - + + - - integer + + integer - - - + + + - - + + - - wallet_amount + + duration - - - + + - - - + + integer - - + + + - - Invoice + + - - + + priceable_id - - + + - - + + string - - Plan + + + - - - + + - - + + priceable_type - - plan + + + - - + + + - - PlansAvailability + + - - - + + Price - - + + - - plans_availabilities + + - - - + + - - - + + - - + + Component - - PlansAvailability + + + - - + + - - + + components - - + + - - + + - - Address + + - - - + + string - - + + + - - address + + - - + + name - - InvoicingProfile + + + - - - + + + - - + + - - placeable + + Component - - + + - - + + - - + + - - string + + - - - + + ProofOfIdentityFile - - + + + - - address + + - - + + proof_of_identity_files - - string + + - - - + + ProofOfIdentityType - - + + + - - country + + - - + + proof_of_identity_type - - string + + - - - + + ProofOfIdentityType - - + + + - - locality + + - - + + proof_of_identity_types - - integer + + - - - + + ProofOfIdentityTypesGroup - - + + + - - placeable_id + + - - + + proof_of_identity_types_groups - - string + + - - - + + - - + + - - placeable_type + + string - - + + + - - string + + - - - + + name - - + + + - - postal_code + + + - - + + - - string + + ProofOfIdentityType - - - + + - - + + - - route + + - - + + - - string + + Address - - - + + + - - + + - - street_number + + address - - - + + - - - + + HistoryValue - - + + + - - Address + + - - + + history_values - - + + - - + + Invoice - - SlotsReservation + + + - - - + + - - + + invoices - - slots_reservations + + - - - + + InvoicingProfile - - - + + + - - + + - - SlotsReservation + + invoicing_profile - - + + - - + + Invoice - - + + + - - TrainingsAvailability + + - - - + + operated_invoices - - + + - - trainings_availabilities + + PaymentSchedule - - - + + + - - - + + - - + + operated_payment_schedules - - TrainingsAvailability + + - - + + InvoicingProfile - - + + + - - + + - - InvoiceItem + + operator_profile - - - + + - - + + Organization - - invoice_item + + + - - + + - - PaymentGatewayObject + + organization - - - + + - - + + PaymentSchedule - - payment_gateway_object + + + - - + + - - PaymentGatewayObject + + payment_schedules - - - + + - - + + Organization - - payment_gateway_objects + + + - - + + - - PaymentSchedule + + placeable - - - + + - - + + ProfileCustomField - - payment_schedule + + + - - + + - - PaymentScheduleItem + + profile_custom_fields - - - + + - - + + UserProfileCustomField - - payment_schedule_item + + + - - + + - - Plan + + user_profile_custom_fields - - - + + - - + + Wallet - - plan + + + - - + + - - Space + + wallet - - - + + - - + + WalletTransaction - - space + + + - - + + - - Subscription + + wallet_transactions - - - + + - - + + - - subscription + + - - + + string - - Training + + + - - - + + - - + + email - - training + + - - + + string - - User + + + - - - + + - - + + first_name - - user + + - - + + string - - + + + - - + + - - string + + last_name - - - + + + - - + + + - - gateway_object_id + + - - + + InvoicingProfile - - string + + - - - + + - - + + - - gateway_object_type + + - - + + Role - - integer + + + - - - + + - - + + role - - item_id + + - - + + User - - string + + + - - - + + - - + + users - - item_type + + - - - + + - - - + + - - + + string - - PaymentGatewayObject + + + - - + + - - + + name - - + + - - HistoryValue + + integer - - - + + + - - + + - - history_values + + resource_id - - + + - - InvoicingProfile + + string - - - + + + - - + + - - invoicing_profile + + resource_type - - + + + - - + + + - - + + - - string + + Role - - - + + - - + + - - footprint + + - - + + - - string + + InvoiceItem - - - + + + - - + + - - value + + invoice_item - - - + + - - - + + InvoiceItem - - + + + - - HistoryValue + + - - + + invoice_items - - + + - - + + PaymentGatewayObject - - + + + - - TrainingImage + + - - - + + payment_gateway_object - - + + - - training_image + + - - + + - - + + integer - - + + + - - string + + - - - + + amount - - + + - - attachment + + text - - + + + - - string + + - - - + + description - - + + - - type + + string - - + + + - - integer + + - - - + + footprint - - + + - - viewable_id + + boolean - - + + + - - string + + - - - + + main - - + + - - viewable_type + + bigint - - - + + + - - - + + - - + + object_id - - TrainingImage + + - - + + string - - + + + - - - + + - - + + object_type - - PaymentDocument + + + - - + + + - - + + - - + + InvoiceItem - - + + - - Availability + + + - - - + + - - + + FreeExtension - - availabilities + + - - + + - - Credit + + - - - + + - - + + AuthProvider - - credits + + + - - + + - - PaymentGatewayObject + + auth_provider - - - + + - - + + DatabaseProvider - - payment_gateway_object + + + - - + + - - Plan + + providable - - - + + + - - + + + - - plans + + - - + + DatabaseProvider - - Reservation + + - - - + + - - + + - - reservations + + - - + + CustomAssetFile - - StatisticProfileTraining + + + - - - + + - - + + custom_asset_file - - statistic_profile_trainings + + - - + + - - StatisticProfile + + - - - + + string - - + + + - - statistic_profiles + + - - + + name - - Training + + + - - - + + + - - + + - - training + + CustomAsset - - + + - - TrainingImage + + - - - + + - - + + - - training_image + + StatisticProfilePrepaidPack - - + + + - - Training + + - - - + + object - - + + - - trainings + + OfferDay - - + + + - - TrainingsAvailability + + - - - + + offer_day - - + + - - trainings_availabilities + + PaymentScheduleObject - - + + + - - TrainingsPricing + + - - - + + payment_schedule_object - - + + - - trainings_pricings + + PaymentScheduleObject - - + + + - - + + - - + + payment_schedule_objects - - text + + - - - + + StatisticProfilePrepaidPack - - + + + - - description + + - - + + statistic_profile_prepaid_pack - - boolean + + - - - + + WalletTransaction - - + + + - - disabled + + - - + + wallet_transaction - - string + + - - - + + - - + + - - name + + string - - + + + - - integer + + - - - + + footprint - - + + - - nb_total_places + + boolean - - + + + - - boolean + + - - - + + main - - + + - - public_page + + bigint - - + + + - - string + + - - - + + object_id - - + + - - slug + + string - - + + + - - string + + - - - + + object_type - - + + + - - stp_product_id + + + - - - + + - - - + + PaymentScheduleObject - - + + - - Training + + - - + + - - + + - - + + ProjectStepImage - - + + + - - CustomAssetFile + + - - - + + project_step_images - - + + - - custom_asset_file + + - - + + - - + + string - - + + + - - string + + - - - + + attachment - - + + - - name + + string - - - + + + - - - + + - - + + type - - CustomAsset + + - - + + integer - - + + + - - + + - - + + viewable_id - - SpaceFile + + - - - + + string - - + + + - - space_files + + - - + + viewable_type - - + + + - - + + + - - string + + - - - + + ProjectStepImage - - + + - - attachment + + - - + + - - string + + - - - + + Address - - + + + - - type + + - - + + address - - integer + + - - - + + Organization - - + + + - - viewable_id + + - - + + placeable - - string + + - - - + + - - + + - - viewable_type + + string - - - + + + - - - + + - - + + address - - SpaceFile + + - - + + string - - + + + - - + + - - StatisticTypeSubType + + country - - - + + - - + + string - - statistic_type_sub_types + + + - - - + + - - - + + locality - - + + - - StatisticTypeSubType + + integer - - + + + - - + + - - + + placeable_id - - + + - - Project + + string - - - + + + - - + + - - projects + + placeable_type - - + + - - + + string - - + + + - - text + + - - - + + postal_code - - + + - - description + + string - - + + + - - string + + - - - + + route - - + + - - name + + string - - - + + + - - - + + - - + + street_number - - Licence + + + - - + + + - - + + - - + + Address - - + + - - string + + - - - + + - - + + - - attachment + + Project - - + + + - - string + + - - - + + projects - - + + - - type + + - - + + - - integer + + text - - - + + + - - + + - - viewable_id + + description - - + + - - string + + string - - - + + + - - + + - - viewable_type + + name - - - + + + - - - + + + - - + + - - Asset + + Licence - - + + - - + + - - + + - - ICalendarEvent + + - - - + + Availability - - + + + - - i_calendar_events + + - - + + availabilities - - + + - - + + Credit - - string + + + - - - + + - - + + credits - - color + + - - + + Machine - - string + + + - - - + + - - + + machine - - name + + - - + + MachineFile - - string + + + - - - + + - - + + machine_files - - text_color + + - - + + MachineImage - - boolean + + + - - - + + - - + + machine_image - - text_hidden + + - - + + MachinesAvailability - - string + + + - - - + + - - + + machines_availabilities - - url + + - - - + + PaymentGatewayObject - - - + + + - - + + - - ICalendar + + payment_gateway_object - - + + - - + + Plan - - + + + - - + + - - InvoiceItem + + plans - - - + + - - + + PrepaidPack - - invoice_items + + + - - + + - - OfferDay + + prepaid_packs - - - + + - - + + Space - - object + + + - - + + - - OfferDay + + priceable - - - + + - - + + Price - - offer_days + + + - - + + - - PaymentGatewayObject + + prices - - - + + - - + + Project - - payment_gateway_object + + + - - + + - - PaymentScheduleObject + + projects - - - + + - - + + Reservation - - payment_schedule_object + + + - - + + - - StatisticProfile + + reservations - - - + + - - + + Training - - statistic_profile + + + - - + + - - Subscription + + trainings - - - + + - - + + - - subscription + + - - + + text - - Subscription + + + - - - + + - - + + description - - subscriptions + + - - + + boolean - - + + + - - + + - - datetime + + disabled - - - + + - - + + string - - canceled_at + + + - - + + - - datetime + + name - - - + + - - + + string - - expiration_date + + + - - - + + - - - + + slug - - + + - - Subscription + + text - - + + + - - + + - - + + spec - - + + + - - EventPriceCategory + + + - - - + + - - + + Machine - - event_price_categories + + - - + + - - EventPriceCategory + + - - - + + - - + + TrainingsAvailability - - event_price_category + + + - - + + - - Ticket + + trainings_availabilities - - - + + + - - + + + - - tickets + + - - + + TrainingsAvailability - - + + - - + + - - integer + + - - - + + - - + + ProjectUser - - amount + + + - - - + + - - - + + project_users - - + + - - EventPriceCategory + + - - + + - - + + boolean - - + + + - - + + - - ProjectStepImage + + is_valid - - - + + - - + + string - - project_step_images + + + - - + + - - + + valid_token - - + + + - - string + + + - - - + + - - + + ProjectUser - - attachment + + - - + + - - string + + - - - + + - - + + EventPriceCategory - - type + + + - - + + - - integer + + event_price_category - - - + + - - + + PriceCategory - - viewable_id + + + - - + + - - string + + price_categories - - - + + - - + + - - viewable_type + + - - - + + text - - - + + + - - + + - - ProjectStepImage + + conditions - - + + - - + + string - - + + + - - Availability + + - - - + + name - - + + + - - availabilities + + + - - + + - - Credit + + PriceCategory - - - + + - - + + - - credits + + - - + + - - PaymentGatewayObject + + SpacesAvailability - - - + + + - - + + - - payment_gateway_object + + spaces_availabilities - - + + + - - Machine + + + - - - + + - - + + SpacesAvailability - - priceable + + - - + + - - Price + + - - - + + - - + + StatisticSubType - - prices + + + - - + + - - Project + + statistic_sub_types - - - + + - - + + StatisticTypeSubType - - projects + + + - - + + - - Reservation + + statistic_type_sub_types - - - + + - - + + - - reservations + + - - + + string - - Space + + + - - - + + - - + + key - - space + + - - + + string - - SpaceFile + + + - - - + + - - + + label - - space_files + + + - - + + + - - SpaceImage + + - - - + + StatisticSubType - - + + - - space_image + + - - + + - - SpacesAvailability + + - - - + + AuthProvider - - + + + - - spaces_availabilities + + - - + + auth_provider - - + + - - + + DatabaseProvider - - text + + + - - - + + - - + + providable - - characteristics + + - - + + - - integer + + - - - + + string - - + + + - - default_places + + - - + + client__authorization_endpoint - - text + + - - - + + string - - + + + - - description + + - - + + client__end_session_endpoint - - boolean + + - - - + + string - - + + + - - disabled + + - - + + client__host - - string + + - - - + + string - - + + + - - name + + - - + + client__identifier - - string + + - - - + + string - - + + + - - slug + + - - + + client__jwks_uri - - string + + - - - + + string - - + + + - - stp_product_id + + - - - + + client__port - - - + + - - + + string - - Space + + + - - + + - - + + client__redirect_uri - - + + - - + + string - - MachineFile + + + - - - + + - - + + client__scheme - - machine_files + + - - + + string - - + + + - - + + - - string + + client__secret - - - + + - - + + string - - attachment + + + - - + + - - string + + client__token_endpoint - - - + + - - + + string - - type + + + - - + + - - integer + + client__userinfo_endpoint - - - + + - - + + string - - viewable_id + + + - - + + - - string + + client_auth_method - - - + + - - + + boolean - - viewable_type + + + - - - + + - - - + + discovery - - + + - - MachineFile + + string - - + + + - - + + - - + + display - - AvailabilityTag + + - - - + + string - - + + + - - availability_tags + + - - - + + issuer - - - + + - - + + string - - AvailabilityTag + + + - - + + - - + + post_logout_redirect_uri - - + + - - datetime + + string - - - + + + - - + + - - closed_at + + profile_url - - + + - - integer + + string - - - + + + - - + + - - closed_by + + prompt - - + + - - date + + string - - - + + + - - + + - - end_at + + response_mode - - + + - - string + + string - - - + + + - - + + - - footprint + + response_type - - + + - - integer + + string - - - + + + - - + + - - period_total + + scope - - + + - - integer + + boolean - - - + + + - - + + - - perpetual_total + + send_scope_to_token_endpoint - - + + - - date + + string - - - + + + - - + + - - start_at + + uid_field - - - + + + - - - + + + - - + + - - AccountingPeriod + + OpenIdConnectProvider - - + + - - + + + - - + + - - StatisticField + + MachineReservation - - - + + - - + + - - statistic_fields + + - - + + - - StatisticIndex + + ICalendarEvent - - - + + + - - + + - - statistic_index + + i_calendar_events - - + + - - + + - - + + - - string + + string - - - + + + - - + + - - data_type + + attendee - - + + - - string + + string - - - + + + - - + + - - key + + description - - + + - - string + + datetime - - - + + + - - + + - - label + + dtend - - - + + - - - + + datetime - - + + + - - StatisticField + + - - + + dtstart - - + + - - + + string - - text + + + - - - + + - - + + summary - - contents + + - - + + string - - string + + + - - - + + - - + + uid - - name + + + - - - + + + - - - + + - - + + ICalendarEvent - - Stylesheet + + - - + + + - - + + - - + + SpaceReservation - - + + - - OfferDay + + - - - + + - - + + - - object + + StatisticProfileTraining - - + + + - - OfferDay + + - - - + + statistic_profile_trainings - - + + + - - offer_day + + + - - + + - - PaymentScheduleObject + + StatisticProfileTraining - - - + + - - + + - - payment_schedule_object + + - - + + - - PaymentScheduleObject + + Category - - - + + + - - + + - - payment_schedule_objects + + category - - + + - - WalletTransaction + + Event - - - + + + - - + + - - wallet_transaction + + events - - + + - - + + - - + + - - string + + string - - - + + + - - + + - - footprint + + name - - + + - - boolean + + string - - - + + + - - + + - - main + + slug - - + + + - - integer + + + - - - + + - - + + Category - - object_id + + - - + + - - string + + - - - + + - - + + string - - object_type + + + - - - + + - - - + + email - - + + - - PaymentScheduleObject + + string - - + + + - - + + - - + + first_name - - StatisticProfile + + - - - + + string - - + + + - - author + + - - + + last_name - - Component + + - - - + + text - - + + + - - components + + - - + + message - - Project + + - - - + + integer - - + + + - - my_projects + + - - + + signaled_id - - ProjectCao + + - - - + + string - - + + + - - project_caos + + - - + + signaled_type - - ProjectImage + + + - - - + + + - - + + - - project_image + + Abuse - - + + - - ProjectStep + + + - - - + + - - + + Project - - project_steps + + - - + + - - ProjectUser + + - - - + + - - + + StatisticCustomAggregation - - project_users + + + - - + + - - Project + + statistic_custom_aggregations - - - + + - - + + StatisticIndex - - projects + + + - - + + - - + + statistic_index - - + + - - integer + + StatisticSubType - - - + + + - - + + - - author_statistic_profile_id + + statistic_sub_types - - + + - - text + + StatisticTypeSubType - - - + + + - - + + - - description + + statistic_type_sub_types - - + + - - string + + StatisticType - - - + + + - - + + - - name + + statistic_types - - + + - - datetime + + - - - + + - - + + boolean - - published_at + + + - - + + - - tsvector + + graph - - - + + - - + + string - - search_vector + + + - - + + - - string + + key - - - + + - - + + string - - slug + + + - - + + - - string + + label - - - + + - - + + boolean - - state + + + - - + + - - text + + simple - - - + + + - - + + + - - tags + + - - - + + StatisticType - - - + + - - + + - - Project + + - - + + - - + + ICalendarEvent - - + + + - - + + - - UserAvatar + + i_calendar_events - - - + + - - + + - - user_avatar + + - - + + string - - + + + - - + + - - string + + color - - - + + - - + + string - - attachment + + + - - + + - - string + + name - - - + + - - + + string - - type + + + - - + + - - integer + + text_color - - - + + - - + + boolean - - viewable_id + + + - - + + - - string + + text_hidden - - - + + - - + + string - - viewable_type + + + - - - + + - - - + + url - - + + + - - UserAvatar + + + - - + + - - + + ICalendar - - + + - - StatisticField + + - - - + + - - + + - - statistic_fields + + Project - - + + + - - StatisticGraph + + - - - + + projects - - + + - - statistic_graph + + - - + + - - StatisticIndex + + string - - - + + + - - + + - - statistic_index + + name - - + + + - - StatisticType + + + - - - + + - - + + Theme - - statistic_types + + - - + + - - + + - - + + - - boolean + + HistoryValue - - - + + + - - + + - - ca + + history_values - - + + - - string + + - - - + + - - + + string - - es_type_key + + + - - + + - - string + + name - - - + + + - - + + + - - label + + - - + + Setting - - boolean + + - - - + + - - + + - - table + + - - - + + Export - - - + + + - - + + - - StatisticIndex + + exports - - + + - - - + + - - + + - - SimpleAuthProvider + + string - - + + + - - + + - - + + category - - AuthProvider + + - - - + + string - - + + + - - auth_provider + + - - + + export_type - - OAuth2Mapping + + - - - + + string - - + + + - - o_auth2_mappings + + - - + + extension - - OAuth2Provider + + - - - + + string - - + + + - - o_auth2_provider + + - - + + key - - DatabaseProvider + + - - - + + string - - + + + - - providable + + - - + + query - - + + + - - + + + - - string + + - - - + + Export - - + + - - authorization_endpoint + + - - + + - - string + + - - - + + StatisticCustomAggregation - - + + + - - base_url + + - - + + statistic_custom_aggregations - - string + + - - - + + - - + + - - client_id + + string - - + + + - - string + + - - - + + es_index - - + + - - client_secret + + string - - + + + - - string + + - - - + + es_type - - + + - - profile_url + + string - - + + + - - string + + - - - + + field - - + + - - token_endpoint + + text - - - + + + - - - + + - - + + query - - OAuth2Provider + + + - - + + + - - - + + - - + + StatisticCustomAggregation - - NotificationType + + - - + + - - + + - - + + - - Availability + + Profile - - - + + + - - + + - - availability + + profile - - + + - - Reservation + + UserAvatar - - - + + + - - + + - - reservations + + user_avatar - - + + - - Slot + + - - - + + - - + + string - - slots + + + - - + + - - SlotsReservation + + dailymotion - - - + + - - + + string - - slots_reservations + + + - - + + - - + + echosciences - - + + - - datetime + + string - - - + + + - - + + - - canceled_at + + facebook - - + + - - boolean + + string - - - + + + - - + + - - destroying + + first_name - - + + - - datetime + + string - - - + + + - - + + - - end_at + + flickr - - + + - - datetime + + string - - - + + + - - + + - - ex_end_at + + github - - + + - - datetime + + string - - - + + + - - + + - - ex_start_at + + google_plus - - + + - - boolean + + string - - - + + + - - + + - - offered + + instagram - - + + - - datetime + + text - - - + + + - - + + - - start_at + + interest - - - + + - - - + + string - - + + + - - Slot + + - - + + job - - + + - - + + string - - ICalendarEvent + + + - - - + + - - + + last_name - - i_calendar_events + + - - + + string - - + + + - - + + - - string + + lastfm - - - + + - - + + string - - attendee + + + - - + + - - string + + linkedin - - - + + - - + + string - - description + + + - - + + - - datetime + + phone - - - + + - - + + string - - dtend + + + - - + + - - datetime + + pinterest - - - + + - - + + text - - dtstart + + + - - + + - - string + + software_mastered - - - + + - - + + string - - summary + + + - - + + - - string + + tours - - - + + - - + + string - - uid + + + - - - + + - - - + + twitter - - + + - - ICalendarEvent + + string - - + + + - - + + - - + + viadeo - - + + - - Group + + string - - - + + + - - + + - - group + + vimeo - - + + - - Price + + string - - - + + + - - + + - - machines_prices + + website - - + + - - Plan + + string - - - + + + - - + + - - plans + + youtube - - + + + - - Price + + + - - - + + - - + + Profile - - spaces_prices + + - - + + - - StatisticProfile + + - - - + + - - + + EventFile - - statistic_profiles + + + - - + + - - TrainingsPricing + + event_files - - - + + - - + + - - trainings_pricings + + - - + + string - - User + + + - - - + + - - + + attachment - - users + + - - + + string - - + + + - - + + - - boolean + + type - - - + + - - + + integer - - disabled + + + - - + + - - string + + viewable_id - - - + + - - + + string - - name + + + - - + + - - string + + viewable_type - - - + + + - - + + + - - slug + + - - - + + EventFile - - - + + - - + + - - Group + + - - + + - - + + ProofOfIdentityTypesGroup - - + + + - - + + - - Address + + proof_of_identity_types_groups - - - + + + - - + + + - - address + + - - + + ProofOfIdentityTypesGroup - - InvoicingProfile + + - - - + + - - + + - - invoicing_profile + + - - + + InvoicingProfile - - Organization + + + - - - + + - - + + invoicing_profile - - organization + + - - + + PaymentSchedule - - InvoicingProfile + + + - - - + + - - + + operated_payment_schedules - - placeable + + - - + + InvoicingProfile - - Profile + + + - - - + + - - + + operator_profile - - profile + + - - + + PaymentGatewayObject - - + + + - - + + - - string + + payment_gateway_objects - - - + + - - + + PaymentSchedule - - name + + + - - - + + - - - + + payment_schedule - - + + - - Organization + + PaymentScheduleItem - - + + + - - + + - - + + payment_schedule_items - - + + - - EventImage + + PaymentScheduleObject - - - + + + - - + + - - event_image + + payment_schedule_objects - - + + - - + + PaymentSchedule - - + + + - - string + + - - - + + payment_schedules - - + + - - attachment + + StatisticProfile - - + + + - - string + + - - - + + statistic_profile - - + + - - type + + WalletTransaction - - + + + - - integer + + - - - + + wallet_transaction - - + + - - viewable_id + + - - + + - - string + + string - - - + + + - - + + - - viewable_type + + environment - - - + + - - - + + string - - + + + - - EventImage + + - - + + footprint - - + + - - + + bigint - - + + + - - AgeRange + + - - - + + operator_profile_id - - + + - - age_range + + string - - + + + - - Event + + - - - + + payment_method - - + + - - events + + string - - + + + - - + + - - + + reference - - string + + - - - + + datetime - - + + + - - name + + - - + + start_at - - string + + - - - + + integer - - + + + - - slug + + - - - + + total - - - + + - - + + integer - - AgeRange + + + - - - - - - - - - + + - - + + wallet_amount - - - + + + - - + + + - - User + + - - + + PaymentSchedule - - + + - - + + - - + + - - Availability + + - - - + + PaymentGatewayObject - - + + + - - availabilities + + - - + + payment_gateway_object - - Credit + + - - - + + PaymentScheduleItem - - + + + - - credits + + - - + + payment_schedule_item - - MachineFile + + - - - + + PaymentScheduleItem - - + + + - - machine_files + + - - + + payment_schedule_items - - MachineImage + + - - - + + - - + + - - machine_image + + integer - - + + + - - MachinesAvailability + + - - - + + amount - - + + - - machines_availabilities + + string - - + + + - - PaymentGatewayObject + + - - - + + client_secret - - + + - - payment_gateway_object + + jsonb - - + + + - - Plan + + - - - + + details - - + + - - plans + + datetime - - + + + - - Machine + + - - - + + due_date - - + + - - priceable + + string - - + + + - - Price + + - - - + + footprint - - + + - - prices + + string - - + + + - - Project + + - - - + + payment_method - - + + - - projects + + string - - + + + - - Reservation + + - - - + + state - - + + + - - reservations + + + - - + + - - Training + + PaymentScheduleItem - - - + + - - + + - - trainings + + - - + + - - + + Availability - - + + + - - text + + - - - + + availabilities - - + + - - description + + Credit - - + + + - - boolean + + - - - + + credits - - + + - - disabled + + PaymentGatewayObject - - + + + - - string + + - - - + + payment_gateway_object - - + + - - name + + Plan - - + + + - - string + + - - - + + plans - - + + - - slug + + Reservation - - + + + - - text + + - - - + + reservations - - + + - - spec + + StatisticProfileTraining - - + + + - - string + + - - - + + statistic_profile_trainings - - + + - - stp_product_id + + StatisticProfile - - - + + + - - - + + - - + + statistic_profiles - - Machine + + - - + + Training - - + + + - - + + - - + + training - - Profile + + - - - + + TrainingImage - - + + + - - profile + + - - + + training_image - - UserAvatar + + - - - + + Training - - + + + - - user_avatar + + - - + + trainings - - + + - - + + TrainingsAvailability - - string + + + - - - + + - - + + trainings_availabilities - - dailymotion + + - - + + TrainingsPricing - - string + + + - - - + + - - + + trainings_pricings - - echosciences + + - - + + - - string + + - - - + + text - - + + + - - facebook + + - - + + description - - string + + - - - + + boolean - - + + + - - first_name + + - - + + disabled - - string + + - - - + + string - - + + + - - flickr + + - - + + name - - string + + - - - + + integer - - + + + - - github + + - - + + nb_total_places - - string + + - - - + + boolean - - + + + - - google_plus + + - - + + public_page - - string + + - - - + + string - - + + + - - instagram + + - - + + slug - - text + + + - - - + + + - - + + - - interest + + Training - - + + - - string + + - - - + + - - + + - - job + + ProofOfIdentityRefusal - - + + + - - string + + - - - + + proof_of_identity_refusals - - + + - - last_name + + ProofOfIdentityType - - + + + - - string + + - - - + + proof_of_identity_types - - + + - - lastfm + + User - - + + + - - string + + - - - + + user - - + + - - linkedin + + - - + + - - string + + text - - - + + + - - + + - - phone + + message - - + + - - string + + integer - - - + + + - - + + - - pinterest + + operator_id - - + + + - - text + + + - - - + + - - + + ProofOfIdentityRefusal - - software_mastered + + - - + + - - string + + - - - + + - - + + string - - tours + + + - - + + - - string + + attachment - - - + + - - + + string - - twitter + + + - - + + - - string + + type - - - + + - - + + integer - - viadeo + + + - - + + - - string + + viewable_id - - - + + - - + + string - - vimeo + + + - - + + - - string + + viewable_type - - - + + + - - + + + - - website + + - - + + Asset - - string + + - - - + + - - + + - - youtube + + - - - + + PlanCategory - - - + + + - - + + - - Profile + + plan_category - - + + - - + + Plan - - + + + - - + + - - integer + + plans - - - + + - - + + - - attached_object_id + + - - + + text - - string + + + - - - + + - - + + description - - attached_object_type + + - - + + string - - boolean + + + - - - + + - - + + name - - is_read + + - - + + integer - - boolean + + + - - - + + - - + + weight - - is_send + + + - - + + + - - integer + + - - - + + PlanCategory - - + + - - receiver_id + + + - - + + - - string + + Space - - - + + - - + + - - receiver_type + + - - - + + - - - + + AvailabilityTag - - + + + - - Notification + + - - + + availability_tags - - + + + - - + + + - - + + - - MachineImage + + AvailabilityTag - - - + + - - + + - - machine_image + + - - + + - - + + ProfileCustomField - - + + + - - string + + - - - + + profile_custom_fields - - + + - - attachment + + UserProfileCustomField - - + + + - - string + + - - - + + user_profile_custom_fields - - + + - - type + + - - + + - - integer + + boolean - - - + + + - - + + - - viewable_id + + actived - - + + - - string + + string - - - + + + - - + + - - viewable_type + + label - - - + + - - - + + boolean - - + + + - - MachineImage + + - - + + required - - + + + - - + + + - - + + - - PlanFile + + ProfileCustomField - - - + + - - + + + - - plan_file + + - - + + TrainingReservation - - + + - - + + - - string + + - - - + + - - + + ProofOfIdentityFile - - attachment + + + - - + + - - string + + proof_of_identity_files - - - + + - - + + ProofOfIdentityType - - type + + + - - + + - - integer + + proof_of_identity_type - - - + + - - + + - - viewable_id + + - - + + string - - string + + + - - - + + - - + + attachment - - viewable_type + + + - - - + + + - - - + + - - + + ProofOfIdentityFile - - PlanFile + + - - + + + - - + + - - + + Account - - + + - - string + + + - - - + + - - + + ParameterError - - data + + - - + + - - string + + - - - + + - - + + Availability - - footprint + + + - - + + - - string + + availability - - - + + - - + + Reservation - - klass + + + - - - + + - - - + + reservations - - + + - - FootprintDebug + + Slot - - + + + - - - + + - - + + slots - - Coupon + + - - + + SlotsReservation - - - + + + - - + + - - Space + + slots_reservations - - + + - - + + - - + + - - + + datetime - - Export + + + - - - + + - - + + canceled_at - - exports + + - - + + boolean - - + + + - - + + - - string + + destroying - - - + + - - + + datetime - - category + + + - - + + - - string + + end_at - - - + + - - + + datetime - - export_type + + + - - + + - - string + + ex_end_at - - - + + - - + + datetime - - extension + + + - - + + - - string + + ex_start_at - - - + + - - + + boolean - - key + + + - - + + - - string + + offered - - - + + - - + + datetime - - query + + + - - - + + - - - + + start_at - - + + + - - Export + + + - - + + - - + + Slot - - + + - - + + - - EventPriceCategory + + - - - + + - - + + ProjectStepImage - - event_price_category + + + - - + + - - PriceCategory + + project_step_images - - - + + - - + + ProjectStep - - price_categories + + + - - + + - - + + project_steps - - + + - - text + + - - - + + - - + + text - - conditions + + + - - + + - - string + + description - - - + + - - + + integer - - name + + + - - - + + - - - + + step_nb - - + + - - PriceCategory + + string - - + + + - - + + - - + + title - - + + + - - Invoice + + + - - - + + - - + + ProjectStep - - invoice + + - - + + - - InvoiceItem + + - - - + + - - + + Group - - invoice_item + + + - - + + - - OfferDay + + group - - - + + - - + + Price - - object + + + - - + + - - PaymentSchedule + + machines_prices - - - + + - - + + Plan - - payment_schedule + + + - - + + - - Reservation + + plans - - - + + - - + + ProofOfIdentityType - - reservation + + + - - + + - - WalletTransaction + + proof_of_identity_types - - - + + - - + + ProofOfIdentityTypesGroup - - wallet_transaction + + + - - + + - - WalletTransaction + + proof_of_identity_types_groups - - - + + - - + + Price - - wallet_transactions + + + - - + + - - + + spaces_prices - - + + - - integer + + StatisticProfile - - - + + + - - + + - - amount + + statistic_profiles - - + + - - integer + + TrainingsPricing - - - + + + - - + + - - transactable_id + + trainings_pricings - - + + - - string + + User - - - + + + - - + + - - transactable_type + + users - - + + - - string + + - - - + + - - + + boolean - - transaction_type + + + - - - + + - - - + + disabled - - + + - - WalletTransaction + + string - - + + + - - + + - - + + name - - MachinesAvailability + + - - - + + string - - + + + - - machines_availabilities + + - - - + + slug - - - + + + - - + + + - - MachinesAvailability + + - - + + Group - - - + + - - + + + - - EventReservation + + - - + + NotificationType - - + + - - + + + - - + + - - EventFile + + Subscription - - - + + - - + + - - event_files + + - - + + - - + + Group - - + + + - - string + + - - - + + group - - + + - - attachment + + Machine - - + + + - - string + + - - - + + machine - - + + - - type + + PrepaidPack - - + + + - - integer + + - - - + + prepaid_packs - - + + - - viewable_id + + Space - - + + + - - string + + - - - + + space - - + + - - viewable_type + + StatisticProfilePrepaidPack - - - + + + - - - + + - - + + statistic_profile_prepaid_packs - - EventFile + + - - + + - - + + - - + + integer - - EventPriceCategory + + + - - - + + - - + + amount - - event_price_category + + - - + + boolean - - Reservation + + + - - - + + - - + + disabled - - reservation + + - - + + integer - - Ticket + + + - - - + + - - + + minutes - - tickets + + - - + + bigint - - + + + - - + + - - integer + + priceable_id - - - + + - - + + string - - booked + + + - - - + + - - - + + priceable_type - - + + - - Ticket + + integer - - + + + - - + + - - + + validity_count - - + + - - PaymentGatewayObject + + string - - - + + + - - + + - - payment_gateway_object + + validity_interval - - + + + - - PaymentScheduleItem + + + - - - + + - - + + PrepaidPack - - payment_schedule_item + + - - + + - - PaymentScheduleItem + + - - - + + - - + + EventPriceCategory - - payment_schedule_items + + + - - + + - - + + event_price_categories - - + + - - integer + + EventPriceCategory - - - + + + - - + + - - amount + + event_price_category - - + + - - string + + Ticket - - - + + + - - + + - - client_secret + + tickets - - + + - - datetime + + - - - + + - - + + integer - - due_date + + + - - + + - - string + + amount - - - + + + - - + + + - - footprint + + - - + + EventPriceCategory - - string + + - - - + + - - + + - - payment_method + + - - + + MachineImage - - string + + + - - - + + - - + + machine_image - - state + + - - - + + - - - + + - - + + string - - PaymentScheduleItem + + + - - + + - - + + attachment - - + + - - + + string - - string + + + - - - + + - - + + type - - email + + - - + + integer - - string + + + - - - + + - - + + viewable_id - - first_name + + - - + + string - - string + + + - - - + + - - + + viewable_type - - last_name + + + - - + + + - - text + + - - - + + MachineImage - - + + - - message + + - - + + - - integer + + - - - + + StatisticProfile - - + + + - - signaled_id + + - - + + author - - string + + - - - + + Project - - + + + - - signaled_type + + - - - + + my_projects - - - + + - - + + PrepaidPack - - Abuse + + + - - + + - - - + + prepaid_packs - - + + - - Subscription + + Reservation - - + + + - - + + - - + + reservations - - StatisticGraph + + - - - + + Role - - + + + - - statistic_graph + + - - + + role - - StatisticIndex + + - - - + + StatisticProfile - - + + + - - statistic_index + + - - + + statistic_profile - - + + - - + + StatisticProfilePrepaidPack - - string + + + - - - + + - - + + statistic_profile_prepaid_packs - - chart_type + + - - + + StatisticProfileTraining - - integer + + + - - - + + - - + + statistic_profile_trainings - - limit + + - - - + + StatisticProfile - - - + + + - - + + - - StatisticGraph + + statistic_profiles - - + + - - + + Subscription - - + + + - - AuthProvider + + - - - + + subscriptions - - + + - - auth_provider + + - - + + - - DatabaseProvider + + date - - - + + + - - + + - - providable + + birthday - - + + - - + + boolean - - + + + - - string + + - - - + + gender - - + + + - - name + + + - - + + - - integer + + StatisticProfile - - - + + - - + + + - - providable_id + + - - + + Training - - string + + - - - + + - - + + - - providable_type + + - - + + InvoiceItem - - string + + + - - - + + - - + + invoice_items - - status + + - - - + + StatisticProfilePrepaidPack - - - + + + - - + + - - AuthProvider + + object - - + + - - - + + PaymentScheduleObject - - + + + - - Training + + - - + + payment_schedule_object - - + + - - + + Reservation - - Credit + + + - - - + + - - + + reservation - - credit + + - - + + Reservation - - Credit + + + - - - + + - - + + reservations - - space_credit + + - - + + SlotsReservation - - Credit + + + - - - + + - - + + slots_reservations - - training_credit + + - - + + StatisticProfile - - UsersCredit + + + - - - + + - - + + statistic_profile - - users_credits + + - - + + Ticket - - + + + - - + + - - integer + + tickets - - - + + - - + + - - hours_used + + - - - + + text - - - + + + - - + + - - UsersCredit + + message - - + + - - - + + integer - - + + + - - Machine + + - - + + nb_reserve_places - - + + - - + + integer - - + + + - - Credit + + - - - + + reservable_id - - + + - - credits + + string - - + + + - - Group + + - - - + + reservable_type - - + + + - - group + + + - - + + - - Credit + + Reservation - - - + + - - + + - - machine_credits + + - - + + - - PaymentGatewayObject + + Invoice - - - + + + - - + + - - payment_gateway_object + + avoir - - + + - - Plan + + Invoice - - - + + + - - + + - - plan + + invoice - - + + - - PlanCategory + + InvoiceItem - - - + + + - - + + - - plan_category + + invoice_items - - + + - - PlanFile + + Invoice - - - + + + - - + + - - plan_file + + invoices - - + + - - Plan + + InvoicingProfile - - - + + + - - + + - - plans + + invoicing_profile - - + + - - Price + + Invoice - - - + + + - - + + - - prices + + operated_invoices - - + + - - Credit + + InvoicingProfile - - - + + + - - + + - - space_credits + + operator_profile - - + + - - Subscription + + PaymentGatewayObject - - - + + + - - + + - - subscriptions + + payment_gateway_object - - + + - - Credit + + PaymentScheduleItem - - - + + + - - + + - - training_credits + + payment_schedule_item - - + + - - + + StatisticProfile - - + + + - - integer + + - - - + + statistic_profile - - + + - - amount + + WalletTransaction - - + + + - - string + + - - - + + wallet_transaction - - + + - - base_name + + - - + + - - text + + datetime - - - + + + - - + + - - description + + avoir_date - - + + - - boolean + + text - - - + + + - - + + - - disabled + + description - - + + - - string + + string - - - + + + - - + + - - interval + + environment - - + + - - integer + + string - - - + + + - - + + - - interval_count + + footprint - - + + - - boolean + + integer - - - + + + - - + + - - is_rolling + + operator_profile_id - - + + - - boolean + + string - - - + + + - - + + - - monthly_payment + + payment_method - - + + - - string + + string - - - + + + - - + + - - name + + reference - - + + - - string + + boolean - - - + + + - - + + - - slug + + subscription_to_expire - - + + - - string + + integer - - - + + + - - + + - - stp_plan_id + + total - - + + - - string + + string - - - + + + - - + + - - stp_product_id + + type - - + + - - integer + + integer - - - + + + - - + + - - training_credit_nb + + wallet_amount - - + + + - - string + + + - - - + + - - + + Invoice - - type + + - - + + - - integer + + - - - + + - - + + Invoice - - ui_weight + + + - - - + + - - - + + invoice - - + + - - Plan + + InvoiceItem - - + + + - - + + - - + + invoice_item - - + + - - Plan + + StatisticProfilePrepaidPack - - - + + + - - + + - - plan + + object - - + + - - PlanCategory + + PaymentSchedule - - - + + + - - + + - - plan_category + + payment_schedule - - + + - - + + Reservation - - + + + - - string + + - - - + + reservation - - + + - - name + + WalletTransaction - - + + + - - integer + + - - - + + wallet_transaction - - + + - - weight + + WalletTransaction - - - + + + - - - + + - - + + wallet_transactions - - PlanCategory + + - - + + - - + + - - + + integer - - + + + - - ProjectImage + + - - - + + amount - - + + - - project_image + + string - - + + + - - + + - - + + transaction_type - - string + + + - - - + + + - - + + - - attachment + + WalletTransaction - - + + - - string + + + - - - + + - - + + Machine - - type + + - - + + - - integer + + - - - + + - - + + Availability - - viewable_id + + + - - + + - - string + + availabilities - - - + + - - + + Credit - - viewable_type + + + - - - + + - - - + + credits - - + + - - ProjectImage + + PaymentGatewayObject - - + + + - - + + - - + + payment_gateway_object - - + + - - integer + + PrepaidPack - - - + + + - - + + - - amount + + prepaid_packs - - + + - - string + + Space - - - + + + - - + + - - base_name + + priceable - - + + - - text + + Price - - - + + + - - + + - - description + + prices - - + + - - boolean + + Project - - - + + + - - + + - - disabled + + projects - - + + - - string + + Reservation - - - + + + - - + + - - interval + + reservations - - + + - - integer + + Space - - - + + + - - + + - - interval_count + + space - - + + - - boolean + + SpaceFile - - - + + + - - + + - - is_rolling + + space_files - - + + - - boolean + + SpaceImage - - - + + + - - + + - - monthly_payment + + space_image - - + + - - string + + SpacesAvailability - - - + + + - - + + - - name + + spaces_availabilities - - + + - - string + + - - - + + - - + + text - - slug + + + - - + + - - string + + characteristics - - - + + - - + + integer - - stp_plan_id + + + - - + + - - string + + default_places - - - + + - - + + text - - stp_product_id + + + - - + + - - integer + + description - - - + + - - + + boolean - - training_credit_nb + + + - - + + - - string + + disabled - - - + + - - + + string - - type + + + - - + + - - integer + + name - - - + + - - + + string - - ui_weight + + + - - - + + - - - + + slug - - + + + - - PartnerPlan + + + - - + + - - - + + Space - - + + - - ShoppingCart + + + - - + + - - + + EventReservation - - + + - - + + + - - AgeRange + + - - - + + Reservation - - + + - - age_range + + - - + + - - Availability + + - - - + + InvoicingProfile - - + + + - - availability + + - - + + invoicing_profile - - Category + + - - - + + Wallet - - + + + - - category + + - - + + wallet - - Event + + - - - + + WalletTransaction - - + + + - - event + + - - + + wallet_transactions - - EventFile + + - - - + + - - + + - - event_files + + integer - - + + + - - EventImage + + - - - + + amount - - + + + - - event_image + + + - - + + - - EventPriceCategory + + Wallet - - - + + - - + + - - event_price_categories + + - - + + - - EventTheme + + Invoice - - - + + + - - + + - - event_themes + + invoice - - + + - - Event + + - - - + + - - + + datetime - - events + + + - - + + - - PriceCategory + + avoir_date - - - + + - - + + text - - price_categories + + + - - + + - - Reservation + + description - - - + + - - + + string - - reservations + + + - - + + - - + + environment - - + + - - integer + + string - - - + + + - - + + - - amount + + footprint - - + + - - text + + integer - - - + + + - - + + - - description + + operator_profile_id - - + + - - integer + + string - - - + + + - - + + - - nb_free_places + + payment_method - - + + - - integer + + string - - - + + + - - + + - - nb_total_places + + reference - - + + - - integer + + boolean - - - + + + - - + + - - recurrence_id + + subscription_to_expire - - + + - - string + + integer - - - + + + - - + + - - title + + total - - - + + - - - + + string - - + + + - - Event + + - - + + type - - + + - - + + integer - - + + + - - Project + + - - - + + wallet_amount - - + + + - - projects + + + - - + + - - + + Avoir - - + + - - string + + - - - + + - - + + - - name + + MachinesAvailability - - - + + + - - - + + - - + + machines_availabilities - - Theme + + + - - + + + - - + + - - + + MachinesAvailability - - Availability + + - - - + + - - + + - - availabilities + + - - + + HistoryValue - - AvailabilityTag + + + - - - + + - - + + history_values - - availability_tags + + - - + + InvoicingProfile - - UserTag + + + - - - + + - - + + invoicing_profile - - user_tags + + - - + + - - User + + - - - + + string - - + + + - - users + + - - + + footprint - - + + - - + + string - - string + + + - - - + + - - + + value - - name + + + - - - + + + - - - + + - - + + HistoryValue - - Tag + + - - + + - - + + - - + + - - + + ProjectCao - - ProjectCao + + + - - - + + - - + + project_caos - - project_caos + + - - + + - - + + - - + + string - - string + + + - - - + + - - + + attachment - - attachment + + - - + + string - - string + + + - - - + + - - + + type - - type + + - - + + integer - - integer + + + - - - + + - - + + viewable_id - - viewable_id + + - - + + string - - string + + + - - - + + - - + + viewable_type - - viewable_type + + + - - - + + + - - - + + - - + + ProjectCao - - ProjectCao + + - - + + - - - + + - - + + - - Event + + TrainingImage - - + + + - - + + - - + + training_image - - CallsCountTracing + + - - - + + - - + + - - calls_count_tracings + + string - - + + + - - + + - - + + attachment - - integer + + - - - + + string - - + + + - - calls_count + + - - + + type - - string + + - - - + + integer - - + + + - - name + + - - + + viewable_id - - string + + - - - + + string - - + + + - - token + + - - - + + viewable_type - - - + + + - - + + + - - Client + + - - + + TrainingImage - - - + + - - + + + - - Footprintable + + - - + + BaseItem - - + + - - - + + - - + + - - ApplicationRecord + + - - + + AuthProvider - - + + + - - + + - - + + auth_provider - - OAuth2Mapping + + - - - + + AuthProviderMapping - - + + + - - o_auth2_mappings + + - - + + auth_provider_mappings - - OAuth2Provider + + - - - + + DatabaseProvider - - + + + - - o_auth2_provider + + - - + + providable - - + + - - + + - - string + + - - - + + string - - + + + - - api_data_type + + - - + + name - - string + + - - - + + integer - - + + + - - api_endpoint + + - - + + providable_id - - string + + - - - + + string - - + + + - - api_field + + - - + + providable_type - - string + + - - - + + string - - + + + - - local_field + + - - + + status - - string + + + - - - + + + - - + + - - local_model + + AuthProvider - - - + + - - - + + - - + + - - OAuth2Mapping + + - - + + StatisticProfile - - + + + - - + + - - + + author - - InvoiceItem + + - - - + + Component - - + + + - - invoice_items + + - - + + components - - OfferDay + + - - - + + Project - - + + + - - object + + - - + + my_projects - - PaymentScheduleObject + + - - - + + ProjectCao - - + + + - - payment_schedule_object + + - - + + project_caos - - Reservation + + - - - + + ProjectImage - - + + + - - reservation + + - - + + project_image - - Reservation + + - - - + + ProjectStep - - + + + - - reservations + + - - + + project_steps - - SlotsReservation + + - - - + + ProjectUser - - + + + - - slots_reservations + + - - + + project_users - - StatisticProfile + + - - - + + Project - - + + + - - statistic_profile + + - - + + projects - - Ticket + + - - - + + - - + + - - tickets + + integer - - + + + - - + + - - + + author_statistic_profile_id - - text + + - - - + + text - - + + + - - message + + - - + + description - - integer + + - - - + + string - - + + + - - nb_reserve_places + + - - + + name - - integer + + - - - + + datetime - - + + + - - reservable_id + + - - + + published_at - - string + + - - - + + string - - + + + - - reservable_type + + - - - + + slug - - - + + - - + + string - - Reservation + + + - - + + - - - + + state - - + + - - Project + + text - - + + + - - + + - - + + tags - - + + + - - CustomAssetFile + + + - - - + + - - + + Project - - custom_asset_file + + - - + + - - + + - - + + - - string + + AuthProviderMapping - - - + + + - - + + - - attachment + + auth_provider_mappings - - + + - - string + + - - - + + - - + + string - - type + + + - - + + - - integer + + api_data_type - - - + + - - + + string - - viewable_id + + + - - + + - - string + + api_endpoint - - - + + - - + + string - - viewable_type + + + - - - + + - - - + + api_field - - + + - - CustomAssetFile + + string - - + + + - - + + - - + + local_field - - + + - - Availability + + string - - - + + + - - + + - - availabilities + + local_model - - + + - - Availability + + jsonb - - - + + + - - + + - - availability + + transformation - - + + + - - AvailabilityTag + + + - - - + + - - + + AuthProviderMapping - - availability_tags + + - - + + - - Event + + - - - + + - - + + UserProfileCustomField - - event + + + - - + + - - MachinesAvailability + + user_profile_custom_fields - - - + + - - + + - - machines_availabilities + + - - + + string - - Plan + + + - - - + + - - + + value - - plans + + + - - + + + - - PlansAvailability + + - - - + + UserProfileCustomField - - + + - - plans_availabilities + + - - + + - - Reservation + + - - - + + UserAvatar - - + + + - - reservations + + - - + + user_avatar - - Slot + + - - - + + - - + + - - slots + + string - - + + + - - SpacesAvailability + + - - - + + attachment - - + + - - spaces_availabilities + + string - - + + + - - TrainingsAvailability + + - - - + + type - - + + - - trainings_availabilities + + integer - - + + + - - + + - - + + viewable_id - - string + + - - - + + string - - + + + - - available_type + + - - + + viewable_type - - boolean + + + - - - + + + - - + + - - destroying + + UserAvatar - - + + - - datetime + + + - - - + + - - + + Footprintable - - end_at + + - - + + - - datetime + + - - - + + - - + + integer - - end_date + + + - - + + - - boolean + + attached_object_id - - - + + - - + + string - - is_recurrent + + + - - + + - - boolean + + attached_object_type - - - + + - - + + boolean - - lock + + + - - + + - - integer + + is_read - - - + + - - + + boolean - - nb_periods + + + - - + + - - integer + + is_send - - - + + - - + + jsonb - - nb_total_places + + + - - + + - - integer + + meta_data - - - + + - - + + integer - - occurrence_id + + + - - + + - - string + + receiver_id - - - + + - - + + string - - period + + + - - + + - - integer + + receiver_type - - - + + + - - + + + - - slot_duration + + - - + + Notification - - datetime + + - - - + + - - + + - - start_at + + - - - + + Invoice - - - + + + - - + + - - Availability + + invoice - - + + - - + + Machine - - + + + - - Address + + - - - + + machine - - + + - - address + + PaymentGatewayObject - - + + + - - HistoryValue + + - - - + + payment_gateway_object - - + + - - history_values + + PaymentGatewayObject - - + + + - - Invoice + + - - - + + payment_gateway_objects - - + + - - invoices + + PaymentSchedule - - + + + - - InvoicingProfile + + - - - + + payment_schedule - - + + - - invoicing_profile + + PaymentScheduleItem - - + + + - - Invoice + + - - - + + payment_schedule_item - - + + - - operated_invoices + + Subscription - - + + + - - PaymentSchedule + + - - - + + subscription - - + + - - operated_payment_schedules + + Training - - + + + - - InvoicingProfile + + - - - + + training - - + + - - operator_profile + + User - - + + + - - Organization + + - - - + + user - - + + - - organization + + - - + + - - PaymentSchedule + + string - - - + + + - - + + - - payment_schedules + + gateway_object_id - - + + - - InvoicingProfile + + string - - - + + + - - + + - - placeable + + gateway_object_type - - + + - - Wallet + + bigint - - - + + + - - + + - - wallet + + item_id - - + + - - WalletTransaction + + string - - - + + + - - + + - - wallet_transactions + + item_type - - + + + - - + + + - - + + - - string + + PaymentGatewayObject - - - + + - - + + + - - email + + - - + + ShoppingCart - - string + + - - - + + - - + + - - first_name + + - - + + integer - - string + + + - - - + + - - + + amount - - last_name + + - - - + + string - - - + + + - - + + - - InvoicingProfile + + base_name - - + + - - + + text - - + + + - - InvoicingProfile + + - - - + + description - - + + - - invoicing_profile + + boolean - - + + + - - PaymentSchedule + + - - - + + disabled - - + + - - operated_payment_schedules + + string - - + + + - - InvoicingProfile + + - - - + + interval - - + + - - operator_profile + + integer - - + + + - - PaymentGatewayObject + + - - - + + interval_count - - + + - - payment_gateway_objects + + boolean - - + + + - - PaymentSchedule + + - - - + + is_rolling - - + + - - payment_schedule + + boolean - - + + + - - PaymentScheduleItem + + - - - + + monthly_payment - - + + - - payment_schedule_items + + string - - + + + - - PaymentScheduleObject + + - - - + + name - - + + - - payment_schedule_objects + + string - - + + + - - PaymentSchedule + + - - - + + slug - - + + - - payment_schedules + + string - - + + + - - StatisticProfile + + - - - + + stp_plan_id - - + + - - statistic_profile + + integer - - + + + - - WalletTransaction + + - - - + + training_credit_nb - - + + - - wallet_transaction + + string - - + + + - - + + - - + + type - - string + + - - - + + integer - - + + + - - environment + + - - + + ui_weight - - string + + + - - - + + + - - + + - - footprint + + PartnerPlan - - + + - - integer + + + - - - + + - - + + Coupon - - operator_profile_id + + - - + + + - - string + + - - - + + ApplicationRecord - - + + - - payment_method + + - - + + - - string + + - - - + + Plan - - + + + - - reference + + - - + + plan - - integer + + - - - + + PlansAvailability - - + + + - - total + + - - + + plans_availabilities - - integer + + + - - - + + + - - + + - - wallet_amount + + PlansAvailability - - - + + - - - + + - - + + - - PaymentSchedule + + - - + + integer - - + + + - - + + - - HistoryValue + + calls_count - - - + + - - + + string - - history_values + + + - - + + - - + + name - - + + - - string + + string - - - + + + - - + + - - name + + token - - - + + + - - - + + + - - + + - - Setting + + Client - - + + - - + + - - + + - - Invoice + + - - - + + UserTag - - + + + - - invoice + + - - + + user_tags - - + + + - - + + + - - datetime + + - - - + + UserTag - - + + - - avoir_date + + + - - + + - - text + + Event - - - + + - - + + - - description + + - - + + - - string + + SlotsReservation - - - + + + - - + + - - environment + + slots_reservations - - + + + - - string + + + - - - + + - - + + SlotsReservation - - footprint + + - - + + - - integer + + - - - + + - - + + AgeRange - - invoiced_id + + + - - + + - - string + + age_range - - - + + - - + + Event - - invoiced_type + + + - - + + - - integer + + events - - - + + - - + + - - operator_profile_id + + - - + + string - - string + + + - - - + + - - + + name - - payment_method + + - - + + string - - string + + + - - - + + - - + + slug - - reference + + + - - + + + - - boolean + + - - - + + AgeRange - - + + - - subscription_to_expire + + - - + + - - integer + + - - - + + StatisticTypeSubType - - + + + - - total + + - - + + statistic_type_sub_types - - string + + + - - - + + + - - + + - - type + + StatisticTypeSubType - - + + - - integer + + - - - + + - - + + - - wallet_amount + + EventImage - - - + + + - - - + + - - + + event_image - - Avoir + + - - + + - - - + + - - + + string - - ParameterError + + + - - + + - - + + attachment - - + + - - + + string - - Role + + + - - - + + - - + + type - - role + + - - + + integer - - User + + + - - - + + - - + + viewable_id - - users + + - - + + string - - + + + - - + + - - string + + viewable_type - - - + + + - - + + + - - name + + - - + + EventImage - - integer + + - - - + + + - - + + - - resource_id + + Subscription - - + + - - string + + - - - + + - - + + - - resource_type + + StatisticField - - - + + + - - - + + - - + + statistic_fields - - Role + + - - + + StatisticIndex - - + + + - - + + - - StatisticCustomAggregation + + statistic_index - - - + + - - + + - - statistic_custom_aggregations + + - - + + string - - + + + - - + + - - string + + data_type - - - + + - - + + string - - es_index + + + - - + + - - string + + key - - - + + - - + + string - - es_type + + + - - + + - - string + + label - - - + + + - - + + + - - field + + - - + + StatisticField - - text + + - - - + + - - + + - - query + + - - - + + string - - - + + + - - + + - - StatisticCustomAggregation + + data - - + + - - + + string - - + + + - - + + - - EventTheme + + footprint - - - + + - - + + string - - event_themes + + + - - + + - - + + klass - - + + + - - string + + + - - - + + - - + + FootprintDebug - - name + + - - + + - - string + + - - - + + - - + + CustomAssetFile - - slug + + + - - - + + - - - + + custom_asset_file - - + + - - EventTheme + + - - + + - - + + string - - + + + - - StatisticSubType + + - - - + + attachment - - + + - - statistic_sub_types + + string - - + + + - - StatisticTypeSubType + + - - - + + type - - + + - - statistic_type_sub_types + + integer - - + + + - - + + - - + + viewable_id - - string + + - - - + + string - - + + + - - key + + - - + + viewable_type - - string + + + - - - + + + - - + + - - label + + CustomAssetFile - - - + + - - - + + + - - + + - - StatisticSubType + + PrepaidPack - - + + - - + + - - + + - - + + - - Price + + AccountingPeriod - - - + + + - - + + - - machines_prices + + accounting_periods - - + + - - Machine + + Credit - - - + + + - - + + - - priceable + + credits - - + + - - Price + + Export - - - + + + - - + + - - prices + + exports - - + + - - Price + + Group - - - + + + - - + + - - spaces_prices + + group - - + + - - + + Import - - + + + - - integer + + - - - + + imports - - + + - - amount + + InvoicingProfile - - + + + - - integer + + - - - + + invoicing_profile - - + + - - priceable_id + + Credit - - + + + - - string + + - - - + + machine_credits - - + + - - priceable_type + + PaymentGatewayObject - - - + + + - - - + + - - + + payment_gateway_object - - Price + + - - + + Profile - - + + + - - + + - - StatisticProfile + + profile - - - + + - - + + ProjectUser - - author + + + - - + + - - Project + + project_users - - - + + - - + + Project - - my_projects + + + - - + + - - Reservation + + projects - - - + + - - + + ProofOfIdentityFile - - reservations + + + - - + + - - Role + + proof_of_identity_files - - - + + - - + + ProofOfIdentityRefusal - - role + + + - - + + - - StatisticProfile + + proof_of_identity_refusals - - - + + - - + + StatisticProfile - - statistic_profile + + + - - + + - - StatisticProfileTraining + + statistic_profile - - - + + - - + + Credit - - statistic_profile_trainings + + + - - + + - - StatisticProfile + + training_credits - - - + + - - + + User - - statistic_profiles + + + - - + + - - Subscription + + user - - - + + - - + + UserTag - - subscriptions + + + - - + + - - + + user_tags - - + + - - date + + User - - - + + + - - + + - - birthday + + users - - + + - - boolean + + UsersCredit - - - + + + - - + + - - gender + + users_credits - - - + + - - - + + - - + + - - StatisticProfile + + string - - + + + - - + + - - + + auth_token - - StatisticCustomAggregation + + - - - + + datetime - - + + + - - statistic_custom_aggregations + + - - + + confirmation_sent_at - - StatisticIndex + + - - - + + string - - + + + - - statistic_index + + - - + + confirmation_token - - StatisticSubType + + - - - + + datetime - - + + + - - statistic_sub_types + + - - + + confirmed_at - - StatisticTypeSubType + + - - - + + datetime - - + + + - - statistic_type_sub_types + + - - + + current_sign_in_at - - StatisticType + + - - - + + string - - + + + - - statistic_types + + - - + + email - - + + - - + + string - - boolean + + + - - - + + - - + + encrypted_password - - graph + + - - + + integer - - string + + + - - - + + - - + + failed_attempts - - key + + - - + + boolean - - string + + + - - - + + - - + + is_active - - label + + - - + + boolean - - boolean + + + - - - + + - - + + is_allow_contact - - simple + + - - - + + boolean - - - + + + - - + + - - StatisticType + + is_allow_newsletter - - + + - - + + datetime - - + + + - - + + - - SpaceImage + + last_sign_in_at - - - + + - - + + datetime - - space_image + + + - - + + - - + + locked_at - - + + - - string + + string - - - + + + - - + + - - attachment + + mapped_from_sso - - + + - - string + + datetime - - - + + + - - + + - - type + + merged_at - - + + - - integer + + string - - - + + + - - + + - - viewable_id + + provider - - + + - - string + + datetime - - - + + + - - + + - - viewable_type + + remember_created_at - - - + + - - - + + datetime - - + + + - - SpaceImage + + - - + + reset_password_sent_at - - + + - - + + string - - + + + - - SpacesAvailability + + - - - + + reset_password_token - - + + - - spaces_availabilities + + integer - - - + + + - - - + + - - + + sign_in_count - - SpacesAvailability + + - - + + string - - - + + + - - + + - - PaymentSchedule + + slug - - + + - - + + string - - + + + - - Credit + + - - - + + uid - - + + - - credits + + string - - + + + - - Export + + - - - + + unconfirmed_email - - + + - - exports + + string - - + + + - - Group + + - - - + + unlock_token - - + + - - group + + string - - + + + - - Import + + - - - + + username - - + + - - imports + + datetime - - + + + - - InvoicingProfile + + - - - + + validated_at - - + + + - - invoicing_profile + + + - - + + - - Credit + + User - - - + + - - + + - - machine_credits + + - - + + - - PaymentGatewayObject + + Address - - - + + + - - + + - - payment_gateway_object + + address - - + + - - Profile + + InvoicingProfile - - - + + + - - + + - - profile + + invoicing_profile - - + + - - ProjectUser + + Organization - - - + + + - - + + - - project_users + + organization - - + + - - Project + + Organization - - - + + + - - + + - - projects + + placeable - - + + - - StatisticProfile + + Profile - - - + + + - - + + - - statistic_profile + + profile - - + + - - Credit + + - - - + + - - + + string - - training_credits + + + - - + + - - User + + name - - - + + + - - + + + - - user + + - - + + Organization - - UserTag + + - - - + + - - + + - - user_tags + + - - + + InvoiceItem - - User + + + - - - + + - - + + invoice_items - - users + + - - + + StatisticProfilePrepaidPack - - UsersCredit + + + - - - + + - - + + object - - users_credits + + - - + + PaymentScheduleObject - - + + + - - + + - - string + + payment_schedule_object - - - + + - - + + StatisticProfilePrepaidPack - - auth_token + + + - - + + - - datetime + + statistic_profile_prepaid_pack - - - + + - - + + StatisticProfilePrepaidPack - - confirmation_sent_at + + + - - + + - - string + + statistic_profile_prepaid_packs - - - + + - - + + - - confirmation_token + + - - + + datetime - - datetime + + + - - - + + - - + + expires_at - - confirmed_at + + - - + + integer - - datetime + + + - - - + + - - + + minutes_used - - current_sign_in_at + + + - - + + + - - string + + - - - + + StatisticProfilePrepaidPack - - + + - - email + + - - + + - - string + + - - - + + Invoice - - + + + - - encrypted_password + + - - + + invoices - - integer + + - - - + + PaymentSchedule - - + + + - - failed_attempts + + - - + + payment_schedule - - boolean + + - - - + + - - + + - - is_active + + boolean - - + + + - - boolean + + - - - + + active - - + + - - is_allow_contact + + integer - - + + + - - boolean + + - - - + + amount_off - - + + - - is_allow_newsletter + + string - - + + + - - datetime + + - - - + + code - - + + - - last_sign_in_at + + integer - - + + + - - datetime + + - - - + + max_usages - - + + - - locked_at + + string - - + + + - - datetime + + - - - + + name - - + + - - merged_at + + integer - - + + + - - string + + - - - + + percent_off - - + + - - provider + + datetime - - + + + - - datetime + + - - - + + valid_until - - + + - - remember_created_at + + string - - + + + - - datetime + + - - - + + validity_per_user - - + + + - - reset_password_sent_at + + + - - + + - - string + + Coupon - - - + + - - + + - - reset_password_token + + - - + + - - integer + + Import - - - + + + - - + + - - sign_in_count + + imports - - + + - - string + + - - - + + - - + + string - - slug + + + - - + + - - string + + attachment - - - + + - - + + string - - uid + + + - - + + - - string + + category - - - + + - - + + text - - unconfirmed_email + + + - - + + - - string + + results - - - + + - - + + string - - unlock_token + + + - - + + - - string + + update_field - - - + + + - - + + + - - username + + - - - + + Import - - - + + - - + + + - - User + + - - + + SimpleAuthProvider - - + + - - + + + - - + + - - Category + + PaymentDocument - - - + + - - + + - - category + + - - + + - - Event + + InvoiceItem - - - + + + - - + + - - events + + invoice_items - - + + - - + + StatisticProfilePrepaidPack - - + + + - - string + + - - - + + object - - + + - - name + + OfferDay - - + + + - - string + + - - - + + offer_days - - + + - - slug + + PaymentGatewayObject - - - + + + - - - + + - - + + payment_gateway_object - - Category + + - - + + PaymentScheduleObject - - + + + - - + + - - + + payment_schedule_object - - InvoiceItem + + - - - + + StatisticProfile - - + + + - - invoice_item + + - - + + statistic_profile - - InvoiceItem + + - - - + + Subscription - - + + + - - invoice_items + + - - + + subscription - - OfferDay + + - - - + + Subscription - - + + + - - object + + - - + + subscriptions - - PaymentGatewayObject + + - - - + + - - + + - - payment_gateway_object + + datetime - - + + + - - Subscription + + - - - + + canceled_at - - + + - - subscription + + datetime - - + + + - - WalletTransaction + + - - - + + expiration_date - - + + - - wallet_transaction + + datetime - - + + + - - + + - - + + start_at - - integer + + + - - - + + + - - + + - - amount + + Subscription - - + + - - text + + - - - + + - - + + - - description + + StatisticField - - + + + - - string + + - - - + + statistic_fields - - + + - - footprint + + StatisticGraph - - + + + - - boolean + + - - - + + statistic_graph - - + + - - main + + StatisticIndex - - - + + + - - - + + - - + + statistic_index - - InvoiceItem + + - - + + StatisticType - - - + + + - - + + - - BaseItem + + statistic_types - - + + - - + + - - + + - - + + boolean - - InvoiceItem + + + - - - + + - - + + ca - - invoice_items + + - - + + string - - OfferDay + + + - - - + + - - + + es_type_key - - object + + - - + + string - - OfferDay + + + - - - + + - - + + label - - offer_day + + - - + + boolean - - OfferDay + + + - - - + + - - + + table - - offer_days + + + - - + + + - - Subscription + + - - - + + StatisticIndex - - + + - - subscription + + - - + + - - + + - - + + EventPriceCategory - - datetime + + + - - - + + - - + + event_price_category - - end_at + + - - + + Reservation - - datetime + + + - - - + + - - + + reservation - - start_at + + - - - + + Ticket - - - + + + - - + + - - OfferDay + + tickets - - + + - - - + + - - + + - - TrainingReservation + + integer - - + + + - - + + - - + + booked - - CallsCountTracing + + + - - - + + + - - + + - - calls_count_tracings + + Ticket - - + + - - + + + - - + + - - datetime + + PaymentSchedule - - - + + - - + + - - at + + - - + + - - integer + + SpaceFile - - - + + + - - + + - - calls_count + + space_files - - + + - - integer + + - - - + + - - + + string - - open_api_client_id + + + - - - + + - - - + + attachment - - + + - - CallsCountTracing + + string - - + + + - - - + + - - + + type - - Subscription + + - - + + integer - - - + + + - - + + - - Reservation + + viewable_id - - + + - - + + string - - + + + - - + + - - Credit + + viewable_type - - - + + + - - + + + - - credit + + - - + + SpaceFile - - Credit + + - - - + + - - + + - - credits + + - - + + EventTheme - - Credit + + + - - - + + - - + + event_themes - - machine_credits + + - - + + - - Credit + + - - - + + string - - + + + - - space_credit + + - - + + name - - Credit + + - - - + + string - - + + + - - space_credits + + - - + + slug - - Credit + + + - - - + + + - - + + - - training_credit + + EventTheme - - + + - - Credit + + - - - + + - - + + - - training_credits + + Credit - - + + + - - UsersCredit + + - - - + + credit - - + + - - users_credits + + Credit - - + + + - - + + - - + + credits - - integer + + - - - + + Credit - - + + + - - creditable_id + + - - + + machine_credits - - string + + - - - + + Credit - - + + + - - creditable_type + + - - + + space_credit - - integer + + - - - + + Credit - - + + + - - hours + + - - - + + space_credits - - - + + - - + + Credit - - Credit + + + - - + + - - - + + training_credit - - + + - - MachineReservation + + Credit - - + + + - - + + - - + + training_credits - - UserTag + + - - - + + UsersCredit - - + + + - - user_tags + + - - - + + users_credits - - - + + - - + + - - UserTag + + - - + + integer - - + + + - - + + - - + + creditable_id - - Component + + - - - + + string - - + + + - - components + + - - + + creditable_type - - + + - - + + integer - - string + + + - - - + + - - + + hours - - name + + + - - - + + + - - - + + - - + + Credit - - Component + + - - + + - - + + - - + + - - Invoice + + AuthProvider - - - + + + - - + + - - invoices + + auth_provider - - + + - - PaymentSchedule + + DatabaseProvider - - - + + + - - + + - - payment_schedule + + providable - - + + - - + + - - + + - - boolean + + string - - - + + + - - + + - - active + + authorization_endpoint - - + + - - integer + + string - - - + + + - - + + - - amount_off + + base_url - - + + - - string + + string - - - + + + - - + + - - code + + client_id - - + + - - integer + + string - - - + + + - - + + - - max_usages + + client_secret - - + + - - string + + string - - - + + + - - + + - - name + + profile_url - - + + - - integer + + string - - - + + + - - + + - - percent_off + + scopes - - + + - - datetime + + string - - - + + + - - + + - - valid_until + + token_endpoint - - + + + - - string + + + - - - + + - - + + OAuth2Provider - - validity_per_user + + - - - + + - - - + + - - + + - - Coupon + + Availability - - + + + - - + + - - + + availabilities - - InvoicingProfile + + - - - + + Availability - - + + + - - invoicing_profile + + - - + + availability - - Wallet + + - - - + + AvailabilityTag - - + + + - - wallet + + - - + + availability_tags - - WalletTransaction + + - - - + + Event - - + + + - - wallet_transactions + + - - + + event - - + + - - + + MachinesAvailability - - integer + + + - - - + + - - + + machines_availabilities - - amount + + - - - + + Plan - - - + + + - - + + - - Wallet + + plans - - + + - - + + PlansAvailability - - + + + - - + + - - TrainingsPricing + + plans_availabilities - - - + + - - + + Reservation - - trainings_pricings + + + - - + + - - + + reservations - - + + - - integer + + Slot - - - + + + - - + + - - amount + + slots - - - + + - - - + + SpacesAvailability - - + + + - - TrainingsPricing + + - - + + spaces_availabilities - - - + + - - - - - Account - - - - - - - - - - - - - SpaceReservation - - - - - - - - - - - - - - - StatisticProfileTraining - - - - - - - - - - statistic_profile_trainings - - - - - - - - - - - - - - StatisticProfileTraining - - - - - - - 0..* - 1 - - - - - - - - 0..* - ? - - - 0..* - 1 - - - 0..* - 1 - - - - - 0..* - 0..* - - - 1 - - - 1 - - - 0..* - 1 - - 0..1 - ? - - - 1 - - 0..* - 0..* - - - 0..* - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 1 - - 0..* - ? - - - 0..* - 1 - - - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 1 - - - - - 0..* - 0..* - - - 0..* - 1 - - - 1 - - - - - 0..* - - - 0..* - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 0..* - 1 - - - - - - - - - 0..* - 1 - - - 0..* - 1 - - - - - 0..* - 0..* - - - 0..1 - 1 - - - - - - 1 - - - 1 - - - 0..* - 1 - - - 0..* - 1 - - - - - - - - - - - - 1 - - - 0..* - 1 - - - 0..1 - 1 - - - - - - 0..1 - 1 - - - 1 - - - - - - 0..* - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 0..* - 1 - - 0..1 - ? - - - - - - 1 - - - 1 - - - 0..1 - 1 - - - - - - 0..* - 1 - - - 0..1 - 1 - - - - - - 1 - - - 0..* - 1 - - - 1 - - - 0..1 - 1 - - - - - - 1 - - - - - - 1 - - - 1 - - - 1 - - - 1 - - - 0..* - 1 - - 0..* - 0..* - - - 0..* - 1 - - - - - - 0..* - 1 - - - 0..* - 1 - - - - - - 0..* - 1 - - - 0..* - 1 - - - - - - - - - 0..* - 1 - - - 0..* - 1 - - - - - - - - - - - - 0..* - 1 - - - 0..1 - 1 - - - - - 0..* - 0..* - - - 0..* - 1 - - - - - - 0..* - 1 - - - 1 - - - - - 0..* - - - 0..1 - 1 - - 0..* - ? - - - 0..* - 1 - - - - - - 1 - - - 1 - - - 0..* - 1 - - - 0..1 - 1 - - - - - - - - - 1 - - - 0..1 - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 1 - - - 1 - - - 1 - - - - - - 0..1 - 1 - - - - - - 0..1 - 1 - - - 0..* - 1 - - - 0..1 - 1 - - - 0..1 - 1 - - - 0..1 - 1 - - - - - - - - - 1 - - - 0..* - 1 - - - - - - - - 0..1 - 0..1 - - - 0..* - 1 - - - 1 - - - - - - - - - 1 - - - 0..1 - 1 - - - 0..1 - 1 - - - 1 - - - - - - - - - - - - 1 - - - 1 - - 0..* - - - 0..* - 1 - - - - - - - - - - - - - - - 1 - - - - - - 0..* - 1 - - - - - - - - - 1 - - 0..* - 0..* - - - 0..1 - 1 - - - 1 - - - 1 - - - 0..1 - 1 - - - 1 - - - - - - 0..* - 1 - - - - - - 0..* - 1 - - - 0..* - 1 - - - 1 - - - 0..1 - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 1 - - - - - - 1 - - - 0..* - 1 - - - 0..* - 1 - - - - - - 0..* - 1 - - - - - 0..* - 0..* - - - - - - - - - 0..* - 1 - - - 0..* - 1 - - - - - - 1 - - - 0..* - 1 - - - - - - 0..1 - 1 - - - - - - 0..* - 1 - - - 0..* - 1 - - - - - - 0..* - 1 - - - - - 0..* - 0..* - - - 0..* - 1 - - - 0..1 - 1 - - - 1 - - - 1 - - - - - - - - - 0..1 - 1 - - 0..* - ? - - - 0..* - 1 - - - - - - 1 - - - 0..1 - 1 - - - 1 - - - - - - 1 - - - 0..* - 1 - - - 0..* - 1 - - - 1 - - - - - - 0..* - 1 - - - 0..1 - 1 - - - 0..* - 1 - - - - - - - - - 0..* - 1 - - - 0..* - 1 - - - - - - 0..* - 1 - - - - - - - - - - - - 0..* - 1 - - 0..* - 0..* - - - 1 - - 0..* - - 0..* - 0..* - - - 0..1 - 1 - - - - - - 0..* - 1 - - - - - - - - - - - - - - - - - - - - - - - 0..* - 0..* - - - 1 - - - - - - 0..* - 1 - - - 1 - - - - - - - - - 0..* - 1 - - - 0..* - 1 - - - 0..* - 1 - - 0..* - - - 0..1 - 1 - - 0..* - 0..* - - - 0..1 - 1 - - - 0..* - 1 - - - - - - 0..* - 1 - - - 1 - - - 1 - - - - - - 0..1 - 1 - - - - - - - - - - - - - - - 0..* - 1 - - - 1 - - - - - - - - - 0..1 - 1 - - - 0..* - 1 - - 0..1 - 0..1 - - - 0..1 - 1 - - - 0..1 - 1 - - - 1 - - - 0..1 - 1 - - 0..* - - - 1 - - - 1 - - - 1 - - - - - - 0..* - 1 - - - 0..1 - 1 - - - 1 - - - 0..* - 1 - - - 0..* - 1 - - - - - 0..* - 0..* - - 0..* - 0..* - - - - - 0..* - 0..* - - - 1 - - 0..* - - - - - - 0..* - 1 - - - - - - - - - - - - - - - - - - + + TrainingsAvailability + + + + + + + + + + trainings_availabilities + + + + + + + + + + + + string + + + + + + + + + + available_type + + + + + + boolean + + + + + + + + + + destroying + + + + + + datetime + + + + + + + + + + end_at + + + + + + datetime + + + + + + + + + + end_date + + + + + + boolean + + + + + + + + + + is_recurrent + + + + + + boolean + + + + + + + + + + lock + + + + + + integer + + + + + + + + + + nb_periods + + + + + + integer + + + + + + + + + + nb_total_places + + + + + + integer + + + + + + + + + + occurrence_id + + + + + + string + + + + + + + + + + period + + + + + + integer + + + + + + + + + + slot_duration + + + + + + datetime + + + + + + + + + + start_at + + + + + + + + + + + + + + Availability + + + + + + + + + + + + + + + + + + + + + + + User + + + + + + + + + + + + + + + text + + + + + + + + + + contents + + + + + + string + + + + + + + + + + name + + + + + + + + + + + + + + Stylesheet + + + + + + + + + + + + + + + AgeRange + + + + + + + + + + age_range + + + + + + Availability + + + + + + + + + + availability + + + + + + Category + + + + + + + + + + category + + + + + + Event + + + + + + + + + + event + + + + + + EventFile + + + + + + + + + + event_files + + + + + + EventImage + + + + + + + + + + event_image + + + + + + EventPriceCategory + + + + + + + + + + event_price_categories + + + + + + EventTheme + + + + + + + + + + event_themes + + + + + + Event + + + + + + + + + + events + + + + + + PriceCategory + + + + + + + + + + price_categories + + + + + + Reservation + + + + + + + + + + reservations + + + + + + + + + + + + integer + + + + + + + + + + amount + + + + + + text + + + + + + + + + + description + + + + + + integer + + + + + + + + + + nb_free_places + + + + + + integer + + + + + + + + + + nb_total_places + + + + + + integer + + + + + + + + + + recurrence_id + + + + + + string + + + + + + + + + + title + + + + + + + + + + + + + + Event + + + + + + + + + + + + + + + MachineFile + + + + + + + + + + machine_files + + + + + + + + + + + + string + + + + + + + + + + attachment + + + + + + string + + + + + + + + + + type + + + + + + integer + + + + + + + + + + viewable_id + + + + + + string + + + + + + + + + + viewable_type + + + + + + + + + + + + + + MachineFile + + + + + + + + + + + + + + + InvoiceItem + + + + + + + + + + invoice_items + + + + + + StatisticProfilePrepaidPack + + + + + + + + + + object + + + + + + OfferDay + + + + + + + + + + offer_day + + + + + + OfferDay + + + + + + + + + + offer_days + + + + + + Subscription + + + + + + + + + + subscription + + + + + + + + + + + + datetime + + + + + + + + + + end_at + + + + + + datetime + + + + + + + + + + start_at + + + + + + + + + + + + + + OfferDay + + + + + + + + + + + + + + + TrainingsPricing + + + + + + + + + + trainings_pricings + + + + + + + + + + + + integer + + + + + + + + + + amount + + + + + + + + + + + + + + TrainingsPricing + + + + + + + + + + + + + + + Credit + + + + + + + + + + credits + + + + + + Group + + + + + + + + + + group + + + + + + Credit + + + + + + + + + + machine_credits + + + + + + PaymentGatewayObject + + + + + + + + + + payment_gateway_object + + + + + + Plan + + + + + + + + + + plan + + + + + + PlanCategory + + + + + + + + + + plan_category + + + + + + PlanFile + + + + + + + + + + plan_file + + + + + + Plan + + + + + + + + + + plans + + + + + + Price + + + + + + + + + + prices + + + + + + Credit + + + + + + + + + + space_credits + + + + + + Subscription + + + + + + + + + + subscriptions + + + + + + Credit + + + + + + + + + + training_credits + + + + + + + + + + + + integer + + + + + + + + + + amount + + + + + + string + + + + + + + + + + base_name + + + + + + text + + + + + + + + + + description + + + + + + boolean + + + + + + + + + + disabled + + + + + + string + + + + + + + + + + interval + + + + + + integer + + + + + + + + + + interval_count + + + + + + boolean + + + + + + + + + + is_rolling + + + + + + boolean + + + + + + + + + + monthly_payment + + + + + + string + + + + + + + + + + name + + + + + + string + + + + + + + + + + slug + + + + + + string + + + + + + + + + + stp_plan_id + + + + + + integer + + + + + + + + + + training_credit_nb + + + + + + string + + + + + + + + + + type + + + + + + integer + + + + + + + + + + ui_weight + + + + + + + + + + + + + + Plan + + + + + + + + + + + + + + + AccountingPeriod + + + + + + + + + + accounting_periods + + + + + + + + + + + + datetime + + + + + + + + + + closed_at + + + + + + integer + + + + + + + + + + closed_by + + + + + + date + + + + + + + + + + end_at + + + + + + string + + + + + + + + + + footprint + + + + + + integer + + + + + + + + + + period_total + + + + + + integer + + + + + + + + + + perpetual_total + + + + + + date + + + + + + + + + + start_at + + + + + + + + + + + + + + AccountingPeriod + + + + + + + + + + 0..1 + 1 + + + + + + + + + + 1 + + 0..* + 0..* + + + + + + + + + + 0..1 + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 0..1 + 1 + + + + + + + + + + 1 + + + 1 + + + 0..1 + 1 + + + 0..1 + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 0..* + 1 + + 0..* + ? + + + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 0..1 + 1 + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + 0..1 + 0..1 + + + + + + + + + + 0..* + 1 + + + 0..* + 1 + + 0..1 + 0..1 + + + 1 + + + 0..1 + 1 + + + + + + + + + + 0..1 + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + 1 + + + 0..* + 1 + + + + + + + + + + 0..* + 1 + + + 0..1 + 1 + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + 1 + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + 1 + + 0..* + 0..* + + + 0..1 + 1 + + + 0..* + 1 + + + 0..1 + 1 + + + 0..1 + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + 0..* + 0..* + + + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + 1 + + + 1 + + + 1 + + + + + + + + + + 0..1 + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 1 + + + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + 1 + + + + + + + + + + 1 + + + + + + + + + + 1 + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + + 1 + + + 1 + + + 0..1 + 1 + + + 0..1 + 1 + + 0..* + 0..* + + 0..* + + + 0..* + 1 + + + 0..* + 1 + + + 0..1 + 1 + + + 0..1 + 1 + + 0..1 + ? + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + 0..* + 1 + + 0..* + 0..* + + 0..* + 0..* + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + + 0..1 + 1 + + 0..* + 0..* + + + 0..* + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + + + + + + + + + + + + + + + + 0..1 + 1 + + + 0..* + 1 + + + 1 + + + 1 + + + + + + + + + + + + + + + + + 1 + + + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + 0..1 + 1 + + + + + + + + + 0..* + + + 0..1 + 1 + + + + + + + + + + 1 + + + + + + + + + + 0..* + 1 + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + 0..* + 0..* + + 0..* + 0..* + + + + + + + + + + 1 + + 0..* + 0..* + + + 1 + + 0..* + 0..* + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + + + + + + + + + 0..* + 1 + + + 0..* + 1 + + + 0..1 + 1 + + + 0..* + 1 + + + 0..* + 1 + + 0..* + 0..* + + + 1 + + + + + + + + + + 1 + + + + + + + + + + 0..1 + 1 + + + 0..* + 1 + + + 1 + + + + + + + + + + 0..1 + 1 + + + + + + + + + 0..* + 0..* + + + 1 + + + 0..* + 1 + + 0..* + ? + + + + + + + + + + 0..* + 1 + + + + + + + + + 0..* + 0..* + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + 0..* + 0..* + + + 0..* + 1 + + 0..* + + + 1 + + + 0..* + 1 + + + 1 + + + 1 + + + 0..* + 1 + + + + + + + + + + 0..* + 1 + + + 0..1 + 1 + + + + + + + + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + 0..* + 1 + + 0..* + 0..* + + + 1 + + + 0..1 + 1 + + + 1 + + 0..* + + 0..* + ? + + + 1 + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + 1 + + + 0..* + 1 + + 0..* + + + 0..* + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + 1 + + + + + + + + + + 1 + + + + + + + + + + 0..* + 1 + + + 0..1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + + 1 + + + 0..* + 1 + + 0..* + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + 1 + + + + + + + + + + 1 + + + + + + + + + + 0..1 + 1 + + + + + + + + + + 0..* + 1 + + 0..1 + ? + + + + + + + + + + + + + + + + + 1 + + + 0..1 + 1 + + + 1 + + + + + + + + + + + + + + + + + 1 + + + 0..* + 1 + + + 0..* + 1 + + 0..* + ? + + + 0..* + 1 + + + 0..1 + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + + + + + + + + 0..* + 1 + + + + + + + + + + + + + + + + + 1 + + + 0..* + 1 + + + 0..* + 1 + + + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + 0..1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + 0..* + 1 + + + 1 + + + 1 + + + + + + + + + 0..* + 0..* + + + 0..* + 1 + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + 0..* + 0..* + + + + + + + + + + 0..* + 1 + + + 0..1 + 1 + + + 1 + + + 1 + + + + + + + + + + 0..1 + 1 + + + 1 + + + 0..1 + 1 + + + 1 + + + 0..1 + 1 + + + 1 + + + 0..* + 1 + + + 0..* + 1 + + 0..* + + + 1 + + 0..* + + + 0..1 + 1 + + + + + + + + + + 0..* + 1 + + + 0..* + 1 + + + + + + + + + + + + + + + + + + + + + + + + 0..* + 1 + + + 0..* + 1 diff --git a/doc/database.svg b/doc/database.svg index 93737bd07..4dfd282b7 100644 --- a/doc/database.svg +++ b/doc/database.svg @@ -1,16556 +1,20795 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - integer + + text - - - + + + - - + + - - id + + contents - - + + - - + + timestamp - - + + + - - varchar + + - - - + + created_at - - + + - - name + + timestamp - - + + + - - timestamp + + - - - + + updated_at - - + + - - created_at + + varchar - - + + + - - timestamp + + - - - + + name - - + + - - updated_at + + - - - + + - - - + + integer - - + + + - - custom_assets + + - - + + id - - + + + - - + + + - - integer + + - - - + + stylesheets - - + + - - id + + - - + + - - + + - - + + - - integer + + varchar - - - + + + - - + + - - event_id + + signaled_type - - + + - - integer + + integer - - - + + + - - + + - - event_theme_id + + signaled_id - - - + + - - - + + varchar - - + + + - - events_event_themes + + - - + + first_name - - + + - - + + varchar - - integer + + + - - - + + - - + + last_name - - id + + - - + + varchar - - + + + - - + + - - integer + + email - - - + + - - + + text - - event_id + + + - - + + - - integer + + message - - - + + - - + + timestamp - - price_category_id + + + - - + + - - integer + + created_at - - - + + - - + + timestamp - - amount + + + - - + + - - timestamp + + updated_at - - - + + - - + + - - created_at + + - - + + integer - - timestamp + + + - - - + + - - + + id - - updated_at + + + - - - + + + - - - + + - - + + abuses - - event_price_categories + + - - + + - - + + - - + + - - bigint + + boolean - - - + + + - - + + - - id + + gender - - + + - - + + date - - + + + - - varchar + + - - - + + birthday - - + + - - footprint + + integer - - + + + - - varchar + + - - - + + group_id - - + + - - data + + integer - - + + + - - varchar + + - - - + + user_id - - + + - - klass + + integer - - + + + - - timestamp + + - - - + + role_id - - + + - - created_at + + timestamp - - + + + - - timestamp + + - - - + + created_at - - + + - - updated_at + + timestamp - - - + + + - - - + + - - + + updated_at - - footprint_debugs + + - - + + - - + + - - + + integer - - + + + - - integer + + - - - + + id - - + + + - - id + + + - - + + - - + + statistic_profiles - - + + - - integer + + - - - + + - - + + - - total + + - - + + varchar - - timestamp + + + - - - + + - - + + name - - created_at + + - - + + integer - - timestamp + + + - - - + + - - + + default_places - - updated_at + + - - + + text - - varchar + + + - - - + + - - + + description - - reference + + - - + + varchar - - varchar + + + - - - + + - - + + slug - - payment_method + + - - + + timestamp - - timestamp + + + - - - + + - - + + created_at - - avoir_date + + - - + + timestamp - - integer + + + - - - + + - - + + updated_at - - invoice_id + + - - + + text - - varchar + + + - - - + + - - + + characteristics - - type + + - - + + boolean - - boolean + + + - - - + + - - + + disabled - - subscription_to_expire + + - - + + - - text + + - - - + + integer - - + + + - - description + + - - + + id - - integer + + + - - - + + + - - + + - - wallet_amount + + spaces - - + + - - integer + + - - - + + - - + + - - wallet_transaction_id + + bigint - - + + + - - integer + + - - - + + invoicing_profile_id - - + + - - coupon_id + + bigint - - + + + - - varchar + + - - - + + profile_custom_field_id - - + + - - footprint + + varchar - - + + + - - varchar + + - - - + + value - - + + - - environment + + timestamp - - + + + - - integer + + - - - + + created_at - - + + - - invoicing_profile_id + + timestamp - - + + + - - integer + + - - - + + updated_at - - + + - - operator_profile_id + + - - + + - - integer + + bigint - - - + + + - - + + - - statistic_profile_id + + id - - - + + + - - - + + + - - + + - - invoices + + user_profile_custom_fields - - + + - - + + - - + + - - integer + + - - - + + integer - - + + + - - id + + - - + + plan_id - - + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - group_id + + - - + + timestamp - - integer + + + - - - + + - - + + updated_at - - plan_id + + - - + + timestamp - - varchar + + + - - - + + - - + + expiration_date - - priceable_type + + - - + + timestamp - - integer + + + - - - + + - - + + canceled_at - - priceable_id + + - - + + integer - - integer + + + - - - + + - - + + statistic_profile_id - - amount + + - - + + timestamp - - timestamp + + + - - - + + - - + + start_at - - created_at + + - - + + - - timestamp + + - - - + + integer - - + + + - - updated_at + + - - - + + id - - - + + + - - + + + - - prices + + - - + + subscriptions - - + + - - + + - - integer + + - - - + + - - + + integer - - id + + + - - + + - - + + project_id - - + + - - text + + integer - - - + + + - - + + - - query + + component_id - - + + - - integer + + - - - + + - - + + integer - - statistic_type_id + + + - - + + - - timestamp + + id - - - + + + - - + + + - - created_at + + - - + + projects_components - - timestamp + + - - - + + - - + + - - updated_at + + - - + + - - varchar + + varchar - - - + + + - - + + - - field + + slug - - + + - - varchar + + integer - - - + + + - - + + - - es_index + + sluggable_id - - + + - - varchar + + varchar(50) - - - + + + - - + + - - es_type + + sluggable_type - - - + + - - - + + varchar - - + + + - - statistic_custom_aggregations + + - - + + scope - - + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - id + + - - + + - - + + - - + + integer - - integer + + + - - - + + - - + + id - - project_id + + + - - + + + - - integer + + - - - + + friendly_id_slugs - - + + - - machine_id + + - - - + + - - - + + - - + + - - projects_machines + + varchar - - + + + - - + + - - + + issuer - - + + - - integer + + boolean - - - + + + - - + + - - id + + discovery - - + + - - + + varchar - - + + + - - integer + + - - - + + client_auth_method - - + + - - user_id + + varchar - - + + + - - integer + + - - - + + scope - - + + - - tag_id + + varchar - - + + + - - timestamp + + - - - + + response_type - - + + - - created_at + + varchar - - + + + - - timestamp + + - - - + + response_mode - - + + - - updated_at + + varchar - - - + + + - - - + + - - + + display - - user_tags + + - - + + varchar - - + + + - - + + - - integer + + prompt - - - + + - - + + boolean - - id + + + - - + + - - + + send_scope_to_token_endpoint - - + + - - integer + + varchar - - - + + + - - + + - - user_id + + post_logout_redirect_uri - - + + - - integer + + varchar - - - + + + - - + + - - credit_id + + uid_field - - + + - - integer + + varchar - - - + + + - - + + - - hours_used + + client__identifier - - + + - - timestamp + + varchar - - - + + + - - + + - - created_at + + client__secret - - + + - - timestamp + + varchar - - - + + + - - + + - - updated_at + + client__redirect_uri - - - + + - - - + + varchar - - + + + - - users_credits + + - - + + client__scheme - - + + - - + + varchar - - integer + + + - - - + + - - + + client__host - - id + + - - + + varchar - - + + + - - + + - - varchar + + client__port - - - + + - - + + varchar - - name + + + - - + + - - timestamp + + client__authorization_endpoint - - - + + - - + + varchar - - created_at + + + - - + + - - timestamp + + client__token_endpoint - - - + + - - + + varchar - - updated_at + + + - - + + - - varchar + + client__userinfo_endpoint - - - + + - - + + varchar - - slug + + + - - + + - - boolean + + client__jwks_uri - - - + + - - + + varchar - - disabled + + + - - - + + - - - + + client__end_session_endpoint - - + + - - groups + + varchar - - + + + - - + + - - + + profile_url - - + + - - integer + + timestamp - - - + + + - - + + - - id + + created_at - - + + - - + + timestamp - - + + + - - integer + + - - - + + updated_at - - + + - - user_id + + - - + + - - varchar + + bigint - - - + + + - - + + - - first_name + + id - - + + + - - varchar + + + - - - + + - - + + open_id_connect_providers - - last_name + + - - + + - - varchar + + - - - + + - - + + integer - - email + + + - - + + - - timestamp + + training_id - - - + + - - + + integer - - created_at + + + - - + + - - timestamp + + availability_id - - - + + - - + + timestamp - - updated_at + + + - - - + + - - - + + created_at - - + + - - invoicing_profiles + + timestamp - - - - - - - - - + + + - - + + - - + + updated_at - - + + - - integer + + - - - + + - - + + integer - - id + + + - - + + - - + + id - - + + + - - varchar + + + - - - + + - - + + trainings_availabilities - - signaled_type + + - - + + - - integer + + - - - + + - - + + varchar - - signaled_id + + + - - + + - - varchar + + name - - - + + - - + + text - - first_name + + + - - + + - - varchar + + description - - - + + - - + + text - - last_name + + + - - + + - - varchar + + spec - - - + + - - + + timestamp - - email + + + - - + + - - text + + created_at - - - + + - - + + timestamp - - message + + + - - + + - - timestamp + + updated_at - - - + + - - + + varchar - - created_at + + + - - + + - - timestamp + + slug - - - + + - - + + boolean - - updated_at + + + - - - + + - - - + + disabled - - + + - - abuses + + - - + + - - + + integer - - + + + - - integer + + - - - + + id - - + + + - - id + + + - - + + - - + + machines - - + + - - text + + - - - + + - - + + - - message + + integer - - + + + - - timestamp + + - - - + + wallet_id - - + + - - created_at + + varchar - - + + + - - timestamp + + - - - + + transaction_type - - + + - - updated_at + + integer - - + + + - - varchar + + - - - + + amount - - + + - - reservable_type + + timestamp - - + + + - - integer + + - - - + + created_at - - + + - - reservable_id + + timestamp - - + + + - - integer + + - - - + + updated_at - - + + - - nb_reserve_places + + integer - - + + + - - integer + + - - - + + invoicing_profile_id - - + + - - statistic_profile_id + + - - - + + - - - + + integer - - + + + - - reservations + + - - + + id - - + + + - - + + + - - integer + + - - - + + wallet_transactions - - + + - - id + + - - + + - - + + - - + + - - boolean + + integer - - - + + + - - + + - - gender + + total - - + + - - date + + varchar - - - + + + - - + + - - birthday + + reference - - + + - - integer + + varchar - - - + + + - - + + - - group_id + + payment_method - - + + - - integer + + integer - - - + + + - - + + - - user_id + + wallet_amount - - + + - - integer + + bigint - - - + + + - - + + - - role_id + + wallet_transaction_id - - + + - - timestamp + + bigint - - - + + + - - + + - - created_at + + coupon_id - - + + - - timestamp + + varchar - - - + + + - - + + - - updated_at + + footprint - - - + + - - - + + varchar - - + + + - - statistic_profiles + + - - + + environment - - + + - - + + bigint - - integer + + + - - - + + - - + + invoicing_profile_id - - id + + - - + + bigint - - + + + - - + + - - integer + + statistic_profile_id - - - + + - - + + bigint - - o_auth2_provider_id + + + - - + + - - varchar + + operator_profile_id - - - + + - - + + timestamp - - local_field + + + - - + + - - varchar + + created_at - - - + + - - + + timestamp - - api_field + + + - - + + - - timestamp + + updated_at - - - + + - - + + timestamp - - created_at + + + - - + + - - timestamp + + start_at - - - + + - - + + - - updated_at + + - - + + bigint - - varchar + + + - - - + + - - + + id - - local_model + + + - - + + + - - varchar + + - - - + + payment_schedules - - + + - - api_endpoint + + - - + + - - varchar + + - - - + + varchar - - + + + - - api_data_type + + - - + + name - - jsonb + + - - - + + timestamp - - + + + - - transformation + + - - - + + created_at - - - + + - - + + timestamp - - o_auth2_mappings + + + - - + + - - + + updated_at - - + + - - + + varchar - - integer + + + - - - + + - - + + slug - - id + + - - + + boolean - - + + + - - + + - - integer + + disabled - - - + + - - + + - - setting_id + + - - + + integer - - varchar + + + - - - + + - - + + id - - value + + + - - + + + - - timestamp + + - - - + + groups - - + + - - created_at + + - - + + - - timestamp + + - - - + + - - + + bigint - - updated_at + + + - - + + - - varchar + + prepaid_pack_id - - - + + - - + + bigint - - footprint + + + - - + + - - integer + + statistic_profile_id - - - + + - - + + integer - - invoicing_profile_id + + + - - - + + - - - + + minutes_used - - + + - - history_values + + timestamp - - + + + - - + + - - + + expires_at - - + + - - integer + + timestamp - - - + + + - - + + - - id + + created_at - - + + - - + + timestamp - - + + + - - varchar + + - - - + + updated_at - - + + - - name + + - - + + - - varchar + + bigint - - - + + + - - + + - - resource_type + + id - - + + + - - integer + + + - - - + + - - + + statistic_profile_prepaid_packs - - resource_id + + - - + + - - timestamp + + - - - + + - - + + varchar - - created_at + + + - - + + - - timestamp + + name - - - + + - - + + timestamp - - updated_at + + + - - - + + - - - + + created_at - - + + - - roles + + timestamp - - + + + - - + + - - + + updated_at - - integer + + - - - + + integer - - + + + - - id + + - - + + nb_total_places - - + + - - + + varchar - - varchar + + + - - - + + - - + + slug - - url + + - - + + text - - varchar + + + - - - + + - - + + description - - name + + - - + + boolean - - varchar + + + - - - + + - - + + public_page - - color + + - - + + boolean - - varchar + + + - - - + + - - + + disabled - - text_color + + - - + + - - boolean + + - - - + + integer - - + + + - - text_hidden + + - - + + id - - timestamp + + + - - - + + + - - + + - - created_at + + trainings - - + + - - timestamp + + - - - + + - - + + - - updated_at + + varchar - - - + + + - - - + + - - + + creditable_type - - i_calendars + + - - + + integer - - + + + - - + + - - integer + + creditable_id - - - + + - - + + integer - - id + + + - - + + - - + + plan_id - - + + - - integer + + integer - - - + + + - - + + - - space_id + + hours - - + + - - integer + + timestamp - - - + + + - - + + - - availability_id + + created_at - - + + - - timestamp + + timestamp - - - + + + - - + + - - created_at + + updated_at - - + + - - timestamp + + - - - + + - - + + integer - - updated_at + + + - - - + + - - - + + id - - + + + - - spaces_availabilities + + + - - + + - - + + credits - - + + - - bigint + + - - - + + - - + + - - id + + - - + + varchar - - + + + - - + + - - integer + + uid - - - + + - - + + timestamp - - amount + + + - - + + - - timestamp + + dtstart - - - + + - - + + timestamp - - due_date + + + - - + + - - varchar + + dtend - - - + + - - + + varchar - - state + + + - - + + - - jsonb + + summary - - - + + - - + + varchar - - details + + + - - + + - - varchar + + description - - - + + - - + + varchar - - payment_method + + + - - + + - - varchar + + attendee - - - + + - - + + integer - - client_secret + + + - - + + - - bigint + + i_calendar_id - - - + + - - + + timestamp - - payment_schedule_id + + + - - + + - - bigint + + created_at - - - + + - - + + timestamp - - invoice_id + + + - - + + - - varchar + + updated_at - - - + + - - + + - - footprint + + - - + + integer - - timestamp + + + - - - + + - - + + id - - created_at + + + - - + + + - - timestamp + + - - - + + i_calendar_events - - + + - - updated_at + + - - - + + - - - + + - - + + integer - - payment_schedule_items + + + - - + + - - + + training_id - - + + - - integer + + integer - - - + + + - - + + - - id + + machine_id - - + + - - + + - - + + - - timestamp + + integer - - - + + + - - + + - - start_at + + id - - + + + - - timestamp + + + - - - + + - - + + trainings_machines - - end_at + + - - + + - - timestamp + + - - - + + - - + + - - created_at + + integer - - + + + - - timestamp + + - - - + + plan_id - - + + - - updated_at + + integer - - + + + - - integer + + - - - + + availability_id - - + + - - availability_id + + - - + + - - timestamp + + integer - - - + + + - - + + - - ex_start_at + + id - - + + + - - timestamp + + + - - - + + - - + + plans_availabilities - - ex_end_at + + - - + + - - timestamp + + - - - + + - - + + varchar - - canceled_at + + + - - + + - - boolean + + object_type - - - + + - - + + bigint - - offered + + + - - + + - - boolean + + object_id - - - + + - - + + bigint - - destroying + + + - - - + + - - - + + payment_schedule_id - - + + - - slots + + boolean - - + + + - - + + - - + + main - - integer + + - - - + + varchar - - + + + - - id + + - - + + footprint - - + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - statistic_index_id + + - - + + timestamp - - varchar + + + - - - + + - - + + updated_at - - key + + - - + + - - varchar + + - - - + + bigint - - + + + - - label + + - - + + id - - boolean + + + - - - + + + - - + + - - graph + + payment_schedule_objects - - + + - - timestamp + + - - - + + - - + + - - created_at + + integer - - + + + - - timestamp + + - - - + + user_id - - + + - - updated_at + + integer - - + + + - - boolean + + - - - + + role_id - - + + + - - simple + + + - - - + + - - - + + users_roles - - + + - - statistic_types + + - - + + - - + + - - + + - - integer + + varchar - - - + + + - - + + - - id + + key - - + + - - + + varchar - - + + + - - integer + + - - - + + label - - + + - - receiver_id + + timestamp - - + + + - - varchar + + - - - + + created_at - - + + - - attached_object_type + + timestamp - - + + + - - integer + + - - - + + updated_at - - + + - - attached_object_id + + - - + + - - integer + + integer - - - + + + - - + + - - notification_type_id + + id - - + + + - - boolean + + + - - - + + - - + + statistic_sub_types - - is_read + + - - + + - - timestamp + + - - - + + - - + + integer - - created_at + + + - - + + - - timestamp + + machine_id - - - + + - - + + integer - - updated_at + + + - - + + - - varchar + + availability_id - - - + + - - + + - - receiver_type + + - - + + integer - - boolean + + + - - - + + - - + + id - - is_send + + + - - + + + - - jsonb + + - - - + + machines_availabilities - - + + - - meta_data + + - - - + + - - - + + - - + + varchar - - notifications + + + - - + + - - + + priceable_type - - + + - - integer + + bigint - - - + + + - - + + - - id + + priceable_id - - + + - - + + bigint - - + + + - - varchar + + - - - + + group_id - - + + - - name + + integer - - + + + - - integer + + - - - + + amount - - + + - - amount + + integer - - + + + - - varchar + + - - - + + minutes - - + + - - interval + + varchar - - + + + - - integer + + - - - + + validity_interval - - + + - - group_id + + integer - - + + + - - varchar + + - - - + + validity_count - - + + - - stp_plan_id + + boolean - - + + + - - timestamp + + - - - + + disabled - - + + - - created_at + + timestamp - - + + + - - timestamp + + - - - + + created_at - - + + - - updated_at + + timestamp - - + + + - - integer + + - - - + + updated_at - - + + - - training_credit_nb + + - - + + - - boolean + + bigint - - - + + + - - + + - - is_rolling + + id - - + + + - - text + + + - - - + + - - + + prepaid_packs - - description + + - - + + - - varchar + + - - - + + - - + + - - type + + varchar - - + + + - - varchar + + - - - + + name - - + + - - base_name + + - - + + - - integer + + integer - - - + + + - - + + - - ui_weight + + id - - + + + - - integer + + + - - - + + - - + + themes - - interval_count + + - - + + - - varchar + + - - - + + - - + + - - slug + + integer - - + + + - - boolean + + - - - + + amount - - + + - - disabled + + timestamp - - + + + - - boolean + + - - - + + created_at - - + + - - monthly_payment + + timestamp - - - + + + - - - + + - - + + updated_at - - plans + + - - + + integer - - + + + - - + + - - + + invoicing_profile_id - - integer + + - - - + + - - + + - - id + + integer - - + + + - - + + - - + + id - - integer + + + - - - + + + - - + + - - availability_id + + wallets - - + + - - integer + + - - - + + - - + + - - tag_id + + integer - - + + + - - timestamp + + - - - + + user_id - - + + - - created_at + + integer - - + + + - - timestamp + + - - - + + tag_id - - + + - - updated_at + + timestamp - - - + + + - - - + + - - + + created_at - - availability_tags + + - - + + timestamp - - + + + - - + + - - integer + + updated_at - - - + + - - + + - - id + + - - + + integer - - + + + - - + + - - integer + + id - - - + + + - - + + + - - statistic_type_id + + - - + + user_tags - - integer + + - - - + + - - + + - - statistic_sub_type_id + + - - + + varchar - - timestamp + + + - - - + + - - + + name - - created_at + + - - + + text - - timestamp + + + - - - + + - - + + description - - updated_at + + - - - + + - - - + + - - + + integer - - statistic_type_sub_types + + + - - + + - - + + id - - + + + - - + + + - - integer + + - - - + + licences - - + + - - id + + - - + + - - + + - - + + - - varchar + + varchar - - - + + + - - + + - - name + + name - - + + - - timestamp + + integer - - - + + + - - + + - - created_at + + calls_count - - + + - - timestamp + + varchar - - - + + + - - + + - - updated_at + + token - - - + + - - - + + timestamp - - + + + - - settings + + - - + + created_at - - + + - - + + timestamp - - integer + + + - - - + + - - + + updated_at - - id + + - - + + - - + + - - + + integer - - integer + + + - - - + + - - + + id - - slot_id + + + - - + + + - - integer + + - - - + + open_api_clients - - + + - - reservation_id + + - - - + + - - - + + - - + + - - slots_reservations + + varchar - - + + + - - + + - - + + base_url - - integer + + - - - + + varchar - - + + + - - id + + - - + + token_endpoint - - + + - - + + varchar - - text + + + - - - + + - - + + authorization_endpoint - - contents + + - - + + varchar - - timestamp + + + - - - + + - - + + client_id - - created_at + + - - + + varchar - - timestamp + + + - - - + + - - + + client_secret - - updated_at + + - - + + timestamp - - varchar + + + - - - + + - - + + created_at - - name + + - - - + + timestamp - - - + + + - - + + - - stylesheets + + updated_at - - + + - - + + varchar - - + + + - - + + - - integer + + profile_url - - - + + - - + + varchar - - id + + + - - + + - - + + scopes - - + + - - varchar + + - - - + + - - + + integer - - category + + + - - + + - - varchar + + id - - - + + + - - + + + - - export_type + + - - + + o_auth2_providers - - varchar + + - - - + + - - + + - - query + + - - + + - - timestamp + + integer - - - + + + - - + + - - created_at + + total - - + + - - timestamp + + timestamp - - - + + + - - + + - - updated_at + + created_at - - + + - - integer + + timestamp - - - + + + - - + + - - user_id + + updated_at - - + + - - varchar + + varchar - - - + + + - - + + - - key + + reference - - + + - - varchar + + varchar - - - + + + - - + + - - extension + + payment_method - - - + + - - - + + timestamp - - + + + - - exports + + - - + + avoir_date - - + + - - + + integer - - varchar + + + - - - + + - - + + invoice_id - - key + + - - + + varchar - - + + + - - + + - - varchar + + type - - - + + - - + + boolean - - value + + + - - + + - - timestamp + + subscription_to_expire - - - + + - - + + text - - created_at + + + - - + + - - timestamp + + description - - - + + - - + + integer - - updated_at + + + - - - + + - - - + + wallet_amount - - + + - - ar_internal_metadata + + integer - - + + + - - + + - - + + wallet_transaction_id - - integer + + - - - + + integer - - + + + - - id + + - - + + coupon_id - - + + - - + + varchar - - integer + + + - - - + + - - + + footprint - - user_id + + - - + + varchar - - varchar + + + - - - + + - - + + environment - - first_name + + - - + + integer - - varchar + + + - - - + + - - + + invoicing_profile_id - - last_name + + - - + + integer - - varchar + + + - - - + + - - + + operator_profile_id - - phone + + - - + + integer - - text + + + - - - + + - - + + statistic_profile_id - - interest + + - - + + - - text + + - - - + + integer - - + + + - - software_mastered + + - - + + id - - timestamp + + + - - - + + + - - + + - - created_at + + invoices - - + + - - timestamp + + - - - + + - - + + - - updated_at + + - - + + bigint - - varchar + + + - - - + + - - + + proof_of_identity_type_id - - facebook + + - - + + bigint - - varchar + + + - - - + + - - + + user_id - - twitter + + - - + + varchar - - varchar + + + - - - + + - - + + attachment - - google_plus + + - - + + timestamp - - varchar + + + - - - + + - - + + created_at - - viadeo + + - - + + timestamp - - varchar + + + - - - + + - - + + updated_at - - linkedin + + - - + + - - varchar + + - - - + + bigint - - + + + - - instagram + + - - + + id - - varchar + + + - - - + + + - - + + - - youtube + + proof_of_identity_files - - + + - - varchar + + - - - + + - - + + - - vimeo + + varchar - - + + + - - varchar + + - - - + + name - - + + - - dailymotion + + timestamp - - + + + - - varchar + + - - - + + created_at - - + + - - github + + timestamp - - + + + - - varchar + + - - - + + updated_at - - + + - - echosciences + + varchar - - + + + - - varchar + + - - - + + slug - - + + - - website + + - - + + - - varchar + + integer - - - + + + - - + + - - pinterest + + id - - + + + - - varchar + + + - - - + + - - + + categories - - lastfm + + - - + + - - varchar + + - - - + + - - + + - - flickr + + integer - - + + + - - varchar + + - - - + + invoice_id - - + + - - job + + integer - - + + + - - varchar + + - - - + + amount - - + + - - tours + + timestamp - - - + + + - - - + + - - + + created_at - - profiles + + - - + + timestamp - - + + + - - + + - - integer + + updated_at - - - + + - - + + text - - id + + + - - + + - - + + description - - + + - - integer + + integer - - - + + + - - + + - - user_id + + invoice_item_id - - + + - - varchar + + varchar - - - + + + - - + + - - attachment + + footprint - - + + - - varchar + + varchar - - - + + + - - + + - - update_field + + object_type - - + + - - varchar + + bigint - - - + + + - - + + - - category + + object_id - - + + - - text + + boolean - - - + + + - - + + - - results + + main - - + + - - timestamp + + - - - + + - - + + integer - - created_at + + + - - + + - - timestamp + + id - - - + + + - - + + + - - updated_at + + - - - + + invoice_items - - - + + - - + + - - imports + + - - + + - - + + - - + + varchar - - integer + + + - - - + + - - + + name - - id + + - - + + timestamp - - + + + - - + + - - varchar + + created_at - - - + + - - + + timestamp - - es_type_key + + + - - + + - - varchar + + updated_at - - - + + - - + + varchar - - label + + + - - + + - - timestamp + + slug - - - + + - - + + - - created_at + + - - + + integer - - timestamp + + + - - - + + - - + + id - - updated_at + + + - - + + + - - boolean + + - - - + + age_ranges - - + + - - table + + - - + + - - boolean + + - - - + + - - + + integer - - ca + + + - - - + + - - - + + project_id - - + + - - statistic_indices + + integer - - + + + - - + + - - + + theme_id - - integer + + - - - + + - - + + - - id + + integer - - + + + - - + + - - + + id - - integer + + + - - - + + + - - + + - - project_id + + projects_themes - - + + - - integer + + - - - + + - - + + - - theme_id + + varchar - - - + + + - - - + + - - + + name - - projects_themes + + - - + + varchar - - + + + - - + + - - integer + + resource_type - - - + + - - + + integer - - id + + + - - + + - - + + resource_id - - + + - - varchar + + timestamp - - - + + + - - + + - - address + + created_at - - + + - - varchar + + timestamp - - - + + + - - + + - - street_number + + updated_at - - + + - - varchar + + - - - + + - - + + integer - - route + + + - - + + - - varchar + + id - - - + + + - - + + + - - locality + + - - + + roles - - varchar + + - - - + + - - + + - - country + + - - + + integer - - varchar + + + - - - + + - - + + group_id - - postal_code + + - - + + integer - - varchar + + + - - - + + - - + + amount - - placeable_type + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - placeable_id + + - - + + timestamp - - timestamp + + + - - - + + - - + + updated_at - - created_at + + - - + + integer - - timestamp + + + - - - + + - - + + training_id - - updated_at + + - - - + + - - - + + - - + + integer - - addresses + + + - - + + - - + + id - - + + + - - integer + + + - - - + + - - + + trainings_pricings - - id + + - - + + - - + + - - + + - - integer + + integer - - - + + + - - + + - - group_id + + user_id - - + + - - integer + + varchar - - - + + + - - + + - - amount + + first_name - - + + - - timestamp + + varchar - - - + + + - - + + - - created_at + + last_name - - + + - - timestamp + + varchar - - - + + + - - + + - - updated_at + + phone - - + + - - integer + + text - - - + + + - - + + - - training_id + + interest - - - + + - - - + + text - - + + + - - trainings_pricings + + - - + + software_mastered - - + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - id + + - - + + timestamp - - + + + - - + + - - varchar + + updated_at - - - + + - - + + varchar - - slug + + + - - + + - - integer + + facebook - - - + + - - + + varchar - - sluggable_id + + + - - + + - - varchar(50) + + twitter - - - + + - - + + varchar - - sluggable_type + + + - - + + - - varchar + + google_plus - - - + + - - + + varchar - - scope + + + - - + + - - timestamp + + viadeo - - - + + - - + + varchar - - created_at + + + - - - + + - - - + + linkedin - - + + - - friendly_id_slugs + + varchar - - + + + - - + + - - + + instagram - - + + - - integer + + varchar - - - + + + - - + + - - id + + youtube - - + + - - + + varchar - - + + + - - integer + + - - - + + vimeo - - + + - - amount + + varchar - - + + + - - timestamp + + - - - + + dailymotion - - + + - - created_at + + varchar - - + + + - - timestamp + + - - - + + github - - + + - - updated_at + + varchar - - + + + - - integer + + - - - + + echosciences - - + + - - invoicing_profile_id + + varchar - - - + + + - - - + + - - + + website - - wallets + + - - + + varchar - - + + + - - + + - - integer + + pinterest - - - + + - - + + varchar - - id + + + - - + + - - + + lastfm - - + + - - varchar + + varchar - - - + + + - - + + - - email + + flickr - - + + - - varchar + + varchar - - - + + + - - + + - - encrypted_password + + job - - + + - - varchar + + varchar - - - + + + - - + + - - reset_password_token + + tours - - + + - - timestamp + + - - - + + - - + + integer - - reset_password_sent_at + + + - - + + - - timestamp + + id - - - + + + - - + + + - - remember_created_at + + - - + + profiles - - integer + + - - - + + - - + + - - sign_in_count + + - - + + varchar - - timestamp + + + - - - + + - - + + name - - current_sign_in_at + + - - + + timestamp - - timestamp + + + - - - + + - - + + created_at - - last_sign_in_at + + - - + + timestamp - - varchar + + + - - - + + - - + + updated_at - - confirmation_token + + - - + + varchar - - timestamp + + + - - - + + - - + + slug - - confirmed_at + + - - + + - - timestamp + + - - - + + integer - - + + + - - confirmation_sent_at + + - - + + id - - varchar + + + - - - + + + - - + + - - unconfirmed_email + + event_themes - - + + - - integer + + - - - + + - - + + - - failed_attempts + + varchar - - + + + - - varchar + + - - - + + name - - + + - - unlock_token + + integer - - + + + - - timestamp + + - - - + + weight - - + + - - locked_at + + timestamp - - + + + - - timestamp + + - - - + + created_at - - + + - - created_at + + timestamp - - + + + - - timestamp + + - - - + + updated_at - - + + - - updated_at + + text - - + + + - - boolean + + - - - + + description - - + + - - is_allow_contact + + - - + + - - integer + + bigint - - - + + + - - + + - - group_id + + id - - + + + - - varchar + + + - - - + + - - + + plan_categories - - username + + - - + + - - varchar + + - - - + + - - + + - - slug + + text - - + + + - - boolean + + - - - + + message - - + + - - is_active + + timestamp - - + + + - - varchar + + - - - + + created_at - - + + - - provider + + timestamp - - + + + - - varchar + + - - - + + updated_at - - + + - - uid + + varchar - - + + + - - varchar + + - - - + + reservable_type - - + + - - auth_token + + integer - - + + + - - timestamp + + - - - + + reservable_id - - + + - - merged_at + + integer - - + + + - - boolean + + - - - + + nb_reserve_places - - + + - - is_allow_newsletter + + integer - - + + + - - inet + + - - - + + statistic_profile_id - - + + - - current_sign_in_ip + + - - + + - - inet + + integer - - - + + + - - + + - - last_sign_in_ip + + id - - - + + + - - - + + + - - + + - - users + + reservations - - + + - - + + - - + + - - integer + + - - - + + integer - - + + + - - id + + - - + + event_id - - + + - - + + integer - - varchar + + + - - - + + - - + + event_theme_id - - name + + - - - + + - - - + + - - + + integer - - themes + + + - - + + - - + + id - - + + + - - + + + - - integer + + - - - + + events_event_themes - - + + - - id + + - - + + - - + + - - + + - - integer + + varchar - - - + + + - - + + - - training_id + + local_field - - + + - - integer + + varchar - - - + + + - - + + - - machine_id + + api_field - - - + + - - - + + timestamp - - + + + - - trainings_machines + + - - + + created_at - - + + - - + + timestamp - - integer + + + - - - + + - - + + updated_at - - id + + - - + + varchar - - + + + - - + + - - varchar + + local_model - - - + + - - + + varchar - - name + + + - - + + - - varchar + + api_endpoint - - - + + - - + + varchar - - code + + + - - + + - - integer + + api_data_type - - - + + - - + + jsonb - - percent_off + + + - - + + - - timestamp + + transformation - - - + + - - + + bigint - - valid_until + + + - - + + - - integer + + auth_provider_id - - - + + - - + + - - max_usages + + - - + + integer - - boolean + + + - - - + + - - + + id - - active + + + - - + + + - - timestamp + + - - - + + auth_provider_mappings - - + + - - created_at + + - - + + - - timestamp + + - - - + + integer - - + + + - - updated_at + + - - + + subscription_id - - varchar + + - - - + + timestamp - - + + + - - validity_per_user + + - - + + start_at - - integer + + - - - + + timestamp - - + + + - - amount_off + + - - - + + end_at - - - + + - - + + timestamp - - coupons + + + - - + + - - + + created_at - - + + - - + + timestamp - - integer + + + - - - + + - - + + updated_at - - id + + - - + + - - + + - - + + integer - - varchar + + + - - - + + - - + + id - - name + + + - - + + + - - timestamp + + - - - + + offer_days - - + + - - created_at + + - - + + - - timestamp + + - - - + + - - + + varchar - - updated_at + + + - - + + - - varchar + + viewable_type - - - + + - - + + integer - - slug + + + - - - + + - - - + + viewable_id - - + + - - categories + + varchar - - + + + - - + + - - + + attachment - - integer + + - - - + + varchar - - + + + - - id + + - - + + type - - + + - - + + timestamp - - varchar + + + - - - + + - - + + created_at - - uid + + - - + + timestamp - - timestamp + + + - - - + + - - + + updated_at - - dtstart + + - - + + - - timestamp + + - - - + + integer - - + + + - - dtend + + - - + + id - - varchar + + + - - - + + + - - + + - - summary + + assets - - + + - - varchar + + - - - + + - - + + - - description + + bigint - - + + + - - varchar + + - - - + + user_id - - + + - - attendee + + integer - - + + + - - integer + + - - - + + operator_id - - + + - - i_calendar_id + + text - - + + + - - timestamp + + - - - + + message - - + + - - created_at + + timestamp - - + + + - - timestamp + + - - - + + created_at - - + + - - updated_at + + timestamp - - - + + + - - - + + - - + + updated_at - - i_calendar_events + + - - + + - - + + - - + + bigint - - bigint + + + - - - + + - - + + id - - id + + + - - + + + - - + + - - + + proof_of_identity_refusals - - integer + + - - - + + - - + + - - total + + - - + + varchar - - varchar + + + - - - + + - - + + title - - reference + + - - + + text - - varchar + + + - - - + + - - + + description - - payment_method + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - wallet_amount + + - - + + timestamp - - bigint + + + - - - + + - - + + updated_at - - wallet_transaction_id + + - - + + integer - - bigint + + + - - - + + - - + + availability_id - - coupon_id + + - - + + integer - - varchar + + + - - - + + - - + + amount - - footprint + + - - + + integer - - varchar + + + - - - + + - - + + nb_total_places - - environment + + - - + + integer - - bigint + + + - - - + + - - + + nb_free_places - - invoicing_profile_id + + - - + + integer - - bigint + + + - - - + + - - + + recurrence_id - - statistic_profile_id + + - - + + integer - - bigint + + + - - - + + - - + + age_range_id - - operator_profile_id + + - - + + integer - - timestamp + + + - - - + + - - + + category_id - - created_at + + - - + + - - timestamp + + - - - + + integer - - + + + - - updated_at + + - - - + + id - - - + + + - - + + + - - payment_schedules + + - - + + events - - + + - - + + - - integer + + - - - + + - - + + - - id + + varchar - - + + + - - + + - - + + name - - varchar + + - - - + + timestamp - - + + + - - base_url + + - - + + created_at - - varchar + + - - - + + timestamp - - + + + - - token_endpoint + + - - + + updated_at - - varchar + + - - - + + integer - - + + + - - authorization_endpoint + + - - + + invoicing_profile_id - - varchar + + - - - + + - - + + - - client_id + + integer - - + + + - - varchar + + - - - + + id - - + + + - - client_secret + + + - - + + - - timestamp + + organizations - - - + + - - + + - - created_at + + - - + + - - timestamp + + - - - + + integer - - + + + - - updated_at + + - - + + statistic_index_id - - varchar + + - - - + + varchar - - + + + - - profile_url + + - - - + + key - - - + + - - + + varchar - - o_auth2_providers + + + - - + + - - + + label - - + + - - integer + + timestamp - - - + + + - - + + - - id + + created_at - - + + - - + + timestamp - - + + + - - varchar + + - - - + + updated_at - - + + - - name + + varchar - - + + + - - integer + + - - - + + data_type - - + + - - calls_count + + - - + + - - varchar + + integer - - - + + + - - + + - - token + + id - - + + + - - timestamp + + + - - - + + - - + + statistic_fields - - created_at + + - - + + - - timestamp + + - - - + + - - + + - - updated_at + + varchar - - - + + + - - - + + - - + + name - - open_api_clients + + - - + + varchar - - + + + - - + + - - + + code - - integer + + - - - + + integer - - + + + - - id + + - - + + percent_off - - + + - - + + timestamp - - date + + + - - - + + - - + + valid_until - - start_at + + - - + + integer - - date + + + - - - + + - - + + max_usages - - end_at + + - - + + boolean - - timestamp + + + - - - + + - - + + active - - closed_at + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - closed_by + + - - + + timestamp - - timestamp + + + - - - + + - - + + updated_at - - created_at + + - - + + varchar - - timestamp + + + - - - + + - - + + validity_per_user - - updated_at + + - - + + integer - - integer + + + - - - + + - - + + amount_off - - period_total + + - - + + - - integer + + - - - + + integer - - + + + - - perpetual_total + + - - + + id - - varchar + + + - - - + + + - - + + - - footprint + + coupons - - - + + - - - + + - - + + - - accounting_periods + + - - + + - - + + varchar - - + + + - - + + - - integer + + name - - - + + - - + + text - - user_id + + + - - + + - - integer + + description - - - + + - - + + timestamp - - role_id + + + - - - + + - - - + + created_at - - + + - - users_roles + + timestamp - - + + + - - + + - - + + updated_at - - integer + + - - - + + text - - + + + - - id + + - - + + tags - - + + - - + + integer - - integer + + + - - - + + - - + + licence_id - - plan_id + + - - + + varchar - - integer + + + - - - + + - - + + state - - availability_id + + - - - + + varchar - - - + + + - - + + - - plans_availabilities + + slug - - + + - - + + timestamp - - + + + - - integer + + - - - + + published_at - - + + - - id + + integer - - + + + - - + + - - + + author_statistic_profile_id - - integer + + - - - + + tsvector - - + + + - - subscription_id + + - - + + search_vector - - timestamp + + - - - + + - - + + - - start_at + + integer - - + + + - - timestamp + + - - - + + id - - + + + - - end_at + + + - - + + - - timestamp + + projects - - - + + - - + + - - created_at + + - - + + - - timestamp + + - - - + + varchar - - + + + - - updated_at + + - - - + + name - - - + + - - + + timestamp - - offer_days + + + - - + + - - + + created_at - - + + - - integer + + timestamp - - - + + + - - + + - - id + + updated_at - - + + - - + + - - + + - - varchar + + bigint - - - + + + - - + + - - viewable_type + + id - - + + + - - integer + + + - - - + + - - + + proof_of_identity_types - - viewable_id + + - - + + - - varchar + + - - - + + - - + + integer - - attachment + + + - - + + - - varchar + + statistic_profile_id - - - + + - - + + integer - - type + + + - - + + - - timestamp + + training_id - - - + + - - + + timestamp - - created_at + + + - - + + - - timestamp + + created_at - - - + + - - + + timestamp - - updated_at + + + - - - + + - - - + + updated_at - - + + - - assets + + - - + + - - + + integer - - + + + - - + + - - integer + + id - - - + + + - - + + + - - id + + - - + + statistic_profile_trainings - - + + - - + + - - integer + + - - - + + - - + + varchar - - project_id + + + - - + + - - integer + + name - - - + + - - + + timestamp - - space_id + + + - - - + + - - - + + created_at - - + + - - projects_spaces + + timestamp - - + + + - - + + - - + + updated_at - - integer + + - - - + + - - + + - - id + + integer - - + + + - - + + - - + + id - - timestamp + + + - - - + + + - - + + - - start_at + + settings - - + + - - timestamp + + - - - + + - - + + - - end_at + + - - + + varchar - - varchar + + + - - - + + - - + + footprint - - available_type + + - - + + varchar - - timestamp + + + - - - + + - - + + data - - created_at + + - - + + varchar - - timestamp + + + - - - + + - - + + klass - - updated_at + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - nb_total_places + + - - + + timestamp - - boolean + + + - - - + + - - + + updated_at - - destroying + + - - + + - - boolean + + - - - + + bigint - - + + + - - lock + + - - + + id - - boolean + + + - - - + + + - - + + - - is_recurrent + + footprint_debugs - - + + - - integer + + - - - + + - - + + - - occurrence_id + + bigint - - + + + - - varchar + + - - - + + proof_of_identity_type_id - - + + - - period + + bigint - - + + + - - integer + + - - - + + group_id - - + + - - nb_periods + + timestamp - - + + + - - timestamp + + - - - + + created_at - - + + - - end_date + + timestamp - - + + + - - integer + + - - - + + updated_at - - + + - - slot_duration + + - - - + + - - - + + bigint - - + + + - - availabilities + + - - + + id - - + + + - - + + + - - + + - - bigint + + proof_of_identity_types_groups - - - + + - - + + - - id + + - - + + - - + + - - + + varchar - - varchar + + + - - - + + - - + + version - - object_type + + + - - + + + - - bigint + + - - - + + schema_migrations - - + + - - object_id + + - - + + - - bigint + + - - - + + - - + + integer - - payment_schedule_id + + + - - + + - - boolean + + user_id - - - + + - - + + integer - - main + + + - - + + - - varchar + + credit_id - - - + + - - + + integer - - footprint + + + - - + + - - timestamp + + hours_used - - - + + - - + + timestamp - - created_at + + + - - + + - - timestamp + + created_at - - - + + - - + + timestamp - - updated_at + + + - - - + + - - - + + updated_at - - + + - - payment_schedule_objects + + - - + + - - + + integer - - + + + - - integer + + - - - + + id - - + + + - - id + + + - - + + - - + + users_credits - - + + - - varchar + + - - - + + - - + + - - name + + - - - + + integer - - - + + + - - + + - - components + + event_id - - + + - - + + integer - - + + + - - + + - - integer + + price_category_id - - - + + - - + + integer - - id + + + - - + + - - + + amount - - + + - - varchar + + timestamp - - - + + + - - + + - - name + + created_at - - + + - - timestamp + + timestamp - - - + + + - - + + - - created_at + + updated_at - - + + - - timestamp + + - - - + + - - + + integer - - updated_at + + + - - + + - - varchar + + id - - - + + + - - + + + - - slug + + - - - + + event_price_categories - - - + + - - + + - - event_themes + + - - + + - - + + - - + + varchar - - integer + + + - - - + + - - + + category - - id + + - - + + varchar - - + + + - - + + - - integer + + export_type - - - + + - - + + varchar - - plan_id + + + - - + + - - timestamp + + query - - - + + - - + + timestamp - - created_at + + + - - + + - - timestamp + + created_at - - - + + - - + + timestamp - - updated_at + + + - - + + - - timestamp + + updated_at - - - + + - - + + integer - - expiration_date + + + - - + + - - timestamp + + user_id - - - + + - - + + varchar - - canceled_at + + + - - + + - - integer + + key - - - + + - - + + varchar - - statistic_profile_id + + + - - - + + - - - + + extension - - + + - - subscriptions + + - - + + - - + + integer - - + + + - - integer + + - - - + + id - - + + + - - id + + + - - + + - - + + exports - - + + - - varchar + + - - - + + - - + + - - name + + integer - - + + + - - timestamp + + - - - + + space_id - - + + - - created_at + + integer - - + + + - - timestamp + + - - - + + availability_id - - + + - - updated_at + + timestamp - - - + + + - - - + + - - + + created_at - - tags + + - - + + timestamp - - + + + - - + + - - integer + + updated_at - - - + + - - + + - - id + + - - + + integer - - + + + - - + + - - varchar + + id - - - + + + - - + + + - - creditable_type + + - - + + spaces_availabilities - - integer + + - - - + + - - + + - - creditable_id + + - - + + integer - - integer + + + - - - + + - - + + user_id - - plan_id + + - - + + varchar - - integer + + + - - - + + - - + + attachment - - hours + + - - + + varchar - - timestamp + + + - - - + + - - + + update_field - - created_at + + - - + + varchar - - timestamp + + + - - - + + - - + + category - - updated_at + + - - - + + text - - - + + + - - + + - - credits + + results - - + + - - + + timestamp - - + + + - - integer + + - - - + + created_at - - + + - - id + + timestamp - - + + + - - + + - - + + updated_at - - varchar + + - - - + + - - + + - - name + + integer - - + + + - - text + + - - - + + id - - + + + - - conditions + + + - - + + - - timestamp + + imports - - - + + - - + + - - created_at + + - - + + - - timestamp + + - - - + + integer - - + + + - - updated_at + + - - - + + statistic_type_id - - - + + - - + + integer - - price_categories + + + - - + + - - + + statistic_sub_type_id - - + + - - integer + + timestamp - - - + + + - - + + - - id + + created_at - - + + - - + + timestamp - - + + + - - integer + + - - - + + updated_at - - + + - - statistic_index_id + + - - + + - - varchar + + integer - - - + + + - - + + - - key + + id - - + + + - - varchar + + + - - - + + - - + + statistic_type_sub_types - - label + + - - + + - - timestamp + + - - - + + - - + + bigint - - created_at + + + - - + + - - timestamp + + proof_of_identity_type_id - - - + + - - + + bigint - - updated_at + + + - - + + - - varchar + + proof_of_identity_refusal_id - - - + + + - - + + + - - data_type + + - - - + + proof_of_identity_refusals_types - - - + + - - + + - - statistic_fields + + - - + + - - + + - - + + integer - - integer + + + - - - + + - - + + slot_id - - id + + - - + + integer - - + + + - - + + - - varchar + + reservation_id - - - + + - - + + - - name + + - - + + integer - - text + + + - - - + + - - + + id - - description + + + - - + + + - - text + + - - - + + slots_reservations - - + + - - spec + + - - + + - - timestamp + + - - - + + varchar - - + + + - - created_at + + - - + + name - - timestamp + + - - - + + - - + + - - updated_at + + integer - - + + + - - varchar + + - - - + + id - - + + + - - slug + + + - - + + - - boolean + + components - - - + + - - + + - - disabled + + - - - + + - - - + + - - + + timestamp - - machines + + + - - + + - - + + start_at - - + + - - + + timestamp - - integer + + + - - - + + - - + + end_at - - id + + - - + + timestamp - - + + + - - + + - - varchar + + created_at - - - + + - - + + timestamp - - name + + + - - + + - - timestamp + + updated_at - - - + + - - + + integer - - created_at + + + - - + + - - timestamp + + availability_id - - - + + - - + + timestamp - - updated_at + + + - - + + - - integer + + ex_start_at - - - + + - - + + timestamp - - nb_total_places + + + - - + + - - varchar + + ex_end_at - - - + + - - + + timestamp - - slug + + + - - + + - - text + + canceled_at - - - + + - - + + boolean - - description + + + - - + + - - boolean + + offered - - - + + - - + + boolean - - public_page + + + - - + + - - boolean + + destroying - - - + + - - + + - - disabled + + - - - + + integer - - - + + + - - + + - - trainings + + id - - + + + - - + + + - - + + - - + + slots - - integer + + - - - + + - - + + - - id + + - - + + varchar - - + + + - - + + - - integer + + name - - - + + - - + + timestamp - - invoice_id + + + - - + + - - integer + + created_at - - - + + - - + + timestamp - - amount + + + - - + + - - timestamp + + updated_at - - - + + - - + + - - created_at + + - - + + integer - - timestamp + + + - - - + + - - + + id - - updated_at + + + - - + + + - - text + + - - - + + tags - - + + - - description + + - - + + - - integer + + - - - + + - - + + timestamp - - invoice_item_id + + + - - + + - - varchar + + start_at - - - + + - - + + timestamp - - footprint + + + - - + + - - varchar + + end_at - - - + + - - + + varchar - - object_type + + + - - + + - - bigint + + available_type - - - + + - - + + timestamp - - object_id + + + - - + + - - boolean + + created_at - - - + + - - + + timestamp - - main + + + - - - + + - - - + + updated_at - - + + - - invoice_items + + integer - - + + + - - + + - - + + nb_total_places - - + + - - integer + + boolean - - - + + + - - + + - - id + + destroying - - + + - - + + boolean - - + + + - - varchar + + - - - + + lock - - + + - - name + + boolean - - + + + - - timestamp + + - - - + + is_recurrent - - + + - - created_at + + integer - - + + + - - timestamp + + - - - + + occurrence_id - - + + - - updated_at + + varchar - - + + + - - integer + + - - - + + period - - + + - - invoicing_profile_id + + integer - - - + + + - - - + + - - + + nb_periods - - organizations + + - - + + timestamp - - + + + - - + + - - integer + + end_date - - - + + - - + + integer - - id + + + - - + + - - + + slot_duration - - + + - - integer + + - - - + + - - + + integer - - reservation_id + + + - - + + - - integer + + id - - - + + + - - + + + - - event_price_category_id + + - - + + availabilities - - integer + + - - - + + - - + + - - booked + + - - + + varchar - - timestamp + + + - - - + + - - + + name - - created_at + + - - + + integer - - timestamp + + + - - - + + - - + + amount - - updated_at + + - - - + + varchar - - - + + + - - + + - - tickets + + interval - - + + - - + + integer - - + + + - - integer + + - - - + + group_id - - + + - - id + + varchar - - + + + - - + + - - + + stp_plan_id - - integer + + - - - + + timestamp - - + + + - - project_id + + - - + + created_at - - integer + + - - - + + timestamp - - + + + - - user_id + + - - + + updated_at - - timestamp + + - - - + + integer - - + + + - - created_at + + - - + + training_credit_nb - - timestamp + + - - - + + boolean - - + + + - - updated_at + + - - + + is_rolling - - boolean + + - - - + + text - - + + + - - is_valid + + - - + + description - - varchar + + - - - + + varchar - - + + + - - valid_token + + - - - + + type - - - + + - - + + varchar - - project_users + + + - - + + - - + + base_name - - + + - - integer + + integer - - - + + + - - + + - - id + + ui_weight - - + + - - + + integer - - + + + - - integer + + - - - + + interval_count - - + + - - statistic_index_id + + varchar - - + + + - - varchar + + - - - + + slug - - + + - - chart_type + + boolean - - + + + - - integer + + - - - + + disabled - - + + - - limit + + boolean - - + + + - - timestamp + + - - - + + monthly_payment - - + + - - created_at + + bigint - - + + + - - timestamp + + - - - + + plan_category_id - - + + - - updated_at + + - - - + + - - - + + integer - - + + + - - statistic_graphs + + - - + + id - - + + + - - + + + - - + + - - integer + + plans - - - + + - - + + - - id + + - - + + - - + + text - - + + + - - varchar + + - - - + + description - - + + - - name + + integer - - + + + - - integer + + - - - + + project_id - - + + - - default_places + + timestamp - - + + + - - text + + - - - + + created_at - - + + - - description + + timestamp - - + + + - - varchar + + - - - + + updated_at - - + + - - slug + + varchar - - + + + - - timestamp + + - - - + + title - - + + - - created_at + + integer - - + + + - - timestamp + + - - - + + step_nb - - + + - - updated_at + + - - + + - - text + + integer - - - + + + - - + + - - characteristics + + id - - + + + - - boolean + + + - - - + + - - + + project_steps - - disabled + + - - - + + - - - + + - - + + - - spaces + + - - + + varchar - - + + + - - + + - - + + email - - integer + + - - - + + varchar - - + + + - - id + + - - + + encrypted_password - - + + - - + + varchar - - varchar + + + - - - + + - - + + reset_password_token - - title + + - - + + timestamp - - text + + + - - - + + - - + + reset_password_sent_at - - description + + - - + + timestamp - - timestamp + + + - - - + + - - + + remember_created_at - - created_at + + - - + + integer - - timestamp + + + - - - + + - - + + sign_in_count - - updated_at + + - - + + timestamp - - integer + + + - - - + + - - + + current_sign_in_at - - availability_id + + - - + + timestamp - - integer + + + - - - + + - - + + last_sign_in_at - - amount + + - - + + varchar - - integer + + + - - - + + - - + + confirmation_token - - nb_total_places + + - - + + timestamp - - integer + + + - - - + + - - + + confirmed_at - - nb_free_places + + - - + + timestamp - - integer + + + - - - + + - - + + confirmation_sent_at - - recurrence_id + + - - + + varchar - - integer + + + - - - + + - - + + unconfirmed_email - - age_range_id + + - - + + integer - - integer + + + - - - + + - - + + failed_attempts - - category_id + + - - - + + varchar - - - + + + - - + + - - events + + unlock_token - - + + - - + + timestamp - - + + + - - bigint + + - - - + + locked_at - - + + - - id + + timestamp - - + + + - - + + - - + + created_at - - varchar + + - - - + + timestamp - - + + + - - gateway_object_id + + - - + + updated_at - - varchar + + - - - + + boolean - - + + + - - gateway_object_type + + - - + + is_allow_contact - - jsonb + + - - - + + integer - - + + + - - metadata + + - - + + group_id - - varchar + + - - - + + varchar - - + + + - - item_type + + - - + + username - - bigint + + - - - + + varchar - - + + + - - item_id + + - - - + + slug - - - + + - - + + boolean - - payment_gateway_objects + + + - - + + - - + + is_active - - + + - - + + varchar - - integer + + + - - - + + - - + + provider - - id + + - - + + varchar - - + + + - - + + - - text + + uid - - - + + - - + + varchar - - description + + + - - + + - - integer + + auth_token - - - + + - - + + timestamp - - project_id + + + - - + + - - timestamp + + merged_at - - - + + - - + + boolean - - created_at + + + - - + + - - timestamp + + is_allow_newsletter - - - + + - - + + inet - - updated_at + + + - - + + - - varchar + + current_sign_in_ip - - - + + - - + + inet - - title + + + - - + + - - integer + + last_sign_in_ip - - - + + - - + + varchar - - step_nb + + + - - - + + - - - + + mapped_from_sso - - + + - - project_steps + + timestamp - - + + + - - + + - - + + validated_at - - + + - - integer + + - - - + + - - + + integer - - id + + + - - + + - - + + id - - + + + - - varchar + + + - - - + + - - + + users - - name + + - - + + - - text + + - - - + + - - + + integer - - description + + + - - + + - - timestamp + + availability_id - - - + + - - + + integer - - created_at + + + - - + + - - timestamp + + tag_id - - - + + - - + + timestamp - - updated_at + + + - - + + - - text + + created_at - - - + + - - + + timestamp - - tags + + + - - + + - - integer + + updated_at - - - + + - - + + - - licence_id + + - - + + integer - - varchar + + + - - - + + - - + + id - - state + + + - - + + + - - varchar + + - - - + + availability_tags - - + + - - slug + + - - + + - - timestamp + + - - - + + - - + + integer - - published_at + + + - - + + - - integer + + amount - - - + + - - + + timestamp - - author_statistic_profile_id + + + - - + + - - tsvector + + due_date - - - + + - - + + varchar - - search_vector + + + - - - + + - - - + + state - - + + - - projects + + jsonb - - + + + - - + + - - + + details - - integer + + - - - + + varchar - - + + + - - id + + - - + + payment_method - - + + - - + + varchar - - integer + + + - - - + + - - + + client_secret - - open_api_client_id + + - - + + bigint - - integer + + + - - - + + - - + + payment_schedule_id - - calls_count + + - - + + bigint - - timestamp + + + - - - + + - - + + invoice_id - - at + + - - + + varchar - - timestamp + + + - - - + + - - + + footprint - - created_at + + - - + + timestamp - - timestamp + + + - - - + + - - + + created_at - - updated_at + + - - - + + timestamp - - - + + + - - + + - - open_api_calls_count_tracings + + updated_at - - + + - - + + - - + + - - integer + + bigint - - - + + + - - + + - - id + + id - - + + + - - + + + - - + + - - varchar + + payment_schedule_items - - - + + - - + + - - name + + - - + + - - text + + - - - + + text - - + + + - - description + + - - - + + query - - - + + - - + + integer - - licences + + + - - + + - - + + statistic_type_id - - + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - id + + - - + + timestamp - - + + + - - + + - - integer + + updated_at - - - + + - - + + varchar - - project_id + + + - - + + - - integer + + field - - - + + - - + + varchar - - component_id + + + - - - + + - - - + + es_index - - + + - - projects_components + + varchar - - + + + - - + + - - + + es_type - - + + - - integer + + - - - + + - - + + integer - - id + + + - - + + - - + + id - - + + + - - integer + + + - - - + + - - + + statistic_custom_aggregations - - wallet_id + + - - + + - - varchar + + - - - + + - - + + integer - - transaction_type + + + - - + + - - integer + + project_id - - - + + - - + + integer - - amount + + + - - + + - - timestamp + + user_id - - - + + - - + + timestamp - - created_at + + + - - + + - - timestamp + + created_at - - - + + - - + + timestamp - - updated_at + + + - - + + - - integer + + updated_at - - - + + - - + + boolean - - invoicing_profile_id + + + - - - + + - - - + + is_valid - - + + - - wallet_transactions + + varchar - - + + + - - + + - - + + valid_token - - integer + + - - - + + - - + + - - id + + integer - - + + + - - + + - - + + id - - integer + + + - - - + + + - - + + - - statistic_profile_id + + project_users - - + + - - integer + + - - - + + - - + + - - training_id + + integer - - + + + - - timestamp + + - - - + + project_id - - + + - - created_at + + integer - - + + + - - timestamp + + - - - + + machine_id - - + + - - updated_at + + - - - + + - - - + + integer - - + + + - - statistic_profile_trainings + + - - + + id - - + + + - - + + + - - integer + + - - - + + projects_machines - - + + - - id + + - - + + - - + + - - + + - - timestamp + + integer - - - + + + - - + + - - created_at + + reservation_id - - + + - - timestamp + + integer - - - + + + - - + + - - updated_at + + event_price_category_id - - - + + - - - + + integer - - + + + - - database_providers + + - - + + booked - - + + - - + + timestamp - - integer + + + - - - + + - - + + created_at - - id + + - - + + timestamp - - + + + - - + + - - integer + + updated_at - - - + + - - + + - - machine_id + + - - + + integer - - integer + + + - - - + + - - + + id - - availability_id + + + - - - + + + - - - + + - - + + tickets - - machines_availabilities + + - - + + - - + + - - + + - - integer + + - - - + + varchar - - + + + - - id + + - - + + name - - + + - - + + timestamp - - varchar + + + - - - + + - - + + created_at - - name + + - - + + timestamp - - varchar + + + - - - + + - - + + updated_at - - status + + - - + + - - timestamp + + - - - + + integer - - + + + - - created_at + + - - + + id - - timestamp + + + - - - + + + - - + + - - updated_at + + custom_assets - - + + - - varchar + + - - - + + - - + + - - providable_type + + - - + + integer - - integer + + + - - - + + - - + + receiver_id - - providable_id + + - - - + + varchar - - - + + + - - + + - - auth_providers + + attached_object_type - - + + - - + + integer - - + + + - - + + - - integer + + attached_object_id - - - + + - - + + integer - - id + + + - - + + - - + + notification_type_id - - + + - - integer + + boolean - - - + + + - - + + - - training_id + + is_read - - + + - - integer + + timestamp - - - + + + - - + + - - availability_id + + created_at - - + + - - timestamp + + timestamp - - - + + + - - + + - - created_at + + updated_at - - + + - - timestamp + + varchar - - - + + + - - + + - - updated_at + + receiver_type - - - + + - - - + + boolean - - + + + - - trainings_availabilities + + - - + + is_send - - + + - - + + jsonb - - integer + + + - - - + + - - + + meta_data - - id + + - - + + - - + + - - + + integer - - varchar + + + - - - + + - - + + id - - key + + + - - + + + - - varchar + + - - - + + notifications - - + + - - label + + - - + + - - timestamp + + - - - + + - - + + varchar - - created_at + + + - - + + - - timestamp + + address - - - + + - - + + varchar - - updated_at + + + - - - + + - - - + + street_number - - + + - - statistic_sub_types + + varchar - - + + + - - + + - - + + route - - + + - - integer + + varchar - - - + + + - - + + - - id + + locality - - + + - - + + varchar - - + + + - - varchar + + - - - + + country - - + + - - name + + varchar - - + + + - - timestamp + + - - - + + postal_code - - + + - - created_at + + varchar - - + + + - - timestamp + + - - - + + placeable_type - - + + - - updated_at + + integer - - + + + - - varchar + + - - - + + placeable_id - - - - - slug - - - - - - - - - - - - - - age_ranges - - - - - - - - - - - - varchar - - - - - - - - - - version - - - - - - - - - - - - - - schema_migrations - - - - - - - availability_id:id - - - wallet_transaction_id:id - - - operator_profile_id:id - - - user_id:id - - - event_id:id - - - machine_id:id - - - invoicing_profile_id:id - - - user_id:id - - - group_id:id - - - statistic_profile_id:id - - - licence_id:id - - - component_id:id - - - group_id:id - - - user_id:id - - - invoice_id:id - - - slot_id:id - - - space_id:id - - - subscription_id:id - - - plan_id:id - - - invoicing_profile_id:id - - - closed_by:id - - - group_id:id - - - training_id:id - - - availability_id:id - - - statistic_profile_id:id - - - machine_id:id - - - availability_id:id - - - user_id:id - - - training_id:id - - - availability_id:id - - - project_id:id - - - reservation_id:id - - - availability_id:id - - - statistic_profile_id:id - - - invoice_item_id:id - - - role_id:id - - - event_theme_id:id - - - project_id:id - - - group_id:id - - - author_statistic_profile_id:id - - - project_id:id - - - tag_id:id - - - user_id:id - - - user_id:id - - - project_id:id - - - group_id:id - - - role_id:id - - - user_id:id - - - category_id:id - - - project_id:id - - - plan_id:id - - - tag_id:id - - - event_price_category_id:id - - - price_category_id:id - - - operator_profile_id:id - - - invoicing_profile_id:id - - - setting_id:id - - - plan_id:id - - - plan_id:id - - - invoice_id:id - - - wallet_transaction_id:id - - - payment_schedule_id:id - - - machine_id:id - - - theme_id:id - - - event_id:id - - - coupon_id:id - - - age_range_id:id - - - payment_schedule_id:id - - - statistic_profile_id:id - - - space_id:id - - - statistic_profile_id:id - - - reservation_id:id - - - project_id:id - - - user_id:id - - - credit_id:id - - - wallet_id:id - - - training_id:id - - - availability_id:id - - - training_id:id - - - user_id:id - - - invoice_id:id - - - availability_id:id - - - invoicing_profile_id:id - - - invoicing_profile_id:id - - - coupon_id:id - - - invoicing_profile_id:id - - - statistic_sub_type_id:id - - - statistic_index_id:id - - - statistic_index_id:id - - - statistic_type_id:id - - - statistic_type_id:id - - - statistic_index_id:id - - - o_auth2_provider_id:id - - - i_calendar_id:id - - - open_api_client_id:id + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + addresses + + + + + + + + + + + + + + + integer + + + + + + + + + + setting_id + + + + + + varchar + + + + + + + + + + value + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + varchar + + + + + + + + + + footprint + + + + + + integer + + + + + + + + + + invoicing_profile_id + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + history_values + + + + + + + + + + + + + + + + + + integer + + + + + + + + + + statistic_index_id + + + + + + varchar + + + + + + + + + + key + + + + + + varchar + + + + + + + + + + label + + + + + + boolean + + + + + + + + + + graph + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + boolean + + + + + + + + + + simple + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + statistic_types + + + + + + + + + + + + + + + date + + + + + + + + + + start_at + + + + + + date + + + + + + + + + + end_at + + + + + + timestamp + + + + + + + + + + closed_at + + + + + + integer + + + + + + + + + + closed_by + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + integer + + + + + + + + + + period_total + + + + + + integer + + + + + + + + + + perpetual_total + + + + + + varchar + + + + + + + + + + footprint + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + accounting_periods + + + + + + + + + + + + + + + + + + varchar + + + + + + + + + + url + + + + + + varchar + + + + + + + + + + name + + + + + + varchar + + + + + + + + + + color + + + + + + varchar + + + + + + + + + + text_color + + + + + + boolean + + + + + + + + + + text_hidden + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + i_calendars + + + + + + + + + + + + + + + + + + varchar + + + + + + + + + + name + + + + + + text + + + + + + + + + + conditions + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + price_categories + + + + + + + + + + + + + + + + + + integer + + + + + + + + + + statistic_index_id + + + + + + varchar + + + + + + + + + + chart_type + + + + + + integer + + + + + + + + + + limit + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + statistic_graphs + + + + + + + + + + + + + + + + + + integer + + + + + + + + + + user_id + + + + + + varchar + + + + + + + + + + first_name + + + + + + varchar + + + + + + + + + + last_name + + + + + + varchar + + + + + + + + + + email + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + invoicing_profiles + + + + + + + + + + + + + + + + + + varchar + + + + + + + + + + value + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + varchar + + + + + + + + + + key + + + + + + + + + + + + + + ar_internal_metadata + + + + + + + + + + + + + + + + + + varchar + + + + + + + + + + es_type_key + + + + + + varchar + + + + + + + + + + label + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + boolean + + + + + + + + + + table + + + + + + boolean + + + + + + + + + + ca + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + statistic_indices + + + + + + + + + + + + + + + + + + varchar + + + + + + + + + + label + + + + + + boolean + + + + + + + + + + required + + + + + + boolean + + + + + + + + + + actived + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + bigint + + + + + + + + + + id + + + + + + + + + + + + + + profile_custom_fields + + + + + + + + + + + + + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + database_providers + + + + + + + + + + + + + + + + + + varchar + + + + + + + + + + gateway_object_id + + + + + + varchar + + + + + + + + + + gateway_object_type + + + + + + varchar + + + + + + + + + + item_type + + + + + + bigint + + + + + + + + + + item_id + + + + + + bigint + + + + + + + + + + payment_gateway_object_id + + + + + + + + + + + + bigint + + + + + + + + + + id + + + + + + + + + + + + + + payment_gateway_objects + + + + + + + + + + + + + + + integer + + + + + + + + + + project_id + + + + + + integer + + + + + + + + + + space_id + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + projects_spaces + + + + + + + + + + + + + + + + + + varchar + + + + + + + + + + name + + + + + + varchar + + + + + + + + + + status + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + varchar + + + + + + + + + + providable_type + + + + + + integer + + + + + + + + + + providable_id + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + auth_providers + + + + + + + + + + + + + + + integer + + + + + + + + + + group_id + + + + + + integer + + + + + + + + + + plan_id + + + + + + varchar + + + + + + + + + + priceable_type + + + + + + integer + + + + + + + + + + priceable_id + + + + + + integer + + + + + + + + + + amount + + + + + + timestamp + + + + + + + + + + created_at + + + + + + timestamp + + + + + + + + + + updated_at + + + + + + integer + + + + + + + + + + duration + + + + + + + + + + + + integer + + + + + + + + + + id + + + + + + + + + + + + + + prices + + + + + + + + + + statistic_profile_id:id + + + proof_of_identity_type_id:id + + + user_id:id + + + statistic_profile_id:id + + + profile_custom_field_id:id + + + tag_id:id + + + user_id:id + + + availability_id:id + + + project_id:id + + + statistic_profile_id:id + + + event_id:id + + + availability_id:id + + + training_id:id + + + reservation_id:id + + + user_id:id + + + payment_schedule_id:id + + + licence_id:id + + + group_id:id + + + invoice_id:id + + + availability_id:id + + + user_id:id + + + credit_id:id + + + age_range_id:id + + + event_theme_id:id + + + payment_schedule_id:id + + + invoicing_profile_id:id + + + training_id:id + + + invoicing_profile_id:id + + + plan_id:id + + + coupon_id:id + + + category_id:id + + + project_id:id + + + prepaid_pack_id:id + + + user_id:id + + + availability_id:id + + + user_id:id + + + invoicing_profile_id:id + + + coupon_id:id + + + invoicing_profile_id:id + + + proof_of_identity_type_id:id + + + training_id:id + + + subscription_id:id + + + invoice_id:id + + + event_id:id + + + group_id:id + + + user_id:id + + + user_id:id + + + plan_category_id:id + + + user_id:id + + + slot_id:id + + + price_category_id:id + + + availability_id:id + + + availability_id:id + + + closed_by:id + + + machine_id:id + + + reservation_id:id + + + training_id:id + + + machine_id:id + + + group_id:id + + + group_id:id + + + setting_id:id + + + invoicing_profile_id:id + + + space_id:id + + + project_id:id + + + tag_id:id + + + statistic_profile_id:id + + + plan_id:id + + + author_statistic_profile_id:id + + + plan_id:id + + + invoice_item_id:id + + + invoicing_profile_id:id + + + group_id:id + + + role_id:id + + + group_id:id + + + invoice_id:id + + + group_id:id + + + user_id:id + + + role_id:id + + + theme_id:id + + + space_id:id + + + event_price_category_id:id + + + machine_id:id + + + invoicing_profile_id:id + + + plan_id:id + + + user_id:id + + + proof_of_identity_refusal_id:id + + + availability_id:id + + + operator_profile_id:id + + + statistic_profile_id:id + + + wallet_transaction_id:id + + + project_id:id + + + component_id:id + + + wallet_id:id + + + statistic_profile_id:id + + + project_id:id + + + operator_profile_id:id + + + wallet_transaction_id:id + + + project_id:id + + + proof_of_identity_type_id:id + + + i_calendar_id:id + + + statistic_type_id:id + + + statistic_sub_type_id:id + + + statistic_index_id:id + + + statistic_type_id:id + + + statistic_index_id:id + + + statistic_index_id:id + + + auth_provider_id:id + + + payment_gateway_object_id:id From 1660987d78499194793ead63a395ebded88f1fee Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 5 Jul 2022 12:22:00 +0200 Subject: [PATCH 003/141] (bug) script mount-proof-of-identity-files creates a volume with an invalid path --- CHANGELOG.md | 2 ++ scripts/mount-proof-of-identity-files.sh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e512678f..6d166bc80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## next deploy +- Fix a bug: script mount-proof-of-identity-files creates a volume with an invalid path + ## v5.4.9 2022 June 29 - Fix a bug: validator of reservation can't find if slot has reserved by reservable type diff --git a/scripts/mount-proof-of-identity-files.sh b/scripts/mount-proof-of-identity-files.sh index d1697e340..54eed737e 100644 --- a/scripts/mount-proof-of-identity-files.sh +++ b/scripts/mount-proof-of-identity-files.sh @@ -22,7 +22,7 @@ add_mount() if [[ ! $(yq eval ".services.$SERVICE.volumes.[] | select (. == \"*proof_of_identity_files\")" docker-compose.yml) ]]; then # change docker-compose.yml permissions for fix yq can't modify file issue chmod 666 docker-compose.yml - yq -i eval ".services.$SERVICE.volumes += [\"\./proof_of_identity_files:/usr/src/app/proof_of_identity_files\"]" docker-compose.yml + yq -i eval ".services.$SERVICE.volumes += [\"./proof_of_identity_files:/usr/src/app/proof_of_identity_files\"]" docker-compose.yml chmod 644 docker-compose.yml fi } From bfc6d76109cea738c081f78bbcc872454de44277 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 5 Jul 2022 16:18:17 +0200 Subject: [PATCH 004/141] (bug) unable to access the new OIDC provider form --- CHANGELOG.md | 1 + .../components/authentication-provider/openid-connect-form.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d166bc80..531702157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## next deploy - Fix a bug: script mount-proof-of-identity-files creates a volume with an invalid path +- Fix a bug: unable to access the new OIDC provider form ## v5.4.9 2022 June 29 diff --git a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx index 28532652d..23b71169b 100644 --- a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx @@ -69,7 +69,7 @@ export const OpenidConnectForm = ) => void): void => { - const current = currentFormValues.scope || []; + const current = currentFormValues?.scope || []; if (scopesAvailable) { // add custom scopes to the list of available scopes const unlisted = difference(current, scopesAvailable); From bccb31ec855fe0e57af328193daffab4ef24e38f Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 5 Jul 2022 16:19:02 +0200 Subject: [PATCH 005/141] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 531702157..f8b094ca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## next deploy +- Increased About page title's size - Fix a bug: script mount-proof-of-identity-files creates a volume with an invalid path - Fix a bug: unable to access the new OIDC provider form From 6092e35defa9288a987f7a2428448da1d240be6a Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 5 Jul 2022 16:22:43 +0200 Subject: [PATCH 006/141] Version 5.4.10 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b094ca6..902c3eb60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## next deploy +## v5.4.10 2022 July 05 + - Increased About page title's size - Fix a bug: script mount-proof-of-identity-files creates a volume with an invalid path - Fix a bug: unable to access the new OIDC provider form diff --git a/package.json b/package.json index c37fe099f..cc3f309e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.9", + "version": "5.4.10", "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 9250ed720f521dc089f16d8df2c19a3418a8c848 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 5 Jul 2022 17:42:21 +0200 Subject: [PATCH 007/141] (bug) Gender, Address and Birthday are not mapped properly from SSO (#365) --- CHANGELOG.md | 2 ++ .../authentication-provider/type-mapping-modal.tsx | 5 +++++ .../src/javascript/models/authentication-provider.ts | 10 ++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 902c3eb60..a9a837e7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## next deploy +- Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) + ## v5.4.10 2022 July 05 - Increased About page title's size diff --git a/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx b/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx index 6b3e19d76..e2fc0539a 100644 --- a/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx @@ -9,6 +9,7 @@ import { mappingType } from '../../models/authentication-provider'; import { BooleanMappingForm } from './boolean-mapping-form'; import { DateMappingForm } from './date-mapping-form'; import { StringMappingForm } from './string-mapping-form'; +import { FormInput } from '../form/form-input'; interface TypeMappingModalProps { model: string, @@ -38,6 +39,10 @@ export const TypeMappingModal = } onConfirm={toggleModal}> {model} > {field} ({t('app.admin.authentication.type_mapping_modal.TYPE_expected', { TYPE: t(`app.admin.authentication.type_mapping_modal.types.${type}`) })}) + {type === 'integer' && } {type === 'boolean' && } {type === 'date' && } diff --git a/app/frontend/src/javascript/models/authentication-provider.ts b/app/frontend/src/javascript/models/authentication-provider.ts index 40ed22c4a..6cfebd092 100644 --- a/app/frontend/src/javascript/models/authentication-provider.ts +++ b/app/frontend/src/javascript/models/authentication-provider.ts @@ -25,10 +25,12 @@ export interface AuthenticationProviderMapping { format: 'iso8601' | 'rfc2822' | 'rfc3339' | 'timestamp-s' | 'timestamp-ms', true_value: string, false_value: string, - mapping: { - from: string, - to: number|string - } + mapping: [ + { + from: string, + to: number|string + } + ] } } From cc1cf38d696af5cbf4f840b1cf65f426e34afe57 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 6 Jul 2022 12:59:45 +0200 Subject: [PATCH 008/141] (bug) OIDC scopes are not shown in the configuration form select --- CHANGELOG.md | 1 + .../authentication-provider/openid-connect-form.tsx | 8 ++++++++ .../src/javascript/components/form/form-multi-select.tsx | 8 +++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a837e7b..d5e41299a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## next deploy - Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) +- Fix a bug: OIDC scopes are not shown in the configuration form select ## v5.4.10 2022 July 05 diff --git a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx index 23b71169b..0aa816334 100644 --- a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx @@ -29,6 +29,8 @@ export const OpenidConnectForm = (false); const [scopesAvailable, setScopesAvailable] = useState(null); + // this is a workaround for https://github.com/JedWatson/react-select/issues/1879 + const [selectKey, setSelectKey] = useState(0); // when we have detected a discovery endpoint, we mark it as available useEffect(() => { @@ -38,6 +40,11 @@ export const OpenidConnectForm = { + setSelectKey(selectKey + 1); + }, [scopesAvailable]); + // when the component is mounted, we try to discover the discovery endpoint for the current configuration (if any) useEffect(() => { checkForDiscoveryEndpoint({ target: { value: currentFormValues?.issuer } } as React.ChangeEvent); @@ -125,6 +132,7 @@ export const OpenidConnectForm = } loadOptions={loadScopes} + selectKey={selectKey.toString()} creatable control={control} /> exten onChange?: (values: Array) => void, placeholder?: string, creatable?: boolean, + selectKey?: string, } // we should provide either an array of options or a function that returns a promise, but not both @@ -35,7 +36,7 @@ type selectOption = { value: TOptionValue, label: string, select?: * This component is a wrapper around react-select to use with react-hook-form. * It is a multi-select component. */ -export const FormMultiSelect = ({ id, label, tooltip, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange, formState, warning, loadOptions, creatable }: FormSelectProps) => { +export const FormMultiSelect = ({ id, label, tooltip, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange, formState, warning, loadOptions, creatable, selectKey }: FormSelectProps) => { const { t } = useTranslation('shared'); const [isDisabled, setIsDisabled] = React.useState(false); @@ -52,7 +53,7 @@ export const FormMultiSelect = { if (typeof loadOptions === 'function') { loadOptions('', options => { - setAllOptions(options); + setTimeout(() => setAllOptions(options), 1); }); } }, [loadOptions]); @@ -73,7 +74,7 @@ export const FormMultiSelect = ): Array> => { - return allOptions.filter(c => value?.includes(c.value)); + return allOptions?.filter(c => value?.includes(c.value)); }; /** @@ -119,6 +120,7 @@ export const FormMultiSelect = Date: Wed, 6 Jul 2022 13:16:09 +0200 Subject: [PATCH 009/141] (bug) OIDC scopes are not saved --- CHANGELOG.md | 1 + app/controllers/api/auth_providers_controller.rb | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e41299a..5a141094f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) - Fix a bug: OIDC scopes are not shown in the configuration form select +- Fix a bug: OIDC scopes are not saved ## v5.4.10 2022 July 05 diff --git a/app/controllers/api/auth_providers_controller.rb b/app/controllers/api/auth_providers_controller.rb index b1cde1a6d..0db2c76f7 100644 --- a/app/controllers/api/auth_providers_controller.rb +++ b/app/controllers/api/auth_providers_controller.rb @@ -85,10 +85,10 @@ class API::AuthProvidersController < API::ApiController def provider_params if params['auth_provider']['providable_type'] == DatabaseProvider.name - params.require(:auth_provider).permit(:name, :providable_type, providable_attributes: [:id]) + params.require(:auth_provider).permit(:id, :name, :providable_type, providable_attributes: [:id]) elsif params['auth_provider']['providable_type'] == OAuth2Provider.name params.require(:auth_provider) - .permit(:name, :providable_type, + .permit(:id, :name, :providable_type, providable_attributes: %i[id base_url token_endpoint authorization_endpoint profile_url client_id client_secret scopes], auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type, @@ -96,10 +96,11 @@ class API::AuthProvidersController < API::ApiController mapping: %i[from to]]]) elsif params['auth_provider']['providable_type'] == OpenIdConnectProvider.name params.require(:auth_provider) - .permit(:name, :providable_type, - providable_attributes: %i[id issuer discovery client_auth_method scope prompt send_scope_to_token_endpoint - client__identifier client__secret client__authorization_endpoint client__token_endpoint - client__userinfo_endpoint client__jwks_uri client__end_session_endpoint profile_url], + .permit(:id, :name, :providable_type, + providable_attributes: [:id, :issuer, :discovery, :client_auth_method, :prompt, :send_scope_to_token_endpoint, + :client__identifier, :client__secret, :client__authorization_endpoint, :client__token_endpoint, + :client__userinfo_endpoint, :client__jwks_uri, :client__end_session_endpoint, :profile_url, + scope: []], auth_provider_mappings_attributes: [:id, :local_model, :local_field, :api_field, :api_endpoint, :api_data_type, :_destroy, transformation: [:type, :format, :true_value, :false_value, mapping: %i[from to]]]) From 52e7a473f94887733d757b05f91b8813edc2611b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:05:11 +0000 Subject: [PATCH 010/141] Bump rails-html-sanitizer from 1.4.2 to 1.4.3 Bumps [rails-html-sanitizer](https://github.com/rails/rails-html-sanitizer) from 1.4.2 to 1.4.3. - [Release notes](https://github.com/rails/rails-html-sanitizer/releases) - [Changelog](https://github.com/rails/rails-html-sanitizer/blob/master/CHANGELOG.md) - [Commits](https://github.com/rails/rails-html-sanitizer/compare/v1.4.2...v1.4.3) --- updated-dependencies: - dependency-name: rails-html-sanitizer dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3debb4145..35bb51c9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -204,7 +204,7 @@ GEM listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - loofah (2.17.0) + loofah (2.18.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -328,7 +328,7 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) + rails-html-sanitizer (1.4.3) loofah (~> 2.3) rails-observers (0.1.5) activemodel (>= 4.0) From 9c72da8e6f7546217e24749155809276f87bb92a Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 6 Jul 2022 14:19:36 +0200 Subject: [PATCH 011/141] (bug) social networks icons not shown in firefox --- CHANGELOG.md | 1 + .../src/javascript/components/socials/edit-socials.tsx | 6 ++++-- .../src/javascript/components/socials/fab-socials.tsx | 8 +++++--- app/frontend/src/stylesheets/app.layout.scss | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a141094f..9ce56027a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## next deploy +- Fix a bug: social networks icons not shown in firefox - Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) - Fix a bug: OIDC scopes are not shown in the configuration form select - Fix a bug: OIDC scopes are not saved diff --git a/app/frontend/src/javascript/components/socials/edit-socials.tsx b/app/frontend/src/javascript/components/socials/edit-socials.tsx index 0164890fc..0c965040b 100644 --- a/app/frontend/src/javascript/components/socials/edit-socials.tsx +++ b/app/frontend/src/javascript/components/socials/edit-socials.tsx @@ -59,7 +59,9 @@ export const EditSocials = ({ register, setVal <>
{userNetworks.map((network, index) => - !selectedNetworks.includes(network) && selectNetwork(network)}> + !selectedNetworks.includes(network) && selectNetwork(network)} viewBox="0 0 24 24" > + + )}
{selectNetwork.length &&
@@ -79,7 +81,7 @@ export const EditSocials = ({ register, setVal label={network.name} disabled={disabled} placeholder={t('app.shared.edit_socials.url_placeholder')} - icon={} + icon={} addOn={} addOnAction={() => dispatch({ type: 'delete', payload: { network, field: `profile_attributes.${network.name}` } })} /> )} diff --git a/app/frontend/src/javascript/components/socials/fab-socials.tsx b/app/frontend/src/javascript/components/socials/fab-socials.tsx index 9db551532..f962bf100 100644 --- a/app/frontend/src/javascript/components/socials/fab-socials.tsx +++ b/app/frontend/src/javascript/components/socials/fab-socials.tsx @@ -85,7 +85,7 @@ export const FabSocials: React.FC = ({ show = false, onError, o {fabNetworks.map((network, index) => selectedNetworks.includes(network) && - + )}
@@ -95,7 +95,9 @@ export const FabSocials: React.FC = ({ show = false, onError, o
{fabNetworks.map((network, index) => !selectedNetworks.includes(network) && - selectNetwork(network)}> + selectNetwork(network)}> + + )}
{selectNetwork.length &&
@@ -114,7 +116,7 @@ export const FabSocials: React.FC = ({ show = false, onError, o defaultValue={network.url} label={network.name} placeholder={t('app.shared.fab_socials.url_placeholder')} - icon={} + icon={} addOn={} addOnAction={() => remove(network)} /> )} diff --git a/app/frontend/src/stylesheets/app.layout.scss b/app/frontend/src/stylesheets/app.layout.scss index 00ccea00d..938c4bfc7 100644 --- a/app/frontend/src/stylesheets/app.layout.scss +++ b/app/frontend/src/stylesheets/app.layout.scss @@ -581,7 +581,7 @@ body.container { border-radius: 3px; overflow: hidden; } - & > img { + & > svg { border: 1px solid var(--gray-soft-dark); background-color: var(--gray-soft-lightest); &:hover { opacity: 0.65; } @@ -589,7 +589,7 @@ body.container { & > a { transition: transform 200ms ease-in-out; &:hover { transform: translateY(-4px); } - img { + svg { max-width: 100%; height: inherit; } From 23702a6048bbf8a37d8e5c03e3a643aba5114cab Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 6 Jul 2022 14:37:34 +0200 Subject: [PATCH 012/141] Version 5.4.11 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce56027a..75fb23d04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## next deploy +## v5.4.11 2022 July 06 + - Fix a bug: social networks icons not shown in firefox - Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) - Fix a bug: OIDC scopes are not shown in the configuration form select diff --git a/package.json b/package.json index cc3f309e9..e9f863b84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.10", + "version": "5.4.11", "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 4fe72269590e68a52600e1e2e9840da1d04b84ff Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 6 Jul 2022 15:37:47 +0200 Subject: [PATCH 013/141] (bug) unable to import a new account from an SSO provider --- CHANGELOG.md | 1 + app/services/members/members_service.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f3a23479..4d8fae127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## next deploy +- Fix a bug: unable to import a new account from an SSO provider - Fix a security issue: updated rails-html-sanitizer to 1.4.3 to fix [CVE-2022-32209](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-32209) ## v5.4.11 2022 July 06 diff --git a/app/services/members/members_service.rb b/app/services/members/members_service.rb index 598805c43..c81005350 100644 --- a/app/services/members/members_service.rb +++ b/app/services/members/members_service.rb @@ -46,7 +46,7 @@ class Members::MembersService up_result = member.update(params) notify_user_profile_complete(not_complete) if up_result - member.notify_group_changed(ex_group, validated_at_changed) if group_changed + member.notify_group_changed(ex_group, validated_at_changed) if group_changed && !ex_group.nil? up_result end From bec2e8a51427102e1530c35f547175f897f926b4 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 6 Jul 2022 15:56:57 +0200 Subject: [PATCH 014/141] (bug) Gender, Address and Birthday are not mapped properly from SSO (#365) --- CHANGELOG.md | 1 + app/models/concerns/single_sign_on_concern.rb | 28 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d8fae127..97e909461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## next deploy +- Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) - Fix a bug: unable to import a new account from an SSO provider - Fix a security issue: updated rails-html-sanitizer to 1.4.3 to fix [CVE-2022-32209](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-32209) diff --git a/app/models/concerns/single_sign_on_concern.rb b/app/models/concerns/single_sign_on_concern.rb index 22d1f4d01..cc1594b70 100644 --- a/app/models/concerns/single_sign_on_concern.rb +++ b/app/models/concerns/single_sign_on_concern.rb @@ -48,24 +48,24 @@ module SingleSignOnConcern profile.user_avatar ||= UserAvatar.new profile.user_avatar.remote_attachment_url = data when 'profile.address' - invoicing_profile ||= InvoicingProfile.new - invoicing_profile.address ||= Address.new - invoicing_profile.address.address = data + self.invoicing_profile ||= InvoicingProfile.new + self.invoicing_profile.address ||= Address.new + self.invoicing_profile.address.address = data when 'profile.organization_name' - invoicing_profile ||= InvoicingProfile.new - invoicing_profile.organization ||= Organization.new - invoicing_profile.organization.name = data + self.invoicing_profile ||= InvoicingProfile.new + self.invoicing_profile.organization ||= Organization.new + self.invoicing_profile.organization.name = data when 'profile.organization_address' - invoicing_profile ||= InvoicingProfile.new - invoicing_profile.organization ||= Organization.new - invoicing_profile.organization.address ||= Address.new - invoicing_profile.organization.address.address = data + self.invoicing_profile ||= InvoicingProfile.new + self.invoicing_profile.organization ||= Organization.new + self.invoicing_profile.organization.address ||= Address.new + self.invoicing_profile.organization.address.address = data when 'profile.gender' - statistic_profile ||= StatisticProfile.new - statistic_profile.gender = data + self.statistic_profile ||= StatisticProfile.new + self.statistic_profile.gender = data when 'profile.birthday' - statistic_profile ||= StatisticProfile.new - statistic_profile.birthday = data + self.statistic_profile ||= StatisticProfile.new + self.statistic_profile.birthday = data else profile[sso_mapping[8..-1].to_sym] = data unless data.nil? end From 95f192893bef7d3de2ef67cdd777940e4ca9592e Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 6 Jul 2022 16:00:04 +0200 Subject: [PATCH 015/141] Version 5.4.12 --- CHANGELOG.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e909461..d21d8c8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## next deploy +## v5.4.12 2022 July 06 + - Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) - Fix a bug: unable to import a new account from an SSO provider - Fix a security issue: updated rails-html-sanitizer to 1.4.3 to fix [CVE-2022-32209](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-32209) diff --git a/package.json b/package.json index e9f863b84..1649dbd36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.11", + "version": "5.4.12", "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 272e105f5af2105eb1b6dcf91725ea78545dcc6b Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 11 Jul 2022 11:33:31 +0200 Subject: [PATCH 016/141] Fix page title's layout --- app/frontend/src/stylesheets/app.layout.scss | 16 ++++++++++++---- app/frontend/src/stylesheets/app.responsive.scss | 1 - app/frontend/templates/admin/members/edit.html | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/frontend/src/stylesheets/app.layout.scss b/app/frontend/src/stylesheets/app.layout.scss index 938c4bfc7..7f23e3a00 100644 --- a/app/frontend/src/stylesheets/app.layout.scss +++ b/app/frontend/src/stylesheets/app.layout.scss @@ -103,12 +103,20 @@ } .heading-title { - //overflow: hidden; - height: 94px; + min-height: 94px; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + padding: 15px; + + & > *:not(:last-child) { + margin-right: 3rem; + } h1 { - margin: 0 0 0 15px; - padding: 36px 15px; + margin: 0; + padding: 15px 0; } } diff --git a/app/frontend/src/stylesheets/app.responsive.scss b/app/frontend/src/stylesheets/app.responsive.scss index 7368103e6..b5987f8ad 100644 --- a/app/frontend/src/stylesheets/app.responsive.scss +++ b/app/frontend/src/stylesheets/app.responsive.scss @@ -34,7 +34,6 @@ .heading-title { h1 { font-size: rem-calc(16); - padding: 26px 15px; } } } diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index fa83b940c..389ec4a9d 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -11,7 +11,7 @@

{{ 'app.shared.user_admin.user' | translate }} {{ user.name }}

{{ 'app.shared.user_admin.incomplete_profile' }} -
+
From 784aa93e308bf87acc7c3a8dc221266db1d2b629 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 11 Jul 2022 12:32:22 +0200 Subject: [PATCH 017/141] (bug) Unable to import accounts from SSO When the transformation modal was opened but leaved empty, the field mapping.transformation.type is defined but not mapping.transformation.mapping, which result in the following error: NoMethodError (undefined method `each' for nil:NilClass): lib/omni_auth/data_mapping/base.rb:16:in `map_transformation' --- CHANGELOG.md | 2 ++ lib/omni_auth/data_mapping/base.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d21d8c8ff..600169ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## next deploy +- Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty + ## v5.4.12 2022 July 06 - Fix a bug: Gender, Address and Birthday are not mapped properly from SSO (#365) diff --git a/lib/omni_auth/data_mapping/base.rb b/lib/omni_auth/data_mapping/base.rb index b6286721a..7d43c7d07 100644 --- a/lib/omni_auth/data_mapping/base.rb +++ b/lib/omni_auth/data_mapping/base.rb @@ -13,7 +13,7 @@ module OmniAuth::DataMapping def map_transformation(transformation, raw_data) value = nil - transformation['mapping'].each do |m| + transformation['mapping']&.each do |m| if m['from'] == raw_data value = m['to'] break From 4dfc01c1a331baa18aab64acd8408aa546e03dd5 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 10:47:10 +0200 Subject: [PATCH 018/141] updated rubocop --- .rubocop.yml | 4 ++-- Gemfile | 2 +- Gemfile.lock | 26 +++++++++++++++----------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 2eca3bfc2..a3b552137 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,5 @@ +AllCops: + NewCops: enable Metrics/LineLength: Max: 140 Metrics/MethodLength: @@ -19,8 +21,6 @@ Metrics/BlockLength: - 'test/**/*.rb' Metrics/ParameterLists: CountKeywordArgs: false -Style/BracesAroundHashParameters: - EnforcedStyle: context_dependent Style/RegexpLiteral: EnforcedStyle: slashes Style/EmptyElse: diff --git a/Gemfile b/Gemfile index 487c6ae3e..a8b962ea1 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ group :development do gem 'rb-readline' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'railroady' - gem 'rubocop', '~> 0.61.1', require: false + gem 'rubocop', '~> 1.31.2' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end diff --git a/Gemfile.lock b/Gemfile.lock index 35bb51c9b..c76abd469 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,7 +57,7 @@ GEM apipie-rails (0.5.17) rails (>= 4.1) arel (9.0.0) - ast (2.4.0) + ast (2.4.2) attr_required (1.0.1) awesome_print (1.8.0) axiom-types (0.1.1) @@ -176,7 +176,6 @@ GEM image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - jaro_winkler (1.5.4) jbuilder (2.10.0) activesupport (>= 5.0.0) jbuilder_cache_multi (0.1.0) @@ -274,8 +273,8 @@ GEM httparty (~> 0.20) orm_adapter (0.5.0) parallel (1.19.1) - parser (2.7.0.4) - ast (~> 2.4.0) + parser (3.1.2.0) + ast (~> 2.4.1) pdf-core (0.7.0) pdf-reader (2.4.0) Ascii85 (~> 1.0.0) @@ -287,7 +286,6 @@ GEM pg_search (2.3.2) activerecord (>= 5.2) activesupport (>= 5.2) - powerpack (0.1.2) prawn (2.2.2) pdf-core (~> 0.7.0) ttfunk (~> 1.5) @@ -353,19 +351,25 @@ GEM activesupport i18n redis (4.6.0) + regexp_parser (2.5.0) repost (0.3.2) responders (2.4.1) actionpack (>= 4.2.0, < 6.0) railties (>= 4.2.0, < 6.0) + rexml (3.2.5) rolify (5.2.0) - rubocop (0.61.1) - jaro_winkler (~> 1.5.1) + rubocop (1.31.2) + json (~> 2.3) parallel (~> 1.10) - parser (>= 2.5, != 2.5.1.1) - powerpack (~> 0.1) + parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.18.0, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.4.0) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.19.1) + parser (>= 3.1.1.0) ruby-progressbar (1.10.1) ruby-rc4 (0.1.5) ruby-vips (2.1.4) @@ -537,7 +541,7 @@ DEPENDENCIES repost responders (~> 2.0) rolify - rubocop (~> 0.61.1) + rubocop (~> 1.31.2) rubyXL rubyzip (>= 1.3.0) sassc (= 2.1.0) From 42472402c93a4374e9fdd4fe174795dae1612868 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 11:00:38 +0200 Subject: [PATCH 019/141] add rubocop linting to gitlab-ci --- .gitlab-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..a6fc6df50 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,6 @@ +rubocop: + stage: linting + image: registry.gitlab.com/pipeline-components/rubocop:latest + script: + - git diff --name-only master --diff-filter AMT | grep \\.rb$ | xargs bundle exec rubocop + From c05e42c2b183cfbb6e0cec5c6f31841229e14eb1 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 11:04:24 +0200 Subject: [PATCH 020/141] run linter only on dev --- .gitlab-ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a6fc6df50..afc14bb88 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,7 @@ rubocop: - stage: linting + stage: test + only: + - dev image: registry.gitlab.com/pipeline-components/rubocop:latest script: - git diff --name-only master --diff-filter AMT | grep \\.rb$ | xargs bundle exec rubocop From cd93af1b5fc703a76990b9c5733c12e9a37337c7 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 11:18:50 +0200 Subject: [PATCH 021/141] use sleede's rubocop image --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index afc14bb88..5e6c17dd0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,7 @@ rubocop: stage: test only: - dev - image: registry.gitlab.com/pipeline-components/rubocop:latest + image: sleede/rubocop-fm:latest script: - git diff --name-only master --diff-filter AMT | grep \\.rb$ | xargs bundle exec rubocop From b8f097cf7c5e963fd62857fd3fac5bae04f73fe8 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 16:54:41 +0200 Subject: [PATCH 022/141] use codeclimate to run rubocop --- .codeclimate.yml | 5 +++++ .gitlab-ci.yml | 12 ++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 .codeclimate.yml diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 000000000..797220f60 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,5 @@ +plugins: + rubocop: + enabled: true + config: + file: .rubocop.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5e6c17dd0..e6d03f248 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,4 @@ -rubocop: - stage: test - only: - - dev - image: sleede/rubocop-fm:latest - script: - - git diff --name-only master --diff-filter AMT | grep \\.rb$ | xargs bundle exec rubocop - +include: + - template: Code-Quality.gitlab-ci.yml + stages: + - test From 8fb21dd46829b48e3a4ab8aac89d9542d58eb16c Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 16:55:50 +0200 Subject: [PATCH 023/141] fix gitlab-ci syntax --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e6d03f248..db1295cc3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ include: - template: Code-Quality.gitlab-ci.yml - stages: - - test +stages: + - test From 05a297ab3f7df6b4f3f8a13969c398d163f238e0 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 17:19:01 +0200 Subject: [PATCH 024/141] upload code quality artifact --- .gitlab-ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index db1295cc3..eb9d8ee34 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,9 @@ -include: - - template: Code-Quality.gitlab-ci.yml stages: - test + +include: + - template: Code-Quality.gitlab-ci.yml + +code_quality: + artifacts: + paths: [ gl-code-quality-report.json ] From d63b8d0dfa9e3ec1c4a9f0c6506554d350049523 Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Thu, 14 Jul 2022 16:11:44 -0300 Subject: [PATCH 025/141] Add username column to the member list --- .../templates/admin/members/members.html | 16 +++++++++------- app/services/members/list_service.rb | 5 ++++- app/views/api/members/list.json.jbuilder | 1 + config/locales/app.admin.de.yml | 1 + config/locales/app.admin.en.yml | 1 + config/locales/app.admin.es.yml | 1 + config/locales/app.admin.fr.yml | 1 + config/locales/app.admin.no.yml | 1 + config/locales/app.admin.pt.yml | 1 + config/locales/app.admin.zu.yml | 1 + 10 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app/frontend/templates/admin/members/members.html b/app/frontend/templates/admin/members/members.html index fdd7d70c7..6bc791e19 100644 --- a/app/frontend/templates/admin/members/members.html +++ b/app/frontend/templates/admin/members/members.html @@ -38,13 +38,14 @@ - {{ 'app.admin.members.surname' | translate }} - {{ 'app.admin.members.first_name' | translate }} - {{ 'app.admin.members.email' | translate }} - {{ 'app.admin.members.phone' | translate }} - {{ 'app.admin.members.user_type' | translate }} - {{ 'app.admin.members.subscription' | translate }} - + {{ 'app.admin.members.username' | translate }} + {{ 'app.admin.members.surname' | translate }} + {{ 'app.admin.members.first_name' | translate }} + {{ 'app.admin.members.email' | translate }} + {{ 'app.admin.members.phone' | translate }} + {{ 'app.admin.members.user_type' | translate }} + {{ 'app.admin.members.subscription' | translate }} + @@ -52,6 +53,7 @@ + {{ m.username }} {{ m.profile.last_name }} {{ m.profile.first_name }} {{ m.email }} diff --git a/app/services/members/list_service.rb b/app/services/members/list_service.rb index 2731d0f3c..8886c1f79 100644 --- a/app/services/members/list_service.rb +++ b/app/services/members/list_service.rb @@ -26,7 +26,8 @@ class Members::ListService # ILIKE => PostgreSQL case-insensitive LIKE if params[:search].size.positive? - @query = @query.where('profiles.first_name ILIKE :search OR ' \ + @query = @query.where('username ILIKE :search OR ' \ + 'profiles.first_name ILIKE :search OR ' \ 'profiles.last_name ILIKE :search OR ' \ 'profiles.phone ILIKE :search OR ' \ 'email ILIKE :search OR ' \ @@ -83,6 +84,8 @@ class Members::ListService offset = ((params[:page]&.to_i || 1) - 1) * (params[:size]&.to_i || 1) order_key = case order_key + when 'username' + 'username' when 'last_name' 'profiles.last_name' when 'first_name' diff --git a/app/views/api/members/list.json.jbuilder b/app/views/api/members/list.json.jbuilder index ee443adb1..38dd1b1a2 100644 --- a/app/views/api/members/list.json.jbuilder +++ b/app/views/api/members/list.json.jbuilder @@ -1,6 +1,7 @@ json.array!(@members) do |member| json.maxMembers @max_members json.id member.id + json.username member.username json.email member.email if current_user json.profile do json.first_name member.profile.first_name diff --git a/config/locales/app.admin.de.yml b/config/locales/app.admin.de.yml index de7d5e514..d45eaf7c8 100644 --- a/config/locales/app.admin.de.yml +++ b/config/locales/app.admin.de.yml @@ -823,6 +823,7 @@ de: search_for_an_user: "Nach einem Benutzer suchen" add_a_new_member: "Neues Mitglied hinzufügen" reservations: "Reservierungen" + username: "Username" surname: "Nachname" first_name: "Vorname" email: "E-Mail" diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 517a86b46..99c8e1775 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -823,6 +823,7 @@ en: search_for_an_user: "Search for an user" add_a_new_member: "Add a new member" reservations: "Reservations" + username: "Username" surname: "Last name" first_name: "First name" email: "Email" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index b52ef9b0e..8a46fcc73 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -823,6 +823,7 @@ es: search_for_an_user: "Buscar un usuario" add_a_new_member: "Añadir un nuevo miembro" reservations: "Reservas" + username: "Username" surname: "Last name" first_name: "First name" email: "Email" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index de6d05e04..7a1ff72c0 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -823,6 +823,7 @@ fr: search_for_an_user: "Recherchez un utilisateur" add_a_new_member: "Ajouter un nouveau membre" reservations: "Réservations" + username: "Username" surname: "Nom" first_name: "Prénom" email: "Courriel" diff --git a/config/locales/app.admin.no.yml b/config/locales/app.admin.no.yml index 36ce0777e..18a1d84ce 100644 --- a/config/locales/app.admin.no.yml +++ b/config/locales/app.admin.no.yml @@ -823,6 +823,7 @@ search_for_an_user: "Søk etter bruker" add_a_new_member: "Legge til nytt medlem" reservations: "Reservasjoner" + username: "Username" surname: "Etternavn" first_name: "Fornavn" email: "E-post" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index 9f3e311e5..0de16f51b 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -823,6 +823,7 @@ pt: search_for_an_user: "Buscar por usuário" add_a_new_member: "Adicionar novo membro" reservations: "Reservas" + username: "Username" surname: "Sobrenome" first_name: "Primeiro nome" email: "Email" diff --git a/config/locales/app.admin.zu.yml b/config/locales/app.admin.zu.yml index cc5fa2c44..5f1073973 100644 --- a/config/locales/app.admin.zu.yml +++ b/config/locales/app.admin.zu.yml @@ -823,6 +823,7 @@ zu: search_for_an_user: "crwdns7741:0crwdne7741:0" add_a_new_member: "crwdns7743:0crwdne7743:0" reservations: "crwdns7745:0crwdne7745:0" + username: "Username" surname: "crwdns7747:0crwdne7747:0" first_name: "crwdns7749:0crwdne7749:0" email: "crwdns7751:0crwdne7751:0" From 64232551167eff993b1739b1de38c63e8924f080 Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Thu, 14 Jul 2022 17:03:22 -0300 Subject: [PATCH 026/141] (bug) Fix admin group being replaced in SSO authentication --- app/models/concerns/single_sign_on_concern.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/concerns/single_sign_on_concern.rb b/app/models/concerns/single_sign_on_concern.rb index cc1594b70..d8aa2264c 100644 --- a/app/models/concerns/single_sign_on_concern.rb +++ b/app/models/concerns/single_sign_on_concern.rb @@ -121,7 +121,7 @@ module SingleSignOnConcern logger.debug "mapping sso field #{field} with value=#{value}" # we do not merge the email field if its end with the special value '-duplicate' as this means # that the user is currently merging with the account that have the same email than the sso - set_data_from_sso_mapping(field, value) unless field == 'user.email' && value.end_with?('-duplicate') + set_data_from_sso_mapping(field, value) unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && user.admin?) end # run the account transfer in an SQL transaction to ensure data integrity From 27d71dcffde8a128fc93bcb37e31eb16475854c0 Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Thu, 14 Jul 2022 17:55:36 -0300 Subject: [PATCH 027/141] (bug) Fix SSO data being overridden when it is empty and the user can change it --- app/models/concerns/single_sign_on_concern.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/single_sign_on_concern.rb b/app/models/concerns/single_sign_on_concern.rb index cc1594b70..5637e6018 100644 --- a/app/models/concerns/single_sign_on_concern.rb +++ b/app/models/concerns/single_sign_on_concern.rb @@ -40,8 +40,10 @@ module SingleSignOnConcern ## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email' ## @param data {*} the data to put in the given key. Eg. 'user@example.com' def set_data_from_sso_mapping(sso_mapping, data) + return if data.nil? || data.blank? || mapped_from_sso&.include?(sso_mapping) + if sso_mapping.to_s.start_with? 'user.' - self[sso_mapping[5..-1].to_sym] = data unless data.nil? || data.blank? + self[sso_mapping[5..-1].to_sym] = data elsif sso_mapping.to_s.start_with? 'profile.' case sso_mapping.to_s when 'profile.avatar' @@ -67,10 +69,9 @@ module SingleSignOnConcern self.statistic_profile ||= StatisticProfile.new self.statistic_profile.birthday = data else - profile[sso_mapping[8..-1].to_sym] = data unless data.nil? + profile[sso_mapping[8..-1].to_sym] = data end end - return if data.nil? || data.blank? || mapped_from_sso&.include?(sso_mapping) self.mapped_from_sso = [mapped_from_sso, sso_mapping].compact.join(',') end @@ -121,7 +122,7 @@ module SingleSignOnConcern logger.debug "mapping sso field #{field} with value=#{value}" # we do not merge the email field if its end with the special value '-duplicate' as this means # that the user is currently merging with the account that have the same email than the sso - set_data_from_sso_mapping(field, value) unless field == 'user.email' && value.end_with?('-duplicate') + set_data_from_sso_mapping(field, value) unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && user.admin?) end # run the account transfer in an SQL transaction to ensure data integrity From 8be24252759e94c58e92f7171425678f0deffca5 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 4 Jul 2022 16:55:02 +0200 Subject: [PATCH 028/141] (wip) refactoring slots to be unique per availability time-slot --- app/models/event.rb | 5 +- app/models/reservation.rb | 15 ++- app/models/slot.rb | 65 +---------- app/models/slots_reservation.rb | 52 ++++++++- .../notify_admin_slot_is_canceled.html.erb | 4 +- .../notify_admin_slot_is_modified.html.erb | 2 +- .../notify_member_slot_is_canceled.html.erb | 2 +- .../notify_member_slot_is_modified.html.erb | 2 +- ...eservation_fields_to_slots_reservations.rb | 104 ++++++++++++++++++ db/schema.rb | 11 +- 10 files changed, 177 insertions(+), 85 deletions(-) create mode 100644 db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb diff --git a/app/models/event.rb b/app/models/event.rb index c0a31bd7b..ebc838c77 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -79,7 +79,10 @@ class Event < ApplicationRecord if nb_total_places.nil? self.nb_free_places = nil else - reserved_places = reservations.joins(:slots).where('slots.canceled_at': nil).map(&:total_booked_seats).inject(0) { |sum, t| sum + t } + reserved_places = reservations.joins(:slots_reservations) + .where('slots_reservations.canceled_at': nil) + .map(&:total_booked_seats) + .inject(0) { |sum, t| sum + t } self.nb_free_places = (nb_total_places - reserved_places) end end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 875bfffb2..43bc42c70 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -35,12 +35,17 @@ class Reservation < ApplicationRecord # @param canceled if true, count the number of seats for this reservation, including canceled seats def total_booked_seats(canceled: false) # cases: - # - machine/training/space reservation => 1 slot = 1 seat (currently not covered by this function) - # - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices) - return 0 if slots.first.canceled_at && !canceled + if reservable_type == 'Event' + # - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices) + total = nb_reserve_places + total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive? - total = nb_reserve_places - total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive? + total = 0 unless slots_reservations.first&.canceled_at.nil? + else + # - machine/training/space reservation => 1 slot_reservation = 1 seat + total = slots_reservations.count + total -= slots_reservations.where.not(canceled_at: nil).count unless canceled + end total end diff --git a/app/models/slot.rb b/app/models/slot.rb index 180c4231b..7e6f84a16 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true -# A Time range. -# Slots have two functions: -# - slicing an Availability -# > Slots duration are defined globally by Setting.get('slot_duration') but can be overridden per availability. -# > These slots are not persisted in database and instantiated on the fly, if needed. -# - hold detailed data about a Reservation. -# > Persisted slots (in DB) represents booked slots and stores data about a time range that have been reserved. +# A Time range, slicing an Availability. +# Slots duration are defined globally by Setting.get('slot_duration') but can be +# overridden per availability. class Slot < ApplicationRecord include NotifyWith::NotificationAttachedObject @@ -16,62 +12,7 @@ class Slot < ApplicationRecord attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :is_reserved_by_current_user - after_update :set_ex_start_end_dates_attrs, if: :dates_were_modified? - after_update :notify_member_and_admin_slot_is_modified, if: :dates_were_modified? - - after_update :notify_member_and_admin_slot_is_canceled, if: :canceled? - after_update :update_event_nb_free_places, if: :canceled? - - # for backward compatibility - def reservation - reservations.first - end - - def destroy - update_column(:destroying, true) - super - end - def complete? reservations.length >= availability.nb_total_places end - - private - - def notify_member_and_admin_slot_is_modified - NotificationCenter.call type: 'notify_member_slot_is_modified', - receiver: reservation.user, - attached_object: self - NotificationCenter.call type: 'notify_admin_slot_is_modified', - receiver: User.admins_and_managers, - attached_object: self - end - - def notify_member_and_admin_slot_is_canceled - NotificationCenter.call type: 'notify_member_slot_is_canceled', - receiver: reservation.user, - attached_object: self - NotificationCenter.call type: 'notify_admin_slot_is_canceled', - receiver: User.admins_and_managers, - attached_object: self - end - - def dates_were_modified? - saved_change_to_start_at? || saved_change_to_end_at? - end - - def canceled? - saved_change_to_canceled_at? - end - - def set_ex_start_end_dates_attrs - update_columns(ex_start_at: start_at_before_last_save, ex_end_at: end_at_before_last_save) - end - - def update_event_nb_free_places - return unless reservation.reservable_type == 'Event' - raise NotImplementedError if reservations.count > 1 - - reservation.update_event_nb_free_places - end end diff --git a/app/models/slots_reservation.rb b/app/models/slots_reservation.rb index ab339e6d9..3594dd14d 100644 --- a/app/models/slots_reservation.rb +++ b/app/models/slots_reservation.rb @@ -1,16 +1,56 @@ # frozen_string_literal: true # SlotsReservation is the relation table between a Slot and a Reservation. +# It holds detailed data about a Reservation for the attached Slot. class SlotsReservation < ApplicationRecord belongs_to :slot belongs_to :reservation - after_destroy :cleanup_slots - # when the SlotsReservation is deleted (from Reservation destroy cascade), we delete the - # corresponding slot - def cleanup_slots - return unless slot.destroying + after_update :set_ex_start_end_dates_attrs, if: :slot_changed? + after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed? - slot.destroy + after_update :notify_member_and_admin_slot_is_canceled, if: :canceled? + after_update :update_event_nb_free_places, if: :canceled? + + def set_ex_start_end_dates_attrs + update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at) + end + + private + + def slot_changed? + saved_change_to_slot_id? + end + + def previous_slot + Slot.find(slot_id_before_last_save) + end + + def canceled? + saved_change_to_canceled_at? + end + + def update_event_nb_free_places + return unless reservation.reservable_type == 'Event' + + reservation.update_event_nb_free_places + end + + def notify_member_and_admin_slot_is_modified + NotificationCenter.call type: 'notify_member_slot_is_modified', + receiver: reservation.user, + attached_object: self + NotificationCenter.call type: 'notify_admin_slot_is_modified', + receiver: User.admins_and_managers, + attached_object: self + end + + def notify_member_and_admin_slot_is_canceled + NotificationCenter.call type: 'notify_member_slot_is_canceled', + receiver: reservation.user, + attached_object: self + NotificationCenter.call type: 'notify_admin_slot_is_canceled', + receiver: User.admins_and_managers, + attached_object: self end end diff --git a/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb b/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb index e1b802385..275b56391 100644 --- a/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb +++ b/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb @@ -2,8 +2,8 @@

<%= t('.body.member_cancelled', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %>

<%= t('.body.item_details', - START: I18n.l(@attached_object.start_at, format: :long), - END: I18n.l(@attached_object.end_at, format: :hour_minute), + START: I18n.l(@attached_object.slot.start_at, format: :long), + END: I18n.l(@attached_object.slot.end_at, format: :hour_minute), RESERVABLE: @attached_object.reservation.reservable.name) %>

<%= t('.body.generate_refund') %>

diff --git a/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb b/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb index 603efe0af..2b1a458db 100644 --- a/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb +++ b/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb @@ -1,5 +1,5 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %>

<%= t('.body.slot_modified', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %>

-

<%= t('.body.new_date') %> <%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %>

+

<%= t('.body.new_date') %> <%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %>

<%= t('.body.old_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %>

diff --git a/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb b/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb index 89808e6b6..d1d2307ee 100644 --- a/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb +++ b/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb @@ -1,4 +1,4 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %>

<%= t('.body.reservation_canceled', RESERVABLE: @attached_object.reservation.reservable.name ) %>

-

<%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %>

+

<%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %>

diff --git a/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb b/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb index bb53fad99..20d8ba306 100644 --- a/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb +++ b/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb @@ -1,5 +1,5 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %>

<%= t('.body.reservation_changed_to') %>

-

<%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %>

+

<%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %>

<%= t('.body.previous_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %>

diff --git a/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb b/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb new file mode 100644 index 000000000..35947be5e --- /dev/null +++ b/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb @@ -0,0 +1,104 @@ +# Previously, the Slot table was holding data about reservations. +# This was a wrong assumption that leads to a bug. +# An Availability should have many slots but a slot can be related to multiple Reservations, +# so a slot must not hold data about a single reservation (like `offered`),these data +# should be stored in SlotsReservation instead. +class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] + def up + add_column :slots_reservations, :ex_start_at, :datetime + add_column :slots_reservations, :ex_end_at, :datetime + add_column :slots_reservations, :canceled_at, :datetime + add_column :slots_reservations, :offered, :boolean + + execute %( + UPDATE slots_reservations + SET + ex_start_at=slots.ex_start_at, + ex_end_at=slots.ex_end_at, + canceled_at=slots.canceled_at, + offered=slots.offered + FROM slots + WHERE slots_reservations.slot_id = slots.id + ) + + remove_column :slots, :ex_start_at + remove_column :slots, :ex_end_at + remove_column :slots, :canceled_at + remove_column :slots, :offered + remove_column :slots, :destroying + + # we gonna keep only only one slot (remove duplicates) because data is now hold in slots_reservations + + # update slots_reservation.slot_id + execute %( + UPDATE slots_reservations + SET slot_id=r.kept + FROM ( + SELECT count(*), start_at, end_at, availability_id, min(id) AS kept, array_agg(id) AS all_ids + FROM slots + GROUP BY start_at, end_at, availability_id + HAVING count(*) > 1) as r + WHERE slot_id = ANY(r.all_ids); + ) + + # remove useless slots + execute %q( + WITH same_slots AS ( + SELECT count(*), start_at, end_at, availability_id, min(id) AS kept, array_agg(id) AS all_ids + FROM slots + GROUP BY start_at, end_at, availability_id + HAVING count(*) > 1 + ) + DELETE FROM slots + WHERE id IN (SELECT unnest(all_ids) FROM same_slots) + AND id NOT IN (SELECT kept FROM same_slots); + ) + end + + def down + ## FIXME, more than one row returned by a subquery used as an expression + ## in UPDATE slots_reservations, slot_slots return all inserted ids + # + # WITH same_slots AS ( + # SELECT count(*), array_agg(id) AS all_ids, slot_id + # FROM slots_reservations + # GROUP BY slot_id + # HAVING count(*) > 1 + # ), slot AS ( + # SELECT * + # FROM slots + # WHERE id IN (SELECT slot_id FROM same_slots) + # ), insert_slot AS ( + # INSERT INTO slots (start_at, end_at, created_at, updated_at, availability_id) + # SELECT start_at, end_at, now(), now(), availability_id + # FROM slot + # RETURNING id + # ) + # UPDATE slots_reservations + # SET slot_id=(SELECT id FROM insert_slot) + # WHERE id IN (SELECT unnest(all_ids) FROM same_slots); + # + + add_column :slots, :ex_start_at, :datetime + add_column :slots, :ex_end_at, :datetime + add_column :slots, :canceled_at, :datetime + add_column :slots, :offered, :boolean + add_column :slots, :destroying, :boolean, default: false + + execute %( + UPDATE slots + SET + ex_start_at=slots_reservations.ex_start_at, + ex_end_at=slots_reservations.ex_end_at, + canceled_at=slots_reservations.canceled_at, + offered=slots_reservations.offered + FROM slots_reservations + WHERE slots_reservations.slot_id = slots.id + ) + + remove_column :slots_reservations, :ex_start_at + remove_column :slots_reservations, :ex_end_at + remove_column :slots_reservations, :canceled_at + remove_column :slots_reservations, :offered + end +end diff --git a/db/schema.rb b/db/schema.rb index 55a7cad70..767a17da4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_05_31_160223) do +ActiveRecord::Schema.define(version: 2022_07_04_084929) do # These are extensions that must be enabled in order to support this database enable_extension "fuzzystrmatch" @@ -760,17 +760,16 @@ ActiveRecord::Schema.define(version: 2022_05_31_160223) do t.datetime "created_at" t.datetime "updated_at" t.integer "availability_id" - t.datetime "ex_start_at" - t.datetime "ex_end_at" - t.datetime "canceled_at" - t.boolean "offered", default: false - t.boolean "destroying", default: false t.index ["availability_id"], name: "index_slots_on_availability_id" end create_table "slots_reservations", id: :serial, force: :cascade do |t| t.integer "slot_id" t.integer "reservation_id" + t.datetime "ex_start_at" + t.datetime "ex_end_at" + t.datetime "canceled_at" + t.boolean "offered" t.index ["reservation_id"], name: "index_slots_reservations_on_reservation_id" t.index ["slot_id"], name: "index_slots_reservations_on_slot_id" end From dbf624c17f699d925c819cc18e3788b0adffb3d7 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 5 Jul 2022 11:26:10 +0200 Subject: [PATCH 029/141] rollback migration --- ...eservation_fields_to_slots_reservations.rb | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb b/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb index 35947be5e..87f5d5bab 100644 --- a/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb +++ b/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb @@ -10,7 +10,7 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] add_column :slots_reservations, :canceled_at, :datetime add_column :slots_reservations, :offered, :boolean - execute %( + execute <<-SQL UPDATE slots_reservations SET ex_start_at=slots.ex_start_at, @@ -19,7 +19,7 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] offered=slots.offered FROM slots WHERE slots_reservations.slot_id = slots.id - ) + SQL remove_column :slots, :ex_start_at remove_column :slots, :ex_end_at @@ -30,7 +30,7 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] # we gonna keep only only one slot (remove duplicates) because data is now hold in slots_reservations # update slots_reservation.slot_id - execute %( + execute <<-SQL UPDATE slots_reservations SET slot_id=r.kept FROM ( @@ -39,45 +39,53 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] GROUP BY start_at, end_at, availability_id HAVING count(*) > 1) as r WHERE slot_id = ANY(r.all_ids); - ) + SQL # remove useless slots - execute %q( - WITH same_slots AS ( - SELECT count(*), start_at, end_at, availability_id, min(id) AS kept, array_agg(id) AS all_ids - FROM slots - GROUP BY start_at, end_at, availability_id - HAVING count(*) > 1 - ) - DELETE FROM slots - WHERE id IN (SELECT unnest(all_ids) FROM same_slots) - AND id NOT IN (SELECT kept FROM same_slots); - ) + execute <<-SQL + WITH same_slots AS ( + SELECT count(*), start_at, end_at, availability_id, min(id) AS kept, array_agg(id) AS all_ids + FROM slots + GROUP BY start_at, end_at, availability_id + HAVING count(*) > 1 + ) + DELETE FROM slots + WHERE id IN (SELECT unnest(all_ids) FROM same_slots) + AND id NOT IN (SELECT kept FROM same_slots); + SQL end def down - ## FIXME, more than one row returned by a subquery used as an expression - ## in UPDATE slots_reservations, slot_slots return all inserted ids - # - # WITH same_slots AS ( - # SELECT count(*), array_agg(id) AS all_ids, slot_id - # FROM slots_reservations - # GROUP BY slot_id - # HAVING count(*) > 1 - # ), slot AS ( - # SELECT * - # FROM slots - # WHERE id IN (SELECT slot_id FROM same_slots) - # ), insert_slot AS ( - # INSERT INTO slots (start_at, end_at, created_at, updated_at, availability_id) - # SELECT start_at, end_at, now(), now(), availability_id - # FROM slot - # RETURNING id - # ) - # UPDATE slots_reservations - # SET slot_id=(SELECT id FROM insert_slot) - # WHERE id IN (SELECT unnest(all_ids) FROM same_slots); - # + execute <<-SQL + DO + $$ + DECLARE + sr_group RECORD; + slot slots%ROWTYPE; + new_slot_id slots.id%TYPE; + curr_slot_reservation_id slots_reservations.id%TYPE; + BEGIN + FOR sr_group IN + SELECT count(*), array_agg(id) AS all_ids, slot_id + FROM slots_reservations + GROUP BY slot_id + HAVING count(*) > 1 + LOOP + SELECT * INTO slot FROM slots WHERE id = sr_group.slot_id; + FOR curr_slot_reservation_id IN + SELECT unnest(sr_group.all_ids[2:]) + LOOP + INSERT INTO slots (start_at, end_at, created_at, updated_at, availability_id) + VALUES (slot.start_at, slot.end_at, now(), now(), slot.availability_id) + RETURNING id INTO new_slot_id; + UPDATE slots_reservations + SET slot_id=new_slot_id + WHERE id=curr_slot_reservation_id; + END LOOP; + END LOOP; + END; + $$ + SQL add_column :slots, :ex_start_at, :datetime add_column :slots, :ex_end_at, :datetime @@ -85,7 +93,7 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] add_column :slots, :offered, :boolean add_column :slots, :destroying, :boolean, default: false - execute %( + execute <<-SQL UPDATE slots SET ex_start_at=slots_reservations.ex_start_at, @@ -94,7 +102,7 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] offered=slots_reservations.offered FROM slots_reservations WHERE slots_reservations.slot_id = slots.id - ) + SQL remove_column :slots_reservations, :ex_start_at remove_column :slots_reservations, :ex_end_at From b2fd2e1b483a733a89f8d173131fe6784f03e53f Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 5 Jul 2022 16:11:45 +0200 Subject: [PATCH 030/141] build all slots at reservation creation time --- .../api/availabilities_controller.rb | 4 ++- app/controllers/api/trainings_controller.rb | 4 +-- .../availabilities/availabilities_service.rb | 32 ++++--------------- .../create_availabilities_service.rb | 9 ++++++ .../availabilities/reservations.json.jbuilder | 2 ++ ...eservation_fields_to_slots_reservations.rb | 6 ++-- .../20220705125232_insert_missing_slots.rb | 23 +++++++++++++ db/schema.rb | 4 +-- test/fixtures/slots.yml | 8 ----- test/fixtures/slots_reservations.yml | 8 +++++ ...iption_extension_after_reservation_test.rb | 2 ++ 11 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 db/migrate/20220705125232_insert_missing_slots.rb diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index 9e2c524ee..fbcd58746 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -98,7 +98,9 @@ class API::AvailabilitiesController < API::ApiController def reservations authorize Availability - @reservation_slots = @availability.slots.includes(reservations: [statistic_profile: [user: [:profile]]]).order('slots.start_at ASC') + @reservation_slots = @availability.slots + .includes(slots_reservations: [reservations: [statistic_profile: [user: [:profile]]]]) + .order('slots.start_at ASC') end def export_availabilities diff --git a/app/controllers/api/trainings_controller.rb b/app/controllers/api/trainings_controller.rb index bb536cd88..b8902da78 100644 --- a/app/controllers/api/trainings_controller.rb +++ b/app/controllers/api/trainings_controller.rb @@ -52,8 +52,8 @@ class API::TrainingsController < API::ApiController authorize Training @training = Training.find(params[:id]) @availabilities = @training.availabilities - .includes(slots: { reservations: { statistic_profile: [:trainings, user: [:profile]] } }) - .where('slots.canceled_at': nil) + .includes(slots: { slots_reservations: { reservations: { statistic_profile: [:trainings, user: [:profile]] } } }) + .where('slots_reservations.canceled_at': nil) .order('availabilities.start_at DESC') end diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index 80f16414a..4342e2623 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -17,18 +17,9 @@ class Availabilities::AvailabilitiesService slots = [] availabilities.each do |a| - slot_duration = a.slot_duration || Setting.get('slot_duration').to_i - ((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i| - next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current || user.admin? - - slot = Slot.new( - start_at: a.start_at + (i * slot_duration).minutes, - end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes, - availability_id: a.id, - availability: a, - machine: machine, - title: '' - ) + a.slots.each do |slot| + slot.machine = machine + slot.title = '' slot = @service.machine_reserved_status(slot, reservations, @current_user) slots << slot end @@ -44,18 +35,9 @@ class Availabilities::AvailabilitiesService slots = [] availabilities.each do |a| - slot_duration = a.slot_duration || Setting.get('slot_duration').to_i - ((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i| - next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current || user.admin? - - slot = Slot.new( - start_at: a.start_at + (i * slot_duration).minutes, - end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes, - availability_id: a.id, - availability: a, - space: space, - title: '' - ) + a.slots.each do |slot| + slot.space = space + slot.title = '' slot = @service.space_reserved_status(slot, reservations, user) slots << slot end @@ -71,7 +53,7 @@ class Availabilities::AvailabilitiesService # first, we get the already-made reservations reservations = user.reservations.where("reservable_type = 'Training'") reservations = reservations.where('reservable_id = :id', id: training_id.to_i) if training_id.is_number? - reservations = reservations.joins(:slots).where('slots.start_at > ?', @current_user.admin? ? 1.month.ago : DateTime.current) + reservations = reservations.joins(slots_reservations: :slots).where('slots.start_at > ?', @current_user.admin? ? 1.month.ago : DateTime.current) # visible availabilities depends on multiple parameters availabilities = training_availabilities(training_id, user) diff --git a/app/services/availabilities/create_availabilities_service.rb b/app/services/availabilities/create_availabilities_service.rb index fb5f54474..567be4db2 100644 --- a/app/services/availabilities/create_availabilities_service.rb +++ b/app/services/availabilities/create_availabilities_service.rb @@ -4,6 +4,7 @@ class Availabilities::CreateAvailabilitiesService def create(availability, occurrences = []) availability.update_attributes(occurrence_id: availability.id) + slot_duration = availability.slot_duration || Setting.get('slot_duration').to_i occurrences.each do |o| next if availability.start_at == o[:start_at] && availability.end_at == o[:end_at] @@ -25,6 +26,14 @@ class Availabilities::CreateAvailabilitiesService slot_duration: availability.slot_duration, plan_ids: availability.plan_ids ).save! + + ((o.end_at - o.start_at) / slot_duration.minutes).to_i.times do |i| + Slot.new( + start_at: o.start_at + (i * slot_duration).minutes, + end_at: o.start_at + (i * slot_duration).minutes + slot_duration.minutes, + availability_id: o.id + ).save! + end end end end diff --git a/app/views/api/availabilities/reservations.json.jbuilder b/app/views/api/availabilities/reservations.json.jbuilder index c34226e2a..4bb1e67e7 100644 --- a/app/views/api/availabilities/reservations.json.jbuilder +++ b/app/views/api/availabilities/reservations.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(@reservation_slots) do |slot| json.slot_id slot.id json.start_at slot.start_at.iso8601 diff --git a/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb b/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb index 87f5d5bab..80407eb3d 100644 --- a/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb +++ b/db/migrate/20220704084929_add_reservation_fields_to_slots_reservations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Previously, the Slot table was holding data about reservations. # This was a wrong assumption that leads to a bug. # An Availability should have many slots but a slot can be related to multiple Reservations, @@ -8,7 +10,7 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] add_column :slots_reservations, :ex_start_at, :datetime add_column :slots_reservations, :ex_end_at, :datetime add_column :slots_reservations, :canceled_at, :datetime - add_column :slots_reservations, :offered, :boolean + add_column :slots_reservations, :offered, :boolean, default: false execute <<-SQL UPDATE slots_reservations @@ -90,7 +92,7 @@ class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2] add_column :slots, :ex_start_at, :datetime add_column :slots, :ex_end_at, :datetime add_column :slots, :canceled_at, :datetime - add_column :slots, :offered, :boolean + add_column :slots, :offered, :boolean, default: false add_column :slots, :destroying, :boolean, default: false execute <<-SQL diff --git a/db/migrate/20220705125232_insert_missing_slots.rb b/db/migrate/20220705125232_insert_missing_slots.rb new file mode 100644 index 000000000..17f2a499b --- /dev/null +++ b/db/migrate/20220705125232_insert_missing_slots.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Previously, only the reserved slots were saved in DB, other slots were created on the fly. +# Now we save all slots in DB, so we must re-create slots for the existing availabilities +class InsertMissingSlots < ActiveRecord::Migration[5.2] + def up + Availability.all.each do |availability| + slot_duration = availability.slot_duration || Setting.get('slot_duration').to_i + + ((availability.end_at - availability.start_at) / slot_duration.minutes).to_i.times do |i| + Slot.find_or_create_by( + start_at: availability.start_at + (i * slot_duration).minutes, + end_at: availability.start_at + (i * slot_duration).minutes + slot_duration.minutes, + availability_id: availability.id + ) + end + end + end + + def down + Slot.where.not(id: SlotsReservation.all.map(&:slot_id)).each(&:destroy) + end +end diff --git a/db/schema.rb b/db/schema.rb index 767a17da4..f8f7df09e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_07_04_084929) do +ActiveRecord::Schema.define(version: 2022_07_05_125232) do # These are extensions that must be enabled in order to support this database enable_extension "fuzzystrmatch" @@ -769,7 +769,7 @@ ActiveRecord::Schema.define(version: 2022_07_04_084929) do t.datetime "ex_start_at" t.datetime "ex_end_at" t.datetime "canceled_at" - t.boolean "offered" + t.boolean "offered", default: false t.index ["reservation_id"], name: "index_slots_reservations_on_reservation_id" t.index ["slot_id"], name: "index_slots_reservations_on_slot_id" end diff --git a/test/fixtures/slots.yml b/test/fixtures/slots.yml index 05f1ce142..515bc616d 100644 --- a/test/fixtures/slots.yml +++ b/test/fixtures/slots.yml @@ -6,10 +6,6 @@ slot_1: created_at: 2012-03-12 13:40:22.342717000 Z updated_at: 2012-03-12 13:40:22.342717000 Z availability_id: 12 - ex_start_at: - canceled_at: - ex_end_at: - offered: slot_2: id: 2 @@ -18,7 +14,3 @@ slot_2: created_at: 2015-06-10 11:20:01.341130000 Z updated_at: 2015-06-10 11:20:01.341130000 Z availability_id: 13 - ex_start_at: - canceled_at: - ex_end_at: - offered: diff --git a/test/fixtures/slots_reservations.yml b/test/fixtures/slots_reservations.yml index c531b981c..038302da9 100644 --- a/test/fixtures/slots_reservations.yml +++ b/test/fixtures/slots_reservations.yml @@ -3,7 +3,15 @@ one: slot_id: 1 reservation_id: 1 + ex_start_at: + canceled_at: + ex_end_at: + offered: two: slot_id: 2 reservation_id: 2 + ex_start_at: + canceled_at: + ex_end_at: + offered: diff --git a/test/services/subscription_extension_after_reservation_test.rb b/test/services/subscription_extension_after_reservation_test.rb index 25ec82036..a0a5f0607 100644 --- a/test/services/subscription_extension_after_reservation_test.rb +++ b/test/services/subscription_extension_after_reservation_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class SubscriptionExtensionAfterReservationTest < ActiveSupport::TestCase From 783e86d9cc2c98e742be8aaf54654d6765b0eec1 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 11 Jul 2022 12:23:04 +0200 Subject: [PATCH 031/141] (bug) fix slots creation --- .../create_availabilities_service.rb | 6 +++--- db/schema.rb | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/services/availabilities/create_availabilities_service.rb b/app/services/availabilities/create_availabilities_service.rb index 567be4db2..3cff26379 100644 --- a/app/services/availabilities/create_availabilities_service.rb +++ b/app/services/availabilities/create_availabilities_service.rb @@ -27,10 +27,10 @@ class Availabilities::CreateAvailabilitiesService plan_ids: availability.plan_ids ).save! - ((o.end_at - o.start_at) / slot_duration.minutes).to_i.times do |i| + (o[:end_at] - o[:start_at] / slot_duration.minutes).to_i.times do |i| Slot.new( - start_at: o.start_at + (i * slot_duration).minutes, - end_at: o.start_at + (i * slot_duration).minutes + slot_duration.minutes, + start_at: o[:start_at] + (i * slot_duration).minutes, + end_at: o[:start_at] + (i * slot_duration).minutes + slot_duration.minutes, availability_id: o.id ).save! end diff --git a/db/schema.rb b/db/schema.rb index f8f7df09e..cd97b668b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do enable_extension "unaccent" create_table "abuses", id: :serial, force: :cascade do |t| - t.integer "signaled_id" t.string "signaled_type" + t.integer "signaled_id" t.string "first_name" t.string "last_name" t.string "email" @@ -49,8 +49,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do t.string "locality" t.string "country" t.string "postal_code" - t.integer "placeable_id" t.string "placeable_type" + t.integer "placeable_id" t.datetime "created_at" t.datetime "updated_at" end @@ -64,8 +64,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do end create_table "assets", id: :serial, force: :cascade do |t| - t.integer "viewable_id" t.string "viewable_type" + t.integer "viewable_id" t.string "attachment" t.string "type" t.datetime "created_at" @@ -146,8 +146,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do end create_table "credits", id: :serial, force: :cascade do |t| - t.integer "creditable_id" t.string "creditable_type" + t.integer "creditable_id" t.integer "plan_id" t.integer "hours" t.datetime "created_at" @@ -369,15 +369,15 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do create_table "notifications", id: :serial, force: :cascade do |t| t.integer "receiver_id" - t.integer "attached_object_id" t.string "attached_object_type" + t.integer "attached_object_id" t.integer "notification_type_id" t.boolean "is_read", default: false t.datetime "created_at" t.datetime "updated_at" t.string "receiver_type" t.boolean "is_send", default: false - t.jsonb "meta_data", default: {} + t.jsonb "meta_data", default: "{}" t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id" t.index ["receiver_id"], name: "index_notifications_on_receiver_id" end @@ -570,8 +570,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do create_table "prices", id: :serial, force: :cascade do |t| t.integer "group_id" t.integer "plan_id" - t.integer "priceable_id" t.string "priceable_type" + t.integer "priceable_id" t.integer "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -729,8 +729,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.integer "reservable_id" t.string "reservable_type" + t.integer "reservable_id" t.integer "nb_reserve_places" t.integer "statistic_profile_id" t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id" @@ -739,8 +739,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do create_table "roles", id: :serial, force: :cascade do |t| t.string "name" - t.integer "resource_id" t.string "resource_type" + t.integer "resource_id" t.datetime "created_at" t.datetime "updated_at" t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" From 5012912eddcd8e00be52e82060bf206f2579a82e Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 11 Jul 2022 17:59:56 +0200 Subject: [PATCH 032/141] (wip) refactoring Availabilities::AvailabilitiesService and Availabilities::StatusService --- .../api/availabilities_controller.rb | 68 +++++---- .../javascript/controllers/machines.js.erb | 6 +- .../src/javascript/controllers/spaces.js.erb | 6 +- .../javascript/controllers/trainings.js.erb | 6 +- app/helpers/availability_helper.rb | 2 +- app/models/slot.rb | 2 +- .../availabilities/availabilities_service.rb | 129 ++++++------------ .../public_availabilities_service.rb | 2 + app/services/availabilities/status_service.rb | 91 ++++-------- .../api/availabilities/_slot.json.jbuilder | 19 +++ .../api/availabilities/machine.json.jbuilder | 19 +-- .../availabilities/trainings.json.jbuilder | 38 ++---- 12 files changed, 145 insertions(+), 243 deletions(-) create mode 100644 app/views/api/availabilities/_slot.json.jbuilder diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index fbcd58746..65be3f7bd 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -4,15 +4,15 @@ class API::AvailabilitiesController < API::ApiController before_action :authenticate_user!, except: [:public] before_action :set_availability, only: %i[show update reservations lock] - before_action :define_max_visibility, only: %i[machine trainings spaces] + before_action :set_operator_role, only: %i[machine spaces] + before_action :set_customer, only: %i[machine spaces trainings] respond_to :json def index authorize Availability - start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) - end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day + display_window = window @availabilities = Availability.includes(:machines, :tags, :trainings, :spaces) - .where('start_at >= ? AND end_at <= ?', start_date, end_date) + .where('start_at >= ? AND end_at <= ?', display_window[:start], display_window[:end]) @availabilities = @availabilities.where.not(available_type: 'event') unless Setting.get('events_in_calendar') @@ -20,17 +20,17 @@ class API::AvailabilitiesController < API::ApiController end def public - start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) - end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day + # FIXME, use AvailabilitiesService + display_window = window @reservations = Reservation.includes(:slots, :statistic_profile) .references(:slots) - .where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date) + .where('slots.start_at >= ? AND slots.end_at <= ?', display_window[:start], display_window[:end]) machine_ids = params[:m] || [] service = Availabilities::PublicAvailabilitiesService.new(current_user) @availabilities = service.public_availabilities( - start_date, - end_date, + display_window[:start], + display_window[:end], @reservations, machines: machine_ids, spaces: params[:s] ) @@ -78,22 +78,25 @@ class API::AvailabilitiesController < API::ApiController end def machine - @current_user_role = current_user.role - - service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year) - @slots = service.machines(params[:machine_id], user) + service = Availabilities::AvailabilitiesService.new(current_user) + @machine = Machine.friendly.find(params[:machine_id]) + @slots = service.machines(@machine, @customer, window) end def trainings - service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year) - @availabilities = service.trainings(params[:training_id], user) + service = Availabilities::AvailabilitiesService.new(current_user) + @trainings = if training_id.is_number? || (training_id.length.positive? && training_id != 'all') + [Training.friendly.find(training_id)] + else + Training.all + end + @slots = service.trainings(@trainings, @customer, window) end def spaces - @current_user_role = current_user.role - - service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year) - @slots = service.spaces(params[:space_id], user) + service = Availabilities::AvailabilitiesService.new(current_user) + @space = Space.friendly.find(space_id) + @slots = service.spaces(@space, @customer, window) end def reservations @@ -133,12 +136,22 @@ class API::AvailabilitiesController < API::ApiController private - def user - if params[:member_id] - User.find(params[:member_id]) - else - current_user - end + def window + start_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:start]) + end_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:end])&.end_of_day + { start: start_date, end: end_date } + end + + def set_customer + @customer = if params[:member_id] + User.find(params[:member_id]) + else + current_user + end + end + + def set_operator_role + @current_user_role = current_user.role end def set_availability @@ -191,9 +204,4 @@ class API::AvailabilitiesController < API::ApiController def remove_full?(availability) params[:dispo] == 'false' && (availability.is_reserved || (availability.try(:full?) && availability.full?)) end - - def define_max_visibility - @visi_max_year = Setting.get('visibility_yearly').to_i.months.since - @visi_max_other = Setting.get('visibility_others').to_i.months.since - end end diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index 68ba37ae7..496199cd9 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -677,11 +677,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran */ const initialize = function () { $scope.eventSources.push({ - events: function (start, end, timezone, callback) { - Availability.machine({ machineId: $transition$.params().id }, function (availabilities) { - callback(availabilities); - }); - }, + url: `/api/availabilities/machines/${$transition$.params().id}`, textColor: 'black' }); diff --git a/app/frontend/src/javascript/controllers/spaces.js.erb b/app/frontend/src/javascript/controllers/spaces.js.erb index d3be7749c..7fa10da71 100644 --- a/app/frontend/src/javascript/controllers/spaces.js.erb +++ b/app/frontend/src/javascript/controllers/spaces.js.erb @@ -607,11 +607,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi // we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise // in the router because this allows to refetchEvents from fullCalendar API. $scope.eventSources.push({ - events: function (start, end, timezone, callback) { - Availability.spaces({ spaceId: $transition$.params().id }, function (availabilities) { - callback(availabilities); - }); - }, + url: `/api/availabilities/spaces/${$transition$.params().id}`, textColor: 'black' }); }; diff --git a/app/frontend/src/javascript/controllers/trainings.js.erb b/app/frontend/src/javascript/controllers/trainings.js.erb index ec381ac13..7750cfe93 100644 --- a/app/frontend/src/javascript/controllers/trainings.js.erb +++ b/app/frontend/src/javascript/controllers/trainings.js.erb @@ -397,11 +397,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra // we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise // in the router because this allows to refetchEvents from fullCalendar API. $scope.eventSources.push({ - events: function (start, end, timezone, callback) { - Availability.trainings({ trainingId: $transition$.params().id }, function (availabilities) { - callback(availabilities); - }); - }, + url: `/api/availabilities/trainings/${$transition$.params().id}`, textColor: 'black' }); }; diff --git a/app/helpers/availability_helper.rb b/app/helpers/availability_helper.rb index e1b106704..59f07352b 100644 --- a/app/helpers/availability_helper.rb +++ b/app/helpers/availability_helper.rb @@ -25,7 +25,7 @@ module AvailabilityHelper def machines_slot_border_color(slot) if slot.is_reserved - slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED + slot.current_user_slots_reservations_ids.empty? ? IS_COMPLETED : IS_RESERVED_BY_CURRENT_USER else MACHINE_COLOR end diff --git a/app/models/slot.rb b/app/models/slot.rb index 7e6f84a16..f30202d47 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -10,7 +10,7 @@ class Slot < ApplicationRecord has_many :reservations, through: :slots_reservations belongs_to :availability - attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :is_reserved_by_current_user + attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids def complete? reservations.length >= availability.nb_total_places diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index 4342e2623..39ac38341 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -3,121 +3,68 @@ # Provides helper methods for Availability resources and properties class Availabilities::AvailabilitiesService - def initialize(current_user, maximum_visibility = {}) + def initialize(current_user) @current_user = current_user - @maximum_visibility = maximum_visibility - @service = Availabilities::StatusService.new(current_user.role) + @maximum_visibility = { + year: Setting.get('visibility_yearly').to_i.months.since, + other: Setting.get('visibility_others').to_i.months.since + } + @service = Availabilities::StatusService.new(current_user&.role) end - # list all slots for the given machine, with reservations info, relatives to the given user - def machines(machine_id, user) - machine = Machine.friendly.find(machine_id) - reservations = reservations(machine, user) - availabilities = availabilities(machine, 'machines', user) + # list all slots for the given machine, with visibility relative to the given user + def machines(machine, user, window) + availabilities = availabilities(machine.availabilities, 'machines', user, window[:start], window[:end]) - slots = [] - availabilities.each do |a| - a.slots.each do |slot| - slot.machine = machine - slot.title = '' - slot = @service.machine_reserved_status(slot, reservations, @current_user) - slots << slot - end - end - slots + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, machine) } end - # list all slots for the given space, with reservations info, relatives to the given user - def spaces(space_id, user) - space = Space.friendly.find(space_id) - reservations = reservations(space, user) - availabilities = availabilities(space, 'space', user) + # list all slots for the given space, with visibility relative to the given user + def spaces(space, user, window) + availabilities = availabilities(space.availabilities, 'space', user, window[:start], window[:end]) - slots = [] - availabilities.each do |a| - a.slots.each do |slot| - slot.space = space - slot.title = '' - slot = @service.space_reserved_status(slot, reservations, user) - slots << slot - end - end - slots.each do |s| - s.title = I18n.t('availabilities.not_available') if s.complete? && !s.is_reserved - end - slots + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, space) } end - # list all slots for the given training, with reservations info, relatives to the given user - def trainings(training_id, user) - # first, we get the already-made reservations - reservations = user.reservations.where("reservable_type = 'Training'") - reservations = reservations.where('reservable_id = :id', id: training_id.to_i) if training_id.is_number? - reservations = reservations.joins(slots_reservations: :slots).where('slots.start_at > ?', @current_user.admin? ? 1.month.ago : DateTime.current) + # list all slots for the given training, with visibility relative to the given user + def trainings(trainings, user, window) + tr_availabilities = Availability.includes('trainings_availabilities') + .where('trainings_availabilities.training_id': trainings.map(&:id)) + availabilities = availabilities(tr_availabilities, 'training', user, window[:start], window[:end]) - # visible availabilities depends on multiple parameters - availabilities = training_availabilities(training_id, user) - - # finally, we merge the availabilities with the reservations - availabilities.each do |a| - a = @service.training_event_reserved_status(a, reservations, user) - end + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, trainings) } end private def subscription_year?(user) - user.subscription && user.subscription.plan.interval == 'year' && user.subscription.expired_at >= DateTime.current + user&.subscription && user.subscription.plan.interval == 'year' && user.subscription.expired_at >= DateTime.current end - # member must have validated at least 1 training and must have a valid yearly subscription. - def show_extended_slots?(user) + # members must have validated at least 1 training and must have a valid yearly subscription to view + # the trainings further in the futur. This is used to prevent users with a rolling subscription to take + # their first training in a very long delay. + def show_more_trainings?(user) user.trainings.size.positive? && subscription_year?(user) end - def reservations(reservable, user) - Reservation.where('reservable_type = ? and reservable_id = ?', reservable.class.name, reservable.id) - .includes(:slots, statistic_profile: [user: [:profile]]) - .references(:slots, :user) - .where('slots.start_at > ?', user.admin? ? 1.month.ago : DateTime.current) - end - - def availabilities(reservable, type, user) - if user.admin? || user.manager? - reservable.availabilities - .includes(:tags, :plans) - .where('end_at > ? AND available_type = ?', 1.month.ago, type) - .where(lock: false) - else - end_at = @maximum_visibility[:other] - end_at = @maximum_visibility[:year] if subscription_year?(user) - reservable.availabilities - .includes(:tags, :plans) - .where('end_at > ? AND end_at < ? AND available_type = ?', DateTime.current, end_at, type) - .where('availability_tags.tag_id' => user.tag_ids.concat([nil])) - .where(lock: false) - end - end - - def training_availabilities(training_id, user) - availabilities = if training_id.is_number? || (training_id.length.positive? && training_id != 'all') - Training.friendly.find(training_id).availabilities - else - Availability.trainings - end - + def availabilities(availabilities, type, user, range_start, range_end) # who made the request? - # 1) an admin (he can see all availabilities of 1 month ago and future) - if @current_user.admin? - availabilities.includes(:tags, :slots, :plans, trainings: [:machines]) - .where('availabilities.start_at > ?', 1.month.ago) + # 1) an admin (he can see all availabilities from 1 month ago to anytime in the future) + if @current_user&.admin? || @current_user&.manager? + window_start = [range_start, 1.month.ago].max + availabilities.includes(:tags, :plans) + .where('start_at <= ? AND end_at >= ? AND available_type = ?', range_end, window_start, type) .where(lock: false) - # 2) an user (he cannot see availabilities further than 1 (or 3) months) + # 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future) else end_at = @maximum_visibility[:other] - end_at = @maximum_visibility[:year] if show_extended_slots?(user) - availabilities.includes(:tags, :slots, :availability_tags, :plans, trainings: [:machines]) - .where('availabilities.start_at > ? AND availabilities.start_at < ?', DateTime.current, end_at) + end_at = @maximum_visibility[:year] if subscription_year?(user) && type != 'training' + end_at = @maximum_visibility[:year] if show_more_trainings?(user) && type == 'training' + window_end = [end_at, range_end].min + window_start = [range_start, DateTime.current].max + availabilities.includes(:tags, :plans) + .where('start_at < ? AND end_at > ? AND available_type = ?', window_end, window_start, type) .where('availability_tags.tag_id' => user.tag_ids.concat([nil])) .where(lock: false) end diff --git a/app/services/availabilities/public_availabilities_service.rb b/app/services/availabilities/public_availabilities_service.rb index efee253c7..ca3b77861 100644 --- a/app/services/availabilities/public_availabilities_service.rb +++ b/app/services/availabilities/public_availabilities_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true # Provides helper methods for public calendar of Availability +# FIXME, Availabilities::StatusService was refactored +# TODO, Use Availabilities::AvailabilitiesService class Availabilities::PublicAvailabilitiesService def initialize(current_user) @current_user = current_user diff --git a/app/services/availabilities/status_service.rb b/app/services/availabilities/status_service.rb index c48ac6d58..71376245d 100644 --- a/app/services/availabilities/status_service.rb +++ b/app/services/availabilities/status_service.rb @@ -7,75 +7,27 @@ class Availabilities::StatusService @show_name = (%w[admin manager].include?(@current_user_role) || Setting.get('display_name_enable')) end - # check that the provided machine slot is reserved or not and modify it accordingly - def machine_reserved_status(slot, reservations, user) + # check that the provided slot is reserved for the given reservable (machine, training or space). + # Mark it accordingly for display in the calendar + def slot_reserved_status(slot, user, reservables) statistic_profile_id = user&.statistic_profile&.id - reservations.each do |r| - r.slots.each do |s| - next unless slot.machine.id == r.reservable_id - next unless s.start_at == slot.start_at && s.canceled_at.nil? + slots_reservations = slot.slots_reservations + .includes(:reservation) + .where('reservations.reservable_type': reservables.map(&:class).map(&:name)) + .where('reservations.reservable_id': reservables.map(&:id)) + .where('slots_reservations.canceled_at': nil) - slot.id = s.id - slot.is_reserved = true - user_name = r.user ? r.user&.profile&.full_name : I18n.t('availabilities.deleted_user'); - slot.title = "#{slot.machine.name} - #{@show_name ? user_name : I18n.t('availabilities.deleted_user')}" - slot.can_modify = true if %w[admin manager].include?(@current_user_role) - slot.reservations.push r + user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': statistic_profile_id) - next unless r.statistic_profile_id == statistic_profile_id + slot.is_reserved = !slots_reservations.empty? + slot.title = slot_title(slots_reservations, user_slots_reservations, reservables) + slot.can_modify = true if %w[admin manager].include?(@current_user_role) || !user_slots_reservations.empty? + slot.current_user_slots_reservations_ids = user_slots_reservations.map(&:id) - slot.title = "#{slot.machine.name} - #{I18n.t('availabilities.i_ve_reserved')}" - slot.can_modify = true - slot.is_reserved_by_current_user = true - end - end slot end - # check that the provided space slot is reserved or not and modify it accordingly - def space_reserved_status(slot, reservations, user) - statistic_profile_id = user&.statistic_profile&.id - reservations.each do |r| - r.slots.each do |s| - next unless slot.space.id == r.reservable_id - - next unless s.start_at == slot.start_at && s.canceled_at.nil? - - slot.can_modify = true if %w[admin manager].include?(@current_user_role) - slot.reservations.push r - - next unless r.statistic_profile_id == statistic_profile_id - - slot.id = s.id - slot.title = I18n.t('availabilities.i_ve_reserved') - slot.can_modify = true - slot.is_reserved = true - end - end - slot - end - - # check that the provided availability (training or event) is reserved or not and modify it accordingly - def training_event_reserved_status(availability, reservations, user) - statistic_profile_id = user&.statistic_profile&.id - reservations.each do |r| - r.slots.each do |s| - next unless ( - (availability.available_type == 'training' && availability.trainings.first.id == r.reservable_id) || - (availability.available_type == 'event' && availability.event.id == r.reservable_id) - ) && s.start_at == availability.start_at && s.canceled_at.nil? - - availability.slot_id = s.id - if r.statistic_profile_id == statistic_profile_id - availability.is_reserved = true - availability.can_modify = true - end - end - end - availability - end - # check that the provided ability is reserved by the given user def reserved_availability?(availability, user) if user @@ -88,4 +40,21 @@ class Availabilities::StatusService false end end + + private + + def slot_title(slots_reservations, user_slots_reservations, reservables) + name = reservables.map(&:name).join(', ') + if user_slots_reservations.empty? && slots_reservations.empty? + name + elsif user_slots_reservations.empty? && !slots_reservations.empty? + user_names = slots_reservations.map(&:reservation) + .map(&:user) + .map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') } + .join(', ') + "#{name} - #{@show_name ? user_names : I18n.t('availabilities.not_available')}" + else + "#{name} - #{I18n.t('availabilities.i_ve_reserved')}" + end + end end diff --git a/app/views/api/availabilities/_slot.json.jbuilder b/app/views/api/availabilities/_slot.json.jbuilder new file mode 100644 index 000000000..da2ff0ce0 --- /dev/null +++ b/app/views/api/availabilities/_slot.json.jbuilder @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +json.slot_id slot.id +json.can_modify slot.can_modify +json.title slot.title +json.start slot.start_at.iso8601 +json.end slot.end_at.iso8601 +json.is_reserved slot.is_reserved +json.backgroundColor 'white' + +json.availability_id slot.availability_id +json.slots_reservations_ids slot.current_user_slots_reservations_ids + +json.tag_ids slot.availability.tag_ids +json.tags slot.availability.tags do |t| + json.id t.id + json.name t.name +end +json.plan_ids slot.availability.plan_ids diff --git a/app/views/api/availabilities/machine.json.jbuilder b/app/views/api/availabilities/machine.json.jbuilder index ab4879c08..d42a92fc4 100644 --- a/app/views/api/availabilities/machine.json.jbuilder +++ b/app/views/api/availabilities/machine.json.jbuilder @@ -1,19 +1,12 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.slot_id slot.id if slot.id - json.can_modify slot.can_modify - json.title slot.title - json.start slot.start_at.iso8601 - json.end slot.end_at.iso8601 - json.is_reserved slot.is_reserved - json.backgroundColor 'white' + json.partial! 'api/availabilities/slot', slot: slot json.borderColor machines_slot_border_color(slot) - json.availability_id slot.availability_id json.machine do - json.id slot.machine.id - json.name slot.machine.name + json.id @machine.id + json.name @machine.name end # the user who booked the slot, if the slot was reserved if (%w[admin manager].include? @current_user_role) && slot.reservation @@ -22,10 +15,4 @@ json.array!(@slots) do |slot| json.name slot.reservation.user&.profile&.full_name end end - json.tag_ids slot.availability.tag_ids - json.tags slot.availability.tags do |t| - json.id t.id - json.name t.name - end - json.plan_ids slot.availability.plan_ids end diff --git a/app/views/api/availabilities/trainings.json.jbuilder b/app/views/api/availabilities/trainings.json.jbuilder index 9030dd6e4..9c9974dc6 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -1,37 +1,19 @@ # frozen_string_literal: true -json.array!(@availabilities) do |a| - json.slot_id a.slot_id if a.slot_id - if a.is_reserved - json.is_reserved true - json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}" - elsif a.full? - json.is_completed true - json.title "#{a.trainings[0].name} - #{t('trainings.completed')}" - else - json.title a.trainings[0].name - end - json.borderColor trainings_events_border_color(a) - json.start a.start_at.iso8601 - json.end a.end_at.iso8601 - json.backgroundColor 'white' - json.can_modify a.can_modify - json.nb_total_places a.nb_total_places - json.availability_id a.id +json.array!(@slots) do |slot| + json.partial! 'api/availabilities/slot', slot: slot + json.borderColor trainings_events_border_color(slot) + + json.is_completed slot.full? + json.nb_total_places slot.nb_total_places json.training do - json.id a.trainings.first.id - json.name a.trainings.first.name - json.description a.trainings.first.description - json.machines a.trainings.first.machines do |m| + json.id slot.availability.trainings.first.id + json.name slot.availability.trainings.first.name + json.description slot.availability.trainings.first.description + json.machines slot.availability.trainings.first.machines do |m| json.id m.id json.name m.name end end - json.tag_ids a.tag_ids - json.tags a.tags do |t| - json.id t.id - json.name t.name - end - json.plan_ids a.plan_ids end From 37b24a8d2fc6a66b76ff59dbeb9dedc5aa96574e Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 12 Jul 2022 17:46:01 +0200 Subject: [PATCH 033/141] refactor slots model --- .../api/availabilities_controller.rb | 10 +- app/controllers/api/slots_controller.rb | 35 - .../api/slots_reservations_controller.rb | 35 + .../reservations/reservations-panel.tsx | 22 +- .../components/machines/reserve-button.tsx | 2 +- .../javascript/controllers/admin/calendar.js | 18 +- .../javascript/controllers/admin/events.js | 2 +- .../src/javascript/controllers/events.js.erb | 16 +- .../src/javascript/directives/cart.js | 12 +- .../src/javascript/models/reservation.ts | 15 +- app/frontend/src/javascript/services/slot.js | 15 - .../javascript/services/slots_reservation.js | 15 + .../shared/valid_reservation_modal.html | 4 +- app/helpers/availability_helper.rb | 10 +- app/models/availability.rb | 17 +- app/models/cart_item/reservation.rb | 7 +- app/models/reservation.rb | 20 +- app/models/slot.rb | 4 +- ..._policy.rb => slots_reservation_policy.rb} | 6 +- .../availabilities/availabilities_service.rb | 8 +- app/services/slot_service.rb | 21 - app/services/slots_reservations_service.rb | 23 + ...ubscription_extension_after_reservation.rb | 2 + .../api/availabilities/_slot.json.jbuilder | 8 + .../api/availabilities/machine.json.jbuilder | 2 +- .../api/availabilities/public.json.jbuilder | 4 +- .../availabilities/reservations.json.jbuilder | 23 +- .../api/availabilities/spaces.json.jbuilder | 28 +- .../availabilities/trainings.json.jbuilder | 2 +- .../reservations/_reservation.json.jbuilder | 15 +- app/views/api/slots/cancel.json.jbuilder | 2 - app/views/api/slots/show.json.jbuilder | 1 - .../slots_reservations/cancel.json.jbuilder | 3 + .../api/slots_reservations/show.json.jbuilder | 3 + config/routes.rb | 2 +- .../20220705125232_insert_missing_slots.rb | 10 +- doc/README.md | 2 + doc/availabilities-reservations-models.md | 89 ++ test/fixtures/slots.yml | 1000 ++++++++++++++++- 39 files changed, 1300 insertions(+), 213 deletions(-) delete mode 100644 app/controllers/api/slots_controller.rb create mode 100644 app/controllers/api/slots_reservations_controller.rb delete mode 100644 app/frontend/src/javascript/services/slot.js create mode 100644 app/frontend/src/javascript/services/slots_reservation.js rename app/policies/{slot_policy.rb => slots_reservation_policy.rb} (63%) delete mode 100644 app/services/slot_service.rb create mode 100644 app/services/slots_reservations_service.rb delete mode 100644 app/views/api/slots/cancel.json.jbuilder delete mode 100644 app/views/api/slots/show.json.jbuilder create mode 100644 app/views/api/slots_reservations/cancel.json.jbuilder create mode 100644 app/views/api/slots_reservations/show.json.jbuilder create mode 100644 doc/availabilities-reservations-models.md diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index 65be3f7bd..6cfdf7392 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -4,7 +4,7 @@ class API::AvailabilitiesController < API::ApiController before_action :authenticate_user!, except: [:public] before_action :set_availability, only: %i[show update reservations lock] - before_action :set_operator_role, only: %i[machine spaces] + before_action :set_operator_role, only: %i[machine spaces trainings] before_action :set_customer, only: %i[machine spaces trainings] respond_to :json @@ -101,9 +101,9 @@ class API::AvailabilitiesController < API::ApiController def reservations authorize Availability - @reservation_slots = @availability.slots - .includes(slots_reservations: [reservations: [statistic_profile: [user: [:profile]]]]) - .order('slots.start_at ASC') + @slots_reservations = @availability.slots_reservations + .includes(:slot, reservation: [statistic_profile: [user: [:profile]]]) + .order('slots.start_at ASC') end def export_availabilities @@ -151,7 +151,7 @@ class API::AvailabilitiesController < API::ApiController end def set_operator_role - @current_user_role = current_user.role + @operator_role = current_user.role end def set_availability diff --git a/app/controllers/api/slots_controller.rb b/app/controllers/api/slots_controller.rb deleted file mode 100644 index e901efb2a..000000000 --- a/app/controllers/api/slots_controller.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -# API Controller for resources of type Slot -# Slots are used to cut Availabilities into reservable slots. The duration of these slots is configured per -# availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration') -class API::SlotsController < API::ApiController - before_action :authenticate_user! - before_action :set_slot, only: %i[update cancel] - respond_to :json - - def update - authorize @slot - if @slot.update(slot_params) - SubscriptionExtensionAfterReservation.new(@slot.reservation).extend_subscription_if_eligible - render :show, status: :created, location: @slot - else - render json: @slot.errors, status: :unprocessable_entity - end - end - - def cancel - authorize @slot - SlotService.new.cancel(@slot) - end - - private - - def set_slot - @slot = Slot.find(params[:id]) - end - - def slot_params - params.require(:slot).permit(:start_at, :end_at, :availability_id) - end -end diff --git a/app/controllers/api/slots_reservations_controller.rb b/app/controllers/api/slots_reservations_controller.rb new file mode 100644 index 000000000..0613ebab1 --- /dev/null +++ b/app/controllers/api/slots_reservations_controller.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# API Controller for resources of type Slot +# Slots are used to cut Availabilities into reservable slots. The duration of these slots is configured per +# availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration') +class API::SlotsReservationsController < API::ApiController + before_action :authenticate_user! + before_action :set_slots_reservation, only: %i[update cancel] + respond_to :json + + def update + authorize @slot_reservation + if @slot_reservation.update(slot_params) + SubscriptionExtensionAfterReservation.new(@slot_reservation.reservation).extend_subscription_if_eligible + render :show, status: :created, location: @slot_reservation + else + render json: @slot_reservation.errors, status: :unprocessable_entity + end + end + + def cancel + authorize @slot_reservation + SlotsReservationsService.cancel(@slot_reservation) + end + + private + + def set_slots_reservation + @slot_reservation = SlotsReservation.find(params[:id]) + end + + def slot_params + params.require(:slots_reservation).permit(:slot_id) + end +end diff --git a/app/frontend/src/javascript/components/dashboard/reservations/reservations-panel.tsx b/app/frontend/src/javascript/components/dashboard/reservations/reservations-panel.tsx index 7206460b1..bbcb49068 100644 --- a/app/frontend/src/javascript/components/dashboard/reservations/reservations-panel.tsx +++ b/app/frontend/src/javascript/components/dashboard/reservations/reservations-panel.tsx @@ -1,6 +1,6 @@ import React, { ReactNode, useEffect, useState } from 'react'; import { FabPanel } from '../../base/fab-panel'; -import { Reservation, ReservationSlot } from '../../../models/reservation'; +import { Reservation, SlotsReservation } from '../../../models/reservation'; import ReservationAPI from '../../../api/reservation'; import { useTranslation } from 'react-i18next'; import moment from 'moment'; @@ -38,16 +38,16 @@ const ReservationsPanel: React.FC = ({ userId, onError, */ const reservationsByDate = (state: 'past' | 'futur'): Array => { return reservations.filter(r => { - return !!r.slots_attributes.find(s => filterSlot(s, state)); + return !!r.slots_reservations_attributes.find(s => filterSlot(s, state)); }); }; /** - * Check if the given slot if past of futur + * Check if the given slot reservation if past of futur */ - const filterSlot = (slot: ReservationSlot, state: 'past' | 'futur'): boolean => { - return (state === 'past' && moment(slot.start_at).isBefore()) || - (state === 'futur' && moment(slot.start_at).isAfter()); + const filterSlot = (sr: SlotsReservation, state: 'past' | 'futur'): boolean => { + return (state === 'past' && moment(sr.slot_attributes.start_at).isBefore()) || + (state === 'futur' && moment(sr.slot_attributes.start_at).isAfter()); }; /** @@ -95,12 +95,12 @@ const ReservationsPanel: React.FC = ({ userId, onError, return (
  • - {reservation.reservable.name} - {FormatLib.date(reservation.slots_attributes[0].start_at)} + {reservation.reservable.name} - {FormatLib.date(reservation.slots_reservations_attributes[0].slot_attributes.start_at)} {details[reservation.id] && - {reservation.slots_attributes.filter(s => filterSlot(s, state)).map( - slot => - {FormatLib.date(slot.start_at)}, {FormatLib.time(slot.start_at)} - {FormatLib.time(slot.end_at)} + {reservation.slots_reservations_attributes.filter(s => filterSlot(s, state)).map( + slotReservation => + {FormatLib.date(slotReservation.slot_attributes.start_at)}, {FormatLib.time(slotReservation.slot_attributes.start_at)} - {FormatLib.time(slotReservation.slot_attributes.end_at)} )} } @@ -109,7 +109,7 @@ const ReservationsPanel: React.FC = ({ userId, onError, }; const futur = reservationsByDate('futur'); - const past = _.orderBy(reservationsByDate('past'), r => r.slots_attributes[0].start_at, 'desc'); + const past = _.orderBy(reservationsByDate('past'), r => r.slots_reservations_attributes[0].slot_attributes.start_at, 'desc'); return ( diff --git a/app/frontend/src/javascript/components/machines/reserve-button.tsx b/app/frontend/src/javascript/components/machines/reserve-button.tsx index e0f68aaaf..367d663c3 100644 --- a/app/frontend/src/javascript/components/machines/reserve-button.tsx +++ b/app/frontend/src/javascript/components/machines/reserve-button.tsx @@ -163,7 +163,7 @@ const ReserveButton: React.FC = ({ currentUser, machineId, o + nextReservation={machine?.current_user_next_training_reservation?.slots_reservations_attributes[0]?.slot_attributes.start_at} /> should use slot_id instead of (start_at + end_at) export interface Reservation { id?: number, @@ -20,7 +23,7 @@ export interface Reservation { message?: string, reservable_id: number, reservable_type: ReservableType, - slots_attributes: Array, + slots_reservations_attributes: Array, reservable?: { id: number, name: string diff --git a/app/frontend/src/javascript/services/slot.js b/app/frontend/src/javascript/services/slot.js deleted file mode 100644 index f0af5610d..000000000 --- a/app/frontend/src/javascript/services/slot.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -Application.Services.factory('Slot', ['$resource', function ($resource) { - return $resource('/api/slots/:id', - { id: '@id' }, { - update: { - method: 'PUT' - }, - cancel: { - method: 'PUT', - url: '/api/slots/:id/cancel' - } - } - ); -}]); diff --git a/app/frontend/src/javascript/services/slots_reservation.js b/app/frontend/src/javascript/services/slots_reservation.js new file mode 100644 index 000000000..4476d6371 --- /dev/null +++ b/app/frontend/src/javascript/services/slots_reservation.js @@ -0,0 +1,15 @@ +'use strict'; + +Application.Services.factory('SlotsReservation', ['$resource', function ($resource) { + return $resource('/api/slots_reservations/:id', + { id: '@id' }, { + update: { + method: 'PUT' + }, + cancel: { + method: 'PUT', + url: '/api/slots_reservations/:id/cancel' + } + } + ); +}]); diff --git a/app/frontend/templates/shared/valid_reservation_modal.html b/app/frontend/templates/shared/valid_reservation_modal.html index 557183950..737dd870d 100644 --- a/app/frontend/templates/shared/valid_reservation_modal.html +++ b/app/frontend/templates/shared/valid_reservation_modal.html @@ -9,8 +9,8 @@

    {{ 'app.shared.valid_reservation_modal.here_is_the_summary_of_the_slots_to_book_for_the_current_user' }}

    -
      -
    • {{slot.start_at | amDateFormat: 'LL'}} : {{slot.start_at | amDateFormat:'LT'}} - {{slot.end_at | amDateFormat:'LT'}}
    • +
        +
      • {{sr.slot_attributes.start_at | amDateFormat: 'LL'}} : {{sr.slot_attributes.start_at | amDateFormat:'LT'}} - {{sr.slot_attributes.end_at | amDateFormat:'LT'}}
    diff --git a/app/helpers/availability_helper.rb b/app/helpers/availability_helper.rb index 59f07352b..9d05176ab 100644 --- a/app/helpers/availability_helper.rb +++ b/app/helpers/availability_helper.rb @@ -8,7 +8,7 @@ module AvailabilityHelper EVENT_COLOR = '#dd7e6b' IS_RESERVED_BY_CURRENT_USER = '#b2e774' MACHINE_IS_RESERVED_BY_USER = '#1d98ec' - IS_COMPLETED = '#eeeeee' + IS_FULL = '#eeeeee' def availability_border_color(availability) case availability.available_type @@ -25,7 +25,7 @@ module AvailabilityHelper def machines_slot_border_color(slot) if slot.is_reserved - slot.current_user_slots_reservations_ids.empty? ? IS_COMPLETED : IS_RESERVED_BY_CURRENT_USER + slot.current_user_slots_reservations_ids.empty? ? IS_FULL : IS_RESERVED_BY_CURRENT_USER else MACHINE_COLOR end @@ -34,8 +34,8 @@ module AvailabilityHelper def space_slot_border_color(slot) if slot.is_reserved IS_RESERVED_BY_CURRENT_USER - elsif slot.complete? - IS_COMPLETED + elsif slot.full? + IS_FULL else SPACE_COLOR end @@ -45,7 +45,7 @@ module AvailabilityHelper if availability.is_reserved IS_RESERVED_BY_CURRENT_USER elsif availability.full? - IS_COMPLETED + IS_FULL else case availability.available_type when 'training' diff --git a/app/models/availability.rb b/app/models/availability.rb index d641350dd..d45e6cc91 100644 --- a/app/models/availability.rb +++ b/app/models/availability.rb @@ -20,6 +20,7 @@ class Availability < ApplicationRecord has_many :spaces, through: :spaces_availabilities has_many :slots + has_many :slots_reservations, through: :slots has_many :reservations, through: :slots has_one :event @@ -116,23 +117,25 @@ class Availability < ApplicationRecord def full? return false if nb_total_places.blank? - if available_type == 'training' || available_type == 'space' - nb_total_places <= slots.to_a.select { |s| s.canceled_at.nil? }.size - elsif available_type == 'event' + if available_type == 'event' event.nb_free_places.zero? + else + slots.map(&:full?).reduce(:&) end end - def nb_total_places + def available_places_per_slot case available_type when 'training' - super.presence || trainings.map(&:nb_total_places).reduce(:+) + nb_total_places || trainings.map(&:nb_total_places).max when 'event' event.nb_total_places when 'space' - super.presence || spaces.map(&:default_places).reduce(:+) + nb_total_places || spaces.map(&:default_places).max + when 'machines' + machines.count else - nil + raise TypeError end end diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index 9f9909217..61b623109 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -50,7 +50,12 @@ class CartItem::Reservation < CartItem::BaseItem end if availability.available_type == 'machines' - s = Slot.includes(:reservations).where(start_at: slot[:start_at], end_at: slot[:end_at], availability_id: slot[:availability_id], canceled_at: nil, "reservations.reservable": @reservable) + s = SlotsReservation.includes(:slot, :reservation) + .where('slots.start_at': slot[:start_at], + 'slots.end_at': slot[:end_at], + 'slots.availability_id': slot[:availability_id], + canceled_at: nil, + 'reservations.reservable': @reservable) unless s.empty? @errors[:slot] = 'slot is reserved' return false diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 43bc42c70..d0a3ddd47 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -98,26 +98,22 @@ class Reservation < ApplicationRecord private def machine_not_already_reserved - already_reserved = false - slots.each do |slot| - same_hour_slots = Slot.joins(:reservations).where( + slots_reservations.each do |slot| + same_hour_slots = SlotsReservation.joins(:reservation).where( reservations: { reservable_type: reservable_type, reservable_id: reservable_id }, - start_at: slot.start_at, - end_at: slot.end_at, - availability_id: slot.availability_id, + slot_id: slot_id, canceled_at: nil - ) - if same_hour_slots.any? - already_reserved = true + ).count + if same_hour_slots.positive? + errors.add(:reservable, 'already reserved') break end end - errors.add(:machine, 'already reserved') if already_reserved end def training_not_fully_reserved - slot = slots.first - errors.add(:training, 'already fully reserved') if Availability.find(slot.availability_id).full? + full = slots_reservations.map(&:slot).map(&:full?).reduce(:&) + errors.add(:reservable, 'already fully reserved') if full end def slots_not_locked diff --git a/app/models/slot.rb b/app/models/slot.rb index f30202d47..24347746c 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -12,7 +12,7 @@ class Slot < ApplicationRecord attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids - def complete? - reservations.length >= availability.nb_total_places + def full? + slots_reservations.where(canceled_at: nil).count >= availability.available_places_per_slot end end diff --git a/app/policies/slot_policy.rb b/app/policies/slots_reservation_policy.rb similarity index 63% rename from app/policies/slot_policy.rb rename to app/policies/slots_reservation_policy.rb index 315ec317b..0d5f6a543 100644 --- a/app/policies/slot_policy.rb +++ b/app/policies/slots_reservation_policy.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -# Check the access policies for API::SlotsController -class SlotPolicy < ApplicationPolicy +# Check the access policies for API::SlotsReservationsController +class SlotsReservationPolicy < ApplicationPolicy def update? # check that the update is allowed and the prevention delay has not expired delay = Setting.get('booking_move_delay').to_i @@ -9,7 +9,7 @@ class SlotPolicy < ApplicationPolicy # these condition does not apply to admins user.admin? || user.manager? || - (record.reservation.user == user && enabled && ((record.start_at - DateTime.current).to_i / 3600 >= delay)) + (record.reservation.user == user && enabled && ((record.slot.start_at - DateTime.current).to_i / 3600 >= delay)) end def cancel? diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index 39ac38341..303a06e74 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -16,23 +16,23 @@ class Availabilities::AvailabilitiesService def machines(machine, user, window) availabilities = availabilities(machine.availabilities, 'machines', user, window[:start], window[:end]) - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, machine) } + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [machine]) } end # list all slots for the given space, with visibility relative to the given user def spaces(space, user, window) availabilities = availabilities(space.availabilities, 'space', user, window[:start], window[:end]) - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, space) } + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [space]) } end - # list all slots for the given training, with visibility relative to the given user + # list all slots for the given training(s), with visibility relative to the given user def trainings(trainings, user, window) tr_availabilities = Availability.includes('trainings_availabilities') .where('trainings_availabilities.training_id': trainings.map(&:id)) availabilities = availabilities(tr_availabilities, 'training', user, window[:start], window[:end]) - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, trainings) } + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, s.availability.trainings) } end private diff --git a/app/services/slot_service.rb b/app/services/slot_service.rb deleted file mode 100644 index 64419f8b0..000000000 --- a/app/services/slot_service.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -# helpers for managing slots (reservations sub-units) -class SlotService - def cancel(slot) - # first we mark ths slot as cancelled in DB, to free a ticket - slot.update_attributes(canceled_at: DateTime.current) - - # then we try to remove this reservation from ElasticSearch, to keep the statistics up-to-date - model_name = slot.reservation.reservable.class.name - client = Elasticsearch::Model.client - - model = "Stats::#{model_name}".constantize - client.delete_by_query( - index: model.index_name, - type: model.document_type, - conflicts: 'proceed', - body: { query: { match: { reservationId: slot.reservation.id } } } - ) - end -end diff --git a/app/services/slots_reservations_service.rb b/app/services/slots_reservations_service.rb new file mode 100644 index 000000000..1ef1846e2 --- /dev/null +++ b/app/services/slots_reservations_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# helpers for managing slots reservations (reservations for a time unit) +class SlotsReservationsService + class << self + def cancel(slot_reservation) + # first we mark ths slot reseravtion as cancelled in DB, to free a ticket + slot_reservation.update_attributes(canceled_at: DateTime.current) + + # then we try to remove this reservation from ElasticSearch, to keep the statistics up-to-date + model_name = slot_reservation.reservation.reservable.class.name + client = Elasticsearch::Model.client + + model = "Stats::#{model_name}".constantize + client.delete_by_query( + index: model.index_name, + type: model.document_type, + conflicts: 'proceed', + body: { query: { match: { reservationId: slot_reservation.reservation_id } } } + ) + end + end +end diff --git a/app/services/subscription_extension_after_reservation.rb b/app/services/subscription_extension_after_reservation.rb index cb8d297d4..0fc66d270 100644 --- a/app/services/subscription_extension_after_reservation.rb +++ b/app/services/subscription_extension_after_reservation.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# Extend the user's current subscription after his first training reservation if +# he subscribed to a rolling plan class SubscriptionExtensionAfterReservation attr_accessor :user, :reservation diff --git a/app/views/api/availabilities/_slot.json.jbuilder b/app/views/api/availabilities/_slot.json.jbuilder index da2ff0ce0..d0f256698 100644 --- a/app/views/api/availabilities/_slot.json.jbuilder +++ b/app/views/api/availabilities/_slot.json.jbuilder @@ -17,3 +17,11 @@ json.tags slot.availability.tags do |t| json.name t.name end json.plan_ids slot.availability.plan_ids + +# the users who booked on this slot, if any +if (%w[admin manager].include? operator_role) && !slot.slots_reservations.empty? + json.users slot.slots_reservations do |sr| + json.id sr.reservation.user&.id + json.name sr.reservation.user&.profile&.full_name + end +end diff --git a/app/views/api/availabilities/machine.json.jbuilder b/app/views/api/availabilities/machine.json.jbuilder index d42a92fc4..95646ba52 100644 --- a/app/views/api/availabilities/machine.json.jbuilder +++ b/app/views/api/availabilities/machine.json.jbuilder @@ -1,7 +1,7 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.partial! 'api/availabilities/slot', slot: slot + json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role json.borderColor machines_slot_border_color(slot) json.machine do diff --git a/app/views/api/availabilities/public.json.jbuilder b/app/views/api/availabilities/public.json.jbuilder index eb93dec91..d656b22fe 100644 --- a/app/views/api/availabilities/public.json.jbuilder +++ b/app/views/api/availabilities/public.json.jbuilder @@ -35,7 +35,7 @@ json.array!(@availabilities) do |availability| json.borderColor availability_border_color(availability) if complete json.title "#{availability.title} - #{t('trainings.completed')}" - json.borderColor AvailabilityHelper::IS_COMPLETED + json.borderColor AvailabilityHelper::IS_FULL end if availability.is_reserved json.is_reserved true @@ -61,7 +61,7 @@ json.array!(@availabilities) do |availability| elsif availability.try(:space) json.space_id availability.space.id json.borderColor space_slot_border_color(availability) - json.is_completed availability.complete? + json.is_completed availability.full? else json.title 'Unknown slot' end diff --git a/app/views/api/availabilities/reservations.json.jbuilder b/app/views/api/availabilities/reservations.json.jbuilder index 4bb1e67e7..8cb5fa96e 100644 --- a/app/views/api/availabilities/reservations.json.jbuilder +++ b/app/views/api/availabilities/reservations.json.jbuilder @@ -1,16 +1,17 @@ # frozen_string_literal: true -json.array!(@reservation_slots) do |slot| - json.slot_id slot.id - json.start_at slot.start_at.iso8601 - json.end_at slot.end_at.iso8601 - json.message slot.reservation.message - json.reservable slot.reservation.reservable - json.reservable_id slot.reservation.reservable_id - json.reservable_type slot.reservation.reservable_type +json.array!(@slots_reservations) do |sr| + json.id sr.id + json.slot_id sr.slot_id + json.start_at sr.slot.start_at.iso8601 + json.end_at sr.slot.end_at.iso8601 + json.message sr.reservation.message + json.reservable sr.reservation.reservable + json.reservable_id sr.reservation.reservable_id + json.reservable_type sr.reservation.reservable_type json.user do - json.id slot.reservation.statistic_profile&.user_id - json.name slot.reservation.statistic_profile&.user&.profile&.full_name + json.id sr.reservation.statistic_profile&.user_id + json.name sr.reservation.statistic_profile&.user&.profile&.full_name end - json.canceled_at slot.canceled_at + json.canceled_at sr.canceled_at end diff --git a/app/views/api/availabilities/spaces.json.jbuilder b/app/views/api/availabilities/spaces.json.jbuilder index 5661bdd35..45cb23d0a 100644 --- a/app/views/api/availabilities/spaces.json.jbuilder +++ b/app/views/api/availabilities/spaces.json.jbuilder @@ -1,32 +1,12 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.slot_id slot.id if slot.id - json.can_modify slot.can_modify - json.title slot.title - json.start slot.start_at.iso8601 - json.end slot.end_at.iso8601 - json.is_reserved slot.is_reserved - json.is_completed slot.complete? - json.backgroundColor 'white' + json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role + json.is_completed slot.full? json.borderColor space_slot_border_color(slot) - json.availability_id slot.availability_id json.space do - json.id slot.space.id - json.name slot.space.name + json.id @space.id + json.name @space.name end - # the user who booked the slot, if the slot was reserved - if (%w[admin manager].include? @current_user_role) && slot.reservation - json.user do - json.id slot.reservation.user&.id - json.name slot.reservation.user&.profile&.full_name - end - end - json.tag_ids slot.availability.tag_ids - json.tags slot.availability.tags do |t| - json.id t.id - json.name t.name - end - json.plan_ids slot.availability.plan_ids end diff --git a/app/views/api/availabilities/trainings.json.jbuilder b/app/views/api/availabilities/trainings.json.jbuilder index 9c9974dc6..b35b7fa98 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -1,7 +1,7 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.partial! 'api/availabilities/slot', slot: slot + json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role json.borderColor trainings_events_border_color(slot) json.is_completed slot.full? diff --git a/app/views/api/reservations/_reservation.json.jbuilder b/app/views/api/reservations/_reservation.json.jbuilder index aeac49a04..7ba3e0e12 100644 --- a/app/views/api/reservations/_reservation.json.jbuilder +++ b/app/views/api/reservations/_reservation.json.jbuilder @@ -4,12 +4,15 @@ json.id reservation.id json.user_id reservation.statistic_profile.user_id json.user_full_name reservation.user&.profile&.full_name json.message reservation.message -json.slots_attributes reservation.slots do |s| - json.id s.id - json.start_at s.start_at.iso8601 - json.end_at s.end_at.iso8601 - json.canceled_at s.canceled_at&.iso8601 - json.is_reserved true +json.slots_reservations_attributes reservation.slots_reservations do |sr| + json.id sr.id + json.canceled_at sr.canceled_at&.iso8601 + json.slot_attributes do + json.id sr.slot_id + json.start_at sr.slot.start_at.iso8601 + json.end_at sr.slot.end_at.iso8601 + json.availability_id sr.slot.availability_id + end end json.nb_reserve_places reservation.nb_reserve_places json.tickets_attributes reservation.tickets do |t| diff --git a/app/views/api/slots/cancel.json.jbuilder b/app/views/api/slots/cancel.json.jbuilder deleted file mode 100644 index de61e70c2..000000000 --- a/app/views/api/slots/cancel.json.jbuilder +++ /dev/null @@ -1,2 +0,0 @@ -json.id @slot.id -json.canceled_at @slot.canceled_at diff --git a/app/views/api/slots/show.json.jbuilder b/app/views/api/slots/show.json.jbuilder deleted file mode 100644 index 5937bfc73..000000000 --- a/app/views/api/slots/show.json.jbuilder +++ /dev/null @@ -1 +0,0 @@ -json.id @slot.id diff --git a/app/views/api/slots_reservations/cancel.json.jbuilder b/app/views/api/slots_reservations/cancel.json.jbuilder new file mode 100644 index 000000000..636fdb469 --- /dev/null +++ b/app/views/api/slots_reservations/cancel.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.extract! @slot_reservation, :id, :canceled_at diff --git a/app/views/api/slots_reservations/show.json.jbuilder b/app/views/api/slots_reservations/show.json.jbuilder new file mode 100644 index 000000000..60bf7d44a --- /dev/null +++ b/app/views/api/slots_reservations/show.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.extract! @slot_reservation, :id, :slot_id, :reservation_id diff --git a/config/routes.rb b/config/routes.rb index cebc582d4..84c0d8c5d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -112,7 +112,7 @@ Rails.application.routes.draw do resources :plans do get 'durations', on: :collection end - resources :slots, only: [:update] do + resources :slots_reservations, only: [:update] do put 'cancel', on: :member end diff --git a/db/migrate/20220705125232_insert_missing_slots.rb b/db/migrate/20220705125232_insert_missing_slots.rb index 17f2a499b..ef752e4ee 100644 --- a/db/migrate/20220705125232_insert_missing_slots.rb +++ b/db/migrate/20220705125232_insert_missing_slots.rb @@ -4,7 +4,7 @@ # Now we save all slots in DB, so we must re-create slots for the existing availabilities class InsertMissingSlots < ActiveRecord::Migration[5.2] def up - Availability.all.each do |availability| + Availability.where(available_type: %w[machines space]).each do |availability| slot_duration = availability.slot_duration || Setting.get('slot_duration').to_i ((availability.end_at - availability.start_at) / slot_duration.minutes).to_i.times do |i| @@ -15,6 +15,14 @@ class InsertMissingSlots < ActiveRecord::Migration[5.2] ) end end + + Availability.where(available_type: %w[training event]).each do |availability| + Slot.find_or_create_by( + start_at: availability.start_at, + end_at: availability.end_at, + availability_id: availability.id + ) + end end def down diff --git a/doc/README.md b/doc/README.md index 38d64ea1b..2e49f744b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -50,6 +50,8 @@ The following guides should help those who want to contribute to the code. - [Plugins](plugins.md) +- [How the data model works for Availabilities and Reservations](availabilities-reservations-models.md) + #### How to setup a development environment - [With docker-compose](development_readme.md) diff --git a/doc/availabilities-reservations-models.md b/doc/availabilities-reservations-models.md new file mode 100644 index 000000000..600b13d38 --- /dev/null +++ b/doc/availabilities-reservations-models.md @@ -0,0 +1,89 @@ +# Availabilities-reservations data models + +## Machines + +ONE Availability may have: +- MANY MachinesAvailability (=> MANY Machines) +- MANY Slot: the Availability is cut in smaller slots + +ONE Slot may have: +- ONE Availability: a Slot is a slice of ONE Availability +- MANY SlotsReservation: ONE SlotsReservation per (User + Machine + Slot) + - Bob reserved a 3D printer from 8am to 9am in Availability 1 (=> ONE SlotsReservation) + - John reserved a Laser cutter from 8am to 9am in Availability 1 (=> ONE SlotsReservation) + +ONE SlotsReservation have: +- ONE Slot +- ONE Reservation + +ONE Reservation may have: +- MANY SlotsReservation (one per reserved slot, for the associated Machine) +- ONE User +- ONE Machine +- NO Ticket + +## Spaces + +ONE Availability may have: +- ONE SpacesAvailability (=> ONE Space) +- MANY Slot: the Availability is cut in smaller slots + +ONE Slot may have: +- ONE Availability: a Slot is a slice of ONE Availability +- MANY SlotsReservation: ONE SlotsReservation per (User + Slot) + - Bob reserved from 8am to 9am (=> ONE SlotsReservation) + - John reserved from 8am to 9am (=> ONE SlotsReservation) + +ONE SlotsReservation have: +- ONE Slot +- ONE Reservation + +ONE Reservation may have: +- MANY SlotsReservation (one per reserved slot, for the associated Space) +- ONE User +- ONE Space +- NO Ticket + +## Trainings + +ONE Availability may have: +- ONE TrainingsAvailability (=> ONE Training) +- ONE Slot: the Availability isn't cut into smaller slots + +ONE Slot may have: +- ONE Availability: a Slot as long as the Availability +- MANY SlotsReservation: ONE SlotsReservation per User + - Bob reserved (=> ONE SlotsReservation) + - John reserved (=> ONE SlotsReservation) + +ONE SlotsReservation have: +- ONE Slot +- ONE Reservation + +ONE Reservation have: +- ONE SlotsReservation +- ONE User +- ONE Training +- NO Tickets + +## Events + +ONE Availability may have: +- ONE Event (from Event.availability_id) +- ONE Slot: the Availability isn't cut into smaller slots + +ONE Slot may have: +- ONE Availability: a Slot as long as the Availability +- MANY SlotsReservation: ONE SlotsReservation per User + - Bob reserved (=> ONE SlotsReservation) + - John reserved (=> ONE SlotsReservation) + +ONE SlotsReservation have: +- ONE Slot +- ONE Reservation + +ONE Reservation may have: +- ONE SlotsReservation +- ONE User +- ONE Training +- MANY Tickets (once per extra booked special price) diff --git a/test/fixtures/slots.yml b/test/fixtures/slots.yml index 515bc616d..7116e7878 100644 --- a/test/fixtures/slots.yml +++ b/test/fixtures/slots.yml @@ -1,16 +1,1000 @@ slot_1: id: 1 - start_at: 2012-04-11 06:00:00.000000000 Z - end_at: 2012-04-11 10:00:00.000000000 Z - created_at: 2012-03-12 13:40:22.342717000 Z - updated_at: 2012-03-12 13:40:22.342717000 Z + start_at: '2012-04-11 06:00:00.000000' + end_at: '2012-04-11 10:00:00.000000' + created_at: '2012-03-12 13:40:22.342717' + updated_at: '2012-03-12 13:40:22.342717' availability_id: 12 slot_2: id: 2 - start_at: 2015-06-15 12:00:28.000000000 Z - end_at: 2015-06-15 13:00:28.000000000 Z - created_at: 2015-06-10 11:20:01.341130000 Z - updated_at: 2015-06-10 11:20:01.341130000 Z + start_at: '2015-06-15 12:00:28.000000' + end_at: '2015-06-15 13:00:28.000000' + created_at: '2015-06-10 11:20:01.341130' + updated_at: '2015-06-10 11:20:01.341130' availability_id: 13 + +slot_9: + id: 9 + start_at: <%= DateTime.current.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.880751' + updated_at: '2022-07-12 15:18:43.880751' + availability_id: 3 + +slot_10: + id: 10 + start_at: <%= DateTime.current.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.882957' + updated_at: '2022-07-12 15:18:43.882957' + availability_id: 3 + +slot_11: + id: 11 + start_at: <%= DateTime.current.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.884691' + updated_at: '2022-07-12 15:18:43.884691' + availability_id: 3 + +slot_12: + id: 12 + start_at: <%= DateTime.current.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.886431' + updated_at: '2022-07-12 15:18:43.886431' + availability_id: 3 + +slot_13: + id: 13 + start_at: <%= DateTime.current.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.888074' + updated_at: '2022-07-12 15:18:43.888074' + availability_id: 3 + +slot_14: + id: 14 + start_at: <%= DateTime.current.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.889691' + updated_at: '2022-07-12 15:18:43.889691' + availability_id: 3 + +slot_15: + id: 15 + start_at: <%= (DateTime.current + 1.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.893096' + updated_at: '2022-07-12 15:18:43.893096' + availability_id: 4 + +slot_16: + id: 16 + start_at: <%= (DateTime.current + 1.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.894777' + updated_at: '2022-07-12 15:18:43.894777' + availability_id: 4 + +slot_17: + id: 17 + start_at: <%= (DateTime.current + 1.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.896423' + updated_at: '2022-07-12 15:18:43.896423' + availability_id: 4 + +slot_18: + id: 18 + start_at: <%= (DateTime.current + 1.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.898021' + updated_at: '2022-07-12 15:18:43.898021' + availability_id: 4 + +slot_19: + id: 19 + start_at: <%= (DateTime.current + 1.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.899592' + updated_at: '2022-07-12 15:18:43.899592' + availability_id: 4 + +slot_20: + id: 20 + start_at: <%= (DateTime.current + 1.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.900938' + updated_at: '2022-07-12 15:18:43.900938' + availability_id: 4 + +slot_21: + id: 21 + start_at: <%= (DateTime.current + 2.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.904013' + updated_at: '2022-07-12 15:18:43.904013' + availability_id: 5 + +slot_22: + id: 22 + start_at: <%= (DateTime.current + 2.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.905470' + updated_at: '2022-07-12 15:18:43.905470' + availability_id: 5 + +slot_23: + id: 23 + start_at: <%= (DateTime.current + 2.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.907030' + updated_at: '2022-07-12 15:18:43.907030' + availability_id: 5 + +slot_24: + id: 24 + start_at: <%= (DateTime.current + 2.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.908585' + updated_at: '2022-07-12 15:18:43.908585' + availability_id: 5 + +slot_25: + id: 25 + start_at: <%= (DateTime.current + 2.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.910138' + updated_at: '2022-07-12 15:18:43.910138' + availability_id: 5 + +slot_26: + id: 26 + start_at: <%= (DateTime.current + 2.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.911643' + updated_at: '2022-07-12 15:18:43.911643' + availability_id: 5 + +slot_27: + id: 27 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.914664' + updated_at: '2022-07-12 15:18:43.914664' + availability_id: 6 + +slot_28: + id: 28 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.916047' + updated_at: '2022-07-12 15:18:43.916047' + availability_id: 6 + +slot_29: + id: 29 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.917304' + updated_at: '2022-07-12 15:18:43.917304' + availability_id: 6 + +slot_30: + id: 30 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.918798' + updated_at: '2022-07-12 15:18:43.918798' + availability_id: 6 + +slot_31: + id: 31 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.920194' + updated_at: '2022-07-12 15:18:43.920194' + availability_id: 6 + +slot_32: + id: 32 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.921662' + updated_at: '2022-07-12 15:18:43.921662' + availability_id: 6 + +slot_33: + id: 33 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.924285' + updated_at: '2022-07-12 15:18:43.924285' + availability_id: 7 + +slot_34: + id: 34 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.925669' + updated_at: '2022-07-12 15:18:43.925669' + availability_id: 7 + +slot_35: + id: 35 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.927038' + updated_at: '2022-07-12 15:18:43.927038' + availability_id: 7 + +slot_36: + id: 36 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.928407' + updated_at: '2022-07-12 15:18:43.928407' + availability_id: 7 + +slot_37: + id: 37 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.929907' + updated_at: '2022-07-12 15:18:43.929907' + availability_id: 7 + +slot_38: + id: 38 + start_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.931295' + updated_at: '2022-07-12 15:18:43.931295' + availability_id: 7 + +slot_39: + id: 39 + start_at: '2015-06-15 13:00:28.000000' + end_at: '2015-06-15 14:00:28.000000' + created_at: '2022-07-12 15:18:43.934465' + updated_at: '2022-07-12 15:18:43.934465' + availability_id: 13 + +slot_40: + id: 40 + start_at: '2015-06-15 14:00:28.000000' + end_at: '2015-06-15 15:00:28.000000' + created_at: '2022-07-12 15:18:43.935716' + updated_at: '2022-07-12 15:18:43.935716' + availability_id: 13 + +slot_41: + id: 41 + start_at: '2015-06-15 15:00:28.000000' + end_at: '2015-06-15 16:00:28.000000' + created_at: '2022-07-12 15:18:43.937025' + updated_at: '2022-07-12 15:18:43.937025' + availability_id: 13 + +slot_42: + id: 42 + start_at: '2015-06-15 16:00:28.000000' + end_at: '2015-06-15 17:00:28.000000' + created_at: '2022-07-12 15:18:43.938379' + updated_at: '2022-07-12 15:18:43.938379' + availability_id: 13 + +slot_43: + id: 43 + start_at: '2015-06-15 17:00:28.000000' + end_at: '2015-06-15 18:00:28.000000' + created_at: '2022-07-12 15:18:43.939737' + updated_at: '2022-07-12 15:18:43.939737' + availability_id: 13 + +slot_44: + id: 44 + start_at: <%= 20.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.942392' + updated_at: '2022-07-12 15:18:43.942392' + availability_id: 14 + +slot_45: + id: 45 + start_at: <%= 20.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.943779' + updated_at: '2022-07-12 15:18:43.943779' + availability_id: 14 + +slot_46: + id: 46 + start_at: <%= 20.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.945154' + updated_at: '2022-07-12 15:18:43.945154' + availability_id: 14 + +slot_47: + id: 47 + start_at: <%= 20.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.946515' + updated_at: '2022-07-12 15:18:43.946515' + availability_id: 14 + +slot_48: + id: 48 + start_at: <%= 40.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.949178' + updated_at: '2022-07-12 15:18:43.949178' + availability_id: 15 + +slot_49: + id: 49 + start_at: <%= 40.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.950348' + updated_at: '2022-07-12 15:18:43.950348' + availability_id: 15 + +slot_50: + id: 50 + start_at: <%= 40.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.951535' + updated_at: '2022-07-12 15:18:43.951535' + availability_id: 15 + +slot_51: + id: 51 + start_at: <%= 40.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.952864' + updated_at: '2022-07-12 15:18:43.952864' + availability_id: 15 + +slot_52: + id: 52 + start_at: <%= 80.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.955443' + updated_at: '2022-07-12 15:18:43.955443' + availability_id: 16 + +slot_53: + id: 53 + start_at: <%= 80.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.956657' + updated_at: '2022-07-12 15:18:43.956657' + availability_id: 16 + +slot_54: + id: 54 + start_at: <%= 80.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.957811' + updated_at: '2022-07-12 15:18:43.957811' + availability_id: 16 + +slot_55: + id: 55 + start_at: <%= 80.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.959063' + updated_at: '2022-07-12 15:18:43.959063' + availability_id: 16 + +slot_56: + id: 56 + start_at: <%= 10.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.961319' + updated_at: '2022-07-12 15:18:43.961319' + availability_id: 17 + +slot_57: + id: 57 + start_at: <%= 10.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.962492' + updated_at: '2022-07-12 15:18:43.962492' + availability_id: 17 + +slot_58: + id: 58 + start_at: <%= 10.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.963665' + updated_at: '2022-07-12 15:18:43.963665' + availability_id: 17 + +slot_59: + id: 59 + start_at: <%= 10.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.964853' + updated_at: '2022-07-12 15:18:43.964853' + availability_id: 17 + +slot_60: + id: 60 + start_at: <%= 10.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.966003' + updated_at: '2022-07-12 15:18:43.966003' + availability_id: 17 + +slot_61: + id: 61 + start_at: <%= 10.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.967147' + updated_at: '2022-07-12 15:18:43.967147' + availability_id: 17 + +slot_62: + id: 62 + start_at:<%= 10.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.968371' + updated_at: '2022-07-12 15:18:43.968371' + availability_id: 17 + +slot_63: + id: 63 + start_at: <%= 10.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.969636' + updated_at: '2022-07-12 15:18:43.969636' + availability_id: 17 + +slot_64: + id: 64 + start_at: <%= 10.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.970899' + updated_at: '2022-07-12 15:18:43.970899' + availability_id: 17 + +slot_65: + id: 65 + start_at: <%= 10.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.972292' + updated_at: '2022-07-12 15:18:43.972292' + availability_id: 17 + +slot_66: + id: 66 + start_at: <%= 10.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.973726' + updated_at: '2022-07-12 15:18:43.973726' + availability_id: 17 + +slot_67: + id: 67 + start_at: <%= 10.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.975239' + updated_at: '2022-07-12 15:18:43.975239' + availability_id: 17 + +slot_68: + id: 68 + start_at: <%= 10.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.976671' + updated_at: '2022-07-12 15:18:43.976671' + availability_id: 17 + +slot_69: + id: 69 + start_at: <%= 10.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.978191' + updated_at: '2022-07-12 15:18:43.978191' + availability_id: 17 + +slot_70: + id: 70 + start_at: <%= 11.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.979560' + updated_at: '2022-07-12 15:18:43.979560' + availability_id: 17 + +slot_71: + id: 71 + start_at: <%= 11.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.980799' + updated_at: '2022-07-12 15:18:43.980799' + availability_id: 17 + +slot_72: + id: 72 + start_at: <%= 11.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.982141' + updated_at: '2022-07-12 15:18:43.982141' + availability_id: 17 + +slot_73: + id: 73 + start_at: <%= 11.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.983501' + updated_at: '2022-07-12 15:18:43.983501' + availability_id: 17 + +slot_74: + id: 74 + start_at: <%= 11.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.984866' + updated_at: '2022-07-12 15:18:43.984866' + availability_id: 17 + +slot_75: + id: 75 + start_at: <%= 11.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.986257' + updated_at: '2022-07-12 15:18:43.986257' + availability_id: 17 + +slot_76: + id: 76 + start_at: <%= 11.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.987589' + updated_at: '2022-07-12 15:18:43.987589' + availability_id: 17 + +slot_77: + id: 77 + start_at: <%= 11.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.988913' + updated_at: '2022-07-12 15:18:43.988913' + availability_id: 17 + +slot_78: + id: 78 + start_at: <%= 11.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.990191' + updated_at: '2022-07-12 15:18:43.990191' + availability_id: 17 + +slot_79: + id: 79 + start_at: <%= 11.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.991488' + updated_at: '2022-07-12 15:18:43.991488' + availability_id: 17 + +slot_80: + id: 80 + start_at: <%= 11.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.992988' + updated_at: '2022-07-12 15:18:43.992988' + availability_id: 17 + +slot_81: + id: 81 + start_at: <%= 11.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.994311' + updated_at: '2022-07-12 15:18:43.994311' + availability_id: 17 + +slot_82: + id: 82 + start_at: <%= 11.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.995812' + updated_at: '2022-07-12 15:18:43.995812' + availability_id: 17 + +slot_83: + id: 83 + start_at: <%= 11.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.997242' + updated_at: '2022-07-12 15:18:43.997242' + availability_id: 17 + +slot_84: + id: 84 + start_at: <%= 11.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:43.998733' + updated_at: '2022-07-12 15:18:43.998733' + availability_id: 17 + +slot_85: + id: 85 + start_at: <%= 11.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.000009' + updated_at: '2022-07-12 15:18:44.000009' + availability_id: 17 + +slot_86: + id: 86 + start_at: <%= 11.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.001315' + updated_at: '2022-07-12 15:18:44.001315' + availability_id: 17 + +slot_87: + id: 87 + start_at: <%= 11.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.002961' + updated_at: '2022-07-12 15:18:44.002961' + availability_id: 17 + +slot_88: + id: 88 + start_at: <%= 11.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.004321' + updated_at: '2022-07-12 15:18:44.004321' + availability_id: 17 + +slot_89: + id: 89 + start_at: <%= 11.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.005828' + updated_at: '2022-07-12 15:18:44.005828' + availability_id: 17 + +slot_90: + id: 90 + start_at: <%= 11.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.007295' + updated_at: '2022-07-12 15:18:44.007295' + availability_id: 17 + +slot_91: + id: 91 + start_at: <%= 11.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.008631' + updated_at: '2022-07-12 15:18:44.008631' + availability_id: 17 + +slot_92: + id: 92 + start_at: <%= 11.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 11.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.010249' + updated_at: '2022-07-12 15:18:44.010249' + availability_id: 17 + +slot_93: + id: 93 + start_at: <%= 11.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.011771' + updated_at: '2022-07-12 15:18:44.011771' + availability_id: 17 + +slot_94: + id: 94 + start_at: <%= 12.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.013133' + updated_at: '2022-07-12 15:18:44.013133' + availability_id: 17 + +slot_95: + id: 95 + start_at: <%= 12.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.014419' + updated_at: '2022-07-12 15:18:44.014419' + availability_id: 17 + +slot_96: + id: 96 + start_at: <%= 12.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.015693' + updated_at: '2022-07-12 15:18:44.015693' + availability_id: 17 + +slot_97: + id: 97 + start_at: <%= 12.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.016849' + updated_at: '2022-07-12 15:18:44.016849' + availability_id: 17 + +slot_98: + id: 98 + start_at: <%= 12.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.018072' + updated_at: '2022-07-12 15:18:44.018072' + availability_id: 17 + +slot_99: + id: 99 + start_at: <%= 12.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.019364' + updated_at: '2022-07-12 15:18:44.019364' + availability_id: 17 + +slot_100: + id: 100 + start_at: <%= 12.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.020527' + updated_at: '2022-07-12 15:18:44.020527' + availability_id: 17 + +slot_101: + id: 101 + start_at: <%= 12.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>' + created_at: '2022-07-12 15:18:44.021871' + updated_at: '2022-07-12 15:18:44.021871' + availability_id: 17 + +slot_102: + id: 102 + start_at: <%= 12.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.023185' + updated_at: '2022-07-12 15:18:44.023185' + availability_id: 17 + +slot_103: + id: 103 + start_at: <%= 12.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.024405' + updated_at: '2022-07-12 15:18:44.024405' + availability_id: 17 + +slot_104: + id: 104 + start_at: <%= 12.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.025867' + updated_at: '2022-07-12 15:18:44.025867' + availability_id: 17 + +slot_105: + id: 105 + start_at: <%= 12.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.027282' + updated_at: '2022-07-12 15:18:44.027282' + availability_id: 17 + +slot_106: + id: 106 + start_at: <%= 12.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.028605' + updated_at: '2022-07-12 15:18:44.028605' + availability_id: 17 + +slot_107: + id: 107 + start_at: <%= 12.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.029949' + updated_at: '2022-07-12 15:18:44.029949' + availability_id: 17 + +slot_108: + id: 108 + start_at: <%= 12.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.031298' + updated_at: '2022-07-12 15:18:44.031298' + availability_id: 17 + +slot_109: + id: 109 + start_at: <%= 12.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.032602' + updated_at: '2022-07-12 15:18:44.032602' + availability_id: 17 + +slot_110: + id: 110 + start_at: <%= 12.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.034111' + updated_at: '2022-07-12 15:18:44.034111' + availability_id: 17 + +slot_111: + id: 111 + start_at: <%= 12.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.035567' + updated_at: '2022-07-12 15:18:44.035567' + availability_id: 17 + +slot_112: + id: 112 + start_at: <%= 2.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.038089' + updated_at: '2022-07-12 15:18:44.038089' + availability_id: 18 + +slot_113: + id: 113 + start_at: <%= 2.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.039392' + updated_at: '2022-07-12 15:18:44.039392' + availability_id: 18 + +slot_114: + id: 114 + start_at: <%= 2.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.040522' + updated_at: '2022-07-12 15:18:44.040522' + availability_id: 18 + +slot_115: + id: 115 + start_at: <%= 2.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.041937' + updated_at: '2022-07-12 15:18:44.041937' + availability_id: 18 + +slot_116: + id: 116 + start_at: <%= 1.day.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.044421' + updated_at: '2022-07-12 15:18:44.044421' + availability_id: 19 + +slot_117: + id: 117 + start_at: <%= 1.day.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.045689' + updated_at: '2022-07-12 15:18:44.045689' + availability_id: 19 + +slot_118: + id: 118 + start_at: <%= 1.day.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.047009' + updated_at: '2022-07-12 15:18:44.047009' + availability_id: 19 + +slot_119: + id: 119 + start_at: <%= 1.day.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.048272' + updated_at: '2022-07-12 15:18:44.048272' + availability_id: 19 + +slot_120: + id: 120 + start_at: <%= 1.day.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.049599' + updated_at: '2022-07-12 15:18:44.049599' + availability_id: 19 + +slot_121: + id: 121 + start_at: <%= 1.day.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.050947' + updated_at: '2022-07-12 15:18:44.050947' + availability_id: 19 + +slot_122: + id: 122 + start_at: <%= 1.day.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.052817' + updated_at: '2022-07-12 15:18:44.052817' + availability_id: 19 + +slot_123: + id: 123 + start_at: <%= 1.day.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.054966' + updated_at: '2022-07-12 15:18:44.054966' + availability_id: 19 + +slot_124: + id: 124 + start_at: <%= 1.day.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.057217' + updated_at: '2022-07-12 15:18:44.057217' + availability_id: 19 + +slot_125: + id: 125 + start_at: <%= 1.day.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.059135' + updated_at: '2022-07-12 15:18:44.059135' + availability_id: 19 + +slot_126: + id: 126 + start_at: <%= DateTime.current.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.061887' + updated_at: '2022-07-12 15:18:44.061887' + availability_id: 1 + +slot_127: + id: 127 + start_at: <%= (DateTime.current + 1.day).utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.063528' + updated_at: '2022-07-12 15:18:44.063528' + availability_id: 2 + +slot_128: + id: 128 + start_at: <%= (DateTime.current + 2.day).utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-12 15:18:44.065114' + updated_at: '2022-07-12 15:18:44.065114' + availability_id: 8 + +slot_129: + id: 129 + start_at: '2016-04-18 16:00:28.000000' + end_at: '2016-04-18 20:00:28.000000' + created_at: '2022-07-12 15:18:44.066837' + updated_at: '2022-07-12 15:18:44.066837' + availability_id: 9 + +slot_130: + id: 130 + start_at: '2016-05-18 16:00:28.000000' + end_at: '2016-05-18 20:00:28.000000' + created_at: '2022-07-12 15:18:44.068259' + updated_at: '2022-07-12 15:18:44.068259' + availability_id: 10 + +slot_131: + id: 131 + start_at: '2016-06-18 16:00:28.000000' + end_at: '2016-06-18 20:00:28.000000' + created_at: '2022-07-12 15:18:44.069870' + updated_at: '2022-07-12 15:18:44.069870' + availability_id: 11 From 66d1348b06921c3723df35f2c8a38086ba083557 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 10:46:46 +0200 Subject: [PATCH 034/141] fix slots fixtures --- test/fixtures/slots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/slots.yml b/test/fixtures/slots.yml index 7116e7878..b5042c3e4 100644 --- a/test/fixtures/slots.yml +++ b/test/fixtures/slots.yml @@ -441,7 +441,7 @@ slot_61: slot_62: id: 62 - start_at:<%= 10.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 10.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> end_at: <%= 10.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.968371' updated_at: '2022-07-12 15:18:43.968371' From b68e47a0ea8cbd3324763f20bf9accdc04948421 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 13 Jul 2022 16:28:43 +0200 Subject: [PATCH 035/141] refactor shopping_cart/reservation Previsouly, the reservation was expecting parameters like: slots_attributes: [{start_at: date, end_at: date, availability_id: number}] Now, the reservation is expecting simpler parameters like: slots_reservations_attributes:[{slot_id: number}] --- app/controllers/api/payzen_controller.rb | 5 +- .../api/reservations_controller.rb | 2 +- .../src/javascript/controllers/events.js.erb | 40 ++++----- .../src/javascript/directives/cart.js | 16 ++-- .../src/javascript/models/reservation.ts | 9 +- app/models/cart_item/event_reservation.rb | 2 +- app/models/cart_item/machine_reservation.rb | 2 +- app/models/cart_item/reservation.rb | 38 +++++---- app/models/cart_item/space_reservation.rb | 2 +- app/models/cart_item/training_reservation.rb | 2 +- app/models/reservation.rb | 4 +- app/models/shopping_cart.rb | 8 +- app/models/slot.rb | 9 +- .../create_availabilities_service.rb | 49 ++++++----- app/services/cart_service.rb | 20 +++-- app/services/invoices_service.rb | 4 +- ...ubscription_extension_after_reservation.rb | 2 +- app/services/users_credits/manager.rb | 50 +++++------ .../subscription_group_validator.rb | 3 + lib/tasks/fablab/fix_invoices.rake | 12 +-- lib/tasks/fablab/payzen.rake | 58 +++++++------ .../4/FabManager_invoice-10_13072022.pdf | Bin 0 -> 44730 bytes .../5/FabManager_invoice-9_13072022.pdf | Bin 0 -> 44810 bytes .../availabilities/as_admin_test.rb | 11 ++- .../availabilities/as_user_test.rb | 7 +- test/integration/events/as_admin_test.rb | 14 ++-- test/integration/events/as_user_test.rb | 8 +- test/integration/payzen_test.rb | 20 ++--- test/integration/prices/compute_test.rb | 25 +++--- .../reservations/create_as_admin_test.rb | 54 ++++-------- test/integration/reservations/create_test.rb | 78 ++++++------------ .../reservations/restricted_test.rb | 46 ++++++----- test/services/statistic_service_test.rb | 8 +- ...iption_extension_after_reservation_test.rb | 14 +++- test/services/users_credits_manager_test.rb | 56 ++++++------- 35 files changed, 325 insertions(+), 353 deletions(-) create mode 100644 test/fixtures/files/invoices/4/FabManager_invoice-10_13072022.pdf create mode 100644 test/fixtures/files/invoices/5/FabManager_invoice-9_13072022.pdf diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index 15b8a9251..acc47ea14 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -55,9 +55,8 @@ class API::PayzenController < API::PaymentsController def check_cart cart = shopping_cart - unless cart.valid? - render json: { error: 'unable to pay' }, status: :unprocessable_entity and return - end + render json: { error: 'unable to pay' }, status: :unprocessable_entity and return unless cart.valid? + render json: { cart: 'ok' }, status: :ok end diff --git a/app/controllers/api/reservations_controller.rb b/app/controllers/api/reservations_controller.rb index d0ef06b8c..42cab8487 100644 --- a/app/controllers/api/reservations_controller.rb +++ b/app/controllers/api/reservations_controller.rb @@ -43,6 +43,6 @@ class API::ReservationsController < API::ApiController def reservation_params params.require(:reservation).permit(:message, :reservable_id, :reservable_type, :nb_reserve_places, tickets_attributes: %i[event_price_category_id booked], - slots_attributes: %i[id start_at end_at availability_id offered]) + slots_reservations_attributes: %i[id slot_id offered]) end end diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 07022eae9..afabe1660 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -360,7 +360,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' reservation: { reservable_id: $scope.event.id, reservable_type: 'Event', - slots_attributes: [], + slots_reservations_attributes: [], nb_reserve_places: $scope.reserve.nbReservePlaces, tickets_attributes: [] } @@ -368,10 +368,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' ] } // a single slot is used for events - cartItems.items[0].reservation.slots_attributes.push({ - start_at: $scope.event.start_date, - end_at: $scope.event.end_date, - availability_id: $scope.event.availability.id + cartItems.items[0].reservation.slots_reservations_attributes.push({ + slot_id: $scope.event.slot_id }); // iterate over reservations per prices for (let price_id in $scope.reserve.tickets) { @@ -405,7 +403,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' /** * Callback to cancel a reservation - * @param reservation {{id:number, reservable_id:number, nb_reserve_places:number, slots_attributes:[{id: number, canceled_at: string}], total_booked_seats: number}} + * @param reservation {Reservation} */ $scope.cancelReservation = function(reservation) { dialogs.confirm({ @@ -434,17 +432,17 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' /** * Test if the provided reservation has been cancelled - * @param reservation {{slots_attributes: [{canceled_at: string}]}} + * @param reservation {Reservation} * @returns {boolean} */ $scope.isCancelled = function(reservation) { - return !!(reservation.slots_attributes[0].canceled_at); + return !!(reservation.slots_reservations_attributes[0].canceled_at); } /** * Callback to alter an already booked reservation date. A modal window will be opened to allow the user to choose * a new date for his reservation (if any available) - * @param reservation {{id:number, reservable_id:number, nb_reserve_places:number}} + * @param reservation {Reservation} */ $scope.modifyReservation = function (reservation) { const index = $scope.reservations.indexOf(reservation); @@ -475,9 +473,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' return eventToPlace = e; } }); - $scope.reservation.slots_reservations_attributes[0].start_at = eventToPlace.start_date; - $scope.reservation.slots_reservations_attributes[0].end_at = eventToPlace.end_date; - $scope.reservation.slots_reservations_attributes[0].availability_id = eventToPlace.availability_id; + $scope.reservation.slots_reservations_attributes[0].slot_id = eventToPlace.slot_id; $scope.attempting = true; Reservation.update({ id: reservation.id }, { reservation: $scope.reservation }, function (reservation) { $uibModalInstance.close(reservation); @@ -514,10 +510,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' /** * Checks if the provided reservation is able to be moved (date change) - * @param reservation {{slots_attributes:[], total_booked_seats:number}} + * @param reservation {Reservation} */ $scope.reservationCanModify = function (reservation) { - const slotStart = moment(reservation.slots_attributes[0].start_at); + const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at); const now = moment(); let isAble = false; @@ -529,10 +525,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' /** * Checks if the provided reservation is able to be cancelled - * @param reservation {{slots_attributes:[]}} + * @param reservation {Reservation} */ $scope.reservationCanCancel = function(reservation) { - const slotStart = moment(reservation.slots_attributes[0].start_at); + const slotStart = moment(reservation.slots_reservations_attributes[0].slot_attributes.start_at); const now = moment(); return $scope.enableBookingCancel && slotStart.diff(now, "hours") >= $scope.cancelBookingDelay; }; @@ -653,22 +649,20 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * Create a hash map implementing the Reservation specs * @param reserve {Object} Reservation parameters (places...) * @param event {Object} Current event - * @return {{reservation: {reservable_id:number, reservable_type:string, slots_attributes:Array, nb_reserve_places:number}}} + * @return {{reservation: Reservation}} */ const mkReservation = function (reserve, event) { const reservation = { reservable_id: event.id, reservable_type: 'Event', - slots_attributes: [], + slots_reservations_attributes: [], nb_reserve_places: reserve.nbReservePlaces, tickets_attributes: [] }; - reservation.slots_attributes.push({ - start_at: event.start_date, - end_at: event.end_date, - availability_id: event.availability.id, - offered: event.offered || false + reservation.slots_reservations_attributes.push({ + offered: event.offered || false, + slot_id: event.slot_id }); for (let evt_px_cat of Array.from(event.prices)) { diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index acfb2145a..d91637b59 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -705,20 +705,18 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', /** * Create a hash map implementing the Reservation specs * @param slots {Array} Array of fullCalendar events: slots selected on the calendar - * @return {{reservation: {reservable_type: string, reservable_id: string, slots_attributes: []}}} + * @return {{reservation: Reservation}} */ const mkReservation = function (slots) { const reservation = { reservable_id: $scope.reservableId, reservable_type: $scope.reservableType, - slots_attributes: [] + slots_reservations_attributes: [] }; angular.forEach(slots, function (slot) { - reservation.slots_attributes.push({ - start_at: slot.start, - end_at: slot.end, - availability_id: slot.availability_id, - offered: slot.offered || false + reservation.slots_reservations_attributes.push({ + offered: slot.offered || false, + slot_id: slot.slot_id }); }); @@ -728,7 +726,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', /** * Create a hash map implementing the Subscription specs * @param planId {number} - * @return {{subscription: {plan_id: number}}} + * @return {{subscription: SubscriptionRequest}} */ const mkSubscription = function (planId) { return { @@ -740,7 +738,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', /** * Build the ShoppingCart object, from the current reservation - * @param items {Array<{reservation:{reservable_type: string, reservable_id: string, slots_attributes: []}}|{subscription: {plan_id: number}}>} + * @param items {Array} * @param paymentMethod {string} * @return {ShoppingCart} */ diff --git a/app/frontend/src/javascript/models/reservation.ts b/app/frontend/src/javascript/models/reservation.ts index 4e097be88..b3fbe012b 100644 --- a/app/frontend/src/javascript/models/reservation.ts +++ b/app/frontend/src/javascript/models/reservation.ts @@ -7,11 +7,12 @@ export interface SlotsReservation { id?: number, canceled_at?: TDateISO, offered?: boolean, + slot_id?: number, slot_attributes?: { - id?: number, - start_at?: TDateISO, - end_at?: TDateISO, - availability_id?: number + id: number, + start_at: TDateISO, + end_at: TDateISO, + availability_id: number } } // TODO, refactor Reservation for cart_items (in payment) => should use slot_id instead of (start_at + end_at) diff --git a/app/models/cart_item/event_reservation.rb b/app/models/cart_item/event_reservation.rb index 65b8cb964..4e6796243 100644 --- a/app/models/cart_item/event_reservation.rb +++ b/app/models/cart_item/event_reservation.rb @@ -38,7 +38,7 @@ class CartItem::EventReservation < CartItem::Reservation ::Reservation.new( reservable_id: @reservable.id, reservable_type: Event.name, - slots_attributes: slots_params, + slots_reservations_attributes: slots_params, tickets_attributes: tickets_params, nb_reserve_places: @normal_tickets, statistic_profile_id: StatisticProfile.find_by(user: @customer).id diff --git a/app/models/cart_item/machine_reservation.rb b/app/models/cart_item/machine_reservation.rb index 2c2140108..2d0c44e12 100644 --- a/app/models/cart_item/machine_reservation.rb +++ b/app/models/cart_item/machine_reservation.rb @@ -16,7 +16,7 @@ class CartItem::MachineReservation < CartItem::Reservation ::Reservation.new( reservable_id: @reservable.id, reservable_type: Machine.name, - slots_attributes: slots_params, + slots_reservations_attributes: slots_params, statistic_profile_id: StatisticProfile.find_by(user: @customer).id ) end diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index 61b623109..30103f330 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -11,7 +11,7 @@ class CartItem::Reservation < CartItem::BaseItem @customer = customer @operator = operator @reservable = reservable - @slots = slots + @slots = slots.map { |s| expand_slot(s) } super end @@ -43,20 +43,24 @@ class CartItem::Reservation < CartItem::BaseItem def valid?(all_items) pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) } @slots.each do |slot| - availability = Availability.find_by(id: slot[:availability_id]) + if Slot.find(slot[:slot_id]).nil? + @errors[:slot] = 'slot does not exist' + return false + end + + availability = Availability.find_by(id: slot[:slot_attributes][:availability_id]) if availability.nil? @errors[:slot] = 'slot availability does not exist' return false end if availability.available_type == 'machines' - s = SlotsReservation.includes(:slot, :reservation) - .where('slots.start_at': slot[:start_at], - 'slots.end_at': slot[:end_at], - 'slots.availability_id': slot[:availability_id], - canceled_at: nil, - 'reservations.reservable': @reservable) - unless s.empty? + same_hour_slots = SlotsReservation.joins(:reservation).where( + reservations: { reservable: @reservable }, + slot_id: slot[:slot_id], + canceled_at: nil + ).count + if same_hour_slots.positive? @errors[:slot] = 'slot is reserved' return false end @@ -93,7 +97,11 @@ class CartItem::Reservation < CartItem::BaseItem def grouped_slots return { all: @slots } unless Setting.get('extended_prices_in_same_day') - @slots.group_by { |slot| slot[:start_at].to_date } + @slots.group_by { |slot| slot[:slot_attributes][:start_at].to_date } + end + + def expand_slot(slot) + slot.merge({ slot_attributes: Slot.find(slot[:slot_id]) }) end ## @@ -104,7 +112,7 @@ class CartItem::Reservation < CartItem::BaseItem def get_slot_price_from_prices(prices, slot, is_privileged, options = {}) options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) - slot_minutes = (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE + slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE price = prices[:prices].find { |p| p[:duration] <= slot_minutes && p[:duration].positive? } price = prices[:prices].first if price.nil? hourly_rate = (price[:price].amount.to_f / price[:price].duration) * MINUTES_PER_HOUR @@ -134,7 +142,7 @@ class CartItem::Reservation < CartItem::BaseItem options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options) slot_rate = options[:has_credits] || (slot[:offered] && is_privileged) ? 0 : hourly_rate - slot_minutes = (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE + slot_minutes = (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE # apply the base price to the real slot duration real_price = if options[:is_division] (slot_rate / MINUTES_PER_HOUR) * slot_minutes @@ -151,7 +159,7 @@ class CartItem::Reservation < CartItem::BaseItem unless options[:elements].nil? options[:elements][:slots].push( - start_at: slot[:start_at], + start_at: slot[:slot_attributes][:start_at], price: real_price, promo: (slot_rate != hourly_rate) ) @@ -165,7 +173,7 @@ class CartItem::Reservation < CartItem::BaseItem # and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12). # All these prices are returned to be applied to the reservation. def applicable_prices(slots) - total_duration = slots.map { |slot| (slot[:end_at].to_time - slot[:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+) + total_duration = slots.map { |slot| (slot[:slot_attributes][:end_at].to_time - slot[:slot_attributes][:start_at].to_time) / SECONDS_PER_MINUTE }.reduce(:+) rates = { prices: [] } remaining_duration = total_duration @@ -201,6 +209,6 @@ class CartItem::Reservation < CartItem::BaseItem end def slots_params - @slots.map { |slot| slot.permit(:id, :start_at, :end_at, :availability_id, :offered) } + @slots.map { |slot| slot.permit(:id, :slot_id, :offered) } end end diff --git a/app/models/cart_item/space_reservation.rb b/app/models/cart_item/space_reservation.rb index 0dd341952..d9e8db730 100644 --- a/app/models/cart_item/space_reservation.rb +++ b/app/models/cart_item/space_reservation.rb @@ -16,7 +16,7 @@ class CartItem::SpaceReservation < CartItem::Reservation ::Reservation.new( reservable_id: @reservable.id, reservable_type: Space.name, - slots_attributes: slots_params, + slots_reservations_attributes: slots_params, statistic_profile_id: StatisticProfile.find_by(user: @customer).id ) end diff --git a/app/models/cart_item/training_reservation.rb b/app/models/cart_item/training_reservation.rb index 9106f9099..bfdc3b528 100644 --- a/app/models/cart_item/training_reservation.rb +++ b/app/models/cart_item/training_reservation.rb @@ -36,7 +36,7 @@ class CartItem::TrainingReservation < CartItem::Reservation ::Reservation.new( reservable_id: @reservable.id, reservable_type: Training.name, - slots_attributes: slots_params, + slots_reservations_attributes: slots_params, statistic_profile_id: StatisticProfile.find_by(user: @customer).id ) end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index d0a3ddd47..04aeba681 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -12,7 +12,7 @@ class Reservation < ApplicationRecord has_many :slots_reservations, dependent: :destroy has_many :slots, through: :slots_reservations - accepts_nested_attributes_for :slots, allow_destroy: true + accepts_nested_attributes_for :slots_reservations, allow_destroy: true belongs_to :reservable, polymorphic: true has_many :tickets @@ -101,7 +101,7 @@ class Reservation < ApplicationRecord slots_reservations.each do |slot| same_hour_slots = SlotsReservation.joins(:reservation).where( reservations: { reservable_type: reservable_type, reservable_id: reservable_id }, - slot_id: slot_id, + slot_id: slot.slot_id, canceled_at: nil ).count if same_hour_slots.positive? diff --git a/app/models/shopping_cart.rb b/app/models/shopping_cart.rb index 03ee86f73..10e961651 100644 --- a/app/models/shopping_cart.rb +++ b/app/models/shopping_cart.rb @@ -55,13 +55,9 @@ class ShoppingCart list = user_validation_required_list.split(',') errors = [] items.each do |item| - if list.include?(item.type) && !@customer.validated_at? - errors.push("User validation is required for reserve #{item.type}") - end - end - unless errors.empty? - return { success: nil, payment: nil, errors: errors } + errors.push("User validation is required to reserve #{item.type}") if list.include?(item.type) && !@customer.validated_at? end + return { success: nil, payment: nil, errors: errors } unless errors.empty? end end diff --git a/app/models/slot.rb b/app/models/slot.rb index 24347746c..38eb8d2b6 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -13,6 +13,13 @@ class Slot < ApplicationRecord attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids def full? - slots_reservations.where(canceled_at: nil).count >= availability.available_places_per_slot + availability_places = availability.available_places_per_slot + return false if availability_places.nil? + + slots_reservations.where(canceled_at: nil).count >= availability_places + end + + def duration + (end_at - start_at).seconds end end diff --git a/app/services/availabilities/create_availabilities_service.rb b/app/services/availabilities/create_availabilities_service.rb index 3cff26379..5535d05e0 100644 --- a/app/services/availabilities/create_availabilities_service.rb +++ b/app/services/availabilities/create_availabilities_service.rb @@ -7,31 +7,36 @@ class Availabilities::CreateAvailabilitiesService slot_duration = availability.slot_duration || Setting.get('slot_duration').to_i occurrences.each do |o| - next if availability.start_at == o[:start_at] && availability.end_at == o[:end_at] + start_at = Time.zone.parse(o[:start_at]) + end_at = Time.zone.parse(o[:end_at]) - Availability.new( - start_at: o[:start_at], - end_at: o[:end_at], - available_type: availability.available_type, - is_recurrent: availability.is_recurrent, - period: availability.period, - nb_periods: availability.nb_periods, - end_date: availability.end_date, - occurrence_id: availability.occurrence_id, - machine_ids: availability.machine_ids, - training_ids: availability.training_ids, - space_ids: availability.space_ids, - tag_ids: availability.tag_ids, - nb_total_places: availability.nb_total_places, - slot_duration: availability.slot_duration, - plan_ids: availability.plan_ids - ).save! + avail = if availability.start_at == start_at && availability.end_at == end_at + availability + else + Availability.create!( + start_at: start_at, + end_at: end_at, + available_type: availability.available_type, + is_recurrent: availability.is_recurrent, + period: availability.period, + nb_periods: availability.nb_periods, + end_date: availability.end_date, + occurrence_id: availability.occurrence_id, + machine_ids: availability.machine_ids, + training_ids: availability.training_ids, + space_ids: availability.space_ids, + tag_ids: availability.tag_ids, + nb_total_places: availability.nb_total_places, + slot_duration: availability.slot_duration, + plan_ids: availability.plan_ids + ) + end - (o[:end_at] - o[:start_at] / slot_duration.minutes).to_i.times do |i| + ((end_at - start_at) / slot_duration.minutes).to_i.times do |i| Slot.new( - start_at: o[:start_at] + (i * slot_duration).minutes, - end_at: o[:start_at] + (i * slot_duration).minutes + slot_duration.minutes, - availability_id: o.id + start_at: start_at + (i * slot_duration).minutes, + end_at: start_at + (i * slot_duration).minutes + slot_duration.minutes, + availability_id: avail.id ).save! end end diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index 1c4bb29a1..baa0e5b1b 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -28,7 +28,9 @@ class CartService end coupon = CartItem::Coupon.new(@customer, @operator, cart_items[:coupon_code]) - schedule = CartItem::PaymentSchedule.new(plan_info[:plan], coupon, cart_items[:payment_schedule], @customer, plan_info[:subscription]&.start_at) + schedule = CartItem::PaymentSchedule.new( + plan_info[:plan], coupon, cart_items[:payment_schedule], @customer, plan_info[:subscription]&.start_at + ) ShoppingCart.new( @customer, @@ -108,28 +110,28 @@ class CartService CartItem::MachineReservation.new(@customer, @operator, reservable, - cart_item[:slots_attributes], + cart_item[:slots_reservations_attributes], plan: plan_info[:plan], new_subscription: plan_info[:new_subscription]) when Training CartItem::TrainingReservation.new(@customer, @operator, reservable, - cart_item[:slots_attributes], + cart_item[:slots_reservations_attributes], plan: plan_info[:plan], new_subscription: plan_info[:new_subscription]) when Event CartItem::EventReservation.new(@customer, @operator, reservable, - cart_item[:slots_attributes], + cart_item[:slots_reservations_attributes], normal_tickets: cart_item[:nb_reserve_places], other_tickets: cart_item[:tickets_attributes]) when Space CartItem::SpaceReservation.new(@customer, @operator, reservable, - cart_item[:slots_attributes], + cart_item[:slots_reservations_attributes], plan: plan_info[:plan], new_subscription: plan_info[:new_subscription]) else @@ -145,28 +147,28 @@ class CartService CartItem::MachineReservation.new(@customer, @operator, reservable, - object.reservation.slots, + object.reservation.slots_reservations, plan: plan, new_subscription: true) when Training CartItem::TrainingReservation.new(@customer, @operator, reservable, - object.reservation.slots, + object.reservation.slots_reservations, plan: plan, new_subscription: true) when Event CartItem::EventReservation.new(@customer, @operator, reservable, - object.reservation.slots, + object.reservation.slots_reservations, normal_tickets: object.reservation.nb_reserve_places, other_tickets: object.reservation.tickets) when Space CartItem::SpaceReservation.new(@customer, @operator, reservable, - object.reservation.slots, + object.reservation.slots_reservations, plan: plan, new_subscription: true) else diff --git a/app/services/invoices_service.rb b/app/services/invoices_service.rb index 7d36fbb38..b70688bfc 100644 --- a/app/services/invoices_service.rb +++ b/app/services/invoices_service.rb @@ -121,7 +121,7 @@ class InvoicesService def self.generate_event_item(invoice, reservation, payment_details, main = false) raise TypeError unless reservation.reservable.is_a? Event - reservation.slots.each do |slot| + reservation.slots_reservations.map(&:slot).each do |slot| description = "#{reservation.reservable.name}\n" description += if slot.start_at.to_date != slot.end_at.to_date I18n.t('events.from_STARTDATE_to_ENDDATE', @@ -152,7 +152,7 @@ class InvoicesService def self.generate_reservation_item(invoice, reservation, payment_details, main = false) raise TypeError unless [Space, Machine, Training].include? reservation.reservable.class - reservation.slots.each do |slot| + reservation.slots_reservations.map(&:slot).each do |slot| description = reservation.reservable.name + " #{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}" diff --git a/app/services/subscription_extension_after_reservation.rb b/app/services/subscription_extension_after_reservation.rb index 0fc66d270..5c2d46171 100644 --- a/app/services/subscription_extension_after_reservation.rb +++ b/app/services/subscription_extension_after_reservation.rb @@ -26,7 +26,7 @@ class SubscriptionExtensionAfterReservation def extend_subscription user.subscription.update_columns( - expiration_date: reservation.slots.first.start_at + user.subscribed_plan.duration + expiration_date: reservation.slots_reservations.first.slot.start_at + user.subscribed_plan.duration ) end end diff --git a/app/services/users_credits/manager.rb b/app/services/users_credits/manager.rb index c2e3ad6c5..3653be30b 100644 --- a/app/services/users_credits/manager.rb +++ b/app/services/users_credits/manager.rb @@ -94,16 +94,16 @@ module UsersCredits super will_use_credits, free_hours_count, machine_credit = _will_use_credits? - if will_use_credits - users_credit = user.users_credits.find_or_initialize_by(credit_id: machine_credit.id) + return unless will_use_credits - if users_credit.new_record? - users_credit.hours_used = free_hours_count - else - users_credit.hours_used += free_hours_count - end - users_credit.save! + users_credit = user.users_credits.find_or_initialize_by(credit_id: machine_credit.id) + + if users_credit.new_record? + users_credit.hours_used = free_hours_count + else + users_credit.hours_used += free_hours_count end + users_credit.save! end private @@ -111,19 +111,16 @@ module UsersCredits def _will_use_credits? return false, 0 unless plan - if machine_credit = plan.machine_credits.find_by(creditable_id: reservation.reservable_id) + machine_credit = plan.machine_credits.find_by(creditable_id: reservation.reservable_id) + if machine_credit users_credit = user.users_credits.find_by(credit_id: machine_credit.id) already_used_hours = users_credit ? users_credit.hours_used : 0 remaining_hours = machine_credit.hours - already_used_hours - free_hours_count = [remaining_hours, reservation.slots.size].min + free_hours_count = [remaining_hours, reservation.slots_reservations.size].min - if free_hours_count.positive? - return true, free_hours_count, machine_credit - else - return false, free_hours_count, machine_credit - end + return free_hours_count&.positive?, free_hours_count, machine_credit end [false, 0] end @@ -138,9 +135,8 @@ module UsersCredits def update_credits super will_use_credits, training_credit = _will_use_credits? - if will_use_credits - user.credits << training_credit # we create a new UsersCredit object - end + + user.credits << training_credit if will_use_credits # we create a new UsersCredit object end private @@ -149,11 +145,10 @@ module UsersCredits return false, nil unless plan # if there is a training_credit defined for this plan and this training - if training_credit = plan.training_credits.find_by(creditable_id: reservation.reservable_id) + training_credit = plan.training_credits.find_by(creditable_id: reservation.reservable_id) + if training_credit # if user has not used all the plan credits - if user.training_credits.where(plan: plan).count < plan.training_credit_nb - return true, training_credit - end + return true, training_credit if user.training_credits.where(plan: plan).count < plan.training_credit_nb end [false, nil] end @@ -200,19 +195,16 @@ module UsersCredits def _will_use_credits? return false, 0 unless plan - if space_credit = plan.space_credits.find_by(creditable_id: reservation.reservable_id) + space_credit = plan.space_credits.find_by(creditable_id: reservation.reservable_id) + if space_credit users_credit = user.users_credits.find_by(credit_id: space_credit.id) already_used_hours = users_credit ? users_credit.hours_used : 0 remaining_hours = space_credit.hours - already_used_hours - free_hours_count = [remaining_hours, reservation.slots.size].min + free_hours_count = [remaining_hours, reservation.slots_reservations.size].min - if free_hours_count.positive? - return true, free_hours_count, space_credit - else - return false, free_hours_count, space_credit - end + return free_hours_count&.positive?, free_hours_count, space_credit end [false, 0] end diff --git a/app/validators/subscription_group_validator.rb b/app/validators/subscription_group_validator.rb index 72ec73680..7f04545e5 100644 --- a/app/validators/subscription_group_validator.rb +++ b/app/validators/subscription_group_validator.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# Check that the current subscription's plan matches the subscribing user's plan class SubscriptionGroupValidator < ActiveModel::Validator def validate(record) return if record.statistic_profile&.group_id == record.plan&.group_id diff --git a/lib/tasks/fablab/fix_invoices.rake b/lib/tasks/fablab/fix_invoices.rake index 74747a2da..fd503f369 100644 --- a/lib/tasks/fablab/fix_invoices.rake +++ b/lib/tasks/fablab/fix_invoices.rake @@ -50,7 +50,7 @@ namespace :fablab do reservation = ::Reservation.create!( reservable_id: reservable.id, reservable_type: reservable.class.name, - slots_attributes: slots_attributes(invoice, reservable), + slots_reservations_attributes: slots_reservations_attributes(invoice, reservable), statistic_profile_id: StatisticProfile.find_by(user: invoice.user).id ) invoice.update_attributes(invoiced: reservation) @@ -118,12 +118,12 @@ namespace :fablab do availability end - def slots_attributes(invoice, reservable) - find_slots(invoice).map do |slot| + def slots_reservations_attributes(invoice, reservable) + find_slots(invoice).map do |slot_dates| + availability = find_availability(reservable, slot_dates) + slot = Slot.find_by(start_at: slot_dates[0], end_at: slot_dates[1], availability_id: availability&.id) { - start_at: slot[0], - end_at: slot[1], - availability_id: find_availability(reservable, slot)&.id, + slot_id: slot&.id, offered: invoice.total.zero? } end diff --git a/lib/tasks/fablab/payzen.rake b/lib/tasks/fablab/payzen.rake index ba956cd5c..89316adb3 100644 --- a/lib/tasks/fablab/payzen.rake +++ b/lib/tasks/fablab/payzen.rake @@ -1,43 +1,51 @@ # frozen_string_literal: true -# Stripe relative tasks +# PayZen relative tasks namespace :fablab do namespace :payzen do - # example: rails fablab:payzen:replay_on_payment_success[54a35f3f6fdd729ac72b6da0,53,57,3,2317,2022-04-27T14:30:34.000+02:00,2022-04-27T17:00:34.000+02:00] + # example: rails fablab:payzen:replay_on_payment_success[54a35f3f6fdd729ac72b6da0,53,57,3,247] # to find the parameters, search the logs, example: # Started POST "/api/payzen/confirm_payment" for 93.27.29.108 at 2022-04-04 20:26:12 +0000 - # Processing by API::PayzenController#confirm_payment as JSON - # Parameters: {"cart_items"=>{"customer_id"=>53, "items"=>[{"reservation"=>{"reservable_id"=>57, "reservable_type"=>"Event", "slots_attributes"=>[{"start_at"=>"2022-04-27T14:30:34.000+02:00", "end_at"=>"2022-04-27T17:00:34.000+02:00", "availability_id"=>2317, "offered"=>false}], "nb_reserve_places"=>3, "tickets_attributes"=>[]}}], "payment_method"=>"card"}, "order_id"=>"704cc55e23f00ac3d238d8de", "payzen"=>{"cart_items"=>{"customer_id"=>53, "items"=>[{"reservation"=>{"reservable_id"=>57, "reservable_type"=>"Event", "slots_attributes"=>[{"start_at"=>"2022-04-27T14:30:34.000+02:00", "end_at"=>"2022-04-27T17:00:34.000+02:00", "availability_id"=>2317, "offered"=>false}], "nb_reserve_places"=>3, "tickets_attributes"=>[]}}], "payment_method"=>"card"}, "order_id"=>"704cc55e23f00ac3d238d8de"}} + # Processing by API::PayzenController#confirm_payment as JSON + # Parameters: {"cart_items"=>{"customer_id"=>53, "items"=>[ + # {"reservation"=>{"reservable_id"=>57, "reservable_type"=>"Event", + # "slots_reservations_attributes"=>[{"slot_id"=>247, "offered"=>false}], + # "nb_reserve_places"=>3, "tickets_attributes"=>[] + # }} + # ], "payment_method"=>"card"}, "order_id"=>"704cc55e23f00ac3d238d8de", + # "payzen"=>{"cart_items"=>{"customer_id"=>53, "items"=>[ + # {"reservation"=>{"reservable_id"=>57, "reservable_type"=>"Event", + # "slots_reservations_attributes"=>[{"slot_id"=>247, "offered"=>false}], + # "nb_reserve_places"=>3, "tickets_attributes"=>[] + # }} + # ], "payment_method"=>"card"}, "order_id"=>"704cc55e23f00ac3d238d8de"}} desc 'replay PayzenController#on_payment_success for a given event' - task :replay_on_payment_success, [:gateway_item_id, :user_id, :event_id, :nb_reserve_places, :availability_id, :slot_start_at, :slot_end_at] => :environment do |_task, args| + task :replay_on_payment_success, %i[gateway_item_id user_id event_id nb_reserve_places slot_id] => :environment do |_task, args| ActiveRecord::Base.logger = Logger.new STDOUT - gateway_item_id, gateway_item_type = args.gateway_item_id, 'PayZen::Order' + gateway_item_type = 'PayZen::Order' ActionController::Parameters.permit_all_parameters = true - params = ActionController::Parameters.new({ - "cart_items" => - { "customer_id" => args.user_id, - "items" => [ - {"reservation" => - { "reservable_id" => args.event_id, "reservable_type" => "Event", - "slots_attributes" =>[ - {"start_at" => args.slot_start_at, "end_at" => args.slot_end_at, "availability_id" => args.availability_id, "offered" => false} - ], - "nb_reserve_places" => args.nb_reserve_places.to_i, "tickets_attributes" =>[] - } - } - ], - "payment_method"=>"card"}, - "order_id"=> gateway_item_id, - } - ) + params = ActionController::Parameters.new( + { 'cart_items' => + { 'customer_id' => args.user_id, + 'items' => [ + { 'reservation' => + { 'reservable_id' => args.event_id, 'reservable_type' => 'Event', + 'slots_reservations_attributes' => [ + { 'slot_id' => args.slot_id, 'offered' => false } + ], + 'nb_reserve_places' => args.nb_reserve_places.to_i, 'tickets_attributes' => [] } } + ], + 'payment_method' => 'card' }, + 'order_id' => args.gateway_item_id } + ) current_user = User.find(args.user_id) cart_service = CartService.new(current_user) cart = cart_service.from_hash(params[:cart_items]) - res = cart.build_and_save(gateway_item_id, gateway_item_type) + cart.build_and_save(args.gateway_item_id, gateway_item_type) end end -end \ No newline at end of file +end diff --git a/test/fixtures/files/invoices/4/FabManager_invoice-10_13072022.pdf b/test/fixtures/files/invoices/4/FabManager_invoice-10_13072022.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9cf2d893bfbc48985ff2f616f5658917bcd456e0 GIT binary patch literal 44730 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 CV~%P7 literal 0 HcmV?d00001 diff --git a/test/fixtures/files/invoices/5/FabManager_invoice-9_13072022.pdf b/test/fixtures/files/invoices/5/FabManager_invoice-9_13072022.pdf new file mode 100644 index 0000000000000000000000000000000000000000..05064d4f6595696a5ea0d36bf528f375272578da GIT binary patch literal 44810 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%0ANAbU(i4fIyycWih)i~J*N{6ca1&XobGeVJj= zYz@v$T*O-!g4UJlB)m#8j}(LE3qmL<`YfcqDtim)8XFH9nsQ}q%YQ69*s_o7T@LnK19rswR)XpPx1S0M#wAxhD- z%-Jh4j87~1s~DjfqxX4|c6@n?&+$nUbIJO|-Cj$;saJ(tI$pfuT5B${S$k*KBmI<`fYWO2M)`yLFr`|1lLNQq%W>Na355F_3hc~%+ zo%;^!xpxB<&IawQ4$H_k=gj^h^Wx^SQz3=>U&FRgo=_(i;n-M|8VF8m8NH`aUh!;m zR>?WX$m>!n50>cI!Hbdm-oUV)dk#^Ihn2qLJb#{D>Z3k8P1v21v(y4GkZ0GTqB+5V3j|sXV!?m(a`tIAU7k$$l60e8^|KP+a*>B?G$& zkJH{6D%*ecM9`0~(B@>mhw_CeDm5wl4y!0_Q4UAxR&5gbTdF3Gp^5VPJ^s~pHzhrO zG+<{VinpS?6BgYuGURS1VeB-(H1XN`0kxT}&jcnDN9WfH3cS$;%;5X&6xEa@Mm{8; z;0+&$EfB|oR+u;hit8jL?^su8XV3R{yULp>Tt3vxH@dgfr6L%4t7+&y!$d5B-^bzpM#n+uXNt8HWF}|3zSYP|yS#J#@#&lk(H_~*7>w2EE zL&U>ckXw7#XUQKMvjk2>qTzkY<{JW=?23F>XiwRuIm-O z$(*d2X?p8y}n==UgocsDO5Qn+`Tb?0*LZ9)zruj3iTz1Oz-3d%FR*#trZV}290NuK%r zST)_c3}gF=hFD$^@3+Zj&W4RJb*EeN%n6BASS|e|Dj>;$ zSvXXJ)>dlKtbnqyw7iNQ3BRbuqJ`{8A1Rd0doe`(=$_}xfnJoum}oq$_n!6;M&A^W z@wEVXlWs0L7j~#25!KxtGxa1FdHP6=^{S$WzO#fH>E8|sZxg-tV?@4hHN|N;?fp18 zap7a%T>>{ui~`*-rjYx>yK-3{b0mPMMeUvyp>{aQXR;ZrGGBf}8}s{$J9@QC5fW?M zHi>xxQOp>fugn{($Tt=5UtXY8%ZOUQ zYljx-=vqD;atSJ)nbm9~mvMeIg!ND5Sccq1^Ye z{dbZ5H+d$utfL9ag%5qw6}hrE@8$sh#oSHp%=$~W>e0hm_i_&d;|w0xg$g1LWObdn zS`gQ&ZsWw5)uo$3p*z@bge5u6eSdB7i#{FyXyOUv;@X$cH@K7nEtmr9G673y;rp$0 zMJxfi!{jYDrQ+DxSyvzhvi>-B?Qm5U*HxF5>Le~7+FB`zm|eL zgC;ubR#{f3ir`gA9^muZ_iFHnHo9Zk+Dc8@h!bNIi)mNNX&A_-%E;aUl`q|1sx*)lPn6XHBa5>+1@F#_aC^!laTET>_RrlMbbCfR zUQD${1`k8USgTfQxKBRSgH~(EQDSJE75eS(DqHprBcr6#BD?L(^jX>RkeGQ-XE+R; zjlW9%H^HDdLC!wc}&Fo z8T6rpp*9FwaVd#={>TKT=pqd*)(VRwbU-|HEb`T-twELiXYxa;Ea_c?;VMACqH9@x z*Q+XxsoriTYwBVyJznJ_qZ}{Zh@elR?Sk~hS`iWFyNwtA^Gd#~$%f2AJmeAG->BSN z#XiPdSLxfM2N*y-J3>*QUaj z6XdyE0mm1HY#Q@uuT}#SE41fBZZ`k=i%*#!C1NNbFZ+_37M@~V3z7>}4pQr~E7q1h zic=-~xVvse@&2=-ZXQyldkfAMz7F(qi*hc9ks-saE2Z`X>7A%9}O8&j>u ztMNoRAM1OygRF)-H$Pdp-gYzV$f0!CBxvdTUbX~=Xy9np<9rd{`IDGv19Gv2{pgv* zm-|X7LKzw9Ze?ZPtEV`}_3oEd#`s)k&F@MdqLVn}d)T7wvsm{-GfHp(0vk)cjLsU z0dgS|a_H&MQ3I*fEvi$G_B0I9fs-M#fOytVV@o#0Bc9lz75muM@fs#tIc9v9EYwYl zTfWzfjIAGdaPFFMzLm2AHy4PlHjd?qe3wi@1W;qAI1w&U{pm#7Lk$B?H}Y>d2f3yCWj|9^vOFT703BP{T&LMyz*z5y0Baq zPGtyJq8@&PBFtT06w04}Ay{U-k6-xZ zepAobvB7QtH+g<{Z4NPX<%2f;{R`r+W4$?vX~Y<9CS{MGi>l-Av^oj|tMr5y%wC7f z^p-V)yNoRnIP}!+ZLh6WxnIZLcCT6ZO&5 ztMi_4A2-NE=;AdQ##wGFvlG2pX1hN|>=KX0Mr&l`*hKDHL0v~=q>cW^+bikB$|t)e zowrO%H{DH4t+h{2#kIm@V&B}ajbbn=kUw;so!iX6WSA&&Kghj5(B+3(RdDG4aM<8s zCDzmlLBpnE4?Dkn?;C^7FSKV`>0v{yn~E3xmb?>?i~03c0`16^F}n9B)?-tvs5y?# zbavC|_19E>W~d(ov~mZ%h)do99kZ;u2657^Z|-BaIeH7s2G7O|fqDQtKsb4P8 z*_i}m@ktw@={Kk_jE%=MbntC9Nh!Cl(Lxs>O8O z?`x>t>0Ei6>U$j--tmErCa2=)wFO_Kds3AHp~W+)0#&-Oy3Io4KBO!*@hA><@F_2PG1YU=wtWQZmPqwzTNkM&7oH0jNAB6MO9=W+6pmuB8e z6=yhe5||h2sdUjqbOf}xHSK6Kyz278>aY~3ZZ+*9iA^{2aJ9FwP^TTOH|q?~yHCKu zsS$OsefDNBn~#_%f1=o9ZX1b0t)huT=+MBnaN_tQi++_Vn&OZ*k!cpm7 zp-TJB646YgLiJ%&;Dlt7lJx|%qz;Yb)&?Te(XYCN=gIYiyHZOG>e#8c?})r}fX~9L zrM&?#teLypIo?ILG6pj0 zox_KO(U{2tljS15&_dDU*w@+EPFwJ*dVE7I-7`Ty(j3mwa5e-hE?aALwbp~^N=uuU z2i3$kasNe$`Jc6CYoj7wK)%*L3)O`+U(4uG(Tw)*LqRq6Vp?ljPf%sL3%9x!=5MVX zy9VU_KT==+zpdfVen8G|9A*8F6J!6FBKbctE%RSY`-3Pw*#6(0_y3pkGMVmPD*YIP zwwOXaUoF3X`y`~Uge*fY;;&9tUMo$gN*dZ2UO3km{6kYT_DfPf60zvq7aIX}ZHRlN za4RDF2yBhK!1>RGbiqjZ7ziM2BU%ATo2SYCI;*P*;lYcz(E1qf1JC+JW=mZrEKSpA zA?of@0^&*!iGVcv@xbb_ z^QLOkx2z_pGbC@<|dlGl^} zv=trqV*0u1R8W!=?qt+%Ko~+eU`K=oh)p(%pvE@pHTZ22;C7wNesV2CLOcK?Jn&iN zp(1%V1P@g;qVIaNneV1wJc7ylD#dq7pFcN*%F?Y5DIVWmZo~_i-4|dl88W4PmEmg^ z;cYm7f{?fq3|C0+fCc~Cl$_Z8m#@H-e#JL`tk1lB9^I!$HuP-k8G)N2xDbK?khpNV zO!-;p{P;IR=nw1`rr5MS0#iBqN>&;u9?oR8;!C~QZQ7mU@O%&g0bASQsHzn*xD{PT z*bAC#Q2o&dpM0TV^hmvi1x__|fn(MKmA#^CO>CY6n`B^Yhs4D}VfTh)s3A>RIkPfs zO1g#}&?l=~n_r`FcLq@obcv$7>-GrtE$!5GV6fKK(btedob0bLYDt}Ne=$!o=>tH|G0niL8q}W9BPdmv$T3T0@i2 z##-Wf;?dBfAV$IE>x#eG4CdvO*=K6qZ+z)y+c1o~MI@NXyj zwU#k$;xCQZ`*LPl_F_lrNXyCZwFDLi6C~x}_g4`XZPoThYZQ%9i?05XWVc;*2to+Q zEeNX6IfA1=xD`B@YQ-QrGuBoU)tDP-_9kKz%W6@goXa!AlK?OMvSkhewnu_I9u9ir zoqhTHkztEG*t7>pY%;-fQ*~9G1k3{3$#c77hQF~@GaKb&YOB=5}f zFsJ+NYa%I^OZlre4<+S6opv9mmafu9<%=-Mx{l9)B?5y+k4nYsR7Sz6CZCxVAn*7< z`2{!AX0>-OQijjY;aiz%0DYOA_(Ee$c+Hz%m{-o7!0V@sG(IF~XSY;R0LmQtfw9W_ zh;QYh{?*t)w73#3ncSMkefG~QMJAYB zr`%y*uv)%XkYS;FVO;t#-A{mW zdbr5>&GD&JyyTz`M?BxOyasII@z;53eBnv3yxe8Oa_C$zRgWhP83m(NGL(M2CNJDp zoPN1KWw?>-VZTJN?F5kP`5)U^li!}s-Szw1kiz5FbO`0cNV|SW}!&D7M*4k%#G1et!Qwf;r()}hoE*MqAN4cNzqk44oeDPF# zV}BhjhPj>|u>*|?9!t~oG~ic$6aMQsT4y_!y3=U?Co>*pY51rB?<+-2gsK3O5#FNr z89sw6W;Kf75$i~tBcBZYVrZ0uKgSkH6YqHm3u{Cheap6{@@O49-!s1&C^e>9Gh2Ka zwYNlx%B?n?ERQ9$=lwm%F=ClaY}e7sl-|+RWL^mUP3Z}-!yQbK!dqOm--`0((s zvBDOaTNEr!+*Pvs_aK@2yjs9i0L-yN@+AsYSrM7tmBRdWcCht|^WjthoaweTOeSu_ z4(GE4@(_2ZSABrI8p8Wniw=L~Jd%x%kn>rj5y}AZMCI_3IFiz7O;D$ZYlWs2zgT_k+OXMP}7AZcbWZYKna#c%t+5 z0pmwybGx3kacjc<5x9r;q`juy-nm8nJOvX?`e`;qfB62C)hfBpA=^0m>$t}+3V&X5 z4`yEFi0jz5C5Hw`u+RXZ9k66hkvE6%MKqOS-Ou%D$NIb6X{%2iUi^$m-&YUms>FZm zZ}Nl%h9NMjDgiSeRmWYD0c7zh+y~)_zoW9gXO7w_4$&hpF(FE&Y@W?-fBHR6+rrEK za*dUOQl{(iy#C-y!1GN(8C8+?m;G#UbknUtCBQM(QIIWWdQm!?8ly9U0NO&K=f~J) ze$b;>RlYX@!nH`bO%Dz_A6C0S#scfHe0QCRt`?nfcQ)`P(lYw=EW!R~#QDz*3c6^ zb2o}Ur4)8s+W!2mE;|zgjIFCprtr)lCxDz~qoV(|z(_{+Vxp83=?O7ACjV{RJ;2-> zwRt!Lh5|4i<|j4Cd(=g+cq;9KIK8}#6FR`tTg~C71ECI2fG|L#l7Q%lV%X7G@gVWX zotmhZ?K5R8Uj{;A&(U-R;#&V&_vV9H0QE%_+oi!2i(G(RwLfI0^XDu)rG)TmI-Dc* zs{&!bD$!7Y2Cfwv3}&xiv`Qrn@UCCdM~K7cL}(c@R9b_lznl-2?kA%oc5+n~0$|U2 zTiKcBBkk??$|^ah>*(qdL$bgOF)h62VC}Zx$p|z%yQ}B$yvzJAcX>Rok;ZS<_Ai9` z%t`@~xL@SQsIhr>b8gBOoj^a*PvbMhtCo-We=t7KHZ{o0{fJtu4%3cpZ$78j0zIEQ z#9RinssTeDzg+;nP~-&lbkRTK&~Ki9-?Z9@Nm4960YeO3JPed~r`pteax5}RFw zX8>C(pnOh0WDWb`6UMobRcpm=*w^Cv@kB;~J*E)^QU$C|RJ2a@U7nrzJIQgvEy;o4 zX9gN-2h936{|do@!m86Vn@C%zk!$fAc-dV!UkmHD>jK9r2lc)0c%DV#s#2rxj;@aV zDQ(FEBGKxj5BQ-^45jMjS6PMq3wdW&L}!!lC*Ra6H(s`LK6gSD46>SDLB-C)e5^K= z_`r1PY*zuYl`l_{+TQ&#+<}SWTI5=d>Ty>!H9+t@fT^pg>BSIQ3+Hlc@%CeF5lRKr z(}xeWj}_mx>}MFZ&~S^DQb{Q1-ku&IMocU$tW0_;Dh~VGp*YslGG{W-)2`l+x&fFf ziZ8D2t{knJk+|t35U4Z6xw!w)j}sDNM|`@hx3Kfv(4GvEKkKh6KBCh<>19*ByM6+}q+-vD|X9PA*N-@gWc zgOG&(jkB)815R66<^7TK0pPlRfn)dS+>Rjj01Wc01uQ@k=1_`f?H!jUVj zntO`#J77L{2K`V`GLoi!aXr^Mgyg;b^Lp&=EL&CN;~V>V=af@G?WMQJr;!vLNH`Dz z04xIb_*%XBZ+&2Rs7(RmS+4TV$tbyxFDQ!mB^rNd4k-h>-82cuHee7)Yxx7&E{AeI zp8ST<3EaM`%XQPLSVXB70kLIGaLj6!PYt#LkR9N~P~yJ%qlrcc$CeQZ%A8I$KL+;Y ze+NoQ%HHJXMH-~`)`>;p-$~EeyllX~eS9b!qFc%JyO7{g$KGiKn@|U<67R}6dRDLO z+~*gjWQv(m)GIknN12|K^z%N{>hMREnezF~6RN-Qmb>Io)z4~8h$A$zU>1DK`<8ag z1;}Gbr*90Efa+bsiR?*bO4_5#7?DJ(oi!+B>KnSpqiLzm&M zkke!vrSAnTUZe-o1~Ygw!*J(28lQ~j!~I-Ll;Yni<~efAU~=#Mo}t;;2!8|Q8#u>5 zf@s&|s#i>l%>HUk&Y%??Be5f?#X!(wOx!!)eu9HwDg+TOC~1}iBwaWRuV$5L6CLG3 z`C=L+#lCbl%0fY5uWkGUug7vU}qrI$)WHLn1p?blq$ zEzf@3%RTZ-m$$FwRM&86_ocmuSV%UYS(4;2%?Ck;c)?%a-?=MlUD@<++b~a55jGP-jJ&y?(@jlY?|ac+e*s{E<(Ora|vU&23c(B z^F3kH%-lAkxXcukqd&PRf&BcgGJ@=JzEBxQyh|}-lZwcUW@#+Br>Dfck)kr~i;gXs z4yXry?&xXro{5r%li(`c661zKxQO;!22z$a@+CQU{d`>b*a8ON6yFOVze19WDTxIP z|1{~jo~IH{$!F~Be12>2(R4H`#@-}Py7GI{xjOXjB zaU=86nK9nP{R+IpwaXI{Zi6Q8;Ze23E)eod#7Bg@m(|b@6ucH#qsEzV0-QGEacVIx;kzEzBjyBACcY7o3uS& zM!yu%sq>faRzlhmtP^|RnI6H*voS;N9)lqoeyem1g-pfxBC_(UVRedQ*`{tK+}ovf zWG|fVz6G7es78LC(KrEPKgkx7<7eh5^eNLsP5`V}fYR7yKEjDn@};!iieqP|@N@dD z&Pj>6YnxI~vJ~t2#`7-i%6Z9gikj-vQ)z@+Dh?i!_;n+x7UQe1j8|V~_)fLOjj-i~ zcD(Kso6C$jY80A)hvOf#93(W{y(qRCsc=PFGm*ObC4UVx?)9QMS{Vw>?rHC591xk0 z?XXout*yZ!hoI@#qOP$F>6rWP-*n!kXSgu3Q5lW56^CP_o2H0XLwmEW_!JY@>X3Jv zpm(vr*_l5_U5XtY*}>V?%#P{9Kp2qpX;kD0(+--4d#_H4EHXB-4Oj>o*3&T~CwAjh zgaUpt@kB~Jqth0Q&LEY2dDlMRr?2STnJ!74MX-FGVPQU4PMSj*>c; zo2eL=DPaO`w+CIZJcXQafB}OuFZa1PBQDzBeC~HBEMJ41X)gV}VK#ZjKFMzyn{E1& z!c9pIHoREYaT_QM#d=CJdqXxuVUN9X%yesThVCHOs8ofUrx@JvS4*GQOMagxg=a#a z9~)T}S~N{BQcd4nw~DTQ?vHQHc+O-a>Uh499&s`F?4<^1Ee}@Eq4yfZMv@le0QvGK@0_YpxEEiY2o0rpPx<85z1gJ1 z(Aw->yd%luq7`V&C(NcD&(SBaDf?jiJDNEVX5iB{X9G#?{gJ4D*sN+m_8}`^gjs&k zV)|{I-?|Ggq-6z;7qiEKFPm>bB=%c*i}pja_3YB+D8Of}%iP-4r{gdzgwSf^Z6?6- zD%ALa-plFc7d?oW@+{l4x%l;|Kqq;EptHk%d%93uuX+8)!*Y~#DG~08cS@GG>)Gwb zUZh33Zd2%?3*u>XA3Qg@yiwaUqEY>q5fNx7yt0T-<$mBiAlD@1tfRJ`z_qk{%>sK(dw*_AuClth? z!rSxA>)$o_9rq;y@>nl|A@wdA7LPBZB??%&m5!V!Hb{1hC8r=UJ0{uq^wx!Z?z*-NJo&ENBu#Ys7RFv&b6n1-nbXNQO+tCe$j=@u@^<2SAWo2ph zZF=g#Yt=*WO1|65mMbDbf3Wpne!xfUg!G!yV#3zA8>Y$S{it|>Y5p7PL{ItN=OGBb zaz9?g{R&}?bqT4WeP(G3^|WtH(CyJQ*CiU6h%-!v#0Z`cezp8@(8ofQ2pA;y`tZR6 z-7}??S(T(A#TnzujmnE#=+^cjmr%pwgdfx~X#pP`&KfEaY^cRusX?6JgcXnx7z%N~ zfiaQd+pR4wjJzqqtj#-v=W2VtcHI!CtsO5ewAz@P?%#SKE|u6_bnNYL)d4dD5h@wT z8VNSs0D03ZLMpNpdc4a|M`%4E)l||82T-cQ(@#~&X-dq=z z4blg9R!2|zZwt<}61v*Zx-PoWB+R;S6?Ih3^}J(&;a(JC<9=;pz#?O&7_%wP>%h5e z8{BQlfcSXArv@h|%a6L7J%Z!{to(Ku`}@{xd>SiL-kFFQj3c6%)G?U4ORm*RdF=(A zM(3|rXtCH@;HUvy(T`!#-@Zbpj+MJ)#1!DHaS%<R z&`q)z*X3<*0KW0$nFCrxfWP6QPk!T*S>2Mrif??oD!FpGdR2(N0b7ZAP*zcm5aJgy zj&&(DcvNT*T~Fb3uSuuV4pE!Iq$HPrk<(n>nB@gwi~X0+(RTW3?7T_pKsQX!?PQv# zui5drccOgDucHgH{xmI!<-1F0GlWOR+v?3<4g?GMZakzX7dPG?dx0DG0-~L2X)(Y} z5ABdeY1tX08SWX*jNqNzQu)LQD4wX2V%Jz!7A@s5j%=xZYV<;TKB)0E z!!V7vBQzSF7B_Ue>H&;Yk0bxv;atSF1l*at^`}WS`3&A}v-f*rq@mow8t03o7k@s) zs{%N4Vk}G#9nDDUsF`AgGUYGi{eMoS=u`GMvQ?$0*(8Ds=M&0<^bkT>fGm`e^Ibxl zjzQQEjNMvH3bF6uz|2I#xCuQ@hs|LuzMN7ILZjAo4#qcYRe4gC2Irf7dfF~r32#Bc zT|5X>dyzB}(U4S)0gSw`d~0^`xXFR3k@N(%YKeIAuU~S7Fr`BNqzF_7s#A>spfYHz z;K{x36P=8V_VkXq5!V{sPfp~evEHAp2Bs-5)|kDS?jmHuR>e1k%#%LZ&!^|$Mjri)Yj0u-!<8?TcSf)!Wi)76g5k^~0Hjjjj7;a%AbQO(vi zn~&hx>>nQc*FjAiWX~4hu(tYzFxRz&Uc!$Q8k$EkMLZ_x72)ZA=ES0G(1-+zkk_IRW?r%m;he?2($wHsR&= zKHe0dohHC;!VwjHD;898YLUiXdf1KF`Wp=KI&_DwHpdAFYoY(+4jw&q*ur@kVtd<+ zqm^6GcC<5*umAML)@we`IELWxRk%NEt#c(W&t}D}Tet>-_R8+)q!Wj%aaC_OgTJ|AMw(vwc z85#)+LgQb|W*x3hAZGPsieq;6`N(lP3?g2R5sb=_VS@6>DO2>v$2cSJQZqi?$rE=i zFCM@GRuxei?*ed6Uz+fwj?@3wiIRQFitKs`#jBKZV{mjtO)ccmAkMXaV4Hj7QKvui zW~@(WH81PY_xxV8pU=yieGl@56k|AQPcvIs1rG(2>n*jv0DPnAdOr?YebUkxahhz~ z>|EZXSzXtZT1xL$F0fX6(b)sTT%*=J$5~{r&7b9YA%Aw8~z*~TEbn@D1S+C^LLv)}8LE^L21 z$byeYdGN~}Gh_^Cyp_1c9ZtnG3iwt=b!$$|OObRn&=o*J?QjGhBFxj!{x)>^m`f@K zA4N2QzTI3NY0w*DY65Y-P9HBiP$6hQHEqdSHm{k5MD(2%{n<*lfY9$QH%Ap_O{9+} z=av;dKgn)z=yf$r^bMU{`msIfdV>jAiI=|SlU-FgaGF&xp`V$RuB1IhGHm+5jrtDTEJN_>n<>v14kkWpdlO@}Q;b4&AONJJ+~ zvgea}r=0!hHjEWsHWp_Qwx|_Q^w5tDIm))!wa7nfsH|0^X7s@Vn6Z_g^;lEV&-(eM zG!c2l{#-h3+!d*XHt;+&%DBeMQ{aX=viea>Qz*xp4<=ucG_HsK(~7 z(*%5CpSv*K)MhQPt+akQY*KeTLo4sR>hT*S;&;1zh-p${INF%evQBx1%n$r@gmCnn ze$n}`5F|}D^GuRO7wdW^vZPC*Jx+!0l@JCLjfliOx?6zAUeB4kL1b&@pEG&DlJ=G( z71xr8Zv+=2PW>6U0zMp1CMEmQ?y|nZ{86Rn<$c!WVxczZ?fIZ7MWH5}XtOiZq?ROQ zE4>llC+YLJYFQ~i>2-bF@&NIiYviW{8UCPIFI6BVDyUPn_%%_b@mZe&Xl#s>phDFZ zobu})E2{dSpvpbhyFIn@44GA@PWt0~J&ROk%hI(G+CuC%R583@Z(9H)fYkE53%qurXVxJP$Mu6H)_SJ0 z=f=?;hk>c*du8sh(n$NPm}%l9z3z+MR=fMo09iC)7W4g&P3F=E8fRa6FlzxF%WCxm zi-Itdo>Q8-Zk~mr8{FG3%9~1`5&?&+vO(Cpr}YM8NJ3D8@d|~5QWH6tc*as0`1@v| z$-2vRMLAz$l62E*1j^Ja0o$k+M&|FG`H8&Jmix%^OGv8e{pkMV`JTNs8oATOV>D&5 zdGok#5xV!7>0HVP@yoN0H4qY|TYKZie$C7FLLw(X(~X=*t3kE>TTT;()Vg}uiW7?YwCETcV0jbobLU$UM{D6H1pixJ(_;THgzCW)D&A#IE zBhhD@tg41**OcP%BuTZ?H&fkBS2L;!KY2p3im*tLx|NOK;-Y27k&r0)8jK%`a1#6n zEvIc4yLo!1A1Q7^h9(RW4lOr*EB}k|`r!s1R<#vX9!p8DUSf*-S%N&Gv^~^h6&^nW z>&5V|6@jkwjgH1m=O9K=>ij zlT>N8$lj&-)q?NJ7A8TcEigepE_#Xek^Ynk^_?i;2C1QCU$e7pMBc~LM03V5e54UT zZBXiekh>A;20iRx*)akVEOI8Mq<%-9=WiG~q==_RoU*xKy$~F1;kLZ3rmq%#Y5yu| zUu2lQ&AQ$Xyz$wsqCyyH8BrR+D@C2RnSVK^xKlcQ00gj=q}`>>YKooA+RVMnVwI#9 zY87x7olGOmVjZ5OI3A)K3x>)H5Srj=B2*Ci#MG8p7e_k$BA8Pff~eM*iWV*lVu#d( zs0U18etNl^+K+t-SFTS1fbjvjMlGgT?!R)4)(bfPs+*HAGqlA(i;L|6~kHK|4QW%C8+3lBNmQmV50C~5enAz}APnD}+N2ciJA^Wigtd5MGG zB|rT${}KxsUV~tLTDbZU2lTGkY&W9Qp?EPF)2~zpFaF5O90H`SJP6kuemI|Tr373l zm!w>({fp4&nTSAhUt21{OfC~C1_%LLBrI}`sf$Nh`t@TU1(J{py+2nZ;S;x{ zUSFxwmw3v4Ud5=gkko9s zvI4yLMR&vW()-Y?Qq4eNMzFz6JbCD?-Kg$3;tYt&5A;B#u8$Vt46(SwcF=3yl#jn1 zG_}1L@&qv&AwgiBM1XXF9cIwhkswFNZ8PApT;S)`@g_dhattH%7Ei50M1UP3LIFF} zmIYf17j9tLwB51LhR+t+>G? zI=@o2xWc9a(*idg69^ouw8cRDVoqH`Eomnyi$1{4K9QwGxeJmj2C%C&v9nFcL+`QW zEKOP0db7TQ{aCLrC?9$1)>A> z6&Gl^T!k+iZqOXq2;I-yjW7KD)RS=hM6VAi%Vf_dlL>*gpO-Z3T!%GQVFO)P3`qwE zCJ(S6oK;i;b~aXONT5C^ec^<|-Cy`pLQQSAX$j5%o2J>!4p~cP2D(b+Ielxu?cxbg z*OlJkD3wmqv;=sBEF3Jp{Gk+FMO_8c$dEyvD=0#emz5+|{~Z5gA6GNbBDbOCccYE2 z^G&AEPI>HR*}>wMn^G(|)|0^f-%AMe70%;1HNj6pvO12IfYk#!GaEC|wM0^CF8SWC z!#1S$D_vEi^84aVXvS0~nB}LC4i%*NTB}d)3x>^;3Ay97k+6-b?w;V|z@a&d@1JXY z1$+=CDmAjS;1<*PbQ*JZ`Oe^(O7*6O8JC5;EiGq1H+fF~5*?lwWCQq;w-nlEX*D*e zsxw1CgyYf2cMezIn~VVj+lH@ad3N7zIkd2V^YaT0Y-3qS%d(ZWnZSpS#4qI_dW7J} zrlAgIelxORmI-&W^G9gn4TxtQ;?bgp4ms>uWgW<-O^IK~wYBxT=%2$81k>mRb?C5( zJ5{@+>e-=-ReRe6F7^R^khXw%woOXo9GUNk@p$Q`tq0J}X7(|RBjec|E8FJK4@Pf0b88iA?1!bikb5i-b{~RhKuNm7l%+<9}H^30Hj> z#z1sxpRmHS;6S>NjK}lr(B2^;FM2e2Lfzh>CU1`}w)ZDs1aI2mmhBmp89Gqy4^u?z z)mx^q5-MtVD@Oazh_{g$^B-*!7imJ3>yn8-Y%+-w)#S(!Dj-;f(KpgrCIMkscKyCrRU{m}&c$~Nvo4aC0?VB#U@pb-8d>GmQ zRtzl-zE*5@aysikI`4DiH&XHM2$T3pacSFfa#HxXcssaX#kli&=)?kai&V-I+Wd`h zaPNOoJ=&{uWV^r)_**dRl{3glpyT?<)f~kZBr; zIVBjDWkqU_M;7WUVh2qPYbQX8$DKYNpVj!79{mI7cM)dBjckExO!@OXrB!*<7mZ{= z^yLoWYJ#hXGN5p20P=+KBVwF5f8xpDPuh V#%5rUr~qAwew_Eoaw@@YG-W$SYic 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 Date: Mon, 18 Jul 2022 17:17:21 +0200 Subject: [PATCH 036/141] public calendar endpoint refactoring & testing --- CHANGELOG.md | 3 + .../api/availabilities_controller.rb | 47 +- app/controllers/api/events_controller.rb | 2 + .../src/javascript/controllers/calendar.js | 5 +- app/models/availability.rb | 2 +- .../availabilities/availabilities_service.rb | 51 +- .../create_availabilities_service.rb | 63 +- .../public_availabilities_service.rb | 102 +-- app/services/availabilities/status_service.rb | 28 +- app/services/event_service.rb | 16 +- .../api/availabilities/public.json.jbuilder | 66 +- test/fixtures/availabilities.yml | 68 +- test/fixtures/invoices.yml | 5 + test/fixtures/invoicing_profiles.yml | 7 + test/fixtures/payment_gateway_objects.yml | 22 + test/fixtures/payment_schedule_items.yml | 169 +++++ test/fixtures/payment_schedule_objects.yml | 12 +- test/fixtures/payment_schedules.yml | 16 + test/fixtures/profiles.yml | 11 + test/fixtures/slots.yml | 688 ++++-------------- test/fixtures/statistic_profiles.yml | 7 + test/fixtures/subscriptions.yml | 9 + test/fixtures/trainings_availabilities.yml | 7 + test/fixtures/users.yml | 32 + test/fixtures/wallets.yml | 4 + .../availabilities/as_public_test.rb | 6 +- .../availabilities/as_user_test.rb | 2 + .../reservations/create_as_admin_test.rb | 2 +- test/services/availabilities_service_test.rb | 94 +++ ...iption_extension_after_reservation_test.rb | 9 +- 30 files changed, 741 insertions(+), 814 deletions(-) create mode 100644 test/services/availabilities_service_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 600169ff8..67ca71b8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## next deploy +- Improved calendars loading time +- Refactored and documented the availability-slot-reservation data model +- Fix a bug: unable to book a space's slot with an existing reservation - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty ## v5.4.12 2022 July 06 diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index 6cfdf7392..c5d2992c0 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -20,19 +20,14 @@ class API::AvailabilitiesController < API::ApiController end def public - # FIXME, use AvailabilitiesService display_window = window - @reservations = Reservation.includes(:slots, :statistic_profile) - .references(:slots) - .where('slots.start_at >= ? AND slots.end_at <= ?', display_window[:start], display_window[:end]) machine_ids = params[:m] || [] service = Availabilities::PublicAvailabilitiesService.new(current_user) @availabilities = service.public_availabilities( - display_window[:start], - display_window[:end], - @reservations, - machines: machine_ids, spaces: params[:s] + display_window, + { machines: machine_ids, spaces: params[:s], trainings: params[:t] }, + (params[:evt] && params[:evt] == 'true') ) @title_filter = { machine_ids: machine_ids.map(&:to_i) } @@ -47,10 +42,8 @@ class API::AvailabilitiesController < API::ApiController authorize Availability @availability = Availability.new(availability_params) if @availability.save - if params[:availability][:occurrences] - service = Availabilities::CreateAvailabilitiesService.new - service.create(@availability, params[:availability][:occurrences]) - end + service = Availabilities::CreateAvailabilitiesService.new + service.create(@availability, params[:availability][:occurrences]) render :show, status: :created, location: @availability else render json: @availability.errors, status: :unprocessable_entity @@ -170,35 +163,7 @@ class API::AvailabilitiesController < API::ApiController end def filter_availabilites(availabilities) - availabilities_filtered = [] - availabilities.to_ary.each do |a| - # machine slot - if !a.try(:available_type) - availabilities_filtered << a - else - availabilities_filtered << a if filter_training?(a) - availabilities_filtered << a if filter_space?(a) - availabilities_filtered << a if filter_machine?(a) - availabilities_filtered << a if filter_event?(a) - end - end - availabilities_filtered.delete_if(&method(:remove_full?)) - end - - def filter_training?(availability) - params[:t] && availability.available_type == 'training' && params[:t].include?(availability.trainings.first.id.to_s) - end - - def filter_space?(availability) - params[:s] && availability.available_type == 'space' && params[:s].include?(availability.spaces.first.id.to_s) - end - - def filter_machine?(availability) - params[:m] && availability.available_type == 'machines' && (params[:m].map(&:to_i) & availability.machine_ids).any? - end - - def filter_event?(availability) - params[:evt] && params[:evt] == 'true' && availability.available_type == 'event' + availabilities.delete_if(&method(:remove_full?)) end def remove_full?(availability) diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index 0c051743a..26f7300e0 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -55,6 +55,8 @@ class API::EventsController < API::ApiController authorize Event @event = Event.new(event_params.permit!) if @event.save + service = Availabilities::CreateAvailabilitiesService.new + service.create_slots(@event.availability) render :show, status: :created, location: @event else render json: @event.errors, status: :unprocessable_entity diff --git a/app/frontend/src/javascript/controllers/calendar.js b/app/frontend/src/javascript/controllers/calendar.js index 9133c5bfb..d04b0dd06 100644 --- a/app/frontend/src/javascript/controllers/calendar.js +++ b/app/frontend/src/javascript/controllers/calendar.js @@ -216,8 +216,9 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ } else if (event.available_type === 'training') { $state.go('app.public.training_show', { id: event.training_id }); } else { - if (event.machine_id) { - $state.go('app.public.machines_show', { id: event.machine_id }); + if (event.machine_ids) { + // TODO open modal to ask the user to select the machine to show + $state.go('app.public.machines_show', { id: event.machine_ids[0] }); } else if (event.space_id) { $state.go('app.public.space_show', { id: event.space_id }); } diff --git a/app/models/availability.rb b/app/models/availability.rb index d45e6cc91..a794c9a50 100644 --- a/app/models/availability.rb +++ b/app/models/availability.rb @@ -37,7 +37,7 @@ class Availability < ApplicationRecord scope :trainings, -> { includes(:trainings).where(available_type: 'training') } scope :spaces, -> { includes(:spaces).where(available_type: 'space') } - attr_accessor :is_reserved, :slot_id, :can_modify + attr_accessor :is_reserved, :current_user_slots_reservations_ids, :can_modify validates :start_at, :end_at, presence: true validate :length_must_be_slot_multiple, unless: proc { end_at.blank? or start_at.blank? } diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index 303a06e74..31242cdfb 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -1,29 +1,38 @@ # frozen_string_literal: true -# Provides helper methods for Availability resources and properties +# List all Availability's slots for the given resources class Availabilities::AvailabilitiesService - def initialize(current_user) + def initialize(current_user, level = 'slot') @current_user = current_user @maximum_visibility = { year: Setting.get('visibility_yearly').to_i.months.since, other: Setting.get('visibility_others').to_i.months.since } @service = Availabilities::StatusService.new(current_user&.role) + @level = level end # list all slots for the given machine, with visibility relative to the given user def machines(machine, user, window) availabilities = availabilities(machine.availabilities, 'machines', user, window[:start], window[:end]) - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [machine]) } + if @level == 'slot' + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [machine]) } + else + availabilities.map { |a| @service.availability_reserved_status(a, user, [machine]) } + end end # list all slots for the given space, with visibility relative to the given user def spaces(space, user, window) availabilities = availabilities(space.availabilities, 'space', user, window[:start], window[:end]) - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [space]) } + if @level == 'slot' + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [space]) } + else + availabilities.map { |a| @service.availability_reserved_status(a, user, [space]) } + end end # list all slots for the given training(s), with visibility relative to the given user @@ -32,7 +41,23 @@ class Availabilities::AvailabilitiesService .where('trainings_availabilities.training_id': trainings.map(&:id)) availabilities = availabilities(tr_availabilities, 'training', user, window[:start], window[:end]) - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, s.availability.trainings) } + if @level == 'slot' + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, s.availability.trainings) } + else + availabilities.map { |a| @service.availability_reserved_status(a, user, a.trainings) } + end + end + + # list all slots for the given event(s), with visibility relative to the given user + def events(events, user, window) + ev_availabilities = Availability.includes('event').where('events.id': events.map(&:id)) + availabilities = availabilities(ev_availabilities, 'event', user, window[:start], window[:end]) + + if @level == 'slot' + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, [s.availability.event]) } + else + availabilities.map { |a| @service.availability_reserved_status(a, user, [a.event]) } + end end private @@ -45,7 +70,7 @@ class Availabilities::AvailabilitiesService # the trainings further in the futur. This is used to prevent users with a rolling subscription to take # their first training in a very long delay. def show_more_trainings?(user) - user.trainings.size.positive? && subscription_year?(user) + user&.trainings&.size&.positive? && subscription_year?(user) end def availabilities(availabilities, type, user, range_start, range_end) @@ -53,8 +78,10 @@ class Availabilities::AvailabilitiesService # 1) an admin (he can see all availabilities from 1 month ago to anytime in the future) if @current_user&.admin? || @current_user&.manager? window_start = [range_start, 1.month.ago].max - availabilities.includes(:tags, :plans) - .where('start_at <= ? AND end_at >= ? AND available_type = ?', range_end, window_start, type) + availabilities.includes(:tags, :plans, :slots) + .joins(:slots) + .where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', range_end, window_start, type) + .where('slots.start_at > ? AND slots.end_at < ?', window_start, range_end) .where(lock: false) # 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future) else @@ -63,9 +90,11 @@ class Availabilities::AvailabilitiesService end_at = @maximum_visibility[:year] if show_more_trainings?(user) && type == 'training' window_end = [end_at, range_end].min window_start = [range_start, DateTime.current].max - availabilities.includes(:tags, :plans) - .where('start_at < ? AND end_at > ? AND available_type = ?', window_end, window_start, type) - .where('availability_tags.tag_id' => user.tag_ids.concat([nil])) + availabilities.includes(:tags, :plans, :slots) + .joins(:slots) + .where('availabilities.start_at <= ? AND availabilities.end_at >= ? AND available_type = ?', window_end, window_start, type) + .where('slots.start_at > ? AND slots.end_at < ?', window_start, window_end) + .where('availability_tags.tag_id' => user&.tag_ids&.concat([nil])) .where(lock: false) end end diff --git a/app/services/availabilities/create_availabilities_service.rb b/app/services/availabilities/create_availabilities_service.rb index 5535d05e0..45961c7a0 100644 --- a/app/services/availabilities/create_availabilities_service.rb +++ b/app/services/availabilities/create_availabilities_service.rb @@ -1,44 +1,47 @@ # frozen_string_literal: true -# Provides helper methods to create an Availability with multiple occurrences +# Provides an helper method to create the slots for an Availability and optionnaly, for its multiple occurrences class Availabilities::CreateAvailabilitiesService def create(availability, occurrences = []) availability.update_attributes(occurrence_id: availability.id) - slot_duration = availability.slot_duration || Setting.get('slot_duration').to_i + create_slots(availability) occurrences.each do |o| start_at = Time.zone.parse(o[:start_at]) end_at = Time.zone.parse(o[:end_at]) - avail = if availability.start_at == start_at && availability.end_at == end_at - availability - else - Availability.create!( - start_at: start_at, - end_at: end_at, - available_type: availability.available_type, - is_recurrent: availability.is_recurrent, - period: availability.period, - nb_periods: availability.nb_periods, - end_date: availability.end_date, - occurrence_id: availability.occurrence_id, - machine_ids: availability.machine_ids, - training_ids: availability.training_ids, - space_ids: availability.space_ids, - tag_ids: availability.tag_ids, - nb_total_places: availability.nb_total_places, - slot_duration: availability.slot_duration, - plan_ids: availability.plan_ids - ) - end + next if availability.start_at == start_at && availability.end_at == end_at - ((end_at - start_at) / slot_duration.minutes).to_i.times do |i| - Slot.new( - start_at: start_at + (i * slot_duration).minutes, - end_at: start_at + (i * slot_duration).minutes + slot_duration.minutes, - availability_id: avail.id - ).save! - end + occ = Availability.create!( + start_at: start_at, + end_at: end_at, + available_type: availability.available_type, + is_recurrent: availability.is_recurrent, + period: availability.period, + nb_periods: availability.nb_periods, + end_date: availability.end_date, + occurrence_id: availability.occurrence_id, + machine_ids: availability.machine_ids, + training_ids: availability.training_ids, + space_ids: availability.space_ids, + tag_ids: availability.tag_ids, + nb_total_places: availability.nb_total_places, + slot_duration: availability.slot_duration, + plan_ids: availability.plan_ids + ) + create_slots(occ) + end + end + + def create_slots(availability) + slot_duration = availability.slot_duration || Setting.get('slot_duration').to_i + + ((availability.end_at - availability.start_at) / slot_duration.minutes).to_i.times do |i| + Slot.new( + start_at: availability.start_at + (i * slot_duration).minutes, + end_at: availability.start_at + (i * slot_duration).minutes + slot_duration.minutes, + availability_id: availability.id + ).save! end end end diff --git a/app/services/availabilities/public_availabilities_service.rb b/app/services/availabilities/public_availabilities_service.rb index ca3b77861..88a9103d4 100644 --- a/app/services/availabilities/public_availabilities_service.rb +++ b/app/services/availabilities/public_availabilities_service.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true # Provides helper methods for public calendar of Availability -# FIXME, Availabilities::StatusService was refactored -# TODO, Use Availabilities::AvailabilitiesService class Availabilities::PublicAvailabilitiesService def initialize(current_user) @current_user = current_user @@ -10,95 +8,39 @@ class Availabilities::PublicAvailabilitiesService end # provides a list of slots and availabilities for the machines, between the given dates - def machines(start_date, end_date, reservations, machine_ids) - availabilities = Availability.includes(:tags, :machines) - .where(available_type: 'machines') - .where('start_at >= ? AND end_at <= ?', start_date, end_date) - .where(lock: false) + def machines(window, machine_ids, level) + machine_ids = [] if machine_ids.nil? + service = Availabilities::AvailabilitiesService.new(@current_user, level) slots = [] - availabilities.each do |a| - slot_duration = a.slot_duration || Setting.get('slot_duration').to_i - a.machines.each do |machine| - next unless machine_ids&.include?(machine.id.to_s) - - ((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i| - slot = Slot.new( - start_at: a.start_at + (i * slot_duration).minutes, - end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes, - availability_id: a.id, - availability: a, - machine: machine, - title: machine.name - ) - slot = @service.machine_reserved_status(slot, reservations, @current_user) - slots << slot - end - end + machine_ids.each do |machine_id| + machine = Machine.friendly.find(machine_id) + slots.concat(service.machines(machine, @current_user, window)) end - { availabilities: availabilities, slots: slots } + slots end # provides a list of slots and availabilities for the spaces, between the given dates - def spaces(start_date, end_date, reservations, available_id) - availabilities = Availability.includes(:tags, :spaces).where(available_type: 'space') - .where('start_at >= ? AND end_at <= ?', start_date, end_date) - .where(lock: false) - - availabilities.where(available_id: available_id) if available_id - + def spaces(window, spaces_ids, level) + spaces_ids = [] if spaces_ids.nil? + service = Availabilities::AvailabilitiesService.new(@current_user, level) slots = [] - availabilities.each do |a| - slot_duration = a.slot_duration || Setting.get('slot_duration').to_i - space = a.spaces.first - ((a.end_at - a.start_at) / slot_duration.minutes).to_i.times do |i| - next unless (a.start_at + (i * slot_duration).minutes) > DateTime.current - - slot = Slot.new( - start_at: a.start_at + (i * slot_duration).minutes, - end_at: a.start_at + (i * slot_duration).minutes + slot_duration.minutes, - availability_id: a.id, - availability: a, - space: space, - title: space.name - ) - slot = @service.space_reserved_status(slot, reservations, @current_user) - slots << slot - end + spaces_ids.each do |space_id| + space = Space.friendly.find(space_id) + slots.concat(service.spaces(space, @current_user, window)) end - { availabilities: availabilities, slots: slots } + slots end - def public_availabilities(start_date, end_date, reservations, ids) - if in_same_day(start_date, end_date) - # request for 1 single day + def public_availabilities(window, ids, events = false) + level = in_same_day(window[:start], window[:end]) ? 'slot' : 'availability' + service = Availabilities::AvailabilitiesService.new(@current_user, level) - # trainings, events - training_event_availabilities = Availability.includes(:tags, :trainings, :slots) - .where(available_type: %w[training event]) - .where('start_at >= ? AND end_at <= ?', start_date, end_date) - .where(lock: false) - # machines - machines_avail = machines(start_date, end_date, reservations, ids[:machines]) - machine_slots = machines_avail[:slots] - # spaces - spaces_avail = spaces(start_date, end_date, reservations, ids[:spaces]) - space_slots = spaces_avail[:slots] + machines_slots = machines(window, ids[:machines], level) + spaces_slots = spaces(window, ids[:spaces], level) + trainings_slots = service.trainings(Training.where(id: ids[:trainings]), @current_user, window) + events_slots = events ? service.events(Event.all, @current_user, window) : [] - [].concat(training_event_availabilities).concat(machine_slots).concat(space_slots) - else - # request for many days (week or month) - avails = Availability.includes(:tags, :machines, :trainings, :spaces, :event, :slots) - .where('start_at >= ? AND end_at <= ?', start_date, end_date) - .where(lock: false) - avails.each do |a| - if a.available_type == 'training' || a.available_type == 'event' - a = @service.training_event_reserved_status(a, reservations, @current_user) - elsif a.available_type == 'space' - a.is_reserved = @service.reserved_availability?(a, @current_user) - end - end - avails - end + [].concat(trainings_slots).concat(events_slots).concat(machines_slots).concat(spaces_slots) end private diff --git a/app/services/availabilities/status_service.rb b/app/services/availabilities/status_service.rb index 71376245d..7b66b454f 100644 --- a/app/services/availabilities/status_service.rb +++ b/app/services/availabilities/status_service.rb @@ -10,6 +10,10 @@ class Availabilities::StatusService # check that the provided slot is reserved for the given reservable (machine, training or space). # Mark it accordingly for display in the calendar def slot_reserved_status(slot, user, reservables) + unless reservables.map(&:class).map(&:name).reduce(:==) + raise TypeError('[Availabilities::StatusService#slot_reserved_status] reservables have differents types') + end + statistic_profile_id = user&.statistic_profile&.id slots_reservations = slot.slots_reservations @@ -29,16 +33,22 @@ class Availabilities::StatusService end # check that the provided ability is reserved by the given user - def reserved_availability?(availability, user) - if user - reserved_slots = [] - availability.slots.each do |s| - reserved_slots << s if s.canceled_at.nil? - end - reserved_slots.map(&:reservations).flatten.map(&:statistic_profile_id).include? user.statistic_profile&.id - else - false + def availability_reserved_status(availability, user, reservables) + unless reservables.map(&:class).map(&:name).reduce(:==) + raise TypeError('[Availabilities::StatusService#availability_reserved_status] reservables have differents types') end + + slots_reservations = availability.slots_reservations + .includes(:reservation) + .where('reservations.reservable_type': reservables.map(&:class).map(&:name)) + .where('reservations.reservable_id': reservables.map(&:id)) + .where('slots_reservations.canceled_at': nil) + + user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id) + + availability.is_reserved = !slots_reservations.empty? + availability.current_user_slots_reservations_ids = user_slots_reservations.map(&:id) + availability end private diff --git a/app/services/event_service.rb b/app/services/event_service.rb index 860391532..c27c6b47d 100644 --- a/app/services/event_service.rb +++ b/app/services/event_service.rb @@ -166,27 +166,33 @@ class EventService e_params = e_params.merge( event_files_attributes: ef_attributes ) + original_slots_ids = event.availability.slots.map(&:id) begin results[:events].push status: !!e.update(e_params.permit!), event: e # rubocop:disable Style/DoubleNegation rescue StandardError => err results[:events].push status: false, event: e, error: err.try(:record).try(:class).try(:name), message: err.message end - results[:slots].concat(update_slots(e.availability_id)) + results[:slots].concat(update_slots(e.availability_id, original_slots_ids)) end + original_slots_ids = event.availability.slots.map(&:id) begin + event_params[:availability_attributes][:id] = event.availability_id results[:events].push status: !!event.update(event_params), event: event # rubocop:disable Style/DoubleNegation rescue StandardError => err results[:events].push status: false, event: event, error: err.try(:record).try(:class).try(:name), message: err.message end - results[:slots].concat(update_slots(event.availability_id)) + results[:slots].concat(update_slots(event.availability_id, original_slots_ids)) results end - def update_slots(availability_id) + def update_slots(availability_id, original_slots_ids) results = [] avail = Availability.find(availability_id) - avail.slots.each do |s| - results.push(status: !!s.update_attributes(start_at: avail.start_at, end_at: avail.end_at), slot: s) # rubocop:disable Style/DoubleNegation + Slot.where(id: original_slots_ids).each do |slot| + results.push( + status: !!slot.update_attributes(availability_id: availability_id, start_at: avail.start_at, end_at: avail.end_at), # rubocop:disable Style/DoubleNegation + slot: slot + ) rescue StandardError => err results.push status: false, slot: s, error: err.try(:record).try(:class).try(:name), message: err.message end diff --git a/app/views/api/availabilities/public.json.jbuilder b/app/views/api/availabilities/public.json.jbuilder index d656b22fe..23c65b64f 100644 --- a/app/views/api/availabilities/public.json.jbuilder +++ b/app/views/api/availabilities/public.json.jbuilder @@ -1,18 +1,18 @@ +# frozen_string_literal: true + json.array!(@availabilities) do |availability| json.id availability.id json.start availability.start_at.iso8601 json.end availability.end_at.iso8601 json.textColor 'black' json.backgroundColor 'white' - # availability object + # availability object (for weeks/months views) if availability.instance_of? Availability json.title availability.title(@title_filter) - if availability.available_type == 'event' - json.event_id availability.event.id - end - if availability.available_type == 'training' - json.training_id availability.trainings.first.id - end + json.event_id availability.event.id if availability.available_type == 'event' + json.training_id availability.trainings.first.id if availability.available_type == 'training' + json.space_id availability.spaces.first.id if availability.available_type == 'space' + json.machines_ids availability.machines.map(&:id) if availability.available_type == 'machines' json.available_type availability.available_type json.tag_ids availability.tag_ids json.tags availability.tags do |t| @@ -20,33 +20,17 @@ json.array!(@availabilities) do |availability| json.name t.name end - if availability.available_type == 'training' or availability.available_type == 'event' - json.borderColor trainings_events_border_color(availability) - if availability.is_reserved - json.is_reserved true - json.title "#{availability.title}' - #{t('trainings.i_ve_reserved')}" - elsif availability.full? - json.is_completed true - json.title "#{availability.title} - #{t('trainings.completed')}" - end - elsif availability.available_type == 'space' - complete = availability.slots.length >= availability.available_space_places - json.is_completed complete - json.borderColor availability_border_color(availability) - if complete - json.title "#{availability.title} - #{t('trainings.completed')}" - json.borderColor AvailabilityHelper::IS_FULL - end - if availability.is_reserved - json.is_reserved true - json.title "#{availability.title} - #{t('trainings.i_ve_reserved')}" - end - else - json.borderColor availability_border_color(availability) + json.is_completed availability.full? + json.is_reserved availability.is_reserved + json.borderColor availability_border_color(availability) + if availability.is_reserved && !availability.current_user_slots_reservations_ids.empty? + json.title "#{availability.title}' - #{t('trainings.i_ve_reserved')}" + elsif availability.full? + json.title "#{availability.title} - #{t('trainings.completed')}" + json.borderColor AvailabilityHelper::IS_FULL end - # slot object ( here => availability = slot ) - # -> machines / spaces + # slot object ( here => availability = slot ), for daily view elsif availability.instance_of? Slot json.title availability.title json.tag_ids availability.availability.tag_ids @@ -54,14 +38,20 @@ json.array!(@availabilities) do |availability| json.id t.id json.name t.name end - if availability.try(:machine) - json.machine_id availability.machine.id + json.is_reserved availability.is_reserved + json.is_completed availability.full? + if availability.availability.available_type == 'machines' + json.machine_ids availability.availability.machines.map(&:id) json.borderColor machines_slot_border_color(availability) - json.is_reserved availability.is_reserved - elsif availability.try(:space) - json.space_id availability.space.id + elsif availability.availability.available_type == 'space' + json.space_id availability.availability.space.first.id json.borderColor space_slot_border_color(availability) - json.is_completed availability.full? + elsif availability.availability.available_type == 'training' + json.training_id availability.availability.trainings.first.id + json.borderColor trainings_events_border_color(availability) + elsif availability.availability.available_type == 'event' + json.event_id availability.availability.event.id + json.borderColor trainings_events_border_color(availability) else json.title 'Unknown slot' end diff --git a/test/fixtures/availabilities.yml b/test/fixtures/availabilities.yml index 70ba0eb65..a97e96ccb 100644 --- a/test/fixtures/availabilities.yml +++ b/test/fixtures/availabilities.yml @@ -1,8 +1,8 @@ availability_1: id: 1 - start_at: <%= DateTime.current.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: training created_at: 2016-04-04 15:24:01.517486000 Z updated_at: 2016-04-04 15:24:01.517486000 Z @@ -11,8 +11,8 @@ availability_1: availability_2: id: 2 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: training created_at: 2016-04-04 15:24:09.169364000 Z updated_at: 2016-04-04 15:24:09.169364000 Z @@ -21,8 +21,8 @@ availability_2: availability_3: id: 3 - start_at: <%= DateTime.current.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:24:27.587583000 Z updated_at: 2016-04-04 15:24:27.587583000 Z @@ -31,8 +31,8 @@ availability_3: availability_4: id: 4 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:24:44.044908000 Z updated_at: 2016-04-04 15:24:44.044908000 Z @@ -41,8 +41,8 @@ availability_4: availability_5: id: 5 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:25:48.584444000 Z updated_at: 2016-04-04 15:25:48.584444000 Z @@ -51,8 +51,8 @@ availability_5: availability_6: id: 6 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:26:17.953216000 Z updated_at: 2016-04-04 15:26:17.953216000 Z @@ -61,8 +61,8 @@ availability_6: availability_7: id: 7 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:26:39.278627000 Z updated_at: 2016-04-04 15:26:39.278627000 Z @@ -71,8 +71,8 @@ availability_7: availability_8: id: 8 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: training created_at: 2016-04-04 15:26:49.572724000 Z updated_at: 2016-04-04 15:26:49.572724000 Z @@ -132,8 +132,8 @@ availability_13: availability_14: id: 14 - start_at: <%= 20.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 20.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 20.days.from_now.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:44:04.023557000 Z updated_at: 2016-04-04 15:44:04.023557000 Z @@ -142,8 +142,8 @@ availability_14: availability_15: id: 15 - start_at: <%= 40.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 40.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 40.days.from_now.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:44:04.023557000 Z updated_at: 2016-04-04 15:44:04.023557000 Z @@ -152,8 +152,8 @@ availability_15: availability_16: id: 16 - start_at: <%= 80.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 80.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 80.days.from_now.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2016-04-04 15:44:04.023557000 Z updated_at: 2016-04-04 15:44:04.023557000 Z @@ -162,9 +162,9 @@ availability_16: availability_17: id: 17 - start_at: <%= 10.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - available_type: machines + start_at: <%= 10.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + available_type: event created_at: 2016-04-04 15:44:04.023557000 Z updated_at: 2016-04-04 15:44:04.023557000 Z nb_total_places: @@ -172,8 +172,8 @@ availability_17: availability_18: id: 18 - start_at: <%= 2.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 2.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 2.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: space created_at: 2017-02-15 15:53:35.154433000 Z updated_at: 2017-02-15 15:53:35.154433000 Z @@ -182,10 +182,20 @@ availability_18: availability_19: id: 19 - start_at: <%= 1.day.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> available_type: machines created_at: 2017-02-15 15:53:35.154433000 Z updated_at: 2017-02-15 15:53:35.154433000 Z nb_total_places: destroying: false + +availability_20: + id: 20 + start_at: <%= 10.days.ago.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.ago.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + available_type: training + created_at: 2022-07-18 12:38:21.616510000 Z + updated_at: 2022-07-18 12:38:21.616510000 Z + nb_total_places: 5 + destroying: false diff --git a/test/fixtures/invoices.yml b/test/fixtures/invoices.yml index 4fbc79153..2fd5ea166 100644 --- a/test/fixtures/invoices.yml +++ b/test/fixtures/invoices.yml @@ -19,6 +19,7 @@ invoice_1: invoicing_profile_id: 3 operator_profile_id: 3 statistic_profile_id: 3 + invoice_2: id: 2 total: 2000 @@ -39,6 +40,7 @@ invoice_2: invoicing_profile_id: 4 operator_profile_id: 1 statistic_profile_id: 4 + invoice_3: id: 3 total: 3000 @@ -59,6 +61,7 @@ invoice_3: invoicing_profile_id: 7 operator_profile_id: 1 statistic_profile_id: 7 + invoice_4: id: 4 total: 0 @@ -79,6 +82,7 @@ invoice_4: invoicing_profile_id: 7 operator_profile_id: 1 statistic_profile_id: 7 + invoice_5: id: 5 total: 1500 @@ -99,6 +103,7 @@ invoice_5: invoicing_profile_id: 3 operator_profile_id: 1 statistic_profile_id: 3 + invoice_6: id: 6 total: 3000 diff --git a/test/fixtures/invoicing_profiles.yml b/test/fixtures/invoicing_profiles.yml index 626c047bb..ae29e2035 100644 --- a/test/fixtures/invoicing_profiles.yml +++ b/test/fixtures/invoicing_profiles.yml @@ -60,3 +60,10 @@ proudhon: first_name: Pierre-Joseph last_name: Proudhon email: pj.proudhon@la-propriete.org + +acamus: + id: 10 + user_id: 10 + first_name: Albert + last_name: Camus + email: albert.camus@letranger.org diff --git a/test/fixtures/payment_gateway_objects.yml b/test/fixtures/payment_gateway_objects.yml index 3dd7fbc88..29d5ea6b0 100644 --- a/test/fixtures/payment_gateway_objects.yml +++ b/test/fixtures/payment_gateway_objects.yml @@ -208,3 +208,25 @@ pgo30: item_type: PaymentSchedule item_id: 12 payment_gateway_object_id: 29 + +pgo31: + id: 31 + gateway_object_id: fakefakefakefake + gateway_object_type: PayZen::Order + item_type: PaymentSchedule + item_id: 13 + +pgo32: + id: 32 + gateway_object_id: fakefakefakefake + gateway_object_type: PayZen::Token + item_type: PaymentSchedule + item_id: 13 + +pgo33: + id: 33 + gateway_object_id: fakefakefakefake + gateway_object_type: PayZen::Subscription + item_type: PaymentSchedule + item_id: 13 + payment_gateway_object_id: 32 diff --git a/test/fixtures/payment_schedule_items.yml b/test/fixtures/payment_schedule_items.yml index e689becc7..d6506532f 100644 --- a/test/fixtures/payment_schedule_items.yml +++ b/test/fixtures/payment_schedule_items.yml @@ -166,3 +166,172 @@ payment_schedule_item_116: footprint: 9b162c2b342d8c82e9163202a22a405b879d4b2ad2409a61ad10942f743bc576 created_at: '2021-06-14 12:24:45.882343' updated_at: '2021-06-14 12:24:45.884709' + +payment_schedule_item_117: + id: 117 + amount: 9474 + due_date: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466, "adjustment": 8, "other_items": 0}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_118: + id: 118 + amount: 9466 + due_date: <%= 8.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + + +payment_schedule_item_119: + id: 119 + amount: 9466 + due_date: <%= 7.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_120: + id: 120 + amount: 9466 + due_date: <%= 6.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_121: + id: 121 + amount: 9466 + due_date: <%= 5.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_122: + id: 122 + amount: 9466 + due_date: <%= 4.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_123: + id: 123 + amount: 9466 + due_date: <%= 3.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_124: + id: 124 + amount: 9466 + due_date: <%= 2.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_125: + id: 125 + amount: 9466 + due_date: <%= 1.month.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: paid + details: '{"recurring": 9466}' + payment_method: card + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_126: + id: 126 + amount: 9466 + due_date: <%= DateTime.current.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: new + details: '{"recurring": 9466}' + payment_method: + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_127: + id: 127 + amount: 9466 + due_date: <%= 1.month.from_now.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: new + details: '{"recurring": 9466}' + payment_method: + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + +payment_schedule_item_128: + id: 128 + amount: 9466 + due_date: <%= 2.months.from_now.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + state: new + details: '{"recurring": 9466}' + payment_method: + client_secret: + payment_schedule_id: 13 + invoice_id: + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> diff --git a/test/fixtures/payment_schedule_objects.yml b/test/fixtures/payment_schedule_objects.yml index 4a366c042..78b92dc18 100644 --- a/test/fixtures/payment_schedule_objects.yml +++ b/test/fixtures/payment_schedule_objects.yml @@ -1,4 +1,4 @@ -payment_schedule_oject_10: +payment_schedule_object_10: id: 10 object_type: Subscription object_id: 5 @@ -7,3 +7,13 @@ payment_schedule_oject_10: footprint: a1ef3a0c3abac7e02bc11193458c0d1e90a5acf290c9e3b4e7c049a7efcbeb30 created_at: '2021-06-14 12:24:45.890373' updated_at: '2021-06-14 12:24:45.894701' + +payment_schedule_object_11: + id: 11 + object_type: Subscription + object_id: 6 + payment_schedule_id: 13 + main: true + footprint: fakefakefakefakefakefake + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> diff --git a/test/fixtures/payment_schedules.yml b/test/fixtures/payment_schedules.yml index 7ccecb8b6..ba981efbf 100644 --- a/test/fixtures/payment_schedules.yml +++ b/test/fixtures/payment_schedules.yml @@ -13,3 +13,19 @@ payment_schedule_12: operator_profile_id: 1 created_at: '2021-06-14 12:24:45.843714' updated_at: '2021-06-14 12:24:45.908386' + +payment_schedule_13: + id: 13 + total: 180000 + reference: <%= 9.months.ago.utc.strftime('%y%m00310/E') %> + payment_method: card + wallet_amount: + wallet_transaction_id: + coupon_id: + footprint: fakefakefakefakefakefake + environment: development + invoicing_profile_id: 10 + statistic_profile_id: 10 + operator_profile_id: 10 + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> diff --git a/test/fixtures/profiles.yml b/test/fixtures/profiles.yml index 439ba2875..3d7434e38 100644 --- a/test/fixtures/profiles.yml +++ b/test/fixtures/profiles.yml @@ -97,3 +97,14 @@ profile_9: software_mastered: created_at: 2020-06-15 18:03:06.553623000 Z updated_at: 2021-06-14 15:51:39.729052000 Z + +profile_10: + id: 10 + user_id: 10 + first_name: Albert + last_name: Camus + phone: '0752145796' + interest: écriture, philosophie + software_mastered: + created_at: 1913-11-07 07:01:51.498120 + updated_at: 1960-01-04 17:05:09.219841 diff --git a/test/fixtures/slots.yml b/test/fixtures/slots.yml index b5042c3e4..febf5eb03 100644 --- a/test/fixtures/slots.yml +++ b/test/fixtures/slots.yml @@ -17,240 +17,240 @@ slot_2: slot_9: id: 9 - start_at: <%= DateTime.current.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.880751' updated_at: '2022-07-12 15:18:43.880751' availability_id: 3 slot_10: id: 10 - start_at: <%= DateTime.current.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.882957' updated_at: '2022-07-12 15:18:43.882957' availability_id: 3 slot_11: id: 11 - start_at: <%= DateTime.current.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.884691' updated_at: '2022-07-12 15:18:43.884691' availability_id: 3 slot_12: id: 12 - start_at: <%= DateTime.current.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.886431' updated_at: '2022-07-12 15:18:43.886431' availability_id: 3 slot_13: id: 13 - start_at: <%= DateTime.current.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.888074' updated_at: '2022-07-12 15:18:43.888074' availability_id: 3 slot_14: id: 14 - start_at: <%= DateTime.current.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.889691' updated_at: '2022-07-12 15:18:43.889691' availability_id: 3 slot_15: id: 15 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.893096' updated_at: '2022-07-12 15:18:43.893096' availability_id: 4 slot_16: id: 16 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.894777' updated_at: '2022-07-12 15:18:43.894777' availability_id: 4 slot_17: id: 17 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.896423' updated_at: '2022-07-12 15:18:43.896423' availability_id: 4 slot_18: id: 18 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.898021' updated_at: '2022-07-12 15:18:43.898021' availability_id: 4 slot_19: id: 19 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.899592' updated_at: '2022-07-12 15:18:43.899592' availability_id: 4 slot_20: id: 20 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.900938' updated_at: '2022-07-12 15:18:43.900938' availability_id: 4 slot_21: id: 21 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.904013' updated_at: '2022-07-12 15:18:43.904013' availability_id: 5 slot_22: id: 22 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.905470' updated_at: '2022-07-12 15:18:43.905470' availability_id: 5 slot_23: id: 23 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.907030' updated_at: '2022-07-12 15:18:43.907030' availability_id: 5 slot_24: id: 24 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.908585' updated_at: '2022-07-12 15:18:43.908585' availability_id: 5 slot_25: id: 25 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.910138' updated_at: '2022-07-12 15:18:43.910138' availability_id: 5 slot_26: id: 26 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.911643' updated_at: '2022-07-12 15:18:43.911643' availability_id: 5 slot_27: id: 27 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.914664' updated_at: '2022-07-12 15:18:43.914664' availability_id: 6 slot_28: id: 28 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.916047' updated_at: '2022-07-12 15:18:43.916047' availability_id: 6 slot_29: id: 29 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.917304' updated_at: '2022-07-12 15:18:43.917304' availability_id: 6 slot_30: id: 30 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.918798' updated_at: '2022-07-12 15:18:43.918798' availability_id: 6 slot_31: id: 31 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.920194' updated_at: '2022-07-12 15:18:43.920194' availability_id: 6 slot_32: id: 32 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.921662' updated_at: '2022-07-12 15:18:43.921662' availability_id: 6 slot_33: id: 33 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.924285' updated_at: '2022-07-12 15:18:43.924285' availability_id: 7 slot_34: id: 34 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.925669' updated_at: '2022-07-12 15:18:43.925669' availability_id: 7 slot_35: id: 35 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.927038' updated_at: '2022-07-12 15:18:43.927038' availability_id: 7 slot_36: id: 36 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.928407' updated_at: '2022-07-12 15:18:43.928407' availability_id: 7 slot_37: id: 37 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.929907' updated_at: '2022-07-12 15:18:43.929907' availability_id: 7 slot_38: id: 38 - start_at: <%= (DateTime.current + 3.day).utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 3.day).utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 3.day).utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 3.day).utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.931295' updated_at: '2022-07-12 15:18:43.931295' availability_id: 7 @@ -297,680 +297,240 @@ slot_43: slot_44: id: 44 - start_at: <%= 20.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 20.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 20.days.from_now.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({:hour => 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.942392' updated_at: '2022-07-12 15:18:43.942392' availability_id: 14 slot_45: id: 45 - start_at: <%= 20.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 20.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 20.days.from_now.utc.change({:hour => 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.943779' updated_at: '2022-07-12 15:18:43.943779' availability_id: 14 slot_46: id: 46 - start_at: <%= 20.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 20.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 20.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.945154' updated_at: '2022-07-12 15:18:43.945154' availability_id: 14 slot_47: id: 47 - start_at: <%= 20.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 20.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 20.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 20.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.946515' updated_at: '2022-07-12 15:18:43.946515' availability_id: 14 slot_48: id: 48 - start_at: <%= 40.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 40.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 40.days.from_now.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({:hour => 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.949178' updated_at: '2022-07-12 15:18:43.949178' availability_id: 15 slot_49: id: 49 - start_at: <%= 40.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 40.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 40.days.from_now.utc.change({:hour => 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.950348' updated_at: '2022-07-12 15:18:43.950348' availability_id: 15 slot_50: id: 50 - start_at: <%= 40.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 40.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 40.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.951535' updated_at: '2022-07-12 15:18:43.951535' availability_id: 15 slot_51: id: 51 - start_at: <%= 40.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 40.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 40.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 40.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.952864' updated_at: '2022-07-12 15:18:43.952864' availability_id: 15 slot_52: id: 52 - start_at: <%= 80.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 80.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 80.days.from_now.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({:hour => 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.955443' updated_at: '2022-07-12 15:18:43.955443' availability_id: 16 slot_53: id: 53 - start_at: <%= 80.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 80.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 80.days.from_now.utc.change({:hour => 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.956657' updated_at: '2022-07-12 15:18:43.956657' availability_id: 16 slot_54: id: 54 - start_at: <%= 80.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 80.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 80.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.957811' updated_at: '2022-07-12 15:18:43.957811' availability_id: 16 slot_55: id: 55 - start_at: <%= 80.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 80.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 80.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 80.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.959063' updated_at: '2022-07-12 15:18:43.959063' availability_id: 16 slot_56: id: 56 - start_at: <%= 10.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 10.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 12.days.from_now.utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:43.961319' updated_at: '2022-07-12 15:18:43.961319' availability_id: 17 -slot_57: - id: 57 - start_at: <%= 10.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.962492' - updated_at: '2022-07-12 15:18:43.962492' - availability_id: 17 - -slot_58: - id: 58 - start_at: <%= 10.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.963665' - updated_at: '2022-07-12 15:18:43.963665' - availability_id: 17 - -slot_59: - id: 59 - start_at: <%= 10.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.964853' - updated_at: '2022-07-12 15:18:43.964853' - availability_id: 17 - -slot_60: - id: 60 - start_at: <%= 10.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.966003' - updated_at: '2022-07-12 15:18:43.966003' - availability_id: 17 - -slot_61: - id: 61 - start_at: <%= 10.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.967147' - updated_at: '2022-07-12 15:18:43.967147' - availability_id: 17 - -slot_62: - id: 62 - start_at: <%= 10.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.968371' - updated_at: '2022-07-12 15:18:43.968371' - availability_id: 17 - -slot_63: - id: 63 - start_at: <%= 10.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.969636' - updated_at: '2022-07-12 15:18:43.969636' - availability_id: 17 - -slot_64: - id: 64 - start_at: <%= 10.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.970899' - updated_at: '2022-07-12 15:18:43.970899' - availability_id: 17 - -slot_65: - id: 65 - start_at: <%= 10.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.972292' - updated_at: '2022-07-12 15:18:43.972292' - availability_id: 17 - -slot_66: - id: 66 - start_at: <%= 10.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.973726' - updated_at: '2022-07-12 15:18:43.973726' - availability_id: 17 - -slot_67: - id: 67 - start_at: <%= 10.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.975239' - updated_at: '2022-07-12 15:18:43.975239' - availability_id: 17 - -slot_68: - id: 68 - start_at: <%= 10.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 10.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.976671' - updated_at: '2022-07-12 15:18:43.976671' - availability_id: 17 - -slot_69: - id: 69 - start_at: <%= 10.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.978191' - updated_at: '2022-07-12 15:18:43.978191' - availability_id: 17 - -slot_70: - id: 70 - start_at: <%= 11.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.979560' - updated_at: '2022-07-12 15:18:43.979560' - availability_id: 17 - -slot_71: - id: 71 - start_at: <%= 11.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.980799' - updated_at: '2022-07-12 15:18:43.980799' - availability_id: 17 - -slot_72: - id: 72 - start_at: <%= 11.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.982141' - updated_at: '2022-07-12 15:18:43.982141' - availability_id: 17 - -slot_73: - id: 73 - start_at: <%= 11.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.983501' - updated_at: '2022-07-12 15:18:43.983501' - availability_id: 17 - -slot_74: - id: 74 - start_at: <%= 11.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.984866' - updated_at: '2022-07-12 15:18:43.984866' - availability_id: 17 - -slot_75: - id: 75 - start_at: <%= 11.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.986257' - updated_at: '2022-07-12 15:18:43.986257' - availability_id: 17 - -slot_76: - id: 76 - start_at: <%= 11.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.987589' - updated_at: '2022-07-12 15:18:43.987589' - availability_id: 17 - -slot_77: - id: 77 - start_at: <%= 11.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.988913' - updated_at: '2022-07-12 15:18:43.988913' - availability_id: 17 - -slot_78: - id: 78 - start_at: <%= 11.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.990191' - updated_at: '2022-07-12 15:18:43.990191' - availability_id: 17 - -slot_79: - id: 79 - start_at: <%= 11.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.991488' - updated_at: '2022-07-12 15:18:43.991488' - availability_id: 17 - -slot_80: - id: 80 - start_at: <%= 11.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.992988' - updated_at: '2022-07-12 15:18:43.992988' - availability_id: 17 - -slot_81: - id: 81 - start_at: <%= 11.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.994311' - updated_at: '2022-07-12 15:18:43.994311' - availability_id: 17 - -slot_82: - id: 82 - start_at: <%= 11.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.995812' - updated_at: '2022-07-12 15:18:43.995812' - availability_id: 17 - -slot_83: - id: 83 - start_at: <%= 11.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.997242' - updated_at: '2022-07-12 15:18:43.997242' - availability_id: 17 - -slot_84: - id: 84 - start_at: <%= 11.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:43.998733' - updated_at: '2022-07-12 15:18:43.998733' - availability_id: 17 - -slot_85: - id: 85 - start_at: <%= 11.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.000009' - updated_at: '2022-07-12 15:18:44.000009' - availability_id: 17 - -slot_86: - id: 86 - start_at: <%= 11.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.001315' - updated_at: '2022-07-12 15:18:44.001315' - availability_id: 17 - -slot_87: - id: 87 - start_at: <%= 11.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.002961' - updated_at: '2022-07-12 15:18:44.002961' - availability_id: 17 - -slot_88: - id: 88 - start_at: <%= 11.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.004321' - updated_at: '2022-07-12 15:18:44.004321' - availability_id: 17 - -slot_89: - id: 89 - start_at: <%= 11.days.from_now.utc.change({hour: 19}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.005828' - updated_at: '2022-07-12 15:18:44.005828' - availability_id: 17 - -slot_90: - id: 90 - start_at: <%= 11.days.from_now.utc.change({hour: 20}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.007295' - updated_at: '2022-07-12 15:18:44.007295' - availability_id: 17 - -slot_91: - id: 91 - start_at: <%= 11.days.from_now.utc.change({hour: 21}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.008631' - updated_at: '2022-07-12 15:18:44.008631' - availability_id: 17 - -slot_92: - id: 92 - start_at: <%= 11.days.from_now.utc.change({hour: 22}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 11.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.010249' - updated_at: '2022-07-12 15:18:44.010249' - availability_id: 17 - -slot_93: - id: 93 - start_at: <%= 11.days.from_now.utc.change({hour: 23}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.011771' - updated_at: '2022-07-12 15:18:44.011771' - availability_id: 17 - -slot_94: - id: 94 - start_at: <%= 12.days.from_now.utc.change({hour: 0}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.013133' - updated_at: '2022-07-12 15:18:44.013133' - availability_id: 17 - -slot_95: - id: 95 - start_at: <%= 12.days.from_now.utc.change({hour: 1}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.014419' - updated_at: '2022-07-12 15:18:44.014419' - availability_id: 17 - -slot_96: - id: 96 - start_at: <%= 12.days.from_now.utc.change({hour: 2}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.015693' - updated_at: '2022-07-12 15:18:44.015693' - availability_id: 17 - -slot_97: - id: 97 - start_at: <%= 12.days.from_now.utc.change({hour: 3}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.016849' - updated_at: '2022-07-12 15:18:44.016849' - availability_id: 17 - -slot_98: - id: 98 - start_at: <%= 12.days.from_now.utc.change({hour: 4}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.018072' - updated_at: '2022-07-12 15:18:44.018072' - availability_id: 17 - -slot_99: - id: 99 - start_at: <%= 12.days.from_now.utc.change({hour: 5}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.019364' - updated_at: '2022-07-12 15:18:44.019364' - availability_id: 17 - -slot_100: - id: 100 - start_at: <%= 12.days.from_now.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.020527' - updated_at: '2022-07-12 15:18:44.020527' - availability_id: 17 - -slot_101: - id: 101 - start_at: <%= 12.days.from_now.utc.change({hour: 7}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %>' - created_at: '2022-07-12 15:18:44.021871' - updated_at: '2022-07-12 15:18:44.021871' - availability_id: 17 - -slot_102: - id: 102 - start_at: <%= 12.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.023185' - updated_at: '2022-07-12 15:18:44.023185' - availability_id: 17 - -slot_103: - id: 103 - start_at: <%= 12.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.024405' - updated_at: '2022-07-12 15:18:44.024405' - availability_id: 17 - -slot_104: - id: 104 - start_at: <%= 12.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.025867' - updated_at: '2022-07-12 15:18:44.025867' - availability_id: 17 - -slot_105: - id: 105 - start_at: <%= 12.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.027282' - updated_at: '2022-07-12 15:18:44.027282' - availability_id: 17 - -slot_106: - id: 106 - start_at: <%= 12.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.028605' - updated_at: '2022-07-12 15:18:44.028605' - availability_id: 17 - -slot_107: - id: 107 - start_at: <%= 12.days.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.029949' - updated_at: '2022-07-12 15:18:44.029949' - availability_id: 17 - -slot_108: - id: 108 - start_at: <%= 12.days.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.031298' - updated_at: '2022-07-12 15:18:44.031298' - availability_id: 17 - -slot_109: - id: 109 - start_at: <%= 12.days.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.032602' - updated_at: '2022-07-12 15:18:44.032602' - availability_id: 17 - -slot_110: - id: 110 - start_at: <%= 12.days.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.034111' - updated_at: '2022-07-12 15:18:44.034111' - availability_id: 17 - -slot_111: - id: 111 - start_at: <%= 12.days.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 12.days.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - created_at: '2022-07-12 15:18:44.035567' - updated_at: '2022-07-12 15:18:44.035567' - availability_id: 17 - slot_112: id: 112 - start_at: <%= 2.days.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 2.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 2.days.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.038089' updated_at: '2022-07-12 15:18:44.038089' availability_id: 18 slot_113: id: 113 - start_at: <%= 2.days.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 2.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 2.days.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.039392' updated_at: '2022-07-12 15:18:44.039392' availability_id: 18 slot_114: id: 114 - start_at: <%= 2.days.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 2.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 2.days.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({:hour => 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.040522' updated_at: '2022-07-12 15:18:44.040522' availability_id: 18 slot_115: id: 115 - start_at: <%= 2.days.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 2.days.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 2.days.from_now.utc.change({:hour => 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 2.days.from_now.utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.041937' updated_at: '2022-07-12 15:18:44.041937' availability_id: 18 slot_116: id: 116 - start_at: <%= 1.day.from_now.utc.change({hour: 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.044421' updated_at: '2022-07-12 15:18:44.044421' availability_id: 19 slot_117: id: 117 - start_at: <%= 1.day.from_now.utc.change({hour: 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 9}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.045689' updated_at: '2022-07-12 15:18:44.045689' availability_id: 19 slot_118: id: 118 - start_at: <%= 1.day.from_now.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.047009' updated_at: '2022-07-12 15:18:44.047009' availability_id: 19 slot_119: id: 119 - start_at: <%= 1.day.from_now.utc.change({hour: 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 11}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.048272' updated_at: '2022-07-12 15:18:44.048272' availability_id: 19 slot_120: id: 120 - start_at: <%= 1.day.from_now.utc.change({hour: 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 12}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.049599' updated_at: '2022-07-12 15:18:44.049599' availability_id: 19 slot_121: id: 121 - start_at: <%= 1.day.from_now.utc.change({hour: 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 13}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.050947' updated_at: '2022-07-12 15:18:44.050947' availability_id: 19 slot_122: id: 122 - start_at: <%= 1.day.from_now.utc.change({hour: 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 14}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.052817' updated_at: '2022-07-12 15:18:44.052817' availability_id: 19 slot_123: id: 123 - start_at: <%= 1.day.from_now.utc.change({hour: 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 15}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.054966' updated_at: '2022-07-12 15:18:44.054966' availability_id: 19 slot_124: id: 124 - start_at: <%= 1.day.from_now.utc.change({hour: 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 16}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.057217' updated_at: '2022-07-12 15:18:44.057217' availability_id: 19 slot_125: id: 125 - start_at: <%= 1.day.from_now.utc.change({hour: 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= 1.day.from_now.utc.change({hour: 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= 1.day.from_now.utc.change({:hour => 17}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 1.day.from_now.utc.change({:hour => 18}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.059135' updated_at: '2022-07-12 15:18:44.059135' availability_id: 19 slot_126: id: 126 - start_at: <%= DateTime.current.utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= DateTime.current.utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= DateTime.current.utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= DateTime.current.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.061887' updated_at: '2022-07-12 15:18:44.061887' availability_id: 1 slot_127: id: 127 - start_at: <%= (DateTime.current + 1.day).utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 1.day).utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 1.day).utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 1.day).utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.063528' updated_at: '2022-07-12 15:18:44.063528' availability_id: 2 slot_128: id: 128 - start_at: <%= (DateTime.current + 2.day).utc.change({hour: 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> - end_at: <%= (DateTime.current + 2.day).utc.change({hour: 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + start_at: <%= (DateTime.current + 2.day).utc.change({:hour => 6}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= (DateTime.current + 2.day).utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> created_at: '2022-07-12 15:18:44.065114' updated_at: '2022-07-12 15:18:44.065114' availability_id: 8 @@ -998,3 +558,11 @@ slot_131: created_at: '2022-07-12 15:18:44.069870' updated_at: '2022-07-12 15:18:44.069870' availability_id: 11 + +slot_132: + id: 132 + start_at: <%= 10.days.ago.utc.change({:hour => 8}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + end_at: <%= 10.days.ago.utc.change({:hour => 10}).strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + created_at: '2022-07-18 12:38:21.616510' + updated_at: '2022-07-18 12:38:21.616510' + availability_id: 20 diff --git a/test/fixtures/statistic_profiles.yml b/test/fixtures/statistic_profiles.yml index 3ecf17ddd..1cb959067 100644 --- a/test/fixtures/statistic_profiles.yml +++ b/test/fixtures/statistic_profiles.yml @@ -60,3 +60,10 @@ proudhon: gender: true birthday: 1809-01-15 group_id: 1 + +acamus: + id: 10 + user_id: 10 + gender: true + birthday: 1913-11-07 + group_id: 1 diff --git a/test/fixtures/subscriptions.yml b/test/fixtures/subscriptions.yml index 87b4fdcd4..711644d4c 100644 --- a/test/fixtures/subscriptions.yml +++ b/test/fixtures/subscriptions.yml @@ -45,3 +45,12 @@ subscription_5: updated_at: 2021-06-14 12:24:45.836045000 Z expiration_date: 2022-06-14 12:24:45.836045000 Z canceled_at: + +subscription_6: + id: 6 + plan_id: 4 + statistic_profile_id: 10 + created_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + updated_at: <%= 9.months.ago.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + expiration_date: <%= 3.months.from_now.utc.strftime('%Y-%m-%d %H:%M:%S.%9N Z') %> + canceled_at: diff --git a/test/fixtures/trainings_availabilities.yml b/test/fixtures/trainings_availabilities.yml index 0827e52de..90ba7736c 100644 --- a/test/fixtures/trainings_availabilities.yml +++ b/test/fixtures/trainings_availabilities.yml @@ -26,3 +26,10 @@ trainings_availability_4: availability_id: 12 created_at: 2020-07-22 10:09:41.841162000 Z updated_at: 2020-07-22 10:09:41.841162000 Z + +trainings_availability_5: + id: 5 + training_id: 2 + availability_id: 20 + created_at: 2020-07-22 10:09:41.841162000 Z + updated_at: 2020-07-22 10:09:41.841162000 Z diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 94b49105a..c98eaebec 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -285,3 +285,35 @@ user_9: auth_token: merged_at: is_allow_newsletter: true + +user_10: + id: 10 + username: acamus + email: albert.camus@letranger.org + encrypted_password: $2a$10$r85b9a2/BZvQsZRPVyhT2uuLnoLpUZ.ch8qca63yvuCn6kC4I2Jam + reset_password_token: + reset_password_sent_at: + remember_created_at: + sign_in_count: 10 + current_sign_in_at: 1960-01-01 11:04:02.566161 + last_sign_in_at: 1960-01-01 11:04:02.566161 + current_sign_in_ip: 177.92.140.29 + last_sign_in_ip: 177.92.140.29 + confirmation_token: + confirmed_at: 1931-11-08 14:29:01.441270 + confirmation_sent_at: 1931-11-07 07:01:51.498120 + unconfirmed_email: + failed_attempts: 0 + unlock_token: + locked_at: + created_at: 1913-11-07 07:01:51.498120 + updated_at: 1960-01-04 17:05:09.219841 + is_allow_contact: true + group_id: 1 + slug: acamus + is_active: true + provider: + uid: + auth_token: + merged_at: + is_allow_newsletter: true diff --git a/test/fixtures/wallets.yml b/test/fixtures/wallets.yml index 77ca99305..a6dc3514d 100644 --- a/test/fixtures/wallets.yml +++ b/test/fixtures/wallets.yml @@ -33,3 +33,7 @@ wallet_8: wallet_9: invoicing_profile_id: 9 amount: 0 + +wallet_10: + invoicing_profile_id: 10 + amount: 0 diff --git a/test/integration/availabilities/as_public_test.rb b/test/integration/availabilities/as_public_test.rb index 234944c4f..9d5e889ce 100644 --- a/test/integration/availabilities/as_public_test.rb +++ b/test/integration/availabilities/as_public_test.rb @@ -67,8 +67,8 @@ class Availabilities::AsPublicTest < ActionDispatch::IntegrationTest end test 'get public events availabilities' do - start_date = DateTime.parse('2016-04-18').to_date - end_date = DateTime.parse('2016-04-24').to_date + start_date = 8.days.from_now.to_date + end_date = 16.days.from_now.to_date get "/api/availabilities/public?start=#{start_date.to_s}&end=#{end_date.to_s}&timezone=Europe%2FParis&evt=true" @@ -100,4 +100,4 @@ class Availabilities::AsPublicTest < ActionDispatch::IntegrationTest def all_spaces Space.all.map { |m| "s%5B%5D=#{m.id}" }.join('&') end -end \ No newline at end of file +end diff --git a/test/integration/availabilities/as_user_test.rb b/test/integration/availabilities/as_user_test.rb index b53ddb478..aae9445a2 100644 --- a/test/integration/availabilities/as_user_test.rb +++ b/test/integration/availabilities/as_user_test.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'test_helper' + class Availabilities::AsUserTest < ActionDispatch::IntegrationTest setup do user = User.find_by(username: 'kdumas') diff --git a/test/integration/reservations/create_as_admin_test.rb b/test/integration/reservations/create_as_admin_test.rb index a13f6b588..25653b6be 100644 --- a/test/integration/reservations/create_as_admin_test.rb +++ b/test/integration/reservations/create_as_admin_test.rb @@ -229,7 +229,7 @@ class Reservations::CreateAsAdminTest < ActionDispatch::IntegrationTest reservation: { reservable_id: machine.id, reservable_type: machine.class.name, - sslots_reservations_attributes: [ + slots_reservations_attributes: [ { slot_id: availability.slots.first.id } diff --git a/test/services/availabilities_service_test.rb b/test/services/availabilities_service_test.rb new file mode 100644 index 000000000..7243885a8 --- /dev/null +++ b/test/services/availabilities_service_test.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'test_helper' + +# Test the service returning the availabilities for the given resources +class AvailabilitiesServiceTest < ActiveSupport::TestCase + setup do + @no_subscription = User.find_by(username: 'jdupond') + @with_subscription = User.find_by(username: 'kdumas') + @with_1y_subscription = User.find_by(username: 'acamus') + @admin = User.find_by(username: 'admin') + end + + test 'no machines availabilities during given window' do + service = Availabilities::AvailabilitiesService.new(@no_subscription) + slots = service.machines(Machine.find(3), @no_subscription, { start: DateTime.current.beginning_of_day, end: 1.day.from_now.end_of_day }) + + assert_empty slots + end + + test 'no machines availabilities for user tags' do + service = Availabilities::AvailabilitiesService.new(@no_subscription) + slots = service.machines(Machine.find(3), @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) + + assert_empty slots + end + + test 'no past availabilities for members' do + service = Availabilities::AvailabilitiesService.new(@no_subscription) + slots = service.machines(Machine.find(2), @no_subscription, { start: DateTime.parse('2015-06-15').beginning_of_day, end: DateTime.parse('2015-06-15').end_of_day }) + + assert_empty slots + end + + test 'admin cannot see past availabilities further than 1 month' do + service = Availabilities::AvailabilitiesService.new(@admin) + slots = service.machines(Machine.find(2), @no_subscription, { start: DateTime.parse('2015-06-15').beginning_of_day, end: DateTime.parse('2015-06-15').end_of_day }) + + assert_empty slots + end + + test 'admin can see past availabilities in 1 month ago' do + service = Availabilities::AvailabilitiesService.new(@admin) + slots = service.trainings([Training.find(2)], @no_subscription, { start: 1.month.ago.beginning_of_day, end: 1.day.ago.end_of_day }) + + assert_not_empty slots + assert_equal Availability.find(20).slots.count, slots.count + assert_equal Availability.find(20).start_at, slots.first.start_at + assert_equal Availability.find(20).end_at, slots.first.end_at + end + + test 'machines availabilities' do + service = Availabilities::AvailabilitiesService.new(@no_subscription) + slots = service.machines(Machine.find(1), @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) + + assert_not_empty slots + assert_equal Availability.find(7).slots.count, slots.count + assert_equal Availability.find(7).start_at, slots.first.start_at + assert_equal Availability.find(7).end_at, slots.last.end_at + end + + test 'spaces availabilities' do + service = Availabilities::AvailabilitiesService.new(@no_subscription) + slots = service.spaces(Space.find(1), @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) + + assert_not_empty slots + assert_equal Availability.find(18).slots.count, slots.count + assert_equal Availability.find(18).start_at, slots.first.start_at + assert_equal Availability.find(18).end_at, slots.last.end_at + end + + test 'trainings availabilities' do + service = Availabilities::AvailabilitiesService.new(@no_subscription) + trainings = [Training.find(1), Training.find(2)] + slots = service.trainings(trainings, @no_subscription, { start: DateTime.current.beginning_of_day, end: 2.days.from_now.end_of_day }) + + assert_not_empty slots + if DateTime.current.hour > 10 + assert_equal Availability.find(2).slots.count, slots.count + else + assert_equal Availability.find(1).slots.count + Availability.find(2).slots.count, slots.count + end + end + + test 'events availability' do + service = Availabilities::AvailabilitiesService.new(@no_subscription) + slots = service.events([Event.find(4)], @no_subscription, { start: DateTime.current.beginning_of_day, end: 30.days.from_now.end_of_day }) + + assert_not_empty slots + assert_equal Availability.find(17).slots.count, slots.count + assert_equal Availability.find(17).start_at, slots.first.start_at + assert_equal Availability.find(17).end_at, slots.first.end_at + end +end diff --git a/test/services/subscription_extension_after_reservation_test.rb b/test/services/subscription_extension_after_reservation_test.rb index aa7e45cb6..07059562c 100644 --- a/test/services/subscription_extension_after_reservation_test.rb +++ b/test/services/subscription_extension_after_reservation_test.rb @@ -14,15 +14,18 @@ class SubscriptionExtensionAfterReservationTest < ActiveSupport::TestCase @user.reservations.destroy_all # ensure no reservations + @slot_reservation_machine = SlotsReservation.new({ slot_id: @machine.availabilities.first.slots.first.id }) + @slot_reservation_training = SlotsReservation.new({ slot_id: @training.availabilities.first.slots.first.id }) + @reservation_machine = Reservation.new( statistic_profile: @user.statistic_profile, reservable: @machine, - slots_reservations: [{ slot_id: @machine.availabilities.first.slots.first.id }] + slots_reservations: [@slot_reservation_machine] ) @reservation_training = Reservation.new( statistic_profile: @user.statistic_profile, reservable: @training, - slots_reservations: [{ slot_id: @training.availabilities.first.slots.first.id }] + slots_reservations: [@slot_reservation_training] ) @reservation_training.save! end @@ -53,6 +56,6 @@ class SubscriptionExtensionAfterReservationTest < ActiveSupport::TestCase test 'method extend_subscription' do SubscriptionExtensionAfterReservation.new(@reservation_training).extend_subscription - assert_equal @reservation_training.slots.first.start_at + @plan.duration, @user.subscription.expired_at + assert_equal @reservation_training.slots_reservations.first.slot.start_at + @plan.duration, @user.subscription.expired_at end end From e955ecc6f8e3ffaacb7e5b2e8b2314b47880915c Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 18 Jul 2022 17:27:40 +0200 Subject: [PATCH 037/141] (bug) create slots when occurences is nil --- app/services/availabilities/create_availabilities_service.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/availabilities/create_availabilities_service.rb b/app/services/availabilities/create_availabilities_service.rb index 45961c7a0..133d448b2 100644 --- a/app/services/availabilities/create_availabilities_service.rb +++ b/app/services/availabilities/create_availabilities_service.rb @@ -2,7 +2,9 @@ # Provides an helper method to create the slots for an Availability and optionnaly, for its multiple occurrences class Availabilities::CreateAvailabilitiesService - def create(availability, occurrences = []) + def create(availability, occurrences) + occurrences = [] if occurrences.nil? + availability.update_attributes(occurrence_id: availability.id) create_slots(availability) From b3795e21ec93c120624e06e3429f624d69b5dc07 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Tue, 19 Jul 2022 08:21:18 +0200 Subject: [PATCH 038/141] bug fix canceled_at was called on slot in members/show.json.jbuilder --- app/views/api/members/show.json.jbuilder | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/api/members/show.json.jbuilder b/app/views/api/members/show.json.jbuilder index ccbbbcb4f..c00788b41 100644 --- a/app/views/api/members/show.json.jbuilder +++ b/app/views/api/members/show.json.jbuilder @@ -17,7 +17,7 @@ json.training_reservations @member.reservations.where(reservable_type: 'Training json.reservable r.reservable json.reservable_type 'Training' json.is_valid @member.statistic_profile.training_ids.include?(r.reservable_id) - json.canceled_at r.slots.first.canceled_at + json.canceled_at r.slots_reservations.first.canceled_at end json.machine_reservations @member.reservations.where(reservable_type: 'Machine') do |r| json.id r.id @@ -25,7 +25,7 @@ json.machine_reservations @member.reservations.where(reservable_type: 'Machine') json.end_at r.slots.first.end_at json.reservable r.reservable json.reservable_type 'Machine' - json.canceled_at r.slots.first.canceled_at + json.canceled_at r.slots_reservations.first.canceled_at end json.space_reservations @member.reservations.where(reservable_type: 'Space') do |r| json.id r.id @@ -33,7 +33,7 @@ json.space_reservations @member.reservations.where(reservable_type: 'Space') do json.end_at r.slots.first.end_at json.reservable r.reservable json.reservable_type 'Space' - json.canceled_at r.slots.first.canceled_at + json.canceled_at r.slots_reservations.first.canceled_at end json.all_projects @member.all_projects do |project| From d26e2ae313d4de30586d2faf5b2cbde95a16ef70 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Jul 2022 11:32:12 +0200 Subject: [PATCH 039/141] (bug) fix various issues due to slots behavior refactoring --- CHANGELOG.md | 1 + Procfile | 2 +- .../api/availabilities_controller.rb | 10 +++---- .../src/javascript/controllers/calendar.js | 4 +-- .../src/javascript/controllers/spaces.js.erb | 9 +++++- .../javascript/controllers/trainings.js.erb | 13 +++++++-- .../admin/settings/reservations.html | 2 +- .../availabilities/availabilities_service.rb | 22 +++++++++------ .../create_availabilities_service.rb | 14 ++++++++-- .../public_availabilities_service.rb | 28 ++----------------- app/services/availabilities/status_service.rb | 10 +++---- .../availabilities/trainings.json.jbuilder | 4 +-- config/locales/app.admin.en.yml | 4 +-- test/services/availabilities_service_test.rb | 12 ++++---- 14 files changed, 69 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ca71b8f..4006b5f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Improved calendars loading time - Refactored and documented the availability-slot-reservation data model +- Display bookers names to connected users now apply to all resources - Fix a bug: unable to book a space's slot with an existing reservation - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty diff --git a/Procfile b/Procfile index 64f020c5e..434aa4a6c 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ -web: bundle exec rails server puma -p $PORT +#web: bundle exec rails server puma -p $PORT worker: bundle exec sidekiq -C ./config/sidekiq.yml webpack: bin/webpacker-dev-server diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index c5d2992c0..d79a171c8 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -73,13 +73,13 @@ class API::AvailabilitiesController < API::ApiController def machine service = Availabilities::AvailabilitiesService.new(current_user) @machine = Machine.friendly.find(params[:machine_id]) - @slots = service.machines(@machine, @customer, window) + @slots = service.machines([@machine], @customer, window) end def trainings service = Availabilities::AvailabilitiesService.new(current_user) - @trainings = if training_id.is_number? || (training_id.length.positive? && training_id != 'all') - [Training.friendly.find(training_id)] + @trainings = if params[:training_id].is_number? || (params[:training_id].length.positive? && params[:training_id] != 'all') + [Training.friendly.find(params[:training_id])] else Training.all end @@ -88,8 +88,8 @@ class API::AvailabilitiesController < API::ApiController def spaces service = Availabilities::AvailabilitiesService.new(current_user) - @space = Space.friendly.find(space_id) - @slots = service.spaces(@space, @customer, window) + @space = Space.friendly.find(params[:space_id]) + @slots = service.spaces([@space], @customer, window) end def reservations diff --git a/app/frontend/src/javascript/controllers/calendar.js b/app/frontend/src/javascript/controllers/calendar.js index d04b0dd06..3d1a1fa59 100644 --- a/app/frontend/src/javascript/controllers/calendar.js +++ b/app/frontend/src/javascript/controllers/calendar.js @@ -16,8 +16,8 @@ * Controller used in the public calendar global */ -Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Slot', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise', - function ($scope, $state, $aside, moment, Availability, Slot, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise) { +Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', 'moment', 'Availability', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise', + function ($scope, $state, $aside, moment, Availability, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise) { /* PRIVATE STATIC CONSTANTS */ let currentMachineEvent = null; machinesPromise.forEach(m => m.checked = true); diff --git a/app/frontend/src/javascript/controllers/spaces.js.erb b/app/frontend/src/javascript/controllers/spaces.js.erb index 7fa10da71..9e76ad8ea 100644 --- a/app/frontend/src/javascript/controllers/spaces.js.erb +++ b/app/frontend/src/javascript/controllers/spaces.js.erb @@ -493,7 +493,14 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi if ($scope.ctrl.member) { Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; - return Availability.spaces({ spaceId: $scope.space.id, member_id: $scope.ctrl.member.id }, function (spaces) { + 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, diff --git a/app/frontend/src/javascript/controllers/trainings.js.erb b/app/frontend/src/javascript/controllers/trainings.js.erb index 7750cfe93..f48a85b8c 100644 --- a/app/frontend/src/javascript/controllers/trainings.js.erb +++ b/app/frontend/src/javascript/controllers/trainings.js.erb @@ -282,10 +282,17 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra if ($scope.ctrl.member) { Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; + const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView'); const id = $transition$.params().id === 'all' ? $transition$.params().id : $scope.training.id; - return Availability.trainings({ trainingId: id, member_id: $scope.ctrl.member.id }, function (trainings) { + 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'); - return $scope.eventSources.splice(0, 1, { + $scope.eventSources.splice(0, 1, { events: trainings, textColor: 'black' } @@ -296,7 +303,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra // as the events are re-fetched for the new user, we must re-init the cart $scope.events.reserved = []; $scope.selectedPlan = null; - return $scope.plansAreShown = false; + $scope.plansAreShown = false; }; /** diff --git a/app/frontend/templates/admin/settings/reservations.html b/app/frontend/templates/admin/settings/reservations.html index e13182ff3..2d99e7ae8 100644 --- a/app/frontend/templates/admin/settings/reservations.html +++ b/app/frontend/templates/admin/settings/reservations.html @@ -179,7 +179,7 @@
    -

    {{ 'app.admin.settings.display_machine_reservation_user_name' }}

    +

    {{ 'app.admin.settings.display_reservation_user_name' }}

    1 raise TypeError('[Availabilities::StatusService#slot_reserved_status] reservables have differents types') end @@ -24,7 +24,7 @@ class Availabilities::StatusService user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': statistic_profile_id) - slot.is_reserved = !slots_reservations.empty? + slot.is_reserved = !user_slots_reservations.empty? slot.title = slot_title(slots_reservations, user_slots_reservations, reservables) slot.can_modify = true if %w[admin manager].include?(@current_user_role) || !user_slots_reservations.empty? slot.current_user_slots_reservations_ids = user_slots_reservations.map(&:id) @@ -34,7 +34,7 @@ class Availabilities::StatusService # check that the provided ability is reserved by the given user def availability_reserved_status(availability, user, reservables) - unless reservables.map(&:class).map(&:name).reduce(:==) + if reservables.map(&:class).map(&:name).uniq.size > 1 raise TypeError('[Availabilities::StatusService#availability_reserved_status] reservables have differents types') end @@ -46,7 +46,7 @@ class Availabilities::StatusService user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id) - availability.is_reserved = !slots_reservations.empty? + availability.is_reserved = !user_slots_reservations.empty? availability.current_user_slots_reservations_ids = user_slots_reservations.map(&:id) availability end diff --git a/app/views/api/availabilities/trainings.json.jbuilder b/app/views/api/availabilities/trainings.json.jbuilder index b35b7fa98..5fc9b27b9 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -2,10 +2,10 @@ json.array!(@slots) do |slot| json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role - json.borderColor trainings_events_border_color(slot) + json.borderColor trainings_events_border_color(slot.availability) json.is_completed slot.full? - json.nb_total_places slot.nb_total_places + json.nb_total_places slot.availability.nb_total_places json.training do json.id slot.availability.trainings.first.id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 517a86b46..fc7ae7a0a 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1373,8 +1373,8 @@ en: visibility_yearly: "maximum visibility for annual subscribers" visibility_others: "maximum visibility for other members" display: "Display" - display_name_info_html: "When enabled, members and visitors browsing the calendar or booking a machine will see the name of the members who has booked some slots. When disabled, only administrators and managers will view the names.
    Warning: if you enable this feature, remember to write it in your privacy policy." - display_machine_reservation_user_name: "Display the full name of the user who booked a machine slot" + display_name_info_html: "When enabled, connected members browsing the calendar or booking a resource will see the name of the members who has already booked some slots. When disabled, only administrators and managers will view the names.
    Warning: if you enable this feature, please write it down in your privacy policy." + display_reservation_user_name: "Display the full name of the user(s) who booked a slots" display_name: "Display the name" display_name_enable: "name display" events_in_the_calendar: "Display the events in the calendar" diff --git a/test/services/availabilities_service_test.rb b/test/services/availabilities_service_test.rb index 7243885a8..74757d0b7 100644 --- a/test/services/availabilities_service_test.rb +++ b/test/services/availabilities_service_test.rb @@ -13,28 +13,28 @@ class AvailabilitiesServiceTest < ActiveSupport::TestCase test 'no machines availabilities during given window' do service = Availabilities::AvailabilitiesService.new(@no_subscription) - slots = service.machines(Machine.find(3), @no_subscription, { start: DateTime.current.beginning_of_day, end: 1.day.from_now.end_of_day }) + slots = service.machines([Machine.find(3)], @no_subscription, { start: DateTime.current.beginning_of_day, end: 1.day.from_now.end_of_day }) assert_empty slots end test 'no machines availabilities for user tags' do service = Availabilities::AvailabilitiesService.new(@no_subscription) - slots = service.machines(Machine.find(3), @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) + slots = service.machines([Machine.find(3)], @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) assert_empty slots end test 'no past availabilities for members' do service = Availabilities::AvailabilitiesService.new(@no_subscription) - slots = service.machines(Machine.find(2), @no_subscription, { start: DateTime.parse('2015-06-15').beginning_of_day, end: DateTime.parse('2015-06-15').end_of_day }) + slots = service.machines([Machine.find(2)], @no_subscription, { start: DateTime.parse('2015-06-15').beginning_of_day, end: DateTime.parse('2015-06-15').end_of_day }) assert_empty slots end test 'admin cannot see past availabilities further than 1 month' do service = Availabilities::AvailabilitiesService.new(@admin) - slots = service.machines(Machine.find(2), @no_subscription, { start: DateTime.parse('2015-06-15').beginning_of_day, end: DateTime.parse('2015-06-15').end_of_day }) + slots = service.machines([Machine.find(2)], @no_subscription, { start: DateTime.parse('2015-06-15').beginning_of_day, end: DateTime.parse('2015-06-15').end_of_day }) assert_empty slots end @@ -51,7 +51,7 @@ class AvailabilitiesServiceTest < ActiveSupport::TestCase test 'machines availabilities' do service = Availabilities::AvailabilitiesService.new(@no_subscription) - slots = service.machines(Machine.find(1), @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) + slots = service.machines([Machine.find(1)], @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) assert_not_empty slots assert_equal Availability.find(7).slots.count, slots.count @@ -61,7 +61,7 @@ class AvailabilitiesServiceTest < ActiveSupport::TestCase test 'spaces availabilities' do service = Availabilities::AvailabilitiesService.new(@no_subscription) - slots = service.spaces(Space.find(1), @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) + slots = service.spaces([Space.find(1)], @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) assert_not_empty slots assert_equal Availability.find(18).slots.count, slots.count From fa45917d6fa63730afe0686cea245770d1eccd77 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Jul 2022 12:13:50 +0200 Subject: [PATCH 040/141] added rubocop-rails --- .rubocop.yml | 4 +++- Gemfile | 3 ++- Gemfile.lock | 7 ++++++- test/integration/subscriptions/create_as_admin_test.rb | 1 - test/integration/subscriptions/create_as_user_test.rb | 2 +- test/integration/subscriptions/renew_as_admin_test.rb | 1 - test/integration/subscriptions/renew_as_user_test.rb | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index a3b552137..b037a56b2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,6 +1,8 @@ +require: rubocop-rails + AllCops: NewCops: enable -Metrics/LineLength: +Layout/LineLength: Max: 140 Metrics/MethodLength: Max: 35 diff --git a/Gemfile b/Gemfile index a8b962ea1..20eb79493 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,8 @@ group :development do gem 'rb-readline' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'railroady' - gem 'rubocop', '~> 1.31.2' + gem 'rubocop', '~> 1.31', require: false + gem 'rubocop-rails', require: false gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end diff --git a/Gemfile.lock b/Gemfile.lock index c76abd469..278d67d6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -370,6 +370,10 @@ GEM unicode-display_width (>= 1.4.0, < 3.0) rubocop-ast (1.19.1) parser (>= 3.1.1.0) + rubocop-rails (2.15.2) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.7.0, < 2.0) ruby-progressbar (1.10.1) ruby-rc4 (0.1.5) ruby-vips (2.1.4) @@ -541,7 +545,8 @@ DEPENDENCIES repost responders (~> 2.0) rolify - rubocop (~> 1.31.2) + rubocop (~> 1.31) + rubocop-rails rubyXL rubyzip (>= 1.3.0) sassc (= 2.1.0) diff --git a/test/integration/subscriptions/create_as_admin_test.rb b/test/integration/subscriptions/create_as_admin_test.rb index 54856a5e3..bf38753b6 100644 --- a/test/integration/subscriptions/create_as_admin_test.rb +++ b/test/integration/subscriptions/create_as_admin_test.rb @@ -38,7 +38,6 @@ class Subscriptions::CreateAsAdminTest < ActionDispatch::IntegrationTest subscription = Invoice.find(result[:id]).invoice_items.first.object assert_equal plan.id, subscription.plan_id, 'subscribed plan does not match' - # Check that the user has only one subscription assert_equal 1, user.subscriptions.count diff --git a/test/integration/subscriptions/create_as_user_test.rb b/test/integration/subscriptions/create_as_user_test.rb index 8679e1c8d..b947be596 100644 --- a/test/integration/subscriptions/create_as_user_test.rb +++ b/test/integration/subscriptions/create_as_user_test.rb @@ -100,7 +100,7 @@ class Subscriptions::CreateAsUserTest < ActionDispatch::IntegrationTest assert_equal Mime[:json], response.content_type # Check the error was handled - assert_match /plan is not compatible/, response.body + assert_match(/plan is not compatible/, response.body) # Check that the user has no subscription assert_nil @user.subscription, "user's subscription was found" diff --git a/test/integration/subscriptions/renew_as_admin_test.rb b/test/integration/subscriptions/renew_as_admin_test.rb index a454bacda..f24e98ed0 100644 --- a/test/integration/subscriptions/renew_as_admin_test.rb +++ b/test/integration/subscriptions/renew_as_admin_test.rb @@ -4,7 +4,6 @@ require 'test_helper' module Subscriptions; end - class Subscriptions::RenewAsAdminTest < ActionDispatch::IntegrationTest setup do @admin = User.find_by(username: 'admin') diff --git a/test/integration/subscriptions/renew_as_user_test.rb b/test/integration/subscriptions/renew_as_user_test.rb index d238fe066..bab37f8a5 100644 --- a/test/integration/subscriptions/renew_as_user_test.rb +++ b/test/integration/subscriptions/renew_as_user_test.rb @@ -104,7 +104,7 @@ class Subscriptions::RenewAsUserTest < ActionDispatch::IntegrationTest assert_equal Mime[:json], response.content_type # Check the error was handled - assert_match /Your card was declined/, response.body + assert_match(/Your card was declined/, response.body) # Check that the user's subscription has not changed assert_equal previous_expiration, @user.subscription.expired_at.to_i, "user's subscription has changed" From 06ee1acea55b179fc859933276163eaed5bba47d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Jul 2022 15:40:28 +0200 Subject: [PATCH 041/141] (bug) fix slot reservation is considered full --- Procfile | 2 +- app/controllers/api/payzen_controller.rb | 2 +- app/controllers/api/stripe_controller.rb | 5 ++--- .../src/javascript/controllers/machines.js.erb | 2 +- app/frontend/src/javascript/directives/cart.js | 10 +++++----- app/services/availabilities/status_service.rb | 6 +++--- app/views/api/availabilities/_slot.json.jbuilder | 1 + config/locales/app.admin.en.yml | 2 ++ test/integration/reservations/restricted_test.rb | 2 +- 9 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Procfile b/Procfile index 434aa4a6c..64f020c5e 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ -#web: bundle exec rails server puma -p $PORT +web: bundle exec rails server puma -p $PORT worker: bundle exec sidekiq -C ./config/sidekiq.yml webpack: bin/webpacker-dev-server diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index acc47ea14..1f528c1a5 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -55,7 +55,7 @@ class API::PayzenController < API::PaymentsController def check_cart cart = shopping_cart - render json: { error: 'unable to pay' }, status: :unprocessable_entity and return unless cart.valid? + render json: { error: 'invalid shopping cart' }, status: :unprocessable_entity and return unless cart.valid? render json: { cart: 'ok' }, status: :ok end diff --git a/app/controllers/api/stripe_controller.rb b/app/controllers/api/stripe_controller.rb index 011ae9364..f08659386 100644 --- a/app/controllers/api/stripe_controller.rb +++ b/app/controllers/api/stripe_controller.rb @@ -19,9 +19,8 @@ class API::StripeController < API::PaymentsController res = nil # json of the API answer cart = shopping_cart - unless cart.valid? - render json: { error: 'unable to pay' }, status: :unprocessable_entity and return - end + render json: { error: 'invalid shopping cart' }, status: :unprocessable_entity and return unless cart.valid? + begin amount = debit_amount(cart) # will contains the amount and the details of each invoice lines if params[:payment_method_id].present? diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index 496199cd9..deb7a41ed 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -697,7 +697,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran return; } $scope.selectedEvent = event; - return $scope.selectionTime = new Date(); + $scope.selectionTime = new Date(); }; /** diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index d91637b59..6c097dff6 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -517,8 +517,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', $scope.slot.group_ids = $scope.slot.plansGrouped.map(function (g) { return g.id; }); } - if (!$scope.slot.is_reserved && !$scope.events.modifiable && !$scope.slot.is_completed) { - // slot is not reserved and we are not currently modifying a slot + if (!$scope.slot.is_completed && !$scope.events.modifiable) { + // slot is not fully reserved, and we are not currently modifying a slot // -> can be added to cart or removed if already present const index = _.findIndex($scope.events.reserved, (e) => e._id === $scope.slot._id); if (index === -1) { @@ -538,9 +538,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', resetCartState(); // finally, we update the prices return updateCartPrice(); - } else if (!$scope.slot.is_reserved && !$scope.slot.is_completed && $scope.events.modifiable) { - // slot is not reserved but we are currently modifying a slot - // -> we request the calender to change the rendering + } else if (!$scope.slot.is_completed && $scope.events.modifiable) { + // slot is not fully reserved, but we are currently modifying a slot + // -> we request the calendar to change the rendering if (typeof $scope.onSlotModifyUnselect === 'function') { // if the callback return false, cancel the selection for the current modification const res = $scope.onSlotModifyUnselect(); diff --git a/app/services/availabilities/status_service.rb b/app/services/availabilities/status_service.rb index f861dbb6b..f9336e9f9 100644 --- a/app/services/availabilities/status_service.rb +++ b/app/services/availabilities/status_service.rb @@ -24,7 +24,7 @@ class Availabilities::StatusService user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': statistic_profile_id) - slot.is_reserved = !user_slots_reservations.empty? + slot.is_reserved = !slots_reservations.empty? slot.title = slot_title(slots_reservations, user_slots_reservations, reservables) slot.can_modify = true if %w[admin manager].include?(@current_user_role) || !user_slots_reservations.empty? slot.current_user_slots_reservations_ids = user_slots_reservations.map(&:id) @@ -46,7 +46,7 @@ class Availabilities::StatusService user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id) - availability.is_reserved = !user_slots_reservations.empty? + availability.is_reserved = !slots_reservations.empty? availability.current_user_slots_reservations_ids = user_slots_reservations.map(&:id) availability end @@ -62,7 +62,7 @@ class Availabilities::StatusService .map(&:user) .map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') } .join(', ') - "#{name} - #{@show_name ? user_names : I18n.t('availabilities.not_available')}" + "#{name} #{@show_name ? "- #{user_names}" : ''}" else "#{name} - #{I18n.t('availabilities.i_ve_reserved')}" end diff --git a/app/views/api/availabilities/_slot.json.jbuilder b/app/views/api/availabilities/_slot.json.jbuilder index d0f256698..0f7730ed2 100644 --- a/app/views/api/availabilities/_slot.json.jbuilder +++ b/app/views/api/availabilities/_slot.json.jbuilder @@ -6,6 +6,7 @@ json.title slot.title json.start slot.start_at.iso8601 json.end slot.end_at.iso8601 json.is_reserved slot.is_reserved +json.is_completed slot.full? json.backgroundColor 'white' json.availability_id slot.availability_id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index fc7ae7a0a..a7f2c0471 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -769,6 +769,8 @@ en: online_payment_info_html: "You can enable your members to book directly online, paying by card. Alternatively, you can restrict the booking and payment processes for administrators and managers." enable_online_payment: "Enable online payment" stripe_keys: "Stripe keys" + public_key: "Public key" + secret_key: "Secret key" error_check_keys: "Error: please check your Stripe keys." stripe_keys_saved: "Stripe keys successfully saved." error_saving_stripe_keys: "Unable to save the Stripe keys. Please try again later." diff --git a/test/integration/reservations/restricted_test.rb b/test/integration/reservations/restricted_test.rb index 982329f98..58f09eeb9 100644 --- a/test/integration/reservations/restricted_test.rb +++ b/test/integration/reservations/restricted_test.rb @@ -142,7 +142,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest end assert_equal 422, response.status - assert_match /unable to pay/, response.body + assert_match(/invalid shopping cart/, response.body) assert_equal reservations_count, Reservation.count assert_equal invoices_count, Invoice.count From c710af04b7c7870185adbfd373483ff08e0c9907 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Jul 2022 15:56:21 +0200 Subject: [PATCH 042/141] (bug) slot title must only contains name of the requested resource --- .../availabilities/availabilities_service.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index 519a20b0a..656e76c0e 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -20,9 +20,9 @@ class Availabilities::AvailabilitiesService availabilities = availabilities(ma_availabilities, 'machines', user, window[:start], window[:end]) if @level == 'slot' - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, s.availability.machines) } + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, (machines & s.availability.machines)) } else - availabilities.map { |a| @service.availability_reserved_status(a, user, a.machines) } + availabilities.map { |a| @service.availability_reserved_status(a, user, (machines & a.machines)) } end end @@ -33,9 +33,9 @@ class Availabilities::AvailabilitiesService availabilities = availabilities(sp_availabilities, 'space', user, window[:start], window[:end]) if @level == 'slot' - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, s.availability.spaces) } + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, (spaces & s.availability.spaces)) } else - availabilities.map { |a| @service.availability_reserved_status(a, user, a.spaces) } + availabilities.map { |a| @service.availability_reserved_status(a, user, (spaces & a.spaces)) } end end @@ -46,9 +46,9 @@ class Availabilities::AvailabilitiesService availabilities = availabilities(tr_availabilities, 'training', user, window[:start], window[:end]) if @level == 'slot' - availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, s.availability.trainings) } + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, (trainings & s.availability.trainings)) } else - availabilities.map { |a| @service.availability_reserved_status(a, user, a.trainings) } + availabilities.map { |a| @service.availability_reserved_status(a, user, (trainings & a.trainings)) } end end From e9a7b609bb249f0322b42e772751de5fd05bdb7d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Jul 2022 16:08:24 +0200 Subject: [PATCH 043/141] (bug) Unable to change the group of a user --- CHANGELOG.md | 1 + app/services/members/members_service.rb | 10 ++++++---- test/integration/members/as_admin_test.rb | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4006b5f0c..a9ff3f58c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Display bookers names to connected users now apply to all resources - Fix a bug: unable to book a space's slot with an existing reservation - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty +- Fix a bug: Unable to change the group of a user ## v5.4.12 2022 July 06 diff --git a/app/services/members/members_service.rb b/app/services/members/members_service.rb index c81005350..9ed5799c3 100644 --- a/app/services/members/members_service.rb +++ b/app/services/members/members_service.rb @@ -36,10 +36,12 @@ class Members::MembersService end end - if params[:invoicing_profile_attributes][:organization] == 'false' - params[:invoicing_profile_attributes].reject! { |p| %w[organization_attributes organization].include?(p) } - else - params[:invoicing_profile_attributes].reject! { |p| p == 'organization' } + if params[:invoicing_profile_attributes] && params[:invoicing_profile_attributes][:organization] + if params[:invoicing_profile_attributes][:organization] == 'false' + params[:invoicing_profile_attributes].reject! { |p| %w[organization_attributes organization].include?(p) } + else + params[:invoicing_profile_attributes].reject! { |p| p == 'organization' } + end end not_complete = member.need_completion? diff --git a/test/integration/members/as_admin_test.rb b/test/integration/members/as_admin_test.rb index 2533a5be4..775d2719d 100644 --- a/test/integration/members/as_admin_test.rb +++ b/test/integration/members/as_admin_test.rb @@ -100,4 +100,23 @@ class MembersTest < ActionDispatch::IntegrationTest assert_match(/Kevin/, res[0][:name]) end + + test 'admin changes the group of a member' do + user = User.find(2) + patch "/api/members/#{user.id}/", + params: { + user: { + id: user.id, + group_id: 2 + } + } + + # Check response format & status + assert_equal 200, response.status, response.body + assert_equal Mime[:json], response.content_type + + # Check search result + res = json_response(response.body) + assert_equal 2, res[:group_id] + end end From b243800f5a70b80209833cce3d3fa071a9628dc2 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Jul 2022 16:29:28 +0200 Subject: [PATCH 044/141] (bug) As admin, unable to create a new member (#374) --- CHANGELOG.md | 1 + app/controllers/api/members_controller.rb | 2 +- app/services/members/members_service.rb | 20 +++++++++++++------- test/integration/members/as_admin_test.rb | 1 + 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ff3f58c..d6feba265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Fix a bug: unable to book a space's slot with an existing reservation - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty - Fix a bug: Unable to change the group of a user +- Fix a bug: As admin, unable to create a new member (#374) ## v5.4.12 2022 July 06 diff --git a/app/controllers/api/members_controller.rb b/app/controllers/api/members_controller.rb index ad314c520..5ca7f455c 100644 --- a/app/controllers/api/members_controller.rb +++ b/app/controllers/api/members_controller.rb @@ -40,7 +40,7 @@ class API::MembersController < API::ApiController def create authorize :user, :create_member? - @member = User.new(user_params.permit!) + @member = User.new(Members::MembersService.handle_organization(user_params.permit!)) members_service = Members::MembersService.new(@member) if members_service.create(current_user, user_params) diff --git a/app/services/members/members_service.rb b/app/services/members/members_service.rb index 9ed5799c3..0adacf571 100644 --- a/app/services/members/members_service.rb +++ b/app/services/members/members_service.rb @@ -36,13 +36,7 @@ class Members::MembersService end end - if params[:invoicing_profile_attributes] && params[:invoicing_profile_attributes][:organization] - if params[:invoicing_profile_attributes][:organization] == 'false' - params[:invoicing_profile_attributes].reject! { |p| %w[organization_attributes organization].include?(p) } - else - params[:invoicing_profile_attributes].reject! { |p| p == 'organization' } - end - end + MembersService.handle_organization(params) not_complete = member.need_completion? up_result = member.update(params) @@ -101,6 +95,18 @@ class Members::MembersService is_updated end + def self.handle_organization(params) + return params unless params[:invoicing_profile_attributes] && params[:invoicing_profile_attributes][:organization] + + if params[:invoicing_profile_attributes][:organization] == 'false' + params[:invoicing_profile_attributes].reject! { |p| %w[organization_attributes organization].include?(p) } + else + params[:invoicing_profile_attributes].reject! { |p| p == 'organization' } + end + + params + end + private def notify_user_profile_complete(previous_state) diff --git a/test/integration/members/as_admin_test.rb b/test/integration/members/as_admin_test.rb index 775d2719d..8a2d08a7e 100644 --- a/test/integration/members/as_admin_test.rb +++ b/test/integration/members/as_admin_test.rb @@ -25,6 +25,7 @@ class MembersTest < ActionDispatch::IntegrationTest phone: '0485232145' }, invoicing_profile_attributes: { + organization: false, address_attributes: { address: '21 grand rue, 73110 Bourget-en-Huile' } From c4d959570f53ff833cfdef72c2734cc8d82baa7f Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 19 Jul 2022 17:32:55 +0200 Subject: [PATCH 045/141] code linting --- app/services/accounting_export_service.rb | 35 ++++++++++--------- .../exports/accounting_export_test.rb | 11 +++--- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/app/services/accounting_export_service.rb b/app/services/accounting_export_service.rb index 05ae70ce0..0a523083c 100644 --- a/app/services/accounting_export_service.rb +++ b/app/services/accounting_export_service.rb @@ -32,7 +32,7 @@ class AccountingExportService invoices = Invoice.where('created_at >= ? AND created_at <= ?', start_date, end_date).order('created_at ASC') invoices = invoices.where('total > 0') unless export_zeros invoices.each do |i| - puts "processing invoice #{i.id}..." unless Rails.env.test? + Rails.logger.debug { "processing invoice #{i.id}..." } unless Rails.env.test? content << generate_rows(i) end @@ -62,20 +62,25 @@ class AccountingExportService # Generate the "subscription" and "reservation" rows associated with the provided invoice def items_rows(invoice) rows = invoice.subscription_invoice? ? "#{subscription_row(invoice)}\n" : '' - if invoice.main_item.object_type == 'Reservation' + case invoice.main_item.object_type + when 'Reservation' items = invoice.invoice_items.reject { |ii| ii.object_type == 'Subscription' } items.each do |item| rows << "#{reservation_row(invoice, item)}\n" end - elsif invoice.main_item.object_type == 'WalletTransaction' + when 'WalletTransaction' rows << "#{wallet_row(invoice)}\n" - elsif invoice.main_item.object_type == 'StatisticProfilePrepaidPack' + when 'StatisticProfilePrepaidPack' rows << "#{pack_row(invoice)}\n" - elsif invoice.main_item.object_type == 'Error' + when 'Error' items = invoice.invoice_items.reject { |ii| ii.object_type == 'Subscription' } items.each do |item| rows << "#{error_row(invoice, item)}\n" end + when 'Subscription' + # do nothing, subscription was already handled by subscription_row + else + Rails.logger.warn { "Unknown main object type #{invoice.main_item.object_type}" } end rows end @@ -185,18 +190,14 @@ class AccountingExportService row << invoice.reference when 'line_label' row << line_label - when 'debit_origin' + when 'debit_origin', 'debit_euro' row << method(debit_method).call(invoice, amount) - when 'credit_origin' - row << method(credit_method).call(invoice, amount) - when 'debit_euro' - row << method(debit_method).call(invoice, amount) - when 'credit_euro' + when 'credit_origin', 'credit_euro' row << method(credit_method).call(invoice, amount) when 'lettering' row << '' else - puts "Unsupported column: #{column}" + Rails.logger.debug { "Unsupported column: #{column}" } end row << separator end @@ -214,30 +215,30 @@ class AccountingExportService if invoice.subscription_invoice? Setting.get("accounting_subscription_#{type}") else - puts "WARN: Invoice #{invoice.id} has no subscription" + Rails.logger.debug { "WARN: Invoice #{invoice.id} has no subscription" } end when :reservation if invoice.main_item.object_type == 'Reservation' Setting.get("accounting_#{invoice.main_item.object.reservable_type}_#{type}") else - puts "WARN: Invoice #{invoice.id} has no reservation" + Rails.logger.debug { "WARN: Invoice #{invoice.id} has no reservation" } end when :wallet if invoice.main_item.object_type == 'WalletTransaction' Setting.get("accounting_wallet_#{type}") else - puts "WARN: Invoice #{invoice.id} is not a wallet credit" + Rails.logger.debug { "WARN: Invoice #{invoice.id} is not a wallet credit" } end when :pack if invoice.main_item.object_type == 'StatisticProfilePrepaidPack' Setting.get("accounting_Pack_#{type}") else - puts "WARN: Invoice #{invoice.id} has no prepaid-pack" + Rails.logger.debug { "WARN: Invoice #{invoice.id} has no prepaid-pack" } end when :error Setting.get("accounting_Error_#{type}") else - puts "Unsupported account #{account}" + Rails.logger.debug { "Unsupported account #{account}" } end || '' end diff --git a/test/integration/exports/accounting_export_test.rb b/test/integration/exports/accounting_export_test.rb index 4718cbf4c..52bde6eed 100644 --- a/test/integration/exports/accounting_export_test.rb +++ b/test/integration/exports/accounting_export_test.rb @@ -14,7 +14,8 @@ class Exports::AccountingExportTest < ActionDispatch::IntegrationTest post '/api/accounting/export', params: { query: { - columns: %w[journal_code date account_code account_label piece line_label debit_origin credit_origin debit_euro credit_euro lettering], + columns: %w[journal_code date account_code account_label piece line_label + debit_origin credit_origin debit_euro credit_euro lettering], encoding: 'ISO-8859-1', date_format: '%d/%m/%Y', start_date: '2012-03-12T00:00:00.000Z', @@ -66,7 +67,7 @@ class Exports::AccountingExportTest < ActionDispatch::IntegrationTest card_client_label = Setting.get('accounting_card_client_label') assert_equal card_client_label, data[0][I18n.t('accounting_export.account_label')], 'Account label for card client is wrong' else - STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} was not paid by card" + warn "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} was not paid by card" end assert_equal first_invoice.reference, data[0][I18n.t('accounting_export.piece')], 'Piece (invoice reference) is wrong' @@ -76,14 +77,14 @@ class Exports::AccountingExportTest < ActionDispatch::IntegrationTest data[0][I18n.t('accounting_export.line_label')], 'Line label does not contains the reference to the invoiced item' else - STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} does not have a subscription" + warn "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} does not have a subscription" end if first_invoice.wallet_transaction_id.nil? assert_equal first_invoice.total / 100.00, data[0][I18n.t('accounting_export.debit_origin')].to_f, 'Origin debit amount does not match' assert_equal first_invoice.total / 100.00, data[0][I18n.t('accounting_export.debit_euro')].to_f, 'Euro debit amount does not match' else - STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} is using wallet" + warn "WARNING: unable to test accurately accounting export: invoice #{first_invoice.id} is using wallet" end assert_equal 0, data[0][I18n.t('accounting_export.credit_origin')].to_f, 'Credit origin amount does not match' @@ -130,7 +131,7 @@ class Exports::AccountingExportTest < ActionDispatch::IntegrationTest assert_equal machine_label, item_row[I18n.t('accounting_export.account_label')], 'Account label for machine reservation is wrong' else - STDERR.puts "WARNING: unable to test accurately accounting export: invoice #{machine_invoice.id} is not a Machine reservation" + warn "WARNING: unable to test accurately accounting export: invoice #{machine_invoice.id} is not a Machine reservation" end # Clean CSV file From 7e0ea151e697ff96ea06cba5ba8fa71a5c4334cd Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 09:55:44 +0200 Subject: [PATCH 046/141] (dep) updated eslint-plugin-fabmanager --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1649dbd36..92a5821c0 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@typescript-eslint/parser": "^5.17.0", "eslint": "~8.12.0", "eslint-config-standard": "~17.0.0-1", - "eslint-plugin-fabmanager": "^0.5.5", + "eslint-plugin-fabmanager": "^0.5.6", "eslint-plugin-html-erb": "^1.0.1", "eslint-plugin-import": "~2.25.4", "eslint-plugin-n": "^15.1.0", diff --git a/yarn.lock b/yarn.lock index a5c08fa21..e752fae62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4082,10 +4082,10 @@ eslint-plugin-es@^4.1.0: eslint-utils "^2.0.0" regexpp "^3.0.0" -eslint-plugin-fabmanager@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-fabmanager/-/eslint-plugin-fabmanager-0.5.5.tgz#675ab9e34fed0d92a3f290d1c38ba377d43d3a77" - integrity sha512-5rXy6UHYkT5Ql0m4nQZ0X+JgxYUMJqppG1ECQqMlp2IqBgJGaHePdcIXdXe8i0pXRfF7VmlIL0pN3ZaOxkm6sw== +eslint-plugin-fabmanager@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/eslint-plugin-fabmanager/-/eslint-plugin-fabmanager-0.5.6.tgz#d08222d03f0d3906054ac0f3e2389a9fb0f0b1bb" + integrity sha512-xaMCwAy4uqEe5dBnll2rIX5xEIAUCllybdL2wqleN3S7d/HQKSk25qkda8RQHcE3VdR+Pvysv+WT3DSm6/LopQ== dependencies: requireindex "^1.2.0" From 7c918ff497661faaf9aea6ec34c5ffbfa58faf7a Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 09:56:55 +0200 Subject: [PATCH 047/141] (bug) missing translation --- CHANGELOG.md | 1 + .../src/javascript/components/user/avatar-input.tsx | 7 +++++-- config/locales/app.shared.en.yml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6feba265..2e016d7d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Improved calendars loading time - Refactored and documented the availability-slot-reservation data model - Display bookers names to connected users now apply to all resources +- Fix a bug: missing translation for avatar changing - Fix a bug: unable to book a space's slot with an existing reservation - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty - Fix a bug: Unable to change the group of a user diff --git a/app/frontend/src/javascript/components/user/avatar-input.tsx b/app/frontend/src/javascript/components/user/avatar-input.tsx index dd509889c..1f2b3ae7b 100644 --- a/app/frontend/src/javascript/components/user/avatar-input.tsx +++ b/app/frontend/src/javascript/components/user/avatar-input.tsx @@ -7,6 +7,7 @@ import { FieldPathValue } from 'react-hook-form/dist/types/path'; import { FieldValues } from 'react-hook-form/dist/types/fields'; import { FormInput } from '../form/form-input'; import { Avatar } from './avatar'; +import { useTranslation } from 'react-i18next'; interface AvatarInputProps { register: UseFormRegister, @@ -20,6 +21,8 @@ interface AvatarInputProps { * This component allows to set the user's avatar, in forms managed by react-hook-form. */ export const AvatarInput = ({ currentAvatar, userName, register, setValue, size }: AvatarInputProps) => { + const { t } = useTranslation('shared'); + const [avatar, setAvatar] = useState(currentAvatar); /** * Check if the provided user has a configured avatar @@ -70,8 +73,8 @@ export const AvatarInput = ({ currentAvatar, u
    - {!hasAvatar() && Add an avatar} - {hasAvatar() && Change} + {!hasAvatar() && {t('app.shared.avatar_input.add_an_avatar')}} + {hasAvatar() && {t('app.shared.avatar_input.change')}} Date: Wed, 20 Jul 2022 10:45:42 +0200 Subject: [PATCH 048/141] (bug) for admins and managers, the current password is not requested before changing their own password --- CHANGELOG.md | 1 + .../src/javascript/components/user/change-password.tsx | 8 +++++--- .../src/javascript/components/user/user-profile-form.tsx | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e016d7d9..b04aa1b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Improved calendars loading time - Refactored and documented the availability-slot-reservation data model - Display bookers names to connected users now apply to all resources +- Fix a bug: for admins and managers, the current password is not requested before changing their own password - Fix a bug: missing translation for avatar changing - Fix a bug: unable to book a space's slot with an existing reservation - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty diff --git a/app/frontend/src/javascript/components/user/change-password.tsx b/app/frontend/src/javascript/components/user/change-password.tsx index 27df0179e..d0307476d 100644 --- a/app/frontend/src/javascript/components/user/change-password.tsx +++ b/app/frontend/src/javascript/components/user/change-password.tsx @@ -9,19 +9,21 @@ import { FieldValues } from 'react-hook-form/dist/types/fields'; import { PasswordInput } from './password-input'; import { FormState } from 'react-hook-form/dist/types/form'; import MemberAPI from '../../api/member'; +import { User } from '../../models/user'; interface ChangePasswordProp { register: UseFormRegister, onError: (message: string) => void, currentFormPassword: string, formState: FormState, + user: User, } /** * This component shows a button that trigger a modal dialog to verify the user's current password. * If the user's current password is correct, the modal dialog is closed and the button is replaced by a form to set the new password. */ -export const ChangePassword = ({ register, onError, currentFormPassword, formState }: ChangePasswordProp) => { +export const ChangePassword = ({ register, onError, currentFormPassword, formState, user }: ChangePasswordProp) => { const { t } = useTranslation('shared'); const [isModalOpen, setIsModalOpen] = React.useState(false); @@ -31,8 +33,8 @@ export const ChangePassword = ({ register, onE const { handleSubmit, register: passwordRegister } = useForm<{ password: string }>(); useEffect(() => { - MemberAPI.current().then(user => { - setIsPrivileged(user.role === 'admin' || user.role === 'manager'); + MemberAPI.current().then(operator => { + setIsPrivileged((operator.role === 'admin' || operator.role === 'manager') && user.id !== operator.id); }).catch(error => onError(error)); }, []); diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index 040335ba7..4b8ed9336 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -252,6 +252,7 @@ export const UserProfileForm: React.FC = ({ action, size, { action === 'update' && } {action === 'create' && Date: Wed, 20 Jul 2022 11:22:00 +0200 Subject: [PATCH 049/141] (bug) 2 people can book the same machine slot Also: fix reservation change behavior --- .../src/javascript/controllers/machines.js.erb | 11 ++++++----- app/models/availability.rb | 8 ++++---- app/models/slot.rb | 4 ++-- app/views/api/availabilities/_slot.json.jbuilder | 2 +- app/views/api/availabilities/machine.json.jbuilder | 2 +- app/views/api/availabilities/public.json.jbuilder | 9 +++++---- app/views/api/availabilities/spaces.json.jbuilder | 2 +- app/views/api/availabilities/trainings.json.jbuilder | 2 +- 8 files changed, 21 insertions(+), 19 deletions(-) diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index deb7a41ed..c7d026334 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -480,6 +480,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran */ $scope.markSlotAsModifying = function () { $scope.selectedEvent.backgroundColor = '#eee'; + $scope.selectedEvent.oldTitle = $scope.selectedEvent.title; $scope.selectedEvent.title = _t('app.logged.machines_reserve.i_change'); updateEvents($scope.selectedEvent); }; @@ -508,8 +509,8 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran const save = { slotId: $scope.events.modifiable.slot_id, borderColor: $scope.events.modifiable.borderColor, - user: angular.copy($scope.events.modifiable.user), - title: (!$scope.events.modifiable.user || $scope.currentUser.id === $scope.events.modifiable.user.id) ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available') + users: angular.copy($scope.events.modifiable.users), + title: (!$scope.events.modifiable.users || $scope.events.modifiable.users.map(u => u.id).includes($scope.currentUser.id)) ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available') }; $scope.events.modifiable.backgroundColor = 'white'; @@ -526,7 +527,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran $scope.events.placable.slot_id = save.slotId; $scope.events.placable.is_reserved = true; $scope.events.placable.can_modify = true; - $scope.events.placable.user = angular.copy(save.user); + $scope.events.placable.users = angular.copy(save.users); updateEvents($scope.events.placable); refetchCalendar(); @@ -540,7 +541,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran $scope.events.placable.backgroundColor = 'white'; $scope.events.placable.title = ''; } - $scope.events.modifiable.title = $scope.currentUser.id === $scope.events.modifiable.user.id ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available'); + $scope.events.modifiable.title = $scope.events.modifiable.oldTitle; $scope.events.modifiable.backgroundColor = 'white'; updateEvents($scope.events.placable, $scope.events.modifiable); @@ -727,7 +728,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran angular.forEach(reservation.slots, function (s) { if (slot.start.isSame(s.start_at)) { slot.slot_id = s.id; - slot.user = user; + slot.users = [user]; } }); updateEvents(slot); diff --git a/app/models/availability.rb b/app/models/availability.rb index a794c9a50..e737c6d03 100644 --- a/app/models/availability.rb +++ b/app/models/availability.rb @@ -124,16 +124,16 @@ class Availability < ApplicationRecord end end - def available_places_per_slot + def available_places_per_slot(reservable = nil) case available_type when 'training' - nb_total_places || trainings.map(&:nb_total_places).max + nb_total_places || reservable&.nb_total_places || trainings.map(&:nb_total_places).max when 'event' event.nb_total_places when 'space' - nb_total_places || spaces.map(&:default_places).max + nb_total_places || reservable&.default_places || spaces.map(&:default_places).max when 'machines' - machines.count + reservable.nil? ? machines.count : 1 else raise TypeError end diff --git a/app/models/slot.rb b/app/models/slot.rb index 38eb8d2b6..32334d2ba 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -12,8 +12,8 @@ class Slot < ApplicationRecord attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids - def full? - availability_places = availability.available_places_per_slot + def full?(reservable = nil) + availability_places = availability.available_places_per_slot(reservable) return false if availability_places.nil? slots_reservations.where(canceled_at: nil).count >= availability_places diff --git a/app/views/api/availabilities/_slot.json.jbuilder b/app/views/api/availabilities/_slot.json.jbuilder index 0f7730ed2..45635e41f 100644 --- a/app/views/api/availabilities/_slot.json.jbuilder +++ b/app/views/api/availabilities/_slot.json.jbuilder @@ -6,7 +6,7 @@ json.title slot.title json.start slot.start_at.iso8601 json.end slot.end_at.iso8601 json.is_reserved slot.is_reserved -json.is_completed slot.full? +json.is_completed slot.full?(reservable) json.backgroundColor 'white' json.availability_id slot.availability_id diff --git a/app/views/api/availabilities/machine.json.jbuilder b/app/views/api/availabilities/machine.json.jbuilder index 95646ba52..6296ee385 100644 --- a/app/views/api/availabilities/machine.json.jbuilder +++ b/app/views/api/availabilities/machine.json.jbuilder @@ -1,7 +1,7 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role + json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role, reservable: @machine json.borderColor machines_slot_border_color(slot) json.machine do diff --git a/app/views/api/availabilities/public.json.jbuilder b/app/views/api/availabilities/public.json.jbuilder index 23c65b64f..c5ac42899 100644 --- a/app/views/api/availabilities/public.json.jbuilder +++ b/app/views/api/availabilities/public.json.jbuilder @@ -40,16 +40,17 @@ json.array!(@availabilities) do |availability| end json.is_reserved availability.is_reserved json.is_completed availability.full? - if availability.availability.available_type == 'machines' + case availability.availability.available_type + when 'machines' json.machine_ids availability.availability.machines.map(&:id) json.borderColor machines_slot_border_color(availability) - elsif availability.availability.available_type == 'space' + when 'space' json.space_id availability.availability.space.first.id json.borderColor space_slot_border_color(availability) - elsif availability.availability.available_type == 'training' + when 'training' json.training_id availability.availability.trainings.first.id json.borderColor trainings_events_border_color(availability) - elsif availability.availability.available_type == 'event' + when 'event' json.event_id availability.availability.event.id json.borderColor trainings_events_border_color(availability) else diff --git a/app/views/api/availabilities/spaces.json.jbuilder b/app/views/api/availabilities/spaces.json.jbuilder index 45cb23d0a..2200fb689 100644 --- a/app/views/api/availabilities/spaces.json.jbuilder +++ b/app/views/api/availabilities/spaces.json.jbuilder @@ -1,7 +1,7 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role + json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role, reservable: @space json.is_completed slot.full? json.borderColor space_slot_border_color(slot) diff --git a/app/views/api/availabilities/trainings.json.jbuilder b/app/views/api/availabilities/trainings.json.jbuilder index 5fc9b27b9..b4efdce10 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -1,7 +1,7 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role + json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role, reservable: slot.availability.trainings.first json.borderColor trainings_events_border_color(slot.availability) json.is_completed slot.full? From e0944746a9d4f5e0e881270d3ee4d8cbac1eca9f Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 11:52:38 +0200 Subject: [PATCH 050/141] (bug) unable to cancel a machine reservation + unable to update member profile --- .../javascript/controllers/machines.js.erb | 19 ++++++++++++++++++- .../src/javascript/directives/cart.js | 7 ++++++- app/services/members/members_service.rb | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index c7d026334..a5e771465 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -554,7 +554,24 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran $scope.updateMember = function () { $scope.plansAreShown = false; $scope.selectedPlan = null; - Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; }); + Member.get({ id: $scope.ctrl.member.id }, function (member) { + $scope.ctrl.member = member; + 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' + } + ); + }); + }); }; /** diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index 6c097dff6..c1f678cfb 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -556,7 +556,12 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', // slot is reserved and currently modified // -> we cancel the modification $scope.cancelModifySlot(); - } else if ($scope.slot.is_reserved && (slotCanBeModified($scope.slot) || slotCanBeCanceled($scope.slot)) && !$scope.events.modifiable && ($scope.events.reserved.length === 0)) { + } else if ($scope.slot.is_reserved && + (slotCanBeModified($scope.slot) || slotCanBeCanceled($scope.slot)) && + !$scope.events.modifiable && + ($scope.events.reserved.length === 0) && + $scope.user && + $scope.slot.users.map(u => u.id).includes($scope.user.id)) { // slot is reserved and is ok to be modified or cancelled // but we are not currently running a modification or having any slots in the cart // -> first affect the modification/cancellation rights attributes to the current slot diff --git a/app/services/members/members_service.rb b/app/services/members/members_service.rb index 0adacf571..f89a2c459 100644 --- a/app/services/members/members_service.rb +++ b/app/services/members/members_service.rb @@ -36,7 +36,7 @@ class Members::MembersService end end - MembersService.handle_organization(params) + Members::MembersService.handle_organization(params) not_complete = member.need_completion? up_result = member.update(params) From 165b3e17b9293f5426c82ed8db732eea7d703c56 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 14:59:42 +0200 Subject: [PATCH 051/141] (bug) unable to move or cancel reservations --- .../javascript/controllers/machines.js.erb | 116 +++++------------- .../src/javascript/controllers/spaces.js.erb | 100 +++++---------- .../javascript/controllers/trainings.js.erb | 83 ++++--------- .../src/javascript/directives/cart.js | 28 ++--- app/views/api/members/show.json.jbuilder | 38 +++--- 5 files changed, 119 insertions(+), 246 deletions(-) diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index a5e771465..db407bf2c 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -451,6 +451,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran */ $scope.markSlotAsAdded = function () { $scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR; + $scope.selectedEvent.oldTitle = $scope.selectedEvent.title; $scope.selectedEvent.title = _t('app.logged.machines_reserve.i_reserve'); updateEvents($scope.selectedEvent); }; @@ -461,19 +462,14 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran $scope.markSlotAsRemoved = function (slot) { slot.backgroundColor = 'white'; slot.borderColor = FREE_SLOT_BORDER_COLOR; - slot.title = ''; - slot.isValid = false; - slot.slot_id = null; - slot.is_reserved = false; - slot.can_modify = false; - slot.offered = false; + slot.title = slot.oldTitle; updateEvents(slot); }; /** * Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book' */ - $scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); }; + $scope.slotCancelled = function () { refreshCalendar() }; /** * Change the last selected slot's appearance to looks like 'currently looking for a new destination to exchange' @@ -506,31 +502,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran * When modifying an already booked reservation, callback when the modification was successfully done. */ $scope.modifyMachineSlot = function () { - const save = { - slotId: $scope.events.modifiable.slot_id, - borderColor: $scope.events.modifiable.borderColor, - users: angular.copy($scope.events.modifiable.users), - title: (!$scope.events.modifiable.users || $scope.events.modifiable.users.map(u => u.id).includes($scope.currentUser.id)) ? _t('app.logged.machines_reserve.i_ve_reserved') : _t('app.logged.machines_reserve.not_available') - }; - - $scope.events.modifiable.backgroundColor = 'white'; - $scope.events.modifiable.title = ''; - $scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR; - $scope.events.modifiable.slot_id = null; - $scope.events.modifiable.is_reserved = false; - $scope.events.modifiable.can_modify = false; - updateEvents($scope.events.modifiable); - - $scope.events.placable.title = save.title; - $scope.events.placable.backgroundColor = 'white'; - $scope.events.placable.borderColor = save.borderColor; - $scope.events.placable.slot_id = save.slotId; - $scope.events.placable.is_reserved = true; - $scope.events.placable.can_modify = true; - $scope.events.placable.users = angular.copy(save.users); - updateEvents($scope.events.placable); - - refetchCalendar(); + refreshCalendar(); }; /** @@ -556,21 +528,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran $scope.selectedPlan = null; Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; - 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' - } - ); - }); + refreshCalendar(); }); }; @@ -626,23 +584,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran */ $scope.afterPayment = function (paymentDocument) { Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) { - angular.forEach($scope.events.reserved, function (machineSlot, key) { - machineSlot.is_reserved = true; - machineSlot.can_modify = true; - if ($scope.currentUser.role === 'admin' || ($scope.currentUser.role === 'manager' && reservation.user_id !== $scope.currentUser.id)) { - // an admin or a manager booked for someone else - machineSlot.title = _t('app.logged.machines_reserve.not_available'); - machineSlot.borderColor = UNAVAILABLE_SLOT_BORDER_COLOR; - updateMachineSlot(machineSlot, reservation, $scope.ctrl.member); - } else { - // booked for "myself" - machineSlot.title = _t('app.logged.machines_reserve.i_ve_reserved'); - machineSlot.borderColor = BOOKED_SLOT_BORDER_COLOR; - updateMachineSlot(machineSlot, reservation, $scope.currentUser); - } - machineSlot.backgroundColor = 'white'; - }); - if ($scope.selectedPlan) { $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan); if ($scope.ctrl.member.id === Auth._currentUser.id) { @@ -651,8 +592,14 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran $scope.plansAreShown = false; $scope.selectedPlan = null; } + $scope.ctrl.member.training_credits = angular.copy(reservation.user.training_credits); + $scope.ctrl.member.machine_credits = angular.copy(reservation.user.machine_credits); + if ($scope.ctrl.member.id === Auth._currentUser.id) { + Auth._currentUser.training_credits = angular.copy(reservation.user.training_credits); + Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits); + } - refetchCalendar(); + refreshCalendar(); // trigger the refresh of react components setTimeout(() => { @@ -718,6 +665,27 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran $scope.selectionTime = new Date(); }; + /** + * 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' + } + ); + }); + } + /** * Triggered when fullCalendar tries to graphically render an event block. * Append the event tag into the block, just after the event title. @@ -733,24 +701,6 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran } }; - /** - * After payment, update the id of the newly reserved slot with the id returned by the server. - * This will allow the user to modify the reservation he just booked. The associated user will also be registered - * with the slot. - * @param slot {Object} - * @param reservation {Object} - * @param user {Object} user associated with the slot - */ - const updateMachineSlot = function (slot, reservation, user) { - angular.forEach(reservation.slots, function (s) { - if (slot.start.isSame(s.start_at)) { - slot.slot_id = s.id; - slot.users = [user]; - } - }); - updateEvents(slot); - }; - /** * Update the calendar's display to render the new attributes of the events * @param events Object[] events to update in full-calendar diff --git a/app/frontend/src/javascript/controllers/spaces.js.erb b/app/frontend/src/javascript/controllers/spaces.js.erb index 9e76ad8ea..d14f08733 100644 --- a/app/frontend/src/javascript/controllers/spaces.js.erb +++ b/app/frontend/src/javascript/controllers/spaces.js.erb @@ -391,6 +391,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi */ $scope.markSlotAsAdded = function () { $scope.selectedEvent.backgroundColor = SELECTED_EVENT_BG_COLOR; + $scope.selectedEvent.oldTitle = $scope.selectedEvent.title; updateEvents($scope.selectedEvent); }; @@ -399,27 +400,22 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi */ $scope.markSlotAsRemoved = function (slot) { slot.backgroundColor = 'white'; - slot.title = ''; + slot.title = slot.oldTitle; slot.borderColor = FREE_SLOT_BORDER_COLOR; - slot.slot_id = null; - slot.isValid = false; - slot.is_reserved = false; - slot.can_modify = false; - slot.offered = false; - if (slot.is_completed) { slot.is_completed = false; } updateEvents(slot); }; /** * Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book' */ - $scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); }; + $scope.slotCancelled = function () { refreshCalendar(); }; /** * Change the last selected slot's appearance to looks like 'currently looking for a new destination to exchange' */ $scope.markSlotAsModifying = function () { $scope.selectedEvent.backgroundColor = '#eee'; + $scope.selectedEvent.oldTitle = $scope.selectedEvent.title; $scope.selectedEvent.title = _t('app.logged.space_reserve.i_change'); updateEvents($scope.selectedEvent); }; @@ -445,30 +441,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi * When modifying an already booked reservation, callback when the modification was successfully done. */ $scope.modifySpaceSlot = function () { - const save = { - slotId: $scope.events.modifiable.slot_id, - borderColor: $scope.events.modifiable.borderColor, - title: _t('app.logged.space_reserve.i_ve_reserved') - }; - - $scope.events.modifiable.backgroundColor = 'white'; - $scope.events.modifiable.title = ''; - $scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR; - $scope.events.modifiable.slot_id = null; - $scope.events.modifiable.is_reserved = false; - $scope.events.modifiable.can_modify = false; - if ($scope.events.modifiable.is_completed) { $scope.events.modifiable.is_completed = false; } - updateEvents($scope.events.modifiable); - - $scope.events.placable.title = save.title; - $scope.events.placable.backgroundColor = 'white'; - $scope.events.placable.borderColor = save.borderColor; - $scope.events.placable.slot_id = save.slotId; - $scope.events.placable.is_reserved = true; - $scope.events.placable.can_modify = true; - updateEvents($scope.events.placable); - - refetchCalendar(); + refreshCalendar(); }; /** @@ -493,21 +466,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi if ($scope.ctrl.member) { Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; - 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' - } - ); - }); + refreshCalendar(); }); } // as the events are re-fetched for the new user, we must re-init the cart @@ -568,16 +527,6 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi */ $scope.afterPayment = function (paymentDocument) { Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) { - angular.forEach($scope.events.paid, function (spaceSlot, key) { - spaceSlot.is_reserved = true; - spaceSlot.can_modify = true; - spaceSlot.title = _t('app.logged.space_reserve.i_ve_reserved'); - spaceSlot.backgroundColor = 'white'; - spaceSlot.borderColor = RESERVED_SLOT_BORDER_COLOR; - updateSpaceSlotId(spaceSlot, reservation); - updateEvents(spaceSlot); - }); - if ($scope.selectedPlan) { $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan); if ($scope.ctrl.member.id === Auth._currentUser.id) { @@ -593,7 +542,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits); } - refetchCalendar(); + refreshCalendar(); }); }; @@ -634,6 +583,27 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi $scope.selectionTime = new Date(); }; + /** + * 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' + } + ); + }); + }; + /** * Triggered when fullCalendar tries to graphically render an event block. * Append the event tag into the block, just after the event title. @@ -649,20 +619,6 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi } }; - /** - * After payment, update the id of the newly reserved slot with the id returned by the server. - * This will allow the user to modify the reservation he just booked. - * @param slot {Object} - * @param reservation {Object} - */ - const updateSpaceSlotId = function (slot, reservation) { - angular.forEach(reservation.slots, function (s) { - if (slot.start.isSame(s.start_at)) { - slot.slot_id = s.id; - } - }); - }; - /** * Update the calendar's display to render the new attributes of the events * @param events Object[] events to update in full-calendar diff --git a/app/frontend/src/javascript/controllers/trainings.js.erb b/app/frontend/src/javascript/controllers/trainings.js.erb index f48a85b8c..5bfd46e64 100644 --- a/app/frontend/src/javascript/controllers/trainings.js.erb +++ b/app/frontend/src/javascript/controllers/trainings.js.erb @@ -188,19 +188,13 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra slot.backgroundColor = 'white'; slot.title = slot.training.name; slot.borderColor = FREE_SLOT_BORDER_COLOR; - slot.id = null; - slot.isValid = false; - slot.is_reserved = false; - slot.can_modify = false; - slot.offered = false; - if (slot.is_completed) { slot.is_completed = false; } updateEvents(slot); }; /** * Callback when a slot was successfully cancelled. Reset the slot style as 'ready to book' */ - $scope.slotCancelled = function () { $scope.markSlotAsRemoved($scope.selectedEvent); }; + $scope.slotCancelled = function () { refreshCalendar() }; /** * Change the last selected slot's appearence to looks like 'currently looking for a new destination to exchange' @@ -234,30 +228,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra * When modifying an already booked reservation, callback when the modification was successfully done. */ $scope.modifyTrainingSlot = function () { - const save = { - slotId: $scope.events.modifiable.slot_id, - borderColor: $scope.events.modifiable.borderColor, - title: !AuthService.isAuthorized(['admin', 'manager']) ? $scope.events.placable.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved') : $scope.events.placable.training.name, - }; - - $scope.events.modifiable.backgroundColor = 'white'; - $scope.events.modifiable.title = $scope.events.modifiable.training.name; - $scope.events.modifiable.borderColor = FREE_SLOT_BORDER_COLOR; - $scope.events.modifiable.slot_id = null; - $scope.events.modifiable.is_reserved = false; - $scope.events.modifiable.can_modify = false; - if ($scope.events.modifiable.is_completed) { $scope.events.modifiable.is_completed = false; } - updateEvents($scope.events.modifiable); - - $scope.events.placable.title = save.title; - $scope.events.placable.backgroundColor = 'white'; - $scope.events.placable.borderColor = save.borderColor; - $scope.events.placable.slot_id = save.slotId; - $scope.events.placable.is_reserved = true; - $scope.events.placable.can_modify = true; - updateEvents($scope.events.placable); - - refetchCalendar(); + refreshCalendar(); }; /** @@ -282,22 +253,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra if ($scope.ctrl.member) { Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; - 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' - } - ); - }); + refreshCalendar(); }); } // as the events are re-fetched for the new user, we must re-init the cart @@ -358,16 +314,6 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra */ $scope.afterPayment = function (paymentDocument) { Reservation.get({ id: paymentDocument.main_object.id }, function (reservation) { - angular.forEach($scope.events.paid, function (trainingSlot, key) { - trainingSlot.backgroundColor = 'white'; - trainingSlot.is_reserved = true; - trainingSlot.can_modify = true; - updateTrainingSlotId(trainingSlot, reservation); - trainingSlot.borderColor = '#b2e774'; - trainingSlot.title = trainingSlot.training.name + ' - ' + _t('app.logged.trainings_reserve.i_ve_reserved'); - updateEvents(trainingSlot); - }); - if ($scope.selectedPlan) { $scope.ctrl.member.subscribed_plan = angular.copy($scope.selectedPlan); if ($scope.ctrl.member.id === Auth._currentUser.id) { @@ -383,7 +329,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra Auth._currentUser.machine_credits = angular.copy(reservation.user.machine_credits); } - refetchCalendar(); + refreshCalendar(); }); }; @@ -427,6 +373,27 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra return $scope.selectionTime = new Date(); }; + /** + * 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' + }); + }); + } + /** * Triggered when fullCalendar tries to graphicaly render an event block. * Append the event tag into the block, just after the event title. diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index c1f678cfb..e67fb4728 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -233,16 +233,16 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', */ $scope.modifySlot = function () { SlotsReservation.update({ id: $scope.events.modifiable.slots_reservations_ids[0] }, { - slot: { + slots_reservation: { slot_id: $scope.events.placable.slot_id } } - , function () { // success + , function (slotReservation) { // success // -> run the callback if (typeof $scope.onSlotModifySuccess === 'function') { $scope.onSlotModifySuccess(); } // -> set the events as successfully moved (to display a summary) $scope.events.moved = { - newSlot: $scope.events.placable, + newSlot: Object.assign($scope.events.placable, { slots_reservations_ids: [slotReservation.id] }), oldSlot: $scope.events.modifiable }; // -> reset the 'moving' status @@ -517,10 +517,10 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', $scope.slot.group_ids = $scope.slot.plansGrouped.map(function (g) { return g.id; }); } - if (!$scope.slot.is_completed && !$scope.events.modifiable) { - // slot is not fully reserved, and we are not currently modifying a slot + if (!$scope.slot.is_completed && $scope.slot.slots_reservations_ids.length === 0 && !$scope.events.modifiable) { + // slot is not fully reserved, and not reserved by the current user, and we are not currently modifying a slot // -> can be added to cart or removed if already present - const index = _.findIndex($scope.events.reserved, (e) => e._id === $scope.slot._id); + const index = _.findIndex($scope.events.reserved, (e) => e.slot_id === $scope.slot.slot_id); if (index === -1) { if (($scope.limitToOneSlot === 'true') && $scope.events.reserved[0]) { // if we limit the number of slots in the cart to 1, and there is already @@ -538,8 +538,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', resetCartState(); // finally, we update the prices return updateCartPrice(); - } else if (!$scope.slot.is_completed && $scope.events.modifiable) { - // slot is not fully reserved, but we are currently modifying a slot + } else if (!$scope.slot.is_completed && $scope.slot.slots_reservations_ids.length === 0 && $scope.events.modifiable) { + // slot is not fully reserved, not reserved by the current user, and we are currently modifying a slot // -> we request the calendar to change the rendering if (typeof $scope.onSlotModifyUnselect === 'function') { // if the callback return false, cancel the selection for the current modification @@ -547,21 +547,21 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', if (!res) return; } // -> then, we re-affect the destination slot - if (!$scope.events.placable || ($scope.events.placable._id !== $scope.slot._id)) { + if (!$scope.events.placable || ($scope.events.placable.slot_id !== $scope.slot.slot_id)) { return $scope.events.placable = $scope.slot; } else { return $scope.events.placable = null; } - } else if ($scope.slot.is_reserved && $scope.events.modifiable && ($scope.slot.is_reserved._id === $scope.events.modifiable._id)) { + } else if ($scope.slot.slots_reservations_ids.length > 0 && + $scope.events.modifiable && + ($scope.slot._id === $scope.events.modifiable._id)) { // slot is reserved and currently modified // -> we cancel the modification $scope.cancelModifySlot(); - } else if ($scope.slot.is_reserved && + } else if ($scope.slot.slots_reservations_ids.length > 0 && (slotCanBeModified($scope.slot) || slotCanBeCanceled($scope.slot)) && !$scope.events.modifiable && - ($scope.events.reserved.length === 0) && - $scope.user && - $scope.slot.users.map(u => u.id).includes($scope.user.id)) { + $scope.events.reserved.length === 0) { // slot is reserved and is ok to be modified or cancelled // but we are not currently running a modification or having any slots in the cart // -> first affect the modification/cancellation rights attributes to the current slot diff --git a/app/views/api/members/show.json.jbuilder b/app/views/api/members/show.json.jbuilder index c00788b41..6cbb9fd4a 100644 --- a/app/views/api/members/show.json.jbuilder +++ b/app/views/api/members/show.json.jbuilder @@ -10,30 +10,30 @@ json.trainings @member.trainings do |t| json.id t.id json.name t.name end -json.training_reservations @member.reservations.where(reservable_type: 'Training') do |r| - json.id r.id - json.start_at r.slots.first.start_at - json.end_at r.slots.first.end_at - json.reservable r.reservable +json.training_reservations @member.reservations.where(reservable_type: 'Training').map(&:slots_reservations).flatten do |sr| + json.id sr.id + json.start_at sr.slot.start_at + json.end_at sr.slot.end_at + json.reservable sr.reservation.reservable json.reservable_type 'Training' - json.is_valid @member.statistic_profile.training_ids.include?(r.reservable_id) - json.canceled_at r.slots_reservations.first.canceled_at + json.is_valid @member.statistic_profile.training_ids.include?(sr.reservation.reservable_id) + json.canceled_at sr.canceled_at end -json.machine_reservations @member.reservations.where(reservable_type: 'Machine') do |r| - json.id r.id - json.start_at r.slots.first.start_at - json.end_at r.slots.first.end_at - json.reservable r.reservable +json.machine_reservations @member.reservations.where(reservable_type: 'Machine').map(&:slots_reservations).flatten do |sr| + json.id sr.id + json.start_at sr.slot.start_at + json.end_at sr.slot.end_at + json.reservable sr.reservation.reservable json.reservable_type 'Machine' - json.canceled_at r.slots_reservations.first.canceled_at + json.canceled_at sr.canceled_at end -json.space_reservations @member.reservations.where(reservable_type: 'Space') do |r| - json.id r.id - json.start_at r.slots.first.start_at - json.end_at r.slots.first.end_at - json.reservable r.reservable +json.space_reservations @member.reservations.where(reservable_type: 'Space').map(&:slots_reservations).flatten do |sr| + json.id sr.id + json.start_at sr.slot.start_at + json.end_at sr.slot.end_at + json.reservable sr.reservation.reservable json.reservable_type 'Space' - json.canceled_at r.slots_reservations.first.canceled_at + json.canceled_at sr.canceled_at end json.all_projects @member.all_projects do |project| From 2bd84e623aac0c260c2e9c36849b99c63ed656e2 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 15:27:01 +0200 Subject: [PATCH 052/141] (bug) missing translations --- CHANGELOG.md | 2 +- app/frontend/templates/dashboard/trainings.html | 4 ++-- app/models/event.rb | 2 +- config/locales/app.logged.en.yml | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b04aa1b52..c40ff8638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Refactored and documented the availability-slot-reservation data model - Display bookers names to connected users now apply to all resources - Fix a bug: for admins and managers, the current password is not requested before changing their own password -- Fix a bug: missing translation for avatar changing +- Fix a bug: missing translations - Fix a bug: unable to book a space's slot with an existing reservation - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty - Fix a bug: Unable to change the group of a user diff --git a/app/frontend/templates/dashboard/trainings.html b/app/frontend/templates/dashboard/trainings.html index 9516ac27a..e5d1184bb 100644 --- a/app/frontend/templates/dashboard/trainings.html +++ b/app/frontend/templates/dashboard/trainings.html @@ -39,7 +39,7 @@
      -
    • +
    • {{r.reservable.name}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
    @@ -54,7 +54,7 @@
      -
    • +
    • {{r.reservable.name}} - {{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}
    diff --git a/app/models/event.rb b/app/models/event.rb index ebc838c77..392cc865e 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -25,8 +25,8 @@ class Event < ApplicationRecord attr_accessor :recurrence, :recurrence_end_at - after_create :event_recurrence before_save :update_nb_free_places + after_create :event_recurrence # update event updated_at for index cache after_save -> { touch } diff --git a/config/locales/app.logged.en.yml b/config/locales/app.logged.en.yml index 762485fd7..ed7164c3a 100644 --- a/config/locales/app.logged.en.yml +++ b/config/locales/app.logged.en.yml @@ -112,6 +112,7 @@ en: subscribe_for_credits: "Subscribe to benefit from free trainings" register_for_free: "Register for free to the following trainings:" book_here: "Book here" + canceled: "Canceled" #dashboard: my events events: your_next_events: "Your next events" From 29b9399ca337687694e1b6c5291a90b9c75946e8 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 15:54:04 +0200 Subject: [PATCH 053/141] (bug) unable to reserve event --- .../src/javascript/controllers/events.js.erb | 12 +++++++++--- .../templates/shared/valid_reservation_modal.html | 7 ++++--- app/models/event.rb | 2 ++ app/views/api/events/_event.json.jbuilder | 1 + 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index afabe1660..6e900143c 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -662,7 +662,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' reservation.slots_reservations_attributes.push({ offered: event.offered || false, - slot_id: event.slot_id + slot_id: event.availability.slot_id }); for (let evt_px_cat of Array.from(event.prices)) { @@ -758,9 +758,12 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' cartItems () { return mkCartItems(reservation, $scope.coupon.applied); }, + event () { + return $scope.event; + } }, - controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'LocalPayment', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', - function ($scope, $uibModalInstance, $state, reservation, price, Auth, LocalPayment, wallet, helpers, $filter, coupon, cartItems) { + controller: ['$scope', '$uibModalInstance', '$state', 'reservation', 'price', 'Auth', 'LocalPayment', 'wallet', 'helpers', '$filter', 'coupon', 'cartItems', 'event', + function ($scope, $uibModalInstance, $state, reservation, price, Auth, LocalPayment, wallet, helpers, $filter, coupon, cartItems, event) { // User's wallet amount $scope.wallet = wallet; @@ -776,6 +779,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' // Reservation $scope.reservation = reservation; + // the event + $scope.bookedEvent = event; + // Used in wallet info template to interpolate some translations $scope.numberFilter = $filter('number'); diff --git a/app/frontend/templates/shared/valid_reservation_modal.html b/app/frontend/templates/shared/valid_reservation_modal.html index 737dd870d..32e01615c 100644 --- a/app/frontend/templates/shared/valid_reservation_modal.html +++ b/app/frontend/templates/shared/valid_reservation_modal.html @@ -7,10 +7,11 @@ {{alert.msg}}
    -
    + +

    {{ 'app.shared.valid_reservation_modal.here_is_the_summary_of_the_slots_to_book_for_the_current_user' }}

    -
      -
    • {{sr.slot_attributes.start_at | amDateFormat: 'LL'}} : {{sr.slot_attributes.start_at | amDateFormat:'LT'}} - {{sr.slot_attributes.end_at | amDateFormat:'LT'}}
    • +
        +
      • {{bookedEvent.availability.start_at | amDateFormat: 'LL'}} : {{bookedEvent.availability.start_at | amDateFormat:'LT'}} - {{bookedEvent.availability.end_at | amDateFormat:'LT'}}
    diff --git a/app/models/event.rb b/app/models/event.rb index 392cc865e..3a75b6f27 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -109,6 +109,7 @@ class Event < ApplicationRecord nil end r = Recurrence.new(every: recurrence, on: on, starts: availability.start_at + 1.day, until: recurrence_end_at) + service = Availabilities::CreateAvailabilitiesService.new r.events.each do |date| days_diff = availability.end_at.day - availability.start_at.day start_at = DateTime.new( @@ -157,6 +158,7 @@ class Event < ApplicationRecord recurrence_id: id ) event.save + service.create_slots(event.availability) end update_columns(recurrence_id: id) end diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder index c94072877..2002a2448 100644 --- a/app/views/api/events/_event.json.jbuilder +++ b/app/views/api/events/_event.json.jbuilder @@ -37,6 +37,7 @@ json.availability do json.id event.availability.id json.start_at event.availability.start_at json.end_at event.availability.end_at + json.slot_id event.availability.slots.first&.id end json.availability_id event.availability_id json.amount (event.amount / 100.0) if event.amount From 7134b8ffac0e8d4ac893103416eaca29e5861d8d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 16:20:09 +0200 Subject: [PATCH 054/141] (bug) fix notification with slots --- ...otify_admin_slot_is_canceled.json.jbuilder | 2 +- ...tify_member_slot_is_canceled.json.jbuilder | 5 ++--- .../notify_admin_slot_is_canceled.html.erb | 6 ++--- .../notify_admin_slot_is_modified.html.erb | 6 ++--- .../notify_member_slot_is_canceled.html.erb | 4 ++-- .../notify_member_slot_is_modified.html.erb | 4 ++-- ...20720135828_migrate_slots_notifications.rb | 22 +++++++++++++++++++ db/schema.rb | 20 ++++++++--------- 8 files changed, 45 insertions(+), 24 deletions(-) create mode 100644 db/migrate/20220720135828_migrate_slots_notifications.rb diff --git a/app/views/api/notifications/_notify_admin_slot_is_canceled.json.jbuilder b/app/views/api/notifications/_notify_admin_slot_is_canceled.json.jbuilder index 9abfcaf65..dad270a8b 100644 --- a/app/views/api/notifications/_notify_admin_slot_is_canceled.json.jbuilder +++ b/app/views/api/notifications/_notify_admin_slot_is_canceled.json.jbuilder @@ -1,5 +1,5 @@ json.title notification.notification_type json.description t('.USER_s_reservation_on_the_DATE_was_cancelled_remember_to_generate_a_refund_invoice_if_applicable_html', USER: notification.attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user'), - DATE: I18n.l(notification.attached_object.start_at, format: :long)) + DATE: I18n.l(notification.attached_object&.slot&.start_at, format: :long)) diff --git a/app/views/api/notifications/_notify_member_slot_is_canceled.json.jbuilder b/app/views/api/notifications/_notify_member_slot_is_canceled.json.jbuilder index 9cb6c05f3..82b50248a 100644 --- a/app/views/api/notifications/_notify_member_slot_is_canceled.json.jbuilder +++ b/app/views/api/notifications/_notify_member_slot_is_canceled.json.jbuilder @@ -2,6 +2,5 @@ json.title notification.notification_type json.description t('.your_reservation_RESERVABLE_of_DATE_was_successfully_cancelled', - RESERVABLE: notification.attached_object.reservation&.reservable&.name, - DATE: I18n.l(notification.attached_object.start_at, format: :long)) - + RESERVABLE: notification.attached_object&.reservation&.reservable&.name, + DATE: I18n.l(notification.attached_object&.start_at, format: :long)) diff --git a/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb b/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb index 275b56391..7d2b93160 100644 --- a/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb +++ b/app/views/notifications_mailer/notify_admin_slot_is_canceled.html.erb @@ -2,8 +2,8 @@

    <%= t('.body.member_cancelled', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %>

    <%= t('.body.item_details', - START: I18n.l(@attached_object.slot.start_at, format: :long), - END: I18n.l(@attached_object.slot.end_at, format: :hour_minute), - RESERVABLE: @attached_object.reservation.reservable.name) %> + START: I18n.l(@attached_object&.slot&.start_at, format: :long), + END: I18n.l(@attached_object&.slot&.end_at, format: :hour_minute), + RESERVABLE: @attached_object&.reservation&.reservable&.name) %>

    <%= t('.body.generate_refund') %>

    diff --git a/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb b/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb index 2b1a458db..7868ac584 100644 --- a/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb +++ b/app/views/notifications_mailer/notify_admin_slot_is_modified.html.erb @@ -1,5 +1,5 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %> -

    <%= t('.body.slot_modified', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %>

    -

    <%= t('.body.new_date') %> <%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %>

    -

    <%= t('.body.old_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %>

    +

    <%= t('.body.slot_modified', NAME: @attached_object&.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %>

    +

    <%= t('.body.new_date') %> <%= "#{I18n.l(@attached_object&.slot&.start_at, format: :long)} - #{I18n.l(@attached_object&.slot.end_at, format: :hour_minute)}" %>

    +

    <%= t('.body.old_date') %> <%= "#{I18n.l(@attached_object&.ex_start_at, format: :long)} - #{I18n.l(@attached_object&.ex_end_at, format: :hour_minute)}" %>

    diff --git a/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb b/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb index d1d2307ee..49f3f837b 100644 --- a/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb +++ b/app/views/notifications_mailer/notify_member_slot_is_canceled.html.erb @@ -1,4 +1,4 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %> -

    <%= t('.body.reservation_canceled', RESERVABLE: @attached_object.reservation.reservable.name ) %>

    -

    <%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %>

    +

    <%= t('.body.reservation_canceled', RESERVABLE: @attached_object&.reservation&.reservable&.name ) %>

    +

    <%= "#{I18n.l(@attached_object&.slot&.start_at, format: :long)} - #{I18n.l(@attached_object&.slot&.end_at, format: :hour_minute)}" %>

    diff --git a/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb b/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb index 20d8ba306..93734acc2 100644 --- a/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb +++ b/app/views/notifications_mailer/notify_member_slot_is_modified.html.erb @@ -1,5 +1,5 @@ <%= render 'notifications_mailer/shared/hello', recipient: @recipient %>

    <%= t('.body.reservation_changed_to') %>

    -

    <%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %>

    -

    <%= t('.body.previous_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %>

    +

    <%= "#{I18n.l(@attached_object&.slot&.start_at, format: :long)} - #{I18n.l(@attached_object&.slot&.end_at, format: :hour_minute)}" %>

    +

    <%= t('.body.previous_date') %> <%= "#{I18n.l(@attached_object&.ex_start_at, format: :long)} - #{I18n.l(@attached_object&.ex_end_at, format: :hour_minute)}" %>

    diff --git a/db/migrate/20220720135828_migrate_slots_notifications.rb b/db/migrate/20220720135828_migrate_slots_notifications.rb new file mode 100644 index 000000000..3970dbcd9 --- /dev/null +++ b/db/migrate/20220720135828_migrate_slots_notifications.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# We migrate existing notifications to be attached to a SlotsReservation instead of a Slot, +# because these notifications are now expecting a SlotsReservation +class MigrateSlotsNotifications < ActiveRecord::Migration[5.2] + def up + Notification.where(attached_object_type: 'Slot').each do |notification| + slot = notification.attached_object + slots_reservation = slot&.slots_reservations + &.includes(:reservation) + &.where('reservations.statistic_profile_id': notification.receiver.statistic_profile.id) + &.first + notification.update(attached_object: slots_reservation) + end + end + + def down + Notification.where(attached_object_type: 'SlotsReservation').each do |notification| + notification.update(attached_object: notification.attached_object&.slot) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index cd97b668b..6ea5b3b4d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2022_07_05_125232) do +ActiveRecord::Schema.define(version: 2022_07_20_135828) do # These are extensions that must be enabled in order to support this database enable_extension "fuzzystrmatch" @@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do enable_extension "unaccent" create_table "abuses", id: :serial, force: :cascade do |t| - t.string "signaled_type" t.integer "signaled_id" + t.string "signaled_type" t.string "first_name" t.string "last_name" t.string "email" @@ -49,8 +49,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do t.string "locality" t.string "country" t.string "postal_code" - t.string "placeable_type" t.integer "placeable_id" + t.string "placeable_type" t.datetime "created_at" t.datetime "updated_at" end @@ -64,8 +64,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do end create_table "assets", id: :serial, force: :cascade do |t| - t.string "viewable_type" t.integer "viewable_id" + t.string "viewable_type" t.string "attachment" t.string "type" t.datetime "created_at" @@ -146,8 +146,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do end create_table "credits", id: :serial, force: :cascade do |t| - t.string "creditable_type" t.integer "creditable_id" + t.string "creditable_type" t.integer "plan_id" t.integer "hours" t.datetime "created_at" @@ -369,15 +369,15 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do create_table "notifications", id: :serial, force: :cascade do |t| t.integer "receiver_id" - t.string "attached_object_type" t.integer "attached_object_id" + t.string "attached_object_type" t.integer "notification_type_id" t.boolean "is_read", default: false t.datetime "created_at" t.datetime "updated_at" t.string "receiver_type" t.boolean "is_send", default: false - t.jsonb "meta_data", default: "{}" + t.jsonb "meta_data", default: {} t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id" t.index ["receiver_id"], name: "index_notifications_on_receiver_id" end @@ -570,8 +570,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do create_table "prices", id: :serial, force: :cascade do |t| t.integer "group_id" t.integer "plan_id" - t.string "priceable_type" t.integer "priceable_id" + t.string "priceable_type" t.integer "amount" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -729,8 +729,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.string "reservable_type" t.integer "reservable_id" + t.string "reservable_type" t.integer "nb_reserve_places" t.integer "statistic_profile_id" t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id" @@ -739,8 +739,8 @@ ActiveRecord::Schema.define(version: 2022_07_05_125232) do create_table "roles", id: :serial, force: :cascade do |t| t.string "name" - t.string "resource_type" t.integer "resource_id" + t.string "resource_type" t.datetime "created_at" t.datetime "updated_at" t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" From 2705b9f6bd9e077aa74c5baa893bd44604f604a8 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 20 Jul 2022 17:46:09 +0200 Subject: [PATCH 055/141] (bug) unable to reserve if user's subscription plan is disabled --- CHANGELOG.md | 1 + app/controllers/api/payzen_controller.rb | 2 +- app/controllers/api/stripe_controller.rb | 4 +- app/models/cart_item/machine_reservation.rb | 16 ++ app/models/cart_item/payment_schedule.rb | 2 + app/models/cart_item/reservation.rb | 40 ++-- app/models/cart_item/space_reservation.rb | 10 + app/models/shopping_cart.rb | 14 +- test/integration/reservations/create_test.rb | 2 +- .../reservations/restricted_test.rb | 2 +- test/services/availabilities_service_test.rb | 28 +-- ...ng_coupon_retrieve_invoice_from_stripe.yml | 81 +++---- ..._machine_and_plan_using_coupon_success.yml | 197 ++++++++++-------- 13 files changed, 230 insertions(+), 169 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c40ff8638..5e8012926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Improved calendars loading time - Refactored and documented the availability-slot-reservation data model - Display bookers names to connected users now apply to all resources +- Fix a bug: unable to reserve if user's subscription plan is disabled - Fix a bug: for admins and managers, the current password is not requested before changing their own password - Fix a bug: missing translations - Fix a bug: unable to book a space's slot with an existing reservation diff --git a/app/controllers/api/payzen_controller.rb b/app/controllers/api/payzen_controller.rb index 1f528c1a5..69fbaa70d 100644 --- a/app/controllers/api/payzen_controller.rb +++ b/app/controllers/api/payzen_controller.rb @@ -55,7 +55,7 @@ class API::PayzenController < API::PaymentsController def check_cart cart = shopping_cart - render json: { error: 'invalid shopping cart' }, status: :unprocessable_entity and return unless cart.valid? + render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid? render json: { cart: 'ok' }, status: :ok end diff --git a/app/controllers/api/stripe_controller.rb b/app/controllers/api/stripe_controller.rb index f08659386..05b3a6e2b 100644 --- a/app/controllers/api/stripe_controller.rb +++ b/app/controllers/api/stripe_controller.rb @@ -19,7 +19,7 @@ class API::StripeController < API::PaymentsController res = nil # json of the API answer cart = shopping_cart - render json: { error: 'invalid shopping cart' }, status: :unprocessable_entity and return unless cart.valid? + render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid? begin amount = debit_amount(cart) # will contains the amount and the details of each invoice lines @@ -73,7 +73,7 @@ class API::StripeController < API::PaymentsController def setup_subscription cart = shopping_cart - raise InvalidSubscriptionError unless cart.valid? + render json: { error: cart.errors }, status: :unprocessable_entity and return unless cart.valid? service = Stripe::Service.new method = service.attach_method_as_default( diff --git a/app/models/cart_item/machine_reservation.rb b/app/models/cart_item/machine_reservation.rb index 2d0c44e12..ef692fe13 100644 --- a/app/models/cart_item/machine_reservation.rb +++ b/app/models/cart_item/machine_reservation.rb @@ -25,6 +25,22 @@ class CartItem::MachineReservation < CartItem::Reservation 'machine' end + def valid?(all_items) + @slots.each do |slot| + same_hour_slots = SlotsReservation.joins(:reservation).where( + reservations: { reservable: @reservable }, + slot_id: slot[:slot_id], + canceled_at: nil + ).count + if same_hour_slots.positive? + @errors[:slot] = 'slot is reserved' + return false + end + end + + super + end + protected def credits diff --git a/app/models/cart_item/payment_schedule.rb b/app/models/cart_item/payment_schedule.rb index c705e473b..cff6ce318 100644 --- a/app/models/cart_item/payment_schedule.rb +++ b/app/models/cart_item/payment_schedule.rb @@ -36,6 +36,8 @@ class CartItem::PaymentSchedule end def valid?(_all_items) + return true unless @requested && @plan&.monthly_payment + if @plan&.disabled @errors[:item] = 'plan is disabled' return false diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb index 30103f330..5bb11af3d 100644 --- a/app/models/cart_item/reservation.rb +++ b/app/models/cart_item/reservation.rb @@ -43,42 +43,27 @@ class CartItem::Reservation < CartItem::BaseItem def valid?(all_items) pending_subscription = all_items.find { |i| i.is_a?(CartItem::Subscription) } @slots.each do |slot| - if Slot.find(slot[:slot_id]).nil? + slot_db = Slot.find(slot[:slot_id]) + if slot_db.nil? @errors[:slot] = 'slot does not exist' return false end availability = Availability.find_by(id: slot[:slot_attributes][:availability_id]) if availability.nil? - @errors[:slot] = 'slot availability does not exist' + @errors[:availability] = 'availability does not exist' return false end - if availability.available_type == 'machines' - same_hour_slots = SlotsReservation.joins(:reservation).where( - reservations: { reservable: @reservable }, - slot_id: slot[:slot_id], - canceled_at: nil - ).count - if same_hour_slots.positive? - @errors[:slot] = 'slot is reserved' - return false - end - elsif availability.available_type == 'space' && availability.spaces.first.disabled - @errors[:slot] = 'space is disabled' - return false - elsif availability.full? - @errors[:slot] = 'availability is complete' + if slot_db.full? + @errors[:slot] = 'availability is full' return false end next if availability.plan_ids.empty? - next if (@customer.subscribed_plan && availability.plan_ids.include?(@customer.subscribed_plan.id)) || - (pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) || - (@operator.manager? && @customer.id != @operator.id) || - @operator.admin? + next if required_subscription?(availability, pending_subscription) - @errors[:slot] = 'slot is restricted for subscribers' + @errors[:availability] = 'availability is restricted for subscribers' return false end @@ -211,4 +196,15 @@ class CartItem::Reservation < CartItem::BaseItem def slots_params @slots.map { |slot| slot.permit(:id, :slot_id, :offered) } end + + ## + # Check if the given availability requires a valid subscription. If so, check if the current customer + # has the required susbcription, otherwise, check if the operator is privileged + ## + def required_subscription?(availability, pending_subscription) + (@customer.subscribed_plan && availability.plan_ids.include?(@customer.subscribed_plan.id)) || + (pending_subscription && availability.plan_ids.include?(pending_subscription.plan.id)) || + (@operator.manager? && @customer.id != @operator.id) || + @operator.admin? + end end diff --git a/app/models/cart_item/space_reservation.rb b/app/models/cart_item/space_reservation.rb index d9e8db730..36a0c36cd 100644 --- a/app/models/cart_item/space_reservation.rb +++ b/app/models/cart_item/space_reservation.rb @@ -9,6 +9,7 @@ class CartItem::SpaceReservation < CartItem::Reservation super(customer, operator, space, slots) @plan = plan + @space = space @new_subscription = new_subscription end @@ -25,6 +26,15 @@ class CartItem::SpaceReservation < CartItem::Reservation 'space' end + def valid?(all_items) + if @space.disabled + @errors[:reservable] = 'space is disabled' + return false + end + + super + end + protected def credits diff --git a/app/models/shopping_cart.rb b/app/models/shopping_cart.rb index 10e961651..8a985ee4d 100644 --- a/app/models/shopping_cart.rb +++ b/app/models/shopping_cart.rb @@ -2,7 +2,7 @@ # Stores data about a shopping data class ShoppingCart - attr_accessor :customer, :operator, :payment_method, :items, :coupon, :payment_schedule + attr_accessor :customer, :operator, :payment_method, :items, :coupon, :payment_schedule, :errors # @param items {Array} # @param coupon {CartItem::Coupon} @@ -18,6 +18,7 @@ class ShoppingCart @items = items @coupon = coupon @payment_schedule = payment_schedule + @errors = {} end # compute the price details of the current shopping cart @@ -87,11 +88,18 @@ class ShoppingCart items.each do |item| next if item.valid?(@items) + @errors = item.errors + return false + end + unless @coupon.valid?(items) + @errors = @coupon.errors return false end - return false unless @coupon.valid?([]) - return false unless @payment_schedule.valid?([]) + unless @payment_schedule.valid?(items) + @errors = @payment_schedule.errors + return false + end true end diff --git a/test/integration/reservations/create_test.rb b/test/integration/reservations/create_test.rb index c4712cccf..336f8ed9f 100644 --- a/test/integration/reservations/create_test.rb +++ b/test/integration/reservations/create_test.rb @@ -526,7 +526,7 @@ class Reservations::CreateTest < ActionDispatch::IntegrationTest login_as(@user_without_subscription, scope: :user) machine = Machine.find(6) - plan = Plan.find_by(group_id: @user_without_subscription.group_id) + plan = Plan.find(4) availability = machine.availabilities.first reservations_count = Reservation.count diff --git a/test/integration/reservations/restricted_test.rb b/test/integration/reservations/restricted_test.rb index 58f09eeb9..35113d87b 100644 --- a/test/integration/reservations/restricted_test.rb +++ b/test/integration/reservations/restricted_test.rb @@ -142,7 +142,7 @@ class Reservations::RestrictedTest < ActionDispatch::IntegrationTest end assert_equal 422, response.status - assert_match(/invalid shopping cart/, response.body) + assert_match(/availability is restricted for subscribers/, response.body) assert_equal reservations_count, Reservation.count assert_equal invoices_count, Invoice.count diff --git a/test/services/availabilities_service_test.rb b/test/services/availabilities_service_test.rb index 74757d0b7..256d17cfb 100644 --- a/test/services/availabilities_service_test.rb +++ b/test/services/availabilities_service_test.rb @@ -44,9 +44,10 @@ class AvailabilitiesServiceTest < ActiveSupport::TestCase slots = service.trainings([Training.find(2)], @no_subscription, { start: 1.month.ago.beginning_of_day, end: 1.day.ago.end_of_day }) assert_not_empty slots - assert_equal Availability.find(20).slots.count, slots.count - assert_equal Availability.find(20).start_at, slots.first.start_at - assert_equal Availability.find(20).end_at, slots.first.end_at + availability = Availability.find(20) + assert_equal availability.slots.count, slots.count + assert_equal availability.start_at, slots.first.start_at + assert_equal availability.end_at, slots.first.end_at end test 'machines availabilities' do @@ -54,9 +55,10 @@ class AvailabilitiesServiceTest < ActiveSupport::TestCase slots = service.machines([Machine.find(1)], @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) assert_not_empty slots - assert_equal Availability.find(7).slots.count, slots.count - assert_equal Availability.find(7).start_at, slots.first.start_at - assert_equal Availability.find(7).end_at, slots.last.end_at + availability = Availability.find(7) + assert_equal availability.slots.count, slots.count + assert_equal availability.start_at, slots.min_by(&:start_at).start_at + assert_equal availability.end_at, slots.max_by(&:end_at).end_at end test 'spaces availabilities' do @@ -64,9 +66,10 @@ class AvailabilitiesServiceTest < ActiveSupport::TestCase slots = service.spaces([Space.find(1)], @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }) assert_not_empty slots - assert_equal Availability.find(18).slots.count, slots.count - assert_equal Availability.find(18).start_at, slots.first.start_at - assert_equal Availability.find(18).end_at, slots.last.end_at + availability = Availability.find(18) + assert_equal availability.slots.count, slots.count + assert_equal availability.start_at, slots.min_by(&:start_at).start_at + assert_equal availability.end_at, slots.max_by(&:end_at).end_at end test 'trainings availabilities' do @@ -87,8 +90,9 @@ class AvailabilitiesServiceTest < ActiveSupport::TestCase slots = service.events([Event.find(4)], @no_subscription, { start: DateTime.current.beginning_of_day, end: 30.days.from_now.end_of_day }) assert_not_empty slots - assert_equal Availability.find(17).slots.count, slots.count - assert_equal Availability.find(17).start_at, slots.first.start_at - assert_equal Availability.find(17).end_at, slots.first.end_at + availability = Availability.find(17) + assert_equal availability.slots.count, slots.count + assert_equal availability.start_at, slots.first.start_at + assert_equal availability.end_at, slots.first.end_at end end diff --git a/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe.yml b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe.yml index 9132aeea9..0ea2498d7 100644 --- a/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe.yml +++ b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_retrieve_invoice_from_stripe.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://api.stripe.com/v1/payment_intents/pi_3JZDIB2sOmf47Nz91NCWzYP6 + uri: https://api.stripe.com/v1/payment_intents/pi_3LNevR2sOmf47Nz90G2zL7T3 body: encoding: US-ASCII string: '' @@ -14,13 +14,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_TAv2FNDcsNhZ0x","request_duration_ms":477}}' + - '{"last_request_metrics":{"request_id":"req_g3z6KJauuKa7Ob","request_duration_ms":838}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -33,11 +33,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:25:46 GMT + - Wed, 20 Jul 2022 15:35:05 GMT Content-Type: - application/json Content-Length: - - '4286' + - '4461' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -53,24 +53,26 @@ http_interactions: Cache-Control: - no-cache, no-store Request-Id: - - req_kM3vfzGXrrL9Yq + - req_uTSAidTgNhke0j Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '0' Strict-Transport-Security: - max-age=31556926; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pi_3JZDIB2sOmf47Nz91NCWzYP6", + "id": "pi_3LNevR2sOmf47Nz90G2zL7T3", "object": "payment_intent", - "amount": 3825, + "amount": 97410, "amount_capturable": 0, - "amount_received": 3825, + "amount_details": { + "tip": {} + }, + "amount_received": 97410, "application": null, "application_fee_amount": null, + "automatic_payment_methods": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", @@ -78,15 +80,15 @@ http_interactions: "object": "list", "data": [ { - "id": "ch_3JZDIB2sOmf47Nz91buHMX2j", + "id": "ch_3LNevR2sOmf47Nz90Mvq9kVm", "object": "charge", - "amount": 3825, - "amount_captured": 3825, + "amount": 97410, + "amount_captured": 97410, "amount_refunded": 0, "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_3JZDIB2sOmf47Nz9120KuoH9", + "balance_transaction": "txn_3LNevR2sOmf47Nz90JS5wwi9", "billing_details": { "address": { "city": null, @@ -102,34 +104,33 @@ http_interactions: }, "calculated_statement_descriptor": "Stripe", "captured": true, - "created": 1631532343, + "created": 1658331301, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, - "fraud_details": { - }, + "fraud_details": {}, "invoice": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 32, + "risk_score": 51, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_3JZDIB2sOmf47Nz91NCWzYP6", - "payment_method": "pm_1JZDIA2sOmf47Nz9rl1nigpQ", + "payment_intent": "pi_3LNevR2sOmf47Nz90G2zL7T3", + "payment_method": "pm_1LNevQ2sOmf47Nz9khvl8vC0", "payment_method_details": { "card": { "brand": "visa", @@ -140,11 +141,12 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2023, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "installments": null, "last4": "4242", + "mandate": null, "network": "visa", "three_d_secure": null, "wallet": null @@ -153,16 +155,14 @@ http_interactions: }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDIB2sOmf47Nz91buHMX2j/rcpt_KDebb83SAs8TQggfARcBOMotuxQ1Vxj", + "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3LNevR2sOmf47Nz90Mvq9kVm/rcpt_M5qcIvwnjpj7tlCm4ws6NUyPXIeLpSL", "refunded": false, "refunds": { "object": "list", - "data": [ - - ], + "data": [], "has_more": false, "total_count": 0, - "url": "/v1/charges/ch_3JZDIB2sOmf47Nz91buHMX2j/refunds" + "url": "/v1/charges/ch_3LNevR2sOmf47Nz90Mvq9kVm/refunds" }, "review": null, "shipping": null, @@ -177,25 +177,25 @@ http_interactions: ], "has_more": false, "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDIB2sOmf47Nz91NCWzYP6" + "url": "/v1/charges?payment_intent=pi_3LNevR2sOmf47Nz90G2zL7T3" }, - "client_secret": "pi_3JZDIB2sOmf47Nz91NCWzYP6_secret_oIY9Vwdf4coECaCgx0zgmkYMF", + "client_secret": "pi_3LNevR2sOmf47Nz90G2zL7T3_secret_EqcVW2yHHRtYlWJG4Qcx0qC1i", "confirmation_method": "manual", - "created": 1631532343, + "created": 1658331301, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", - "description": "Invoice reference: 2109001/VL", + "description": "Invoice reference: 2207001/VL", "invoice": null, "last_payment_error": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "next_action": null, "on_behalf_of": null, - "payment_method": "pm_1JZDIA2sOmf47Nz9rl1nigpQ", + "payment_method": "pm_1LNevQ2sOmf47Nz9khvl8vC0", "payment_method_options": { "card": { "installments": null, + "mandate_options": null, "network": null, "request_three_d_secure": "automatic" } @@ -203,6 +203,7 @@ http_interactions: "payment_method_types": [ "card" ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, @@ -214,5 +215,5 @@ http_interactions: "transfer_data": null, "transfer_group": null } - recorded_at: Mon, 13 Sep 2021 11:25:46 GMT + recorded_at: Wed, 20 Jul 2022 15:35:06 GMT recorded_with: VCR 6.0.0 diff --git a/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_success.yml b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_success.yml index f4e2eaedd..3bf7d2604 100644 --- a/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_success.yml +++ b/test/vcr_cassettes/reservations_machine_and_plan_using_coupon_success.yml @@ -5,7 +5,7 @@ http_interactions: uri: https://api.stripe.com/v1/payment_methods body: encoding: UTF-8 - string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2022&card[cvc]=314 + string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2023&card[cvc]=314 headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -14,13 +14,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_tlpIDMIqln9wab","request_duration_ms":616}}' + - '{"last_request_metrics":{"request_id":"req_RmcFNPXaqrPUG5","request_duration_ms":2}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -33,11 +33,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:25:42 GMT + - Wed, 20 Jul 2022 15:35:00 GMT Content-Type: - application/json Content-Length: - - '934' + - '930' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -52,19 +52,23 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 25a0718d-2479-4458-9741-b67628d9f46c + Original-Request: + - req_OGHLOjhvZGzaDh Request-Id: - - req_nAADKjXEme5vlz + - req_OGHLOjhvZGzaDh + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '6' Strict-Transport-Security: - max-age=31556926; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pm_1JZDIA2sOmf47Nz9rl1nigpQ", + "id": "pm_1LNevQ2sOmf47Nz9khvl8vC0", "object": "payment_method", "billing_details": { "address": { @@ -88,7 +92,7 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2023, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "generated_from": null, @@ -104,20 +108,19 @@ http_interactions: }, "wallet": null }, - "created": 1631532342, + "created": 1658331300, "customer": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "type": "card" } - recorded_at: Mon, 13 Sep 2021 11:25:42 GMT + recorded_at: Wed, 20 Jul 2022 15:35:00 GMT - request: method: post uri: https://api.stripe.com/v1/payment_intents body: encoding: UTF-8 - string: payment_method=pm_1JZDIA2sOmf47Nz9rl1nigpQ&amount=3825¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt + string: payment_method=pm_1LNevQ2sOmf47Nz9khvl8vC0&amount=97410¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -126,13 +129,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_nAADKjXEme5vlz","request_duration_ms":583}}' + - '{"last_request_metrics":{"request_id":"req_OGHLOjhvZGzaDh","request_duration_ms":991}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -145,11 +148,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:25:44 GMT + - Wed, 20 Jul 2022 15:35:03 GMT Content-Type: - application/json Content-Length: - - '4259' + - '4434' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -164,25 +167,33 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 0e16450f-e8fa-4565-b41d-fe54b0aa033b + Original-Request: + - req_oqY6Y4FnzGhvIE Request-Id: - - req_9IAUcJuiq87CeO + - req_oqY6Y4FnzGhvIE + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '10' Strict-Transport-Security: - max-age=31556926; includeSubDomains; preload body: encoding: UTF-8 - string: | + string: |- { - "id": "pi_3JZDIB2sOmf47Nz91NCWzYP6", + "id": "pi_3LNevR2sOmf47Nz90G2zL7T3", "object": "payment_intent", - "amount": 3825, + "amount": 97410, "amount_capturable": 0, - "amount_received": 3825, + "amount_details": { + "tip": {} + }, + "amount_received": 97410, "application": null, "application_fee_amount": null, + "automatic_payment_methods": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", @@ -190,15 +201,15 @@ http_interactions: "object": "list", "data": [ { - "id": "ch_3JZDIB2sOmf47Nz91buHMX2j", + "id": "ch_3LNevR2sOmf47Nz90Mvq9kVm", "object": "charge", - "amount": 3825, - "amount_captured": 3825, + "amount": 97410, + "amount_captured": 97410, "amount_refunded": 0, "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_3JZDIB2sOmf47Nz9120KuoH9", + "balance_transaction": "txn_3LNevR2sOmf47Nz90JS5wwi9", "billing_details": { "address": { "city": null, @@ -214,34 +225,33 @@ http_interactions: }, "calculated_statement_descriptor": "Stripe", "captured": true, - "created": 1631532343, + "created": 1658331301, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, - "fraud_details": { - }, + "fraud_details": {}, "invoice": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "on_behalf_of": null, "order": null, "outcome": { "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 32, + "risk_score": 51, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_3JZDIB2sOmf47Nz91NCWzYP6", - "payment_method": "pm_1JZDIA2sOmf47Nz9rl1nigpQ", + "payment_intent": "pi_3LNevR2sOmf47Nz90G2zL7T3", + "payment_method": "pm_1LNevQ2sOmf47Nz9khvl8vC0", "payment_method_details": { "card": { "brand": "visa", @@ -252,11 +262,12 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2023, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "installments": null, "last4": "4242", + "mandate": null, "network": "visa", "three_d_secure": null, "wallet": null @@ -265,16 +276,14 @@ http_interactions: }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDIB2sOmf47Nz91buHMX2j/rcpt_KDebb83SAs8TQggfARcBOMotuxQ1Vxj", + "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3LNevR2sOmf47Nz90Mvq9kVm/rcpt_M5qcIvwnjpj7tlCm4ws6NUyPXIeLpSL", "refunded": false, "refunds": { "object": "list", - "data": [ - - ], + "data": [], "has_more": false, "total_count": 0, - "url": "/v1/charges/ch_3JZDIB2sOmf47Nz91buHMX2j/refunds" + "url": "/v1/charges/ch_3LNevR2sOmf47Nz90Mvq9kVm/refunds" }, "review": null, "shipping": null, @@ -289,25 +298,25 @@ http_interactions: ], "has_more": false, "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDIB2sOmf47Nz91NCWzYP6" + "url": "/v1/charges?payment_intent=pi_3LNevR2sOmf47Nz90G2zL7T3" }, - "client_secret": "pi_3JZDIB2sOmf47Nz91NCWzYP6_secret_oIY9Vwdf4coECaCgx0zgmkYMF", + "client_secret": "pi_3LNevR2sOmf47Nz90G2zL7T3_secret_EqcVW2yHHRtYlWJG4Qcx0qC1i", "confirmation_method": "manual", - "created": 1631532343, + "created": 1658331301, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", "description": null, "invoice": null, "last_payment_error": null, "livemode": false, - "metadata": { - }, + "metadata": {}, "next_action": null, "on_behalf_of": null, - "payment_method": "pm_1JZDIA2sOmf47Nz9rl1nigpQ", + "payment_method": "pm_1LNevQ2sOmf47Nz9khvl8vC0", "payment_method_options": { "card": { "installments": null, + "mandate_options": null, "network": null, "request_three_d_secure": "automatic" } @@ -315,6 +324,7 @@ http_interactions: "payment_method_types": [ "card" ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, @@ -326,13 +336,13 @@ http_interactions: "transfer_data": null, "transfer_group": null } - recorded_at: Mon, 13 Sep 2021 11:25:44 GMT + recorded_at: Wed, 20 Jul 2022 15:35:03 GMT - request: method: post - uri: https://api.stripe.com/v1/payment_intents/pi_3JZDIB2sOmf47Nz91NCWzYP6 + uri: https://api.stripe.com/v1/payment_intents/pi_3LNevR2sOmf47Nz90G2zL7T3 body: encoding: UTF-8 - string: description=Invoice+reference%3A+2109001%2FVL + string: description=Invoice+reference%3A+2207001%2FVL headers: User-Agent: - Stripe/v1 RubyBindings/5.29.0 @@ -341,13 +351,13 @@ http_interactions: Content-Type: - application/x-www-form-urlencoded X-Stripe-Client-Telemetry: - - '{"last_request_metrics":{"request_id":"req_9IAUcJuiq87CeO","request_duration_ms":1477}}' + - '{"last_request_metrics":{"request_id":"req_oqY6Y4FnzGhvIE","request_duration_ms":2680}}' Stripe-Version: - '2019-08-14' X-Stripe-Client-User-Agent: - - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.3 p62 (2019-04-16)","platform":"x86_64-darwin18","engine":"ruby","publisher":"stripe","uname":"Darwin - MacBook-Pro-Sleede-Peng 20.5.0 Darwin Kernel Version 20.5.0: Sat May 8 05:10:33 - PDT 2021; root:xnu-7195.121.3~9/RELEASE_X86_64 x86_64","hostname":"MacBook-Pro-Sleede-Peng"}' + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' Accept-Encoding: - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 Accept: @@ -360,11 +370,11 @@ http_interactions: Server: - nginx Date: - - Mon, 13 Sep 2021 11:25:44 GMT + - Wed, 20 Jul 2022 15:35:04 GMT Content-Type: - application/json Content-Length: - - '4286' + - '4500' Connection: - keep-alive Access-Control-Allow-Credentials: @@ -379,25 +389,34 @@ http_interactions: - '300' Cache-Control: - no-cache, no-store + Idempotency-Key: + - 650ff0fe-0d75-4a3d-9942-d6403534a323 + Original-Request: + - req_g3z6KJauuKa7Ob Request-Id: - - req_TAv2FNDcsNhZ0x + - req_g3z6KJauuKa7Ob + Stripe-Should-Retry: + - 'false' Stripe-Version: - '2019-08-14' - X-Stripe-C-Cost: - - '0' Strict-Transport-Security: - max-age=31556926; includeSubDomains; preload body: encoding: UTF-8 string: | { - "id": "pi_3JZDIB2sOmf47Nz91NCWzYP6", + "id": "pi_3LNevR2sOmf47Nz90G2zL7T3", "object": "payment_intent", - "amount": 3825, + "amount": 97410, "amount_capturable": 0, - "amount_received": 3825, + "amount_details": { + "tip": { + } + }, + "amount_received": 97410, "application": null, "application_fee_amount": null, + "automatic_payment_methods": null, "canceled_at": null, "cancellation_reason": null, "capture_method": "automatic", @@ -405,15 +424,15 @@ http_interactions: "object": "list", "data": [ { - "id": "ch_3JZDIB2sOmf47Nz91buHMX2j", + "id": "ch_3LNevR2sOmf47Nz90Mvq9kVm", "object": "charge", - "amount": 3825, - "amount_captured": 3825, + "amount": 97410, + "amount_captured": 97410, "amount_refunded": 0, "application": null, "application_fee": null, "application_fee_amount": null, - "balance_transaction": "txn_3JZDIB2sOmf47Nz9120KuoH9", + "balance_transaction": "txn_3LNevR2sOmf47Nz90JS5wwi9", "billing_details": { "address": { "city": null, @@ -429,13 +448,14 @@ http_interactions: }, "calculated_statement_descriptor": "Stripe", "captured": true, - "created": 1631532343, + "created": 1658331301, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", "description": null, "destination": null, "dispute": null, "disputed": false, + "failure_balance_transaction": null, "failure_code": null, "failure_message": null, "fraud_details": { @@ -450,13 +470,13 @@ http_interactions: "network_status": "approved_by_network", "reason": null, "risk_level": "normal", - "risk_score": 32, + "risk_score": 51, "seller_message": "Payment complete.", "type": "authorized" }, "paid": true, - "payment_intent": "pi_3JZDIB2sOmf47Nz91NCWzYP6", - "payment_method": "pm_1JZDIA2sOmf47Nz9rl1nigpQ", + "payment_intent": "pi_3LNevR2sOmf47Nz90G2zL7T3", + "payment_method": "pm_1LNevQ2sOmf47Nz9khvl8vC0", "payment_method_details": { "card": { "brand": "visa", @@ -467,11 +487,12 @@ http_interactions: }, "country": "US", "exp_month": 4, - "exp_year": 2022, + "exp_year": 2023, "fingerprint": "o52jybR7bnmNn6AT", "funding": "credit", "installments": null, "last4": "4242", + "mandate": null, "network": "visa", "three_d_secure": null, "wallet": null @@ -480,7 +501,7 @@ http_interactions: }, "receipt_email": null, "receipt_number": null, - "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3JZDIB2sOmf47Nz91buHMX2j/rcpt_KDebb83SAs8TQggfARcBOMotuxQ1Vxj", + "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3LNevR2sOmf47Nz90Mvq9kVm/rcpt_M5qcIvwnjpj7tlCm4ws6NUyPXIeLpSL", "refunded": false, "refunds": { "object": "list", @@ -489,7 +510,7 @@ http_interactions: ], "has_more": false, "total_count": 0, - "url": "/v1/charges/ch_3JZDIB2sOmf47Nz91buHMX2j/refunds" + "url": "/v1/charges/ch_3LNevR2sOmf47Nz90Mvq9kVm/refunds" }, "review": null, "shipping": null, @@ -504,14 +525,14 @@ http_interactions: ], "has_more": false, "total_count": 1, - "url": "/v1/charges?payment_intent=pi_3JZDIB2sOmf47Nz91NCWzYP6" + "url": "/v1/charges?payment_intent=pi_3LNevR2sOmf47Nz90G2zL7T3" }, - "client_secret": "pi_3JZDIB2sOmf47Nz91NCWzYP6_secret_oIY9Vwdf4coECaCgx0zgmkYMF", + "client_secret": "pi_3LNevR2sOmf47Nz90G2zL7T3_secret_EqcVW2yHHRtYlWJG4Qcx0qC1i", "confirmation_method": "manual", - "created": 1631532343, + "created": 1658331301, "currency": "usd", "customer": "cus_8Di1wjdVktv5kt", - "description": "Invoice reference: 2109001/VL", + "description": "Invoice reference: 2207001/VL", "invoice": null, "last_payment_error": null, "livemode": false, @@ -519,10 +540,11 @@ http_interactions: }, "next_action": null, "on_behalf_of": null, - "payment_method": "pm_1JZDIA2sOmf47Nz9rl1nigpQ", + "payment_method": "pm_1LNevQ2sOmf47Nz9khvl8vC0", "payment_method_options": { "card": { "installments": null, + "mandate_options": null, "network": null, "request_three_d_secure": "automatic" } @@ -530,6 +552,7 @@ http_interactions: "payment_method_types": [ "card" ], + "processing": null, "receipt_email": null, "review": null, "setup_future_usage": null, @@ -541,5 +564,5 @@ http_interactions: "transfer_data": null, "transfer_group": null } - recorded_at: Mon, 13 Sep 2021 11:25:44 GMT + recorded_at: Wed, 20 Jul 2022 15:35:04 GMT recorded_with: VCR 6.0.0 From fda93cb7dfd5db3e051dd852ee979298b6a96ab7 Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Wed, 20 Jul 2022 16:28:17 -0300 Subject: [PATCH 056/141] Fix SSO properties not updating --- app/models/concerns/single_sign_on_concern.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/single_sign_on_concern.rb b/app/models/concerns/single_sign_on_concern.rb index 5637e6018..38f332ffd 100644 --- a/app/models/concerns/single_sign_on_concern.rb +++ b/app/models/concerns/single_sign_on_concern.rb @@ -40,7 +40,7 @@ module SingleSignOnConcern ## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email' ## @param data {*} the data to put in the given key. Eg. 'user@example.com' def set_data_from_sso_mapping(sso_mapping, data) - return if data.nil? || data.blank? || mapped_from_sso&.include?(sso_mapping) + return if data.nil? || data.blank? if sso_mapping.to_s.start_with? 'user.' self[sso_mapping[5..-1].to_sym] = data @@ -73,6 +73,8 @@ module SingleSignOnConcern end end + return if mapped_from_sso&.include?(sso_mapping) + self.mapped_from_sso = [mapped_from_sso, sso_mapping].compact.join(',') end @@ -122,7 +124,7 @@ module SingleSignOnConcern logger.debug "mapping sso field #{field} with value=#{value}" # we do not merge the email field if its end with the special value '-duplicate' as this means # that the user is currently merging with the account that have the same email than the sso - set_data_from_sso_mapping(field, value) unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && user.admin?) + set_data_from_sso_mapping(field, value) unless field == 'user.email' && value.end_with?('-duplicate') end # run the account transfer in an SQL transaction to ensure data integrity From 417064256558d8bfddc0b3828da82946f482f7e1 Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Wed, 20 Jul 2022 16:33:45 -0300 Subject: [PATCH 057/141] Remove unnecessary translation changes --- config/locales/app.admin.de.yml | 1 - config/locales/app.admin.es.yml | 1 - config/locales/app.admin.fr.yml | 1 - config/locales/app.admin.no.yml | 1 - config/locales/app.admin.pt.yml | 1 - config/locales/app.admin.zu.yml | 1 - 6 files changed, 6 deletions(-) diff --git a/config/locales/app.admin.de.yml b/config/locales/app.admin.de.yml index d45eaf7c8..de7d5e514 100644 --- a/config/locales/app.admin.de.yml +++ b/config/locales/app.admin.de.yml @@ -823,7 +823,6 @@ de: search_for_an_user: "Nach einem Benutzer suchen" add_a_new_member: "Neues Mitglied hinzufügen" reservations: "Reservierungen" - username: "Username" surname: "Nachname" first_name: "Vorname" email: "E-Mail" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index 8a46fcc73..b52ef9b0e 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -823,7 +823,6 @@ es: search_for_an_user: "Buscar un usuario" add_a_new_member: "Añadir un nuevo miembro" reservations: "Reservas" - username: "Username" surname: "Last name" first_name: "First name" email: "Email" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 7a1ff72c0..de6d05e04 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -823,7 +823,6 @@ fr: search_for_an_user: "Recherchez un utilisateur" add_a_new_member: "Ajouter un nouveau membre" reservations: "Réservations" - username: "Username" surname: "Nom" first_name: "Prénom" email: "Courriel" diff --git a/config/locales/app.admin.no.yml b/config/locales/app.admin.no.yml index 18a1d84ce..36ce0777e 100644 --- a/config/locales/app.admin.no.yml +++ b/config/locales/app.admin.no.yml @@ -823,7 +823,6 @@ search_for_an_user: "Søk etter bruker" add_a_new_member: "Legge til nytt medlem" reservations: "Reservasjoner" - username: "Username" surname: "Etternavn" first_name: "Fornavn" email: "E-post" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index 0de16f51b..9f3e311e5 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -823,7 +823,6 @@ pt: search_for_an_user: "Buscar por usuário" add_a_new_member: "Adicionar novo membro" reservations: "Reservas" - username: "Username" surname: "Sobrenome" first_name: "Primeiro nome" email: "Email" diff --git a/config/locales/app.admin.zu.yml b/config/locales/app.admin.zu.yml index 5f1073973..cc5fa2c44 100644 --- a/config/locales/app.admin.zu.yml +++ b/config/locales/app.admin.zu.yml @@ -823,7 +823,6 @@ zu: search_for_an_user: "crwdns7741:0crwdne7741:0" add_a_new_member: "crwdns7743:0crwdne7743:0" reservations: "crwdns7745:0crwdne7745:0" - username: "Username" surname: "crwdns7747:0crwdne7747:0" first_name: "crwdns7749:0crwdne7749:0" email: "crwdns7751:0crwdne7751:0" From dc43f58c1e09676dcee993e73b4bdf845bfef65d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Jul 2022 23:57:13 +0000 Subject: [PATCH 058/141] Bump terser from 4.8.0 to 4.8.1 Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a5c08fa21..60391d7a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7342,9 +7342,9 @@ terser-webpack-plugin@5, terser-webpack-plugin@^5.1.3: terser "^5.7.2" terser@^4.6.3: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== dependencies: commander "^2.20.0" source-map "~0.6.1" From 026a09194ac4c100cd5f906d9154782e36f7749f Mon Sep 17 00:00:00 2001 From: Guilherme Chaguri Date: Thu, 21 Jul 2022 13:21:06 -0300 Subject: [PATCH 059/141] Improve filter and sorting to include the users prefix --- app/services/members/list_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/members/list_service.rb b/app/services/members/list_service.rb index 8886c1f79..d5033c645 100644 --- a/app/services/members/list_service.rb +++ b/app/services/members/list_service.rb @@ -26,7 +26,7 @@ class Members::ListService # ILIKE => PostgreSQL case-insensitive LIKE if params[:search].size.positive? - @query = @query.where('username ILIKE :search OR ' \ + @query = @query.where('users.username ILIKE :search OR ' \ 'profiles.first_name ILIKE :search OR ' \ 'profiles.last_name ILIKE :search OR ' \ 'profiles.phone ILIKE :search OR ' \ @@ -85,7 +85,7 @@ class Members::ListService order_key = case order_key when 'username' - 'username' + 'users.username' when 'last_name' 'profiles.last_name' when 'first_name' From 25aee139aad01089292c42651285d14a5f7ab0d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 23:29:20 +0000 Subject: [PATCH 060/141] Bump tzinfo from 1.2.9 to 1.2.10 Bumps [tzinfo](https://github.com/tzinfo/tzinfo) from 1.2.9 to 1.2.10. - [Release notes](https://github.com/tzinfo/tzinfo/releases) - [Changelog](https://github.com/tzinfo/tzinfo/blob/master/CHANGES.md) - [Commits](https://github.com/tzinfo/tzinfo/compare/v1.2.9...v1.2.10) --- updated-dependencies: - dependency-name: tzinfo dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 35bb51c9b..85acd6fde 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -440,7 +440,7 @@ GEM camertron-eprun cldr-plurals-runtime-rb (~> 1.0) tzinfo - tzinfo (1.2.9) + tzinfo (1.2.10) thread_safe (~> 0.1) tzinfo-data (1.2020.4) tzinfo (>= 1.0.0) From adaea48349db6cf80ba45e6cc2ec37e446c61e86 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 25 Jul 2022 15:13:28 +0200 Subject: [PATCH 061/141] (bug) wrong currency on invoices files --- CHANGELOG.md | 2 + config/initializers/sidekiq.rb | 5 + config/locales/rails.de-AT.yml | 117 +++++++++-------- config/locales/rails.de-CH.yml | 109 +++++++++------- config/locales/rails.de-DE.yml | 117 +++++++++-------- config/locales/rails.de.yml | 118 +++++++++-------- config/locales/rails.en-AU.yml | 107 +++++++++------- config/locales/rails.en-CA.yml | 107 +++++++++------- config/locales/rails.en-GB.yml | 107 +++++++++------- config/locales/rails.en-IE.yml | 107 +++++++++------- config/locales/rails.en-IN.yml | 107 +++++++++------- config/locales/rails.en-NZ.yml | 107 +++++++++------- config/locales/rails.en-US.yml | 107 +++++++++------- config/locales/rails.en-ZA.yml | 107 +++++++++------- config/locales/rails.en.yml | 104 ++++++++------- config/locales/rails.es-419.yml | 115 ++++++++++------- config/locales/rails.es-AR.yml | 116 +++++++++-------- config/locales/rails.es-CL.yml | 128 +++++++++++-------- config/locales/rails.es-CO.yml | 115 +++++++++-------- config/locales/rails.es-CR.yml | 116 +++++++++-------- config/locales/rails.es-DO.yml | 103 ++++++++------- config/locales/rails.es-EC.yml | 115 +++++++++-------- config/locales/rails.es-ES.yml | 116 +++++++++-------- config/locales/rails.es-MX.yml | 115 +++++++++-------- config/locales/rails.es-PA.yml | 115 +++++++++-------- config/locales/rails.es-PE.yml | 116 +++++++++-------- config/locales/rails.es-US.yml | 116 +++++++++-------- config/locales/rails.es-VE.yml | 116 +++++++++-------- config/locales/rails.es.yml | 219 ++++++++++++++++++++++++++++++++ config/locales/rails.fr-CA.yml | 111 +++++++++------- config/locales/rails.fr-CH.yml | 112 ++++++++-------- config/locales/rails.fr-CM.yml | 104 ++++++++------- config/locales/rails.fr-FR.yml | 105 ++++++++------- config/locales/rails.fr.yml | 106 +++++++++------- config/locales/rails.no.yml | 46 +++---- config/locales/rails.pt-BR.yml | 110 ++++++++-------- config/locales/rails.pt.yml | 115 +++++++++-------- config/locales/rails.zu.yml | 104 ++++++++------- lib/sidekiq/server_locale.rb | 12 ++ 39 files changed, 2401 insertions(+), 1673 deletions(-) create mode 100644 config/locales/rails.es.yml create mode 100644 lib/sidekiq/server_locale.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8012926..9cb697dfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Improved calendars loading time - Refactored and documented the availability-slot-reservation data model - Display bookers names to connected users now apply to all resources +- Updated rails locales files +- Fix a bug: wrong currency on invoices files - Fix a bug: unable to reserve if user's subscription plan is disabled - Fix a bug: for admins and managers, the current password is not requested before changing their own password - Fix a bug: missing translations diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 8a62e32b6..4dca7c99d 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -2,6 +2,8 @@ require 'sidekiq' require 'sidekiq-scheduler' +require 'sidekiq/middleware/i18n' +require 'sidekiq/server_locale' redis_host = ENV['REDIS_HOST'] || 'localhost' redis_url = "redis://#{redis_host}:6379" @@ -36,6 +38,9 @@ Sidekiq.configure_client do |config| config.client_middleware do |chain| chain.add SidekiqUniqueJobs::Middleware::Client end + config.server_middleware do |chain| + chain.add FabManager::Middleware::ServerLocale + end end # Quieting logging in the test environment diff --git a/config/locales/rails.de-AT.yml b/config/locales/rails.de-AT.yml index e87bb6f76..80bf25e91 100644 --- a/config/locales/rails.de-AT.yml +++ b/config/locales/rails.de-AT.yml @@ -1,57 +1,65 @@ de-AT: + activerecord: + errors: + messages: + record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' + restrict_dependent_destroy: + has_one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz + existiert. + has_many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. date: abbr_day_names: - - So - - Mo - - Di - - Mi - - Do - - Fr - - Sa + - So + - Mo + - Di + - Mi + - Do + - Fr + - Sa abbr_month_names: - - - - Jän - - Feb - - Mär - - Apr - - Mai - - Jun - - Jul - - Aug - - Sep - - Okt - - Nov - - Dez + - + - Jan + - Feb + - Mär + - Apr + - Mai + - Jun + - Jul + - Aug + - Sep + - Okt + - Nov + - Dez day_names: - - Sonntag - - Montag - - Dienstag - - Mittwoch - - Donnerstag - - Freitag - - Samstag + - Sonntag + - Montag + - Dienstag + - Mittwoch + - Donnerstag + - Freitag + - Samstag formats: default: "%d.%m.%Y" long: "%e. %B %Y" short: "%e. %b" month_names: - - - - Jänner - - Februar - - März - - April - - Mai - - Juni - - Juli - - August - - September - - Oktober - - November - - Dezember + - + - Januar + - Februar + - März + - April + - Mai + - Juni + - Juli + - August + - September + - Oktober + - November + - Dezember order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +93,9 @@ de-AT: x_months: one: ein Monat other: "%{count} Monate" + x_years: + one: ein Jahr + other: "%{count} Jahr" x_seconds: one: eine Sekunde other: "%{count} Sekunden" @@ -112,14 +123,11 @@ de-AT: invalid: ist nicht gültig less_than: muss kleiner als %{count} sein less_than_or_equal_to: muss kleiner oder gleich %{count} sein + model_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' not_a_number: ist keine Zahl not_an_integer: muss ganzzahlig sein odd: muss ungerade sein - record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' - restrict_dependent_destroy: - one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz - existiert. - many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. + required: muss ausgefüllt werden taken: ist bereits vergeben too_long: one: ist zu lang (mehr als 1 Zeichen) @@ -131,6 +139,7 @@ de-AT: one: hat die falsche Länge (muss genau 1 Zeichen haben) other: hat die falsche Länge (muss genau %{count} Zeichen haben) other_than: darf nicht gleich %{count} sein + wrong_content_type: "Inhaltstyp ist nicht zulässig" template: body: 'Bitte überprüfen Sie die folgenden Felder:' header: @@ -166,12 +175,16 @@ de-AT: billion: one: Milliarde other: Milliarden - million: Millionen + million: + one: Million + other: Millionen quadrillion: one: Billiarde other: Billiarden thousand: Tausend - trillion: Billionen + trillion: + one: Billion + other: Billionen unit: '' format: delimiter: '' @@ -188,6 +201,8 @@ de-AT: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -206,4 +221,4 @@ de-AT: default: "%A, %d. %B %Y, %H:%M Uhr" long: "%A, %d. %B %Y, %H:%M Uhr" short: "%d. %B, %H:%M Uhr" - pm: nachmittags + pm: nachmittags \ No newline at end of file diff --git a/config/locales/rails.de-CH.yml b/config/locales/rails.de-CH.yml index 74eb15cec..07dc380ca 100644 --- a/config/locales/rails.de-CH.yml +++ b/config/locales/rails.de-CH.yml @@ -1,57 +1,65 @@ de-CH: + activerecord: + errors: + messages: + record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' + restrict_dependent_destroy: + has_one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz + existiert. + has_many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. date: abbr_day_names: - - So - - Mo - - Di - - Mi - - Do - - Fr - - Sa + - So + - Mo + - Di + - Mi + - Do + - Fr + - Sa abbr_month_names: - - - - Jan - - Feb - - Mär - - Apr - - Mai - - Jun - - Jul - - Aug - - Sep - - Okt - - Nov - - Dez + - + - Jan + - Feb + - Mär + - Apr + - Mai + - Jun + - Jul + - Aug + - Sep + - Okt + - Nov + - Dez day_names: - - Sonntag - - Montag - - Dienstag - - Mittwoch - - Donnerstag - - Freitag - - Samstag + - Sonntag + - Montag + - Dienstag + - Mittwoch + - Donnerstag + - Freitag + - Samstag formats: default: "%d.%m.%Y" long: "%e. %B %Y" short: "%e. %b" month_names: - - - - Januar - - Februar - - März - - April - - Mai - - Juni - - Juli - - August - - September - - Oktober - - November - - Dezember + - + - Januar + - Februar + - März + - April + - Mai + - Juni + - Juli + - August + - September + - Oktober + - November + - Dezember order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +93,9 @@ de-CH: x_months: one: ein Monat other: "%{count} Monate" + x_years: + one: ein Jahr + other: "%{count} Jahr" x_seconds: one: eine Sekunde other: "%{count} Sekunden" @@ -112,14 +123,11 @@ de-CH: invalid: ist nicht gültig less_than: muss kleiner als %{count} sein less_than_or_equal_to: muss kleiner oder gleich %{count} sein + model_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' not_a_number: ist keine Zahl not_an_integer: muss ganzzahlig sein odd: muss ungerade sein - record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' - restrict_dependent_destroy: - one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz - existiert. - many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. + required: muss ausgefüllt werden taken: ist bereits vergeben too_long: one: ist zu lang (mehr als 1 Zeichen) @@ -131,6 +139,7 @@ de-CH: one: hat die falsche Länge (muss genau 1 Zeichen haben) other: hat die falsche Länge (muss genau %{count} Zeichen haben) other_than: darf nicht gleich %{count} sein + wrong_content_type: "Inhaltstyp ist nicht zulässig" template: body: 'Bitte überprüfen Sie die folgenden Felder:' header: @@ -192,6 +201,8 @@ de-CH: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -210,4 +221,4 @@ de-CH: default: "%A, %d. %B %Y, %H:%M Uhr" long: "%A, %d. %B %Y, %H:%M Uhr" short: "%d. %B, %H:%M Uhr" - pm: nachmittags + pm: nachmittags \ No newline at end of file diff --git a/config/locales/rails.de-DE.yml b/config/locales/rails.de-DE.yml index 8dd43df7c..57eba4ce5 100644 --- a/config/locales/rails.de-DE.yml +++ b/config/locales/rails.de-DE.yml @@ -1,57 +1,65 @@ de-DE: + activerecord: + errors: + messages: + record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' + restrict_dependent_destroy: + has_one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz + existiert. + has_many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. date: abbr_day_names: - - So - - Mo - - Di - - Mi - - Do - - Fr - - Sa + - So + - Mo + - Di + - Mi + - Do + - Fr + - Sa abbr_month_names: - - - - Jan - - Feb - - Mär - - Apr - - Mai - - Jun - - Jul - - Aug - - Sep - - Okt - - Nov - - Dez + - + - Jan + - Feb + - Mär + - Apr + - Mai + - Jun + - Jul + - Aug + - Sep + - Okt + - Nov + - Dez day_names: - - Sonntag - - Montag - - Dienstag - - Mittwoch - - Donnerstag - - Freitag - - Samstag + - Sonntag + - Montag + - Dienstag + - Mittwoch + - Donnerstag + - Freitag + - Samstag formats: default: "%d.%m.%Y" long: "%e. %B %Y" short: "%e. %b" month_names: - - - - Januar - - Februar - - März - - April - - Mai - - Juni - - Juli - - August - - September - - Oktober - - November - - Dezember + - + - Januar + - Februar + - März + - April + - Mai + - Juni + - Juli + - August + - September + - Oktober + - November + - Dezember order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +93,9 @@ de-DE: x_months: one: ein Monat other: "%{count} Monate" + x_years: + one: ein Jahr + other: "%{count} Jahr" x_seconds: one: eine Sekunde other: "%{count} Sekunden" @@ -112,14 +123,11 @@ de-DE: invalid: ist nicht gültig less_than: muss kleiner als %{count} sein less_than_or_equal_to: muss kleiner oder gleich %{count} sein + model_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' not_a_number: ist keine Zahl not_an_integer: muss ganzzahlig sein odd: muss ungerade sein - record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' - restrict_dependent_destroy: - one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz - existiert. - many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. + required: muss ausgefüllt werden taken: ist bereits vergeben too_long: one: ist zu lang (mehr als 1 Zeichen) @@ -131,6 +139,7 @@ de-DE: one: hat die falsche Länge (muss genau 1 Zeichen haben) other: hat die falsche Länge (muss genau %{count} Zeichen haben) other_than: darf nicht gleich %{count} sein + wrong_content_type: "Inhaltstyp ist nicht zulässig" template: body: 'Bitte überprüfen Sie die folgenden Felder:' header: @@ -166,12 +175,16 @@ de-DE: billion: one: Milliarde other: Milliarden - million: Millionen + million: + one: Million + other: Millionen quadrillion: one: Billiarde other: Billiarden thousand: Tausend - trillion: Billionen + trillion: + one: Billion + other: Billionen unit: '' format: delimiter: '' @@ -188,6 +201,8 @@ de-DE: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -206,4 +221,4 @@ de-DE: default: "%A, %d. %B %Y, %H:%M Uhr" long: "%A, %d. %B %Y, %H:%M Uhr" short: "%d. %B, %H:%M Uhr" - pm: nachmittags + pm: nachmittags \ No newline at end of file diff --git a/config/locales/rails.de.yml b/config/locales/rails.de.yml index aafdf0059..06b8a6ec2 100644 --- a/config/locales/rails.de.yml +++ b/config/locales/rails.de.yml @@ -1,57 +1,65 @@ de: + activerecord: + errors: + messages: + record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' + restrict_dependent_destroy: + has_one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz + existiert. + has_many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. date: abbr_day_names: - - So - - Mo - - Di - - Mi - - Do - - Fr - - Sa + - So + - Mo + - Di + - Mi + - Do + - Fr + - Sa abbr_month_names: - - - - Jan - - Feb - - Mär - - Apr - - Mai - - Jun - - Jul - - Aug - - Sep - - Okt - - Nov - - Dez + - + - Jan + - Feb + - Mär + - Apr + - Mai + - Jun + - Jul + - Aug + - Sep + - Okt + - Nov + - Dez day_names: - - Sonntag - - Montag - - Dienstag - - Mittwoch - - Donnerstag - - Freitag - - Samstag + - Sonntag + - Montag + - Dienstag + - Mittwoch + - Donnerstag + - Freitag + - Samstag formats: default: "%d.%m.%Y" long: "%e. %B %Y" short: "%e. %b" month_names: - - - - Januar - - Februar - - März - - April - - Mai - - Juni - - Juli - - August - - September - - Oktober - - November - - Dezember + - + - Januar + - Februar + - März + - April + - Mai + - Juni + - Juli + - August + - September + - Oktober + - November + - Dezember order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +93,9 @@ de: x_months: one: ein Monat other: "%{count} Monate" + x_years: + one: ein Jahr + other: "%{count} Jahr" x_seconds: one: eine Sekunde other: "%{count} Sekunden" @@ -112,14 +123,11 @@ de: invalid: ist nicht gültig less_than: muss kleiner als %{count} sein less_than_or_equal_to: muss kleiner oder gleich %{count} sein + model_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' not_a_number: ist keine Zahl not_an_integer: muss ganzzahlig sein odd: muss ungerade sein - record_invalid: 'Gültigkeitsprüfung ist fehlgeschlagen: %{errors}' - restrict_dependent_destroy: - one: Datensatz kann nicht gelöscht werden, da ein abhängiger %{record}-Datensatz - existiert. - many: Datensatz kann nicht gelöscht werden, da abhängige %{record} existieren. + required: muss ausgefüllt werden taken: ist bereits vergeben too_long: one: ist zu lang (mehr als 1 Zeichen) @@ -131,6 +139,7 @@ de: one: hat die falsche Länge (muss genau 1 Zeichen haben) other: hat die falsche Länge (muss genau %{count} Zeichen haben) other_than: darf nicht gleich %{count} sein + wrong_content_type: "Inhaltstyp ist nicht zulässig" template: body: 'Bitte überprüfen Sie die folgenden Felder:' header: @@ -166,12 +175,16 @@ de: billion: one: Milliarde other: Milliarden - million: Millionen + million: + one: Million + other: Millionen quadrillion: one: Billiarde other: Billiarden thousand: Tausend - trillion: Billionen + trillion: + one: Billion + other: Billionen unit: '' format: delimiter: '' @@ -188,6 +201,8 @@ de: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -206,5 +221,4 @@ de: default: "%A, %d. %B %Y, %H:%M Uhr" long: "%A, %d. %B %Y, %H:%M Uhr" short: "%d. %B, %H:%M Uhr" - pm: nachmittags - + pm: nachmittags \ No newline at end of file diff --git a/config/locales/rails.en-AU.yml b/config/locales/rails.en-AU.yml index 87b499fbb..526f56e8c 100644 --- a/config/locales/rails.en-AU.yml +++ b/config/locales/rails.en-AU.yml @@ -1,57 +1,64 @@ en-AU: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%d-%m-%Y" long: "%d %B, %Y" short: "%d %b" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-AU: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -100,6 +110,7 @@ en-AU: messages: accepted: must be accepted blank: can't be blank + present: must be blank confirmation: doesn't match %{attribute} empty: can't be empty equal_to: must be equal to %{count} @@ -111,10 +122,11 @@ en-AU: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -125,6 +137,8 @@ en-AU: wrong_length: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) + other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -178,9 +192,12 @@ en-AU: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n%" precision: format: delimiter: '' @@ -195,4 +212,4 @@ en-AU: default: "%a, %d %b %Y %H:%M:%S %z" long: "%d %B, %Y %H:%M" short: "%d %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en-CA.yml b/config/locales/rails.en-CA.yml index 94ea41022..e9cce5faa 100644 --- a/config/locales/rails.en-CA.yml +++ b/config/locales/rails.en-CA.yml @@ -1,57 +1,64 @@ en-CA: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%d-%m-%Y" long: "%B %d, %Y" short: "%d %b" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-CA: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -100,6 +110,7 @@ en-CA: messages: accepted: must be accepted blank: can't be blank + present: must be blank confirmation: doesn't match %{attribute} empty: can't be empty equal_to: must be equal to %{count} @@ -111,10 +122,11 @@ en-CA: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -125,6 +137,8 @@ en-CA: wrong_length: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) + other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -178,9 +192,12 @@ en-CA: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n%" precision: format: delimiter: '' @@ -195,4 +212,4 @@ en-CA: default: "%a, %d %b %Y %I:%M:%S %p %Z" long: "%B %d, %Y %I:%M %p" short: "%d %b %I:%M %p" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en-GB.yml b/config/locales/rails.en-GB.yml index 37544fd78..3f45e8e05 100644 --- a/config/locales/rails.en-GB.yml +++ b/config/locales/rails.en-GB.yml @@ -1,57 +1,64 @@ en-GB: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%d-%m-%Y" long: "%d %B, %Y" short: "%d %b" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-GB: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -100,6 +110,7 @@ en-GB: messages: accepted: must be accepted blank: can't be blank + present: must be blank confirmation: doesn't match %{attribute} empty: can't be empty equal_to: must be equal to %{count} @@ -111,10 +122,11 @@ en-GB: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -125,6 +137,8 @@ en-GB: wrong_length: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) + other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -178,9 +192,12 @@ en-GB: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n%" precision: format: delimiter: '' @@ -195,4 +212,4 @@ en-GB: default: "%a, %d %b %Y %H:%M:%S %z" long: "%d %B, %Y %H:%M" short: "%d %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en-IE.yml b/config/locales/rails.en-IE.yml index 367a029d4..e07f16323 100644 --- a/config/locales/rails.en-IE.yml +++ b/config/locales/rails.en-IE.yml @@ -1,57 +1,64 @@ en-IE: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%d-%m-%Y" long: "%d %B, %Y" short: "%d %b" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-IE: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -100,6 +110,7 @@ en-IE: messages: accepted: must be accepted blank: can't be blank + present: must be blank confirmation: doesn't match %{attribute} empty: can't be empty equal_to: must be equal to %{count} @@ -111,10 +122,11 @@ en-IE: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -125,6 +137,8 @@ en-IE: wrong_length: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) + other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -178,9 +192,12 @@ en-IE: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n%" precision: format: delimiter: '' @@ -195,4 +212,4 @@ en-IE: default: "%a, %d %b %Y %H:%M:%S %z" long: "%d %B, %Y %H:%M" short: "%d %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en-IN.yml b/config/locales/rails.en-IN.yml index b88c424f8..9cdd550fe 100644 --- a/config/locales/rails.en-IN.yml +++ b/config/locales/rails.en-IN.yml @@ -1,57 +1,64 @@ en-IN: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%d-%m-%Y" long: "%B %d, %Y" short: "%b %d" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-IN: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -100,6 +110,7 @@ en-IN: messages: accepted: must be accepted blank: can't be blank + present: must be blank confirmation: doesn't match %{attribute} empty: can't be empty equal_to: must be equal to %{count} @@ -111,10 +122,11 @@ en-IN: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -125,6 +137,8 @@ en-IN: wrong_length: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) + other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -178,9 +192,12 @@ en-IN: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n%" precision: format: delimiter: '' @@ -195,4 +212,4 @@ en-IN: default: "%a, %d %b %Y %H:%M:%S %z" long: "%B %d, %Y %H:%M" short: "%d %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en-NZ.yml b/config/locales/rails.en-NZ.yml index 21f7902c9..d7d0dfcd3 100644 --- a/config/locales/rails.en-NZ.yml +++ b/config/locales/rails.en-NZ.yml @@ -1,57 +1,64 @@ en-NZ: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%d-%m-%Y" long: "%d %B, %Y" short: "%d %b" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-NZ: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -100,6 +110,7 @@ en-NZ: messages: accepted: must be accepted blank: can't be blank + present: must be blank confirmation: doesn't match %{attribute} empty: can't be empty equal_to: must be equal to %{count} @@ -111,10 +122,11 @@ en-NZ: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -125,6 +137,8 @@ en-NZ: wrong_length: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) + other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -178,9 +192,12 @@ en-NZ: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n%" precision: format: delimiter: '' @@ -195,4 +212,4 @@ en-NZ: default: "%a, %d %b %Y %H:%M:%S %z" long: "%d %B, %Y %H:%M" short: "%d %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en-US.yml b/config/locales/rails.en-US.yml index 3157d9eb0..47c283086 100644 --- a/config/locales/rails.en-US.yml +++ b/config/locales/rails.en-US.yml @@ -1,57 +1,64 @@ en-US: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%m-%d-%Y" long: "%B %d, %Y" short: "%b %d" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :month - - :day - - :year + - :month + - :day + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-US: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -112,13 +122,11 @@ en-US: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' - restrict_dependent_destroy: - one: Cannot delete record because a dependent %{record} exists - many: Cannot delete record because dependent %{record} exist + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -130,6 +138,7 @@ en-US: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -183,6 +192,8 @@ en-US: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -201,4 +212,4 @@ en-US: default: "%a, %d %b %Y %I:%M:%S %p %Z" long: "%B %d, %Y %I:%M %p" short: "%d %b %I:%M %p" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en-ZA.yml b/config/locales/rails.en-ZA.yml index d927e5a9a..bd8755d44 100644 --- a/config/locales/rails.en-ZA.yml +++ b/config/locales/rails.en-ZA.yml @@ -1,57 +1,64 @@ en-ZA: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%Y-%m-%d" long: "%B %d, %Y" short: "%b %d" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en-ZA: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -112,13 +122,11 @@ en-ZA: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' - restrict_dependent_destroy: - one: Cannot delete record because a dependent %{record} exists - many: Cannot delete record because dependent %{record} exist + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -130,6 +138,7 @@ en-ZA: one: is the wrong length (should be 1 character) other: is the wrong length (should be %{count} characters) other_than: must be other than %{count} + wrong_content_type: "content type is not allowed" template: body: 'There were problems with the following fields:' header: @@ -183,6 +192,8 @@ en-ZA: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -201,4 +212,4 @@ en-ZA: default: "%a, %d %b %Y %H:%M:%S %z" long: "%B %d, %Y %H:%M" short: "%d %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.en.yml b/config/locales/rails.en.yml index 42f0b3800..bea29cf4b 100644 --- a/config/locales/rails.en.yml +++ b/config/locales/rails.en.yml @@ -1,57 +1,64 @@ en: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%Y-%m-%d" long: "%B %d, %Y" short: "%b %d" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ en: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -112,13 +122,11 @@ en: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' - restrict_dependent_destroy: - one: Cannot delete record because a dependent %{record} exists - many: Cannot delete record because dependent %{record} exist + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -184,6 +192,8 @@ en: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' diff --git a/config/locales/rails.es-419.yml b/config/locales/rails.es-419.yml index ea0cd5427..f6f7f910b 100644 --- a/config/locales/rails.es-419.yml +++ b/config/locales/rails.es-419.yml @@ -1,57 +1,64 @@ es-419: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-419: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -100,6 +110,7 @@ es-419: messages: accepted: debe ser aceptado blank: no puede estar en blanco + present: debe estar en blanco confirmation: no coincide empty: no puede estar vacío equal_to: debe ser igual a %{count} @@ -111,10 +122,11 @@ es-419: invalid: es inválido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número non - record_invalid: 'La validación falló: %{errors}' + required: debe existir taken: ya ha sido tomado too_long: one: es demasiado largo (máximo 1 caracter) @@ -125,6 +137,8 @@ es-419: wrong_length: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) + other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -158,10 +172,14 @@ es-419: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: "," @@ -178,9 +196,12 @@ es-419: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," + format: "%n%" precision: format: delimiter: "," @@ -195,4 +216,4 @@ es-419: default: "%a, %d de %b de %Y a las %H:%M:%S %Z" long: "%A, %d de %B de %Y a las %I:%M %p" short: "%d de %b a las %H:%M hrs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-AR.yml b/config/locales/rails.es-AR.yml index 08bb935cb..dffebe026 100644 --- a/config/locales/rails.es-AR.yml +++ b/config/locales/rails.es-AR.yml @@ -1,57 +1,64 @@ es-AR: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-AR: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-AR: invalid: es inválido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número non - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya ha sido tomado too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-AR: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-AR: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: "," @@ -183,9 +196,12 @@ es-AR: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," + format: "%n%" precision: format: delimiter: "," @@ -200,4 +216,4 @@ es-AR: default: "%a, %d de %b de %Y a las %H:%M:%S %Z" long: "%A, %d de %B de %Y a las %I:%M %p" short: "%d de %b a las %H:%M hrs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-CL.yml b/config/locales/rails.es-CL.yml index 43e439ab7..d7a26d48b 100644 --- a/config/locales/rails.es-CL.yml +++ b/config/locales/rails.es-CL.yml @@ -1,57 +1,64 @@ es-CL: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-CL: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,18 +122,23 @@ es-CL: invalid: no es válido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor que o igual a %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser impar - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya está en uso - too_long: es demasiado largo (%{count} caracteres máximo) - too_short: es demasiado corto (%{count} caracteres mínimo) - wrong_length: no tiene la longitud correcta (%{count} caracteres exactos) + too_long: + one: "es demasiado largo (1 carácter máximo)" + other: "es demasiado largo (%{count} caracteres máximo)" + too_short: + one: "es demasiado corto (1 carácter mínimo)" + other: "es demasiado corto (%{count} caracteres mínimo)" + wrong_length: + one: "no tiene la longitud correcta (1 carácter exactos)" + other: "no tiene la longitud correcta (%{count} caracteres exactos)" other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Se encontraron problemas con los siguientes campos:' header: @@ -157,10 +172,14 @@ es-CL: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: '' @@ -177,9 +196,12 @@ es-CL: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n %" precision: format: delimiter: '' @@ -194,4 +216,4 @@ es-CL: default: "%A, %d de %B de %Y %H:%M:%S %z" long: "%A %d de %B de %Y %H:%M" short: "%d de %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-CO.yml b/config/locales/rails.es-CO.yml index e5a56a75c..c66cddfad 100644 --- a/config/locales/rails.es-CO.yml +++ b/config/locales/rails.es-CO.yml @@ -1,57 +1,64 @@ es-CO: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-CO: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-CO: invalid: es inválido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número impar - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya ha sido tomado too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-CO: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-CO: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: "," @@ -183,6 +196,8 @@ es-CO: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," @@ -200,4 +215,4 @@ es-CO: default: "%a, %d de %b de %Y a las %H:%M:%S %Z" long: "%A, %d de %B de %Y a las %I:%M %p" short: "%d de %b a las %H:%M hrs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-CR.yml b/config/locales/rails.es-CR.yml index a988a6f1a..cae170db2 100644 --- a/config/locales/rails.es-CR.yml +++ b/config/locales/rails.es-CR.yml @@ -1,57 +1,64 @@ es-CR: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-CR: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-CR: invalid: es inválido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número impar - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya ha sido utilizado too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-CR: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-CR: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: "," @@ -183,9 +196,12 @@ es-CR: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," + format: "%n %" precision: format: delimiter: "," @@ -200,4 +216,4 @@ es-CR: default: "%a, %d de %b de %Y a las %H:%M:%S %Z" long: "%A, %d de %B de %Y a las %I:%M %p" short: "%d de %b a las %H:%M hrs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-DO.yml b/config/locales/rails.es-DO.yml index f2ab40e16..229166cd3 100644 --- a/config/locales/rails.es-DO.yml +++ b/config/locales/rails.es-DO.yml @@ -1,57 +1,64 @@ es-DO: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%-d/%-m/%Y" long: "%A, %-d de %B de %Y" short: "%-d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-DO: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,9 +122,11 @@ es-DO: invalid: no es válido(a) less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor que o igual a %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un número entero odd: debe ser un número impar + required: debe existir taken: ya está en uso too_long: one: es demasiado largo(a) (máximo 1 caracter) @@ -126,6 +138,7 @@ es-DO: one: no tiene la longitud correcta (debe ser de 1 caracter) other: no tiene la longitud correcta (debe ser de %{count} caracteres) other_than: debe ser diferente de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: "Revise que los siguientes campos sean válidos:" header: @@ -183,6 +196,8 @@ es-DO: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "" @@ -201,4 +216,4 @@ es-DO: default: "%A, %-d de %B de %Y a las %-I:%M:%S %p %Z" long: "%-d de %B de %Y a las %-I:%M %p" short: "%-d %b %-I:%M %p" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-EC.yml b/config/locales/rails.es-EC.yml index 584d778ba..3fadc88a9 100644 --- a/config/locales/rails.es-EC.yml +++ b/config/locales/rails.es-EC.yml @@ -1,57 +1,64 @@ es-EC: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un(a) %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%-d/%m/%Y" long: "%A, %-d de %B de %Y" short: "%-d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-EC: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-EC: invalid: no es válido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor que o igual a %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número impar - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un(a) %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya está en uso too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-EC: one: no tiene la longitud correcta (debe ser de 1 caracter) other: no tiene la longitud correcta (debe ser de %{count} caracteres) other_than: debe ser diferente de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-EC: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: '' @@ -183,6 +196,8 @@ es-EC: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -201,4 +216,4 @@ es-EC: default: "%A, %-d de %B de %Y a las %-I:%M:%S %p %Z" long: "%-d de %B de %Y a las %-I:%M %p" short: "%-d %b %-I:%M %p" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-ES.yml b/config/locales/rails.es-ES.yml index 61a2c43a5..12edad102 100644 --- a/config/locales/rails.es-ES.yml +++ b/config/locales/rails.es-ES.yml @@ -1,57 +1,64 @@ es-ES: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%-d/%-m/%Y" long: "%-d de %B de %Y" short: "%-d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-ES: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-ES: invalid: no es válido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor que o igual a %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser impar - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya está en uso too_long: one: "es demasiado largo (1 carácter máximo)" @@ -130,6 +138,7 @@ es-ES: one: "no tiene la longitud correcta (1 carácter exactos)" other: "no tiene la longitud correcta (%{count} caracteres exactos)" other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Se encontraron problemas con los siguientes campos:' header: @@ -163,10 +172,14 @@ es-ES: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: '' @@ -183,9 +196,12 @@ es-ES: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' + format: "%n %" precision: format: delimiter: '' @@ -200,4 +216,4 @@ es-ES: default: "%A, %-d de %B de %Y %H:%M:%S %z" long: "%-d de %B de %Y %H:%M" short: "%-d de %b %H:%M" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-MX.yml b/config/locales/rails.es-MX.yml index b82b4b566..ca4407c79 100644 --- a/config/locales/rails.es-MX.yml +++ b/config/locales/rails.es-MX.yml @@ -1,57 +1,64 @@ es-MX: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: El registro no puede ser eliminado pues existe un %{record} dependiente + has_many: El registro no puede ser eliminado pues existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-MX: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-MX: invalid: es inválido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número non - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: El registro no puede ser eliminado pues existe un %{record} dependiente - many: El registro no puede ser eliminado pues existen %{record} dependientes + required: debe existir taken: ya ha sido tomado too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-MX: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) other_than: debe ser diferente a %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-MX: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: "," @@ -183,6 +196,8 @@ es-MX: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," @@ -201,4 +216,4 @@ es-MX: default: "%a, %d de %b de %Y a las %H:%M:%S %Z" long: "%A, %d de %B de %Y a las %I:%M %p" short: "%d de %b a las %H:%M hrs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-PA.yml b/config/locales/rails.es-PA.yml index e4769347e..fe624d931 100644 --- a/config/locales/rails.es-PA.yml +++ b/config/locales/rails.es-PA.yml @@ -1,57 +1,64 @@ es-PA: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un(a) %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%-d/%m/%Y" long: "%A, %-d de %B de %Y" short: "%-d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-PA: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-PA: invalid: no es válido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor que o igual a %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número impar - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un(a) %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya está en uso too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-PA: one: no tiene la longitud correcta (debe ser de 1 caracter) other: no tiene la longitud correcta (debe ser de %{count} caracteres) other_than: debe ser diferente de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-PA: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: '' @@ -183,6 +196,8 @@ es-PA: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -201,4 +216,4 @@ es-PA: default: "%A, %-d de %B de %Y a las %-I:%M:%S %p %Z" long: "%-d de %B de %Y a las %-I:%M %p" short: "%-d %b %-I:%M %p" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-PE.yml b/config/locales/rails.es-PE.yml index ebf01e0e6..a73f2686b 100644 --- a/config/locales/rails.es-PE.yml +++ b/config/locales/rails.es-PE.yml @@ -1,57 +1,64 @@ es-PE: + activerecord: + errors: + messages: + record_invalid: "Falla de validación: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B del %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-PE: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-PE: invalid: es inválido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número non - record_invalid: 'Falla de validación: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya ha sido tomado too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-PE: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-PE: format: "%n %u" units: billion: billón - million: millón + million: + one: millón + other: millones quadrillion: cuatrillón thousand: mil - trillion: trillón + trillion: + one: billón + other: billones unit: '' format: delimiter: "," @@ -183,9 +196,12 @@ es-PE: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," + format: "%n%" precision: format: delimiter: "," @@ -200,4 +216,4 @@ es-PE: default: "%a, %d de %b del %Y a las %H:%M:%S %Z" long: "%A, %d de %B del %Y a las %I:%M %p" short: "%d de %b a las %H:%M hrs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-US.yml b/config/locales/rails.es-US.yml index 3d6cdb681..e0db58933 100644 --- a/config/locales/rails.es-US.yml +++ b/config/locales/rails.es-US.yml @@ -1,57 +1,64 @@ es-US: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-US: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-US: invalid: es inválido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número non - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya ha sido tomado too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-US: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-US: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: "," @@ -183,9 +196,12 @@ es-US: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," + format: "%n%" precision: format: delimiter: "," @@ -200,4 +216,4 @@ es-US: default: "%a, %d de %b de %Y a las %H:%M:%S %Z" long: "%A, %d de %B de %Y a las %I:%M %p" short: "%d de %b a las %H:%M hrs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es-VE.yml b/config/locales/rails.es-VE.yml index f35abc3ad..b9f5b9f63 100644 --- a/config/locales/rails.es-VE.yml +++ b/config/locales/rails.es-VE.yml @@ -1,57 +1,64 @@ es-VE: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes date: abbr_day_names: - - dom - - lun - - mar - - mié - - jue - - vie - - sáb + - dom + - lun + - mar + - mié + - jue + - vie + - sáb abbr_month_names: - - - - ene - - feb - - mar - - abr - - may - - jun - - jul - - ago - - sep - - oct - - nov - - dic + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic day_names: - - domingo - - lunes - - martes - - miércoles - - jueves - - viernes - - sábado + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado formats: default: "%d/%m/%Y" long: "%A, %d de %B de %Y" short: "%d de %b" month_names: - - - - enero - - febrero - - marzo - - abril - - mayo - - junio - - julio - - agosto - - septiembre - - octubre - - noviembre - - diciembre + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ es-VE: x_months: one: 1 mes other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" x_seconds: one: 1 segundo other: "%{count} segundos" @@ -112,13 +122,11 @@ es-VE: invalid: no es válido less_than: debe ser menor que %{count} less_than_or_equal_to: debe ser menor o igual que %{count} + model_invalid: "La validación falló: %{errors}" not_a_number: no es un número not_an_integer: debe ser un entero odd: debe ser un número impar - record_invalid: 'La validación falló: %{errors}' - restrict_dependent_destroy: - one: No se puede eliminar el registro porque existe un %{record} dependiente - many: No se puede eliminar el registro porque existen %{record} dependientes + required: debe existir taken: ya está en uso too_long: one: es demasiado largo (máximo 1 caracter) @@ -130,6 +138,7 @@ es-VE: one: longitud errónea (debe ser de 1 caracter) other: longitud errónea (debe ser de %{count} caracteres) other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" template: body: 'Revise que los siguientes campos sean válidos:' header: @@ -163,10 +172,14 @@ es-VE: format: "%n %u" units: billion: mil millones - million: millón + million: + one: millón + other: millones quadrillion: mil billones thousand: mil - trillion: billón + trillion: + one: billón + other: billones unit: '' format: delimiter: "." @@ -183,9 +196,12 @@ es-VE: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "," + format: "%n%" precision: format: delimiter: "," @@ -200,4 +216,4 @@ es-VE: default: "%a, %d de %b de %Y a las %H:%M:%S%p %Z" long: "%A, %d de %B de %Y a las %I:%M%p" short: "%d de %b a las %H:%M%p" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.es.yml b/config/locales/rails.es.yml new file mode 100644 index 000000000..88104d927 --- /dev/null +++ b/config/locales/rails.es.yml @@ -0,0 +1,219 @@ +es: + activerecord: + errors: + messages: + record_invalid: "La validación falló: %{errors}" + restrict_dependent_destroy: + has_one: No se puede eliminar el registro porque existe un %{record} dependiente + has_many: No se puede eliminar el registro porque existen %{record} dependientes + date: + abbr_day_names: + - dom + - lun + - mar + - mié + - jue + - vie + - sáb + abbr_month_names: + - + - ene + - feb + - mar + - abr + - may + - jun + - jul + - ago + - sep + - oct + - nov + - dic + day_names: + - domingo + - lunes + - martes + - miércoles + - jueves + - viernes + - sábado + formats: + default: "%-d/%-m/%Y" + long: "%-d de %B de %Y" + short: "%-d de %b" + month_names: + - + - enero + - febrero + - marzo + - abril + - mayo + - junio + - julio + - agosto + - septiembre + - octubre + - noviembre + - diciembre + order: + - :day + - :month + - :year + datetime: + distance_in_words: + about_x_hours: + one: alrededor de 1 hora + other: alrededor de %{count} horas + about_x_months: + one: alrededor de 1 mes + other: alrededor de %{count} meses + about_x_years: + one: alrededor de 1 año + other: alrededor de %{count} años + almost_x_years: + one: casi 1 año + other: casi %{count} años + half_a_minute: medio minuto + less_than_x_minutes: + one: menos de 1 minuto + other: menos de %{count} minutos + less_than_x_seconds: + one: menos de 1 segundo + other: menos de %{count} segundos + over_x_years: + one: más de 1 año + other: más de %{count} años + x_days: + one: 1 día + other: "%{count} días" + x_minutes: + one: 1 minuto + other: "%{count} minutos" + x_months: + one: 1 mes + other: "%{count} meses" + x_years: + one: 1 año + other: "%{count} años" + x_seconds: + one: 1 segundo + other: "%{count} segundos" + prompts: + day: Día + hour: Hora + minute: Minutos + month: Mes + second: Segundos + year: Año + errors: + format: "%{attribute} %{message}" + messages: + accepted: debe ser aceptado + blank: no puede estar en blanco + present: debe estar en blanco + confirmation: no coincide + empty: no puede estar vacío + equal_to: debe ser igual a %{count} + even: debe ser par + exclusion: está reservado + greater_than: debe ser mayor que %{count} + greater_than_or_equal_to: debe ser mayor que o igual a %{count} + inclusion: no está incluido en la lista + invalid: no es válido + less_than: debe ser menor que %{count} + less_than_or_equal_to: debe ser menor que o igual a %{count} + model_invalid: "La validación falló: %{errors}" + not_a_number: no es un número + not_an_integer: debe ser un entero + odd: debe ser impar + required: debe existir + taken: ya está en uso + too_long: + one: "es demasiado largo (1 carácter máximo)" + other: "es demasiado largo (%{count} caracteres máximo)" + too_short: + one: "es demasiado corto (1 carácter mínimo)" + other: "es demasiado corto (%{count} caracteres mínimo)" + wrong_length: + one: "no tiene la longitud correcta (1 carácter exactos)" + other: "no tiene la longitud correcta (%{count} caracteres exactos)" + other_than: debe ser distinto de %{count} + wrong_content_type: "el tipo de contenido no está permitido" + template: + body: 'Se encontraron problemas con los siguientes campos:' + header: + one: No se pudo guardar este/a %{model} porque se encontró 1 error + other: No se pudo guardar este/a %{model} porque se encontraron %{count} errores + helpers: + select: + prompt: Por favor seleccione + submit: + create: Crear %{model} + submit: Guardar %{model} + update: Actualizar %{model} + number: + currency: + format: + delimiter: "." + format: "%n %u" + precision: 2 + separator: "," + significant: false + strip_insignificant_zeros: false + unit: "€" + format: + delimiter: "." + precision: 3 + separator: "," + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: mil millones + million: + one: millón + other: millones + quadrillion: mil billones + thousand: mil + trillion: + one: billón + other: billones + unit: '' + format: + delimiter: '' + precision: 1 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: Byte + other: Bytes + gb: GB + kb: KB + mb: MB + tb: TB + pb: PB + eb: EB + percentage: + format: + delimiter: '' + format: "%n %" + precision: + format: + delimiter: '' + support: + array: + last_word_connector: " y " + two_words_connector: " y " + words_connector: ", " + time: + am: am + formats: + default: "%A, %-d de %B de %Y %H:%M:%S %z" + long: "%-d de %B de %Y %H:%M" + short: "%-d de %b %H:%M" + pm: pm \ No newline at end of file diff --git a/config/locales/rails.fr-CA.yml b/config/locales/rails.fr-CA.yml index b7bb0c2ee..c717b5574 100644 --- a/config/locales/rails.fr-CA.yml +++ b/config/locales/rails.fr-CA.yml @@ -1,57 +1,64 @@ fr-CA: + activerecord: + errors: + messages: + record_invalid: 'La validation a échoué : %{errors}' + restrict_dependent_destroy: + has_one: "Vous ne pouvez pas supprimer l'enregistrement car un(e) %{record} dépendant(e) existe" + has_many: "Vous ne pouvez pas supprimer l'enregistrement parce que les %{record} dépendants existent" date: abbr_day_names: - - dim - - lun - - mar - - mer - - jeu - - ven - - sam + - dim + - lun + - mar + - mer + - jeu + - ven + - sam abbr_month_names: - - - - jan. - - fév. - - mar. - - avr. - - mai - - juin - - juil. - - août - - sept. - - oct. - - nov. - - déc. + - + - jan. + - fév. + - mar. + - avr. + - mai + - juin + - juil. + - août + - sept. + - oct. + - nov. + - déc. day_names: - - dimanche - - lundi - - mardi - - mercredi - - jeudi - - vendredi - - samedi + - dimanche + - lundi + - mardi + - mercredi + - jeudi + - vendredi + - samedi formats: default: "%Y-%m-%d" - long: "%d %B %Y" short: "%y-%m-%d" + long: "%d %B %Y" month_names: - - - - janvier - - février - - mars - - avril - - mai - - juin - - juillet - - août - - septembre - - octobre - - novembre - - décembre + - + - janvier + - février + - mars + - avril + - mai + - juin + - juillet + - août + - septembre + - octobre + - novembre + - décembre order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -68,13 +75,13 @@ fr-CA: other: presque %{count} ans half_a_minute: une demi-minute less_than_x_minutes: + zero: moins d'une minute one: moins d'une minute other: moins de %{count} minutes - zero: moins d'une minute less_than_x_seconds: + zero: moins d'une seconde one: moins d'une seconde other: moins de %{count} secondes - zero: moins d'une seconde over_x_years: one: plus d'un an other: plus de %{count} ans @@ -87,6 +94,9 @@ fr-CA: x_months: one: 1 mois other: "%{count} mois" + x_years: + one: 1 an + other: "%{count} ans" x_seconds: one: 1 seconde other: "%{count} secondes" @@ -102,6 +112,7 @@ fr-CA: messages: accepted: doit être accepté(e) blank: doit être rempli(e) + present: doit être vide confirmation: ne concorde pas avec %{attribute} empty: doit être rempli(e) equal_to: doit être égal à %{count} @@ -113,10 +124,11 @@ fr-CA: invalid: n'est pas valide less_than: doit être inférieur à %{count} less_than_or_equal_to: doit être inférieur ou égal à %{count} + model_invalid: "Validation échouée : %{errors}" not_a_number: n'est pas un nombre not_an_integer: doit être un nombre entier odd: doit être impair - record_invalid: 'La validation a échoué : %{errors}' + required: doit exister taken: n'est pas disponible too_long: one: est trop long (pas plus d'un caractère) @@ -127,6 +139,8 @@ fr-CA: wrong_length: one: ne fait pas la bonne longueur (doit comporter un seul caractère) other: ne fait pas la bonne longueur (doit comporter %{count} caractères) + other_than: doit être différent de %{count} + wrong_content_type: "ce type de contenu n'est pas autorisé" template: body: 'Veuillez vérifier les champs suivants : ' header: @@ -180,9 +194,12 @@ fr-CA: kb: ko mb: Mo tb: To + pb: Po + eb: Eo percentage: format: delimiter: '' + format: "%n%" precision: format: delimiter: '' diff --git a/config/locales/rails.fr-CH.yml b/config/locales/rails.fr-CH.yml index 6e44d5808..c7e250c51 100644 --- a/config/locales/rails.fr-CH.yml +++ b/config/locales/rails.fr-CH.yml @@ -1,57 +1,64 @@ fr-CH: - date: + activerecord: + errors: + messages: + record_invalid: 'La validation a échoué : %{errors}' + restrict_dependent_destroy: + has_one: "Vous ne pouvez pas supprimer l'enregistrement car un(e) %{record} dépendant(e) existe" + has_many: "Vous ne pouvez pas supprimer l'enregistrement parce que les %{record} dépendants existent" + date: abbr_day_names: - - dim - - lun - - mar - - mer - - jeu - - ven - - sam + - dim + - lun + - mar + - mer + - jeu + - ven + - sam abbr_month_names: - - - - jan. - - fév. - - mar. - - avr. - - mai - - juin - - juil. - - août - - sept. - - oct. - - nov. - - déc. + - + - jan. + - fév. + - mar. + - avr. + - mai + - juin + - juil. + - août + - sept. + - oct. + - nov. + - déc. day_names: - - dimanche - - lundi - - mardi - - mercredi - - jeudi - - vendredi - - samedi + - dimanche + - lundi + - mardi + - mercredi + - jeudi + - vendredi + - samedi formats: default: "%d.%m.%Y" - long: "%e %B %Y" short: "%e %b" + long: "%e %B %Y" month_names: - - - - janvier - - février - - mars - - avril - - mai - - juin - - juillet - - août - - septembre - - octobre - - novembre - - décembre + - + - janvier + - février + - mars + - avril + - mai + - juin + - juillet + - août + - septembre + - octobre + - novembre + - décembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -68,13 +75,13 @@ fr-CH: other: presque %{count} ans half_a_minute: une demi-minute less_than_x_minutes: + zero: moins d'une minute one: moins d'une minute other: moins de %{count} minutes - zero: moins d'une minute less_than_x_seconds: + zero: moins d'une seconde one: moins d'une seconde other: moins de %{count} secondes - zero: moins d'une seconde over_x_years: one: plus d'un an other: plus de %{count} ans @@ -87,6 +94,9 @@ fr-CH: x_months: one: 1 mois other: "%{count} mois" + x_years: + one: 1 an + other: "%{count} ans" x_seconds: one: 1 seconde other: "%{count} secondes" @@ -114,13 +124,11 @@ fr-CH: invalid: n'est pas valide less_than: doit être inférieur à %{count} less_than_or_equal_to: doit être inférieur ou égal à %{count} + model_invalid: "Validation échouée : %{errors}" not_a_number: n'est pas un nombre not_an_integer: doit être un nombre entier odd: doit être impair - record_invalid: 'La validation a échoué : %{errors}' - restrict_dependent_destroy: - one: 'Suppression impossible: un autre enregistrement est lié' - many: 'Suppression impossible: d''autres enregistrements sont liés' + required: doit exister taken: n'est pas disponible too_long: one: est trop long (pas plus d'un caractère) @@ -186,6 +194,8 @@ fr-CH: kb: ko mb: Mo tb: To + pb: Po + eb: Eo percentage: format: delimiter: '' diff --git a/config/locales/rails.fr-CM.yml b/config/locales/rails.fr-CM.yml index c8608ec23..0a9ba1306 100644 --- a/config/locales/rails.fr-CM.yml +++ b/config/locales/rails.fr-CM.yml @@ -1,57 +1,64 @@ fr-CM: + activerecord: + errors: + messages: + record_invalid: 'La validation a échoué : %{errors}' + restrict_dependent_destroy: + has_one: "Vous ne pouvez pas supprimer l'enregistrement car un(e) %{record} dépendant(e) existe" + has_many: "Vous ne pouvez pas supprimer l'enregistrement parce que les %{record} dépendants existent" date: abbr_day_names: - - dim - - lun - - mar - - mer - - jeu - - ven - - sam + - dim + - lun + - mar + - mer + - jeu + - ven + - sam abbr_month_names: - - - - jan. - - fév. - - mar. - - avr. - - mai - - juin - - juil. - - août - - sept. - - oct. - - nov. - - déc. + - + - jan. + - fév. + - mar. + - avr. + - mai + - juin + - juil. + - août + - sept. + - oct. + - nov. + - déc. day_names: - - dimanche - - lundi - - mardi - - mercredi - - jeudi - - vendredi - - samedi + - dimanche + - lundi + - mardi + - mercredi + - jeudi + - vendredi + - samedi formats: default: "%d/%m/%Y" short: "%e %b" long: "%A%e %B %Y" month_names: - - - - janvier - - février - - mars - - avril - - mai - - juin - - juillet - - août - - septembre - - octobre - - novembre - - décembre + - + - janvier + - février + - mars + - avril + - mai + - juin + - juillet + - août + - septembre + - octobre + - novembre + - décembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -87,6 +94,9 @@ fr-CM: x_months: one: 1 mois other: "%{count} mois" + x_years: + one: 1 an + other: "%{count} ans" x_seconds: one: 1 seconde other: "%{count} secondes" @@ -114,13 +124,11 @@ fr-CM: invalid: n'est pas valide less_than: doit être inférieur à %{count} less_than_or_equal_to: doit être inférieur ou égal à %{count} + model_invalid: "Validation échouée : %{errors}" not_a_number: n'est pas un nombre not_an_integer: doit être un nombre entier odd: doit être impair - record_invalid: 'La validation a échoué : %{errors}' - restrict_dependent_destroy: - one: 'Suppression impossible: un autre enregistrement est lié' - many: 'Suppression impossible: d''autres enregistrements sont liés' + required: doit exister taken: n'est pas disponible too_long: one: est trop long (pas plus d'un caractère) @@ -186,6 +194,8 @@ fr-CM: kb: ko mb: Mo tb: To + pb: Po + eb: Eo percentage: format: delimiter: '' diff --git a/config/locales/rails.fr-FR.yml b/config/locales/rails.fr-FR.yml index 11c2eef79..96c566564 100644 --- a/config/locales/rails.fr-FR.yml +++ b/config/locales/rails.fr-FR.yml @@ -1,57 +1,64 @@ fr-FR: + activerecord: + errors: + messages: + record_invalid: 'La validation a échoué : %{errors}' + restrict_dependent_destroy: + has_one: "Vous ne pouvez pas supprimer l'enregistrement car un(e) %{record} dépendant(e) existe" + has_many: "Vous ne pouvez pas supprimer l'enregistrement parce que les %{record} dépendants existent" date: abbr_day_names: - - dim - - lun - - mar - - mer - - jeu - - ven - - sam + - dim + - lun + - mar + - mer + - jeu + - ven + - sam abbr_month_names: - - - - jan. - - fév. - - mar. - - avr. - - mai - - juin - - juil. - - août - - sept. - - oct. - - nov. - - déc. + - + - jan. + - fév. + - mar. + - avr. + - mai + - juin + - juil. + - août + - sept. + - oct. + - nov. + - déc. day_names: - - dimanche - - lundi - - mardi - - mercredi - - jeudi - - vendredi - - samedi + - dimanche + - lundi + - mardi + - mercredi + - jeudi + - vendredi + - samedi formats: default: "%d/%m/%Y" short: "%e %b" long: "%e %B %Y" month_names: - - - - janvier - - février - - mars - - avril - - mai - - juin - - juillet - - août - - septembre - - octobre - - novembre - - décembre + - + - janvier + - février + - mars + - avril + - mai + - juin + - juillet + - août + - septembre + - octobre + - novembre + - décembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -87,6 +94,9 @@ fr-FR: x_months: one: 1 mois other: "%{count} mois" + x_years: + one: 1 an + other: "%{count} ans" x_seconds: one: 1 seconde other: "%{count} secondes" @@ -114,13 +124,11 @@ fr-FR: invalid: n'est pas valide less_than: doit être inférieur à %{count} less_than_or_equal_to: doit être inférieur ou égal à %{count} + model_invalid: "Validation échouée : %{errors}" not_a_number: n'est pas un nombre not_an_integer: doit être un nombre entier odd: doit être impair - record_invalid: 'La validation a échoué : %{errors}' - restrict_dependent_destroy: - one: 'Suppression impossible: un autre enregistrement est lié' - many: 'Suppression impossible: d''autres enregistrements sont liés' + required: doit exister taken: n'est pas disponible too_long: one: est trop long (pas plus d'un caractère) @@ -132,6 +140,7 @@ fr-FR: one: ne fait pas la bonne longueur (doit comporter un seul caractère) other: ne fait pas la bonne longueur (doit comporter %{count} caractères) other_than: doit être différent de %{count} + wrong_content_type: "ce type de contenu n'est pas autorisé" template: body: 'Veuillez vérifier les champs suivants : ' header: @@ -185,6 +194,8 @@ fr-FR: kb: ko mb: Mo tb: To + pb: Po + eb: Eo percentage: format: delimiter: '' diff --git a/config/locales/rails.fr.yml b/config/locales/rails.fr.yml index 802a0e86a..0d2dbd6af 100644 --- a/config/locales/rails.fr.yml +++ b/config/locales/rails.fr.yml @@ -1,57 +1,64 @@ fr: + activerecord: + errors: + messages: + record_invalid: 'La validation a échoué : %{errors}' + restrict_dependent_destroy: + has_one: "Vous ne pouvez pas supprimer l'enregistrement car un(e) %{record} dépendant(e) existe" + has_many: "Vous ne pouvez pas supprimer l'enregistrement parce que les %{record} dépendants existent" date: abbr_day_names: - - dim - - lun - - mar - - mer - - jeu - - ven - - sam + - dim + - lun + - mar + - mer + - jeu + - ven + - sam abbr_month_names: - - - - jan. - - fév. - - mar. - - avr. - - mai - - juin - - juil. - - août - - sept. - - oct. - - nov. - - déc. + - + - jan. + - fév. + - mar. + - avr. + - mai + - juin + - juil. + - août + - sept. + - oct. + - nov. + - déc. day_names: - - dimanche - - lundi - - mardi - - mercredi - - jeudi - - vendredi - - samedi + - dimanche + - lundi + - mardi + - mercredi + - jeudi + - vendredi + - samedi formats: default: "%d/%m/%Y" short: "%e %b" - long: "%A%e %B %Y" + long: "%e %B %Y" month_names: - - - - janvier - - février - - mars - - avril - - mai - - juin - - juillet - - août - - septembre - - octobre - - novembre - - décembre + - + - janvier + - février + - mars + - avril + - mai + - juin + - juillet + - août + - septembre + - octobre + - novembre + - décembre order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -87,6 +94,9 @@ fr: x_months: one: 1 mois other: "%{count} mois" + x_years: + one: 1 an + other: "%{count} ans" x_seconds: one: 1 seconde other: "%{count} secondes" @@ -114,13 +124,11 @@ fr: invalid: n'est pas valide less_than: doit être inférieur à %{count} less_than_or_equal_to: doit être inférieur ou égal à %{count} + model_invalid: "Validation échouée : %{errors}" not_a_number: n'est pas un nombre not_an_integer: doit être un nombre entier odd: doit être impair - record_invalid: 'La validation a échoué : %{errors}' - restrict_dependent_destroy: - one: 'Suppression impossible: un autre enregistrement est lié' - many: 'Suppression impossible: d''autres enregistrements sont liés' + required: doit exister taken: n'est pas disponible too_long: one: est trop long (pas plus d'un caractère) @@ -186,6 +194,8 @@ fr: kb: ko mb: Mo tb: To + pb: Po + eb: Eo percentage: format: delimiter: '' diff --git a/config/locales/rails.no.yml b/config/locales/rails.no.yml index cb317ecdf..09fdbddb1 100644 --- a/config/locales/rails.no.yml +++ b/config/locales/rails.no.yml @@ -75,42 +75,43 @@ one: nesten 1 år other: nesten %{count} år half_a_minute: et halvt minutt - less_than_x_seconds: - one: mindre enn 1 sekund - other: mindre enn %{count} sekunder less_than_x_minutes: one: mindre enn 1 minutt other: mindre enn %{count} minutter + less_than_x_seconds: + one: mindre enn 1 sekund + other: mindre enn %{count} sekunder over_x_years: one: over 1 år other: over %{count} år - x_seconds: - one: 1 sekund - other: "%{count} sekunder" - x_minutes: - one: 1 minutt - other: "%{count} minutter" x_days: one: 1 dag other: "%{count} dager" + x_minutes: + one: 1 minutt + other: "%{count} minutter" x_months: one: 1 måned other: "%{count} måneder" x_years: one: 1 år other: "%{count} år" + x_seconds: + one: 1 sekund + other: "%{count} sekunder" prompts: - second: sekund - minute: minutt - hour: time day: dag + hour: time + minute: minutt month: måned + second: sekund year: år errors: format: "%{attribute} %{message}" messages: accepted: må være akseptert - blank: kan ikke være tom + blank: kan ikke være blank + present: må være blank confirmation: er ikke lik %{attribute} empty: kan ikke være tom equal_to: må være lik %{count} @@ -122,23 +123,16 @@ invalid: er ugyldig less_than: må være mindre enn %{count} less_than_or_equal_to: må være mindre enn eller lik %{count} - model_invalid: 'Det oppstod feil: %{errors}' + model_invalid: "Det oppstod feil: %{errors}" not_a_number: er ikke et tall not_an_integer: er ikke et heltall odd: må være oddetall - other_than: kan ikke være nøyaktig %{count} - present: må være tom required: må eksistere taken: er allerede i bruk - too_long: - one: er for lang (maksimalt 1 tegn) - other: er for lang (maksimalt %{count} tegn) - too_short: - one: er for kort (minst 1 tegn) - other: er for kort (minst %{count} tegn) - wrong_length: - one: har feil lengde (må være 1 tegn) - other: har feil lengde (må være %{count} tegn) + too_long: er for lang (maksimum %{count} tegn) + too_short: er for kort (minimum %{count} tegn) + wrong_length: er av feil lengde (maksimum %{count} tegn) + other_than: kan ikke være nøyaktig %{count} template: body: 'Det oppstod problemer med følgende felt:' header: @@ -218,4 +212,4 @@ default: "%A, %e. %B %Y, %H:%M" long: "%A, %e. %B %Y, %H:%M" short: "%e. %B, %H:%M" - pm: '' + pm: '' \ No newline at end of file diff --git a/config/locales/rails.pt-BR.yml b/config/locales/rails.pt-BR.yml index edcef1b9a..d6850bfe2 100755 --- a/config/locales/rails.pt-BR.yml +++ b/config/locales/rails.pt-BR.yml @@ -1,57 +1,64 @@ pt-BR: + activerecord: + errors: + messages: + record_invalid: 'A validação falhou: %{errors}' + restrict_dependent_destroy: + has_many: Não pode ser eliminado por existirem dependências de %{record} + has_one: Não pode ser eliminado por existir uma dependência de %{record} date: abbr_day_names: - - Dom - - Seg - - Ter - - Qua - - Qui - - Sex - - Sáb + - Dom + - Seg + - Ter + - Qua + - Qui + - Sex + - Sáb abbr_month_names: - - - - Jan - - Fev - - Mar - - Abr - - Mai - - Jun - - Jul - - Ago - - Set - - Out - - Nov - - Dez + - + - Jan + - Fev + - Mar + - Abr + - Mai + - Jun + - Jul + - Ago + - Set + - Out + - Nov + - Dez day_names: - - Domingo - - Segunda-feira - - Terça-feira - - Quarta-feira - - Quinta-feira - - Sexta-feira - - Sábado + - Domingo + - Segunda-feira + - Terça-feira + - Quarta-feira + - Quinta-feira + - Sexta-feira + - Sábado formats: default: "%d/%m/%Y" long: "%d de %B de %Y" short: "%d de %B" month_names: - - - - Janeiro - - Fevereiro - - Março - - Abril - - Maio - - Junho - - Julho - - Agosto - - Setembro - - Outubro - - Novembro - - Dezembro + - + - Janeiro + - Fevereiro + - Março + - Abril + - Maio + - Junho + - Julho + - Agosto + - Setembro + - Outubro + - Novembro + - Dezembro order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -88,6 +95,9 @@ pt-BR: x_seconds: one: 1 segundo other: "%{count} segundos" + x_years: + one: 1 ano + other: "%{count} anos" prompts: day: Dia hour: Hora @@ -100,7 +110,6 @@ pt-BR: messages: accepted: deve ser aceito blank: não pode ficar em branco - present: deve ficar em branco confirmation: não é igual a %{attribute} empty: não pode ficar vazio equal_to: deve ser igual a %{count} @@ -112,18 +121,17 @@ pt-BR: invalid: não é válido less_than: deve ser menor que %{count} less_than_or_equal_to: deve ser menor ou igual a %{count} + model_invalid: 'A validação falhou: %{errors}' not_a_number: não é um número not_an_integer: não é um número inteiro odd: deve ser ímpar - record_invalid: 'A validação falhou: %{errors}' - restrict_dependent_destroy: - one: Não é possível excluir o registro pois existe um %{record} dependente - many: Não é possível excluir o registro pois existem %{record} dependentes + other_than: deve ser diferente de %{count} + present: deve ficar em branco + required: é obrigatório taken: já está em uso too_long: 'é muito longo (máximo: %{count} caracteres)' too_short: 'é muito curto (mínimo: %{count} caracteres)' wrong_length: não possui o tamanho esperado (%{count} caracteres) - other_than: deve ser diferente de %{count} template: body: 'Por favor, verifique o(s) seguinte(s) campo(s):' header: @@ -185,6 +193,8 @@ pt-BR: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: "." @@ -203,4 +213,4 @@ pt-BR: default: "%a, %d de %B de %Y, %H:%M:%S %z" long: "%d de %B de %Y, %H:%M" short: "%d de %B, %H:%M" - pm: '' + pm: '' \ No newline at end of file diff --git a/config/locales/rails.pt.yml b/config/locales/rails.pt.yml index eb91ad48c..8ad57ec5f 100644 --- a/config/locales/rails.pt.yml +++ b/config/locales/rails.pt.yml @@ -1,57 +1,64 @@ pt: + activerecord: + errors: + messages: + record_invalid: 'A validação falhou: %{errors}' + restrict_dependent_destroy: + has_many: Não pode ser eliminado por existirem dependências de %{record} + has_one: Não pode ser eliminado por existir uma dependência de %{record} date: abbr_day_names: - - Dom - - Seg - - Ter - - Qua - - Qui - - Sex - - Sáb + - Dom + - Seg + - Ter + - Qua + - Qui + - Sex + - Sáb abbr_month_names: - - - - Jan - - Fev - - Mar - - Abr - - Mai - - Jun - - Jul - - Ago - - Set - - Out - - Nov - - Dez + - + - Jan + - Fev + - Mar + - Abr + - Mai + - Jun + - Jul + - Ago + - Set + - Out + - Nov + - Dez day_names: - - Domingo - - Segunda-feira - - Terça-feira - - Quarta-feira - - Quinta-feira - - Sexta-feira - - Sábado + - Domingo + - Segunda-feira + - Terça-feira + - Quarta-feira + - Quinta-feira + - Sexta-feira + - Sábado formats: default: "%d/%m/%Y" long: "%d de %B de %Y" short: "%d de %B" month_names: - - - - Janeiro - - Fevereiro - - Março - - Abril - - Maio - - Junho - - Julho - - Agosto - - Setembro - - Outubro - - Novembro - - Dezembro + - + - Janeiro + - Fevereiro + - Março + - Abril + - Maio + - Junho + - Julho + - Agosto + - Setembro + - Outubro + - Novembro + - Dezembro order: - - :day - - :month - - :year + - :day + - :month + - :year datetime: distance_in_words: about_x_hours: @@ -88,6 +95,9 @@ pt: x_seconds: one: 1 segundo other: "%{count} segundos" + x_years: + one: 1 ano + other: "%{count} anos" prompts: day: Dia hour: Hora @@ -111,10 +121,13 @@ pt: invalid: é inválido less_than: tem de ser menor que %{count} less_than_or_equal_to: tem de ser menor ou igual a %{count} + model_invalid: 'A validação falhou: %{errors}' not_a_number: não é um número not_an_integer: tem de ser um inteiro odd: tem de ser ímpar - record_invalid: 'A validação falhou: %{errors}' + other_than: tem de ser diferente de %{count} + present: não pode estar em branco + required: é obrigatório taken: não está disponível too_long: é demasiado grande (o máximo é de %{count} caracteres) too_short: é demasiado pequeno (o mínimo é de %{count} caracteres) @@ -122,8 +135,8 @@ pt: template: body: 'Por favor, verifique os seguintes campos:' header: - one: 'Não foi possível guardar %{model}: 1 erro' - other: 'Não foi possível guardar %{model}: %{count} erros' + one: '1 erro impediu guardar este %{model}' + other: '%{count} erros impediram guardar este %{model}' helpers: select: prompt: Por favor seleccione @@ -134,8 +147,8 @@ pt: number: currency: format: - delimiter: " " - format: "%n %u" + delimiter: "." + format: "%u %n" precision: 2 separator: "," significant: false @@ -180,6 +193,8 @@ pt: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' @@ -188,7 +203,7 @@ pt: delimiter: '' support: array: - last_word_connector: ", e" + last_word_connector: " e " two_words_connector: " e " words_connector: ", " time: @@ -197,4 +212,4 @@ pt: default: "%A, %d de %B de %Y, %H:%Mh" long: "%A, %d de %B de %Y, %H:%Mh" short: "%d/%m, %H:%M hs" - pm: pm + pm: pm \ No newline at end of file diff --git a/config/locales/rails.zu.yml b/config/locales/rails.zu.yml index a12932fb7..44b5840c1 100644 --- a/config/locales/rails.zu.yml +++ b/config/locales/rails.zu.yml @@ -1,57 +1,64 @@ zu: + activerecord: + errors: + messages: + record_invalid: "Validation failed: %{errors}" + restrict_dependent_destroy: + has_one: "Cannot delete record because a dependent %{record} exists" + has_many: "Cannot delete record because dependent %{record} exist" date: abbr_day_names: - - Sun - - Mon - - Tue - - Wed - - Thu - - Fri - - Sat + - Sun + - Mon + - Tue + - Wed + - Thu + - Fri + - Sat abbr_month_names: - - - - Jan - - Feb - - Mar - - Apr - - May - - Jun - - Jul - - Aug - - Sep - - Oct - - Nov - - Dec + - + - Jan + - Feb + - Mar + - Apr + - May + - Jun + - Jul + - Aug + - Sep + - Oct + - Nov + - Dec day_names: - - Sunday - - Monday - - Tuesday - - Wednesday - - Thursday - - Friday - - Saturday + - Sunday + - Monday + - Tuesday + - Wednesday + - Thursday + - Friday + - Saturday formats: default: "%Y-%m-%d" long: "%B %d, %Y" short: "%b %d" month_names: - - - - January - - February - - March - - April - - May - - June - - July - - August - - September - - October - - November - - December + - + - January + - February + - March + - April + - May + - June + - July + - August + - September + - October + - November + - December order: - - :year - - :month - - :day + - :year + - :month + - :day datetime: distance_in_words: about_x_hours: @@ -85,6 +92,9 @@ zu: x_months: one: 1 month other: "%{count} months" + x_years: + one: 1 year + other: "%{count} years" x_seconds: one: 1 second other: "%{count} seconds" @@ -112,13 +122,11 @@ zu: invalid: is invalid less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} + model_invalid: "Validation failed: %{errors}" not_a_number: is not a number not_an_integer: must be an integer odd: must be odd - record_invalid: 'Validation failed: %{errors}' - restrict_dependent_destroy: - one: Cannot delete record because a dependent %{record} exists - many: Cannot delete record because dependent %{record} exist + required: must exist taken: has already been taken too_long: one: is too long (maximum is 1 character) @@ -184,6 +192,8 @@ zu: kb: KB mb: MB tb: TB + pb: PB + eb: EB percentage: format: delimiter: '' diff --git a/lib/sidekiq/server_locale.rb b/lib/sidekiq/server_locale.rb new file mode 100644 index 000000000..7add106a4 --- /dev/null +++ b/lib/sidekiq/server_locale.rb @@ -0,0 +1,12 @@ +module FabManager + module Middleware + class ServerLocale + def call(worker_class, job, queue) + locale = job['locale'] || Rails.application.secrets.rails_locale + I18n.with_locale(locale) do + yield + end + end + end + end +end \ No newline at end of file From 2dacb66011af0e6ba2e0aee84fbb4e8d48a41635 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 25 Jul 2022 15:24:21 +0200 Subject: [PATCH 062/141] Fix form-switch layout --- .../components/form/abstract-form-item.tsx | 12 +++++++-- .../components/form/form-switch.tsx | 1 + .../modules/form/abstract-form-item.scss | 1 + .../stylesheets/modules/form/form-switch.scss | 26 +++++++------------ 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/app/frontend/src/javascript/components/form/abstract-form-item.tsx b/app/frontend/src/javascript/components/form/abstract-form-item.tsx index 15ef6467a..679c2bde9 100644 --- a/app/frontend/src/javascript/components/form/abstract-form-item.tsx +++ b/app/frontend/src/javascript/components/form/abstract-form-item.tsx @@ -10,13 +10,14 @@ export interface AbstractFormItemProps extends PropsWithChildren
    boolean), onLabelClick?: (event: React.MouseEvent) => void, + inLine?: boolean, } /** * This abstract component should not be used directly. * Other forms components that are intended to be used with react-hook-form must extend this component. */ -export const AbstractFormItem = ({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, children }: AbstractFormItemProps) => { +export const AbstractFormItem = ({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, children }: AbstractFormItemProps) => { const [isDirty, setIsDirty] = useState(false); const [fieldError, setFieldError] = useState<{ message: string }>(error); const [isDisabled, setIsDisabled] = useState(false); @@ -59,14 +60,21 @@ export const AbstractFormItem = ({ id, label, return (
    + {(label && !inLine) &&

    {label}

    {tooltip &&
    {tooltip}
    }
    } +
    + {inLine &&

    {label}

    + {tooltip &&
    + +
    {tooltip}
    +
    } +
    } {children}
    {(isDirty && fieldError) &&
    {fieldError.message}
    } diff --git a/app/frontend/src/javascript/components/form/form-switch.tsx b/app/frontend/src/javascript/components/form/form-switch.tsx index a80c42820..2deda376c 100644 --- a/app/frontend/src/javascript/components/form/form-switch.tsx +++ b/app/frontend/src/javascript/components/form/form-switch.tsx @@ -29,6 +29,7 @@ export const FormSwitch = ({ id, label, t } control={control} diff --git a/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss b/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss index 60b96919f..59ac7a110 100644 --- a/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss +++ b/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss @@ -12,6 +12,7 @@ p { @include text-sm; margin: 0; + cursor: pointer; &::first-letter { text-transform: uppercase; } } diff --git a/app/frontend/src/stylesheets/modules/form/form-switch.scss b/app/frontend/src/stylesheets/modules/form/form-switch.scss index 72599a4a3..c08e3dc34 100644 --- a/app/frontend/src/stylesheets/modules/form/form-switch.scss +++ b/app/frontend/src/stylesheets/modules/form/form-switch.scss @@ -1,28 +1,22 @@ .form-switch { - position: relative; - .form-item-header { - position: absolute; - top: 16px; - left: 16px; - padding-top: 2px; - width: fit-content; + margin-bottom: 0; - .item-tooltip { - margin-left: 12px; - position: unset; + & > *:not(:first-child) { + margin-left: 1.5rem; + } - .content { - right: unset; - left: 0; - } + .item-tooltip .content { + max-width: min(75vw, 30ch); } } .form-item-field { display: flex; - flex-direction: row-reverse; - background-color: white; padding: 16px; + background-color: white; + & > *:not(:first-child) { + margin-left: 2rem; + } } } From 783e43f3a4b6ddb8f6333ae76312787a1d85e820 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 25 Jul 2022 15:25:44 +0200 Subject: [PATCH 063/141] Fix user porfil bio's link display --- app/frontend/src/stylesheets/app.layout.scss | 15 ++++++++++----- app/frontend/src/stylesheets/overrides.scss | 4 ++++ .../templates/shared/publicProfile.html.erb | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/frontend/src/stylesheets/app.layout.scss b/app/frontend/src/stylesheets/app.layout.scss index 7f23e3a00..8af5310c2 100644 --- a/app/frontend/src/stylesheets/app.layout.scss +++ b/app/frontend/src/stylesheets/app.layout.scss @@ -673,13 +673,18 @@ body.container { } } } - -.bio-title { - display: inherit; - text-align: center; - height: 50px; +.profile-bio { + .bio-title { + display: inherit; + height: 50px; + } + a { + color: var(--gray-soft-lightest) !important; + text-decoration: underline; + } } + .calendar-filter { h3 { line-height: 2.1rem !important; diff --git a/app/frontend/src/stylesheets/overrides.scss b/app/frontend/src/stylesheets/overrides.scss index 3dfa11c0c..31a6d34b5 100644 --- a/app/frontend/src/stylesheets/overrides.scss +++ b/app/frontend/src/stylesheets/overrides.scss @@ -23,4 +23,8 @@ &-inner > .item { height: 100%; } +} + +.ui-select-bootstrap .ui-select-choices-row > span { + white-space: normal; } \ No newline at end of file diff --git a/app/frontend/templates/shared/publicProfile.html.erb b/app/frontend/templates/shared/publicProfile.html.erb index 6c6b61ab0..dc7ff7a4d 100644 --- a/app/frontend/templates/shared/publicProfile.html.erb +++ b/app/frontend/templates/shared/publicProfile.html.erb @@ -48,11 +48,11 @@
    -
    +
    -
    +
    {{ 'app.shared.public_profile.interests' }}
    From 10a918ced7ebf648e306b8a798cc297bfd9c95b6 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 25 Jul 2022 15:32:29 +0200 Subject: [PATCH 064/141] (bug) unable to reserve an event --- .../src/javascript/controllers/events.js.erb | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 6e900143c..c7957ca3f 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -353,34 +353,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' * Callback to validate the booking of a free event */ $scope.validReserveEvent = function () { - const cartItems = { - customer_id: $scope.ctrl.member.id, - items: [ - { - reservation: { - reservable_id: $scope.event.id, - reservable_type: 'Event', - slots_reservations_attributes: [], - nb_reserve_places: $scope.reserve.nbReservePlaces, - tickets_attributes: [] - } - } - ] - } - // a single slot is used for events - cartItems.items[0].reservation.slots_reservations_attributes.push({ - slot_id: $scope.event.slot_id - }); - // iterate over reservations per prices - for (let price_id in $scope.reserve.tickets) { - if (Object.prototype.hasOwnProperty.call($scope.reserve.tickets, price_id)) { - const seats = $scope.reserve.tickets[price_id]; - cartItems.items[0].reservation.tickets_attributes.push({ - event_price_category_id: price_id, - booked: seats - }); - } - } + const reservation = mkReservation($scope.reserve, $scope.event) + const cartItems = mkCartItems(reservation, $scope.coupon.applied); // set the attempting marker $scope.attempting = true; // save the reservation to the API @@ -391,14 +365,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } , function (response) { // reservation failed - $scope.alerts = []; - $scope.alerts.push({ - msg: response.data.card[0], - type: 'danger' - }); + growl.error(response && response.data && response.data.card && response.data.card[0] || 'server error'); // unset the attempting marker - return $scope.attempting = false; - }); + $scope.attempting = false; + }) }; /** From 7432bb848eb3a3b86c9e1a13c649083bb1df0eaf Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 25 Jul 2022 16:41:48 +0200 Subject: [PATCH 065/141] (bug) unable to reserve a machine slot when another machine was already reserved on the same slot --- app/models/slot.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/slot.rb b/app/models/slot.rb index 32334d2ba..7cce52b6a 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -16,7 +16,11 @@ class Slot < ApplicationRecord availability_places = availability.available_places_per_slot(reservable) return false if availability_places.nil? - slots_reservations.where(canceled_at: nil).count >= availability_places + if reservable.nil? + slots_reservations.where(canceled_at: nil).count >= availability_places + else + slots_reservations.includes(:reservation).where(canceled_at: nil).where('reservations.reservable': reservable).count >= availability_places + end end def duration From 8015a457a48903a490394d02d4b81f652bdcd802 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 25 Jul 2022 16:48:33 +0200 Subject: [PATCH 066/141] (bug) missing modal translation --- .../javascript/components/payment/abstract-payment-modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx index c40892a02..2fc93aae3 100644 --- a/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx +++ b/app/frontend/src/javascript/components/payment/abstract-payment-modal.tsx @@ -249,7 +249,7 @@ export const AbstractPaymentModal: React.FC = ({ isOp }; AbstractPaymentModal.defaultProps = { - title: 'app.shared.payment.online_payment', + title: 'app.shared.abstract_payment_modal.online_payment', preventCgv: false, preventScheduleInfo: false, modalSize: ModalSize.medium From 6937a426c387d18af93bf325db3097a01cef1156 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 12:12:59 +0200 Subject: [PATCH 067/141] (bug) unable to show daily slots on public calendar --- app/views/api/availabilities/public.json.jbuilder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/api/availabilities/public.json.jbuilder b/app/views/api/availabilities/public.json.jbuilder index c5ac42899..7add5228c 100644 --- a/app/views/api/availabilities/public.json.jbuilder +++ b/app/views/api/availabilities/public.json.jbuilder @@ -45,7 +45,7 @@ json.array!(@availabilities) do |availability| json.machine_ids availability.availability.machines.map(&:id) json.borderColor machines_slot_border_color(availability) when 'space' - json.space_id availability.availability.space.first.id + json.space_id availability.availability.spaces.first.id json.borderColor space_slot_border_color(availability) when 'training' json.training_id availability.availability.trainings.first.id From 239e23990131ed673f909003e09f2525a578a9be Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 12:31:37 +0200 Subject: [PATCH 068/141] fix text --- config/locales/mails.en.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index 749b7852f..4c8a16b76 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -31,7 +31,7 @@ en: user_changed_group_html: "User %{NAME} has changed group." previous_group: "Previous group:" new_group: "New group:" - user_invalidated: "The user's account is invalidated." + user_invalidated: "The user's account was invalidated." notify_admin_subscription_extended: subject: "A subscription has been extended" body: @@ -356,13 +356,13 @@ en: user_update_proof_of_identity_file: "Member %{NAME} has modified the supporting documents below:" validate_user: "Please validate this account" notify_user_is_validated: - subject: "The account is validated" + subject: "Account validated" body: - account_validated: "Your account is valid." + account_validated: "Your account was validated. Now, you have access to booking features." notify_user_is_invalidated: - subject: "The account is invalid" + subject: "Account invalidated" body: - account_invalidated: "Your account is invalid." + account_invalidated: "Your account was invalidated. You won't be able to book anymore, until your account is validated again." notify_user_proof_of_identity_refusal: subject: "Your supporting documents were refused" body: From b53644d3a50d1c02eaa8e2a5964c8c39e9987983 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 12:44:30 +0200 Subject: [PATCH 069/141] (bug) profile completion form is not shown is T&C were not filled --- CHANGELOG.md | 1 + .../src/javascript/components/user/user-profile-form.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb697dfb..959c72642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Fix a bug: Unable to import accounts from SSO when the transformation modal was opened but leaved empty - Fix a bug: Unable to change the group of a user - Fix a bug: As admin, unable to create a new member (#374) +- Fix a bug: profile completion form is not shown is T&C were not filled ## v5.4.12 2022 July 06 diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index 4b8ed9336..18feb3570 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -82,7 +82,9 @@ export const UserProfileForm: React.FC = ({ action, size, }).catch(error => onError(error)); } if (showTermsAndConditionsInput) { - CustomAssetAPI.get(CustomAssetName.CguFile).then(setTermsAndConditions).catch(error => onError(error)); + CustomAssetAPI.get(CustomAssetName.CguFile).then(cgu => { + if (cgu?.custom_asset_file_attributes) setTermsAndConditions(cgu); + }).catch(error => onError(error)); } ProfileCustomFieldAPI.index().then(data => { const fData = data.filter(f => f.actived); From f67c286497d41ea19c210b92d2e5d08241b01d49 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 13:35:14 +0200 Subject: [PATCH 070/141] (bug) prevent same slot booking feature ignores canceled reservations --- CHANGELOG.md | 1 + app/frontend/src/javascript/directives/cart.js | 12 +++++++----- app/views/api/members/show.json.jbuilder | 15 ++++++++------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 959c72642..4c3895556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Refactored and documented the availability-slot-reservation data model - Display bookers names to connected users now apply to all resources - Updated rails locales files +- Fix a bug: prevent same slot booking feature ignores canceled reservations - Fix a bug: wrong currency on invoices files - Fix a bug: unable to reserve if user's subscription plan is disabled - Fix a bug: for admins and managers, the current password is not requested before changing their own password diff --git a/app/frontend/src/javascript/directives/cart.js b/app/frontend/src/javascript/directives/cart.js index e67fb4728..d21b37618 100644 --- a/app/frontend/src/javascript/directives/cart.js +++ b/app/frontend/src/javascript/directives/cart.js @@ -460,11 +460,13 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs', */ const validateSameTimeReservations = function (slot, callback) { let sameTimeReservations = $scope.settings.overlapping_categories.split(',').map(function (k) { - return _.filter($scope.user[k], function (r) { - return slot.start.isSame(r.start_at) || - (slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) || - (slot.start.isAfter(r.start_at) && slot.start.isBefore(r.end_at)) || - (slot.start.isBefore(r.start_at) && slot.end.isAfter(r.end_at)); + return _.filter($scope.user[k], function (sr) { + return !sr.canceled_at && ( + slot.start.isSame(sr.start_at) || + (slot.end.isAfter(sr.start_at) && slot.end.isBefore(sr.end_at)) || + (slot.start.isAfter(sr.start_at) && slot.start.isBefore(sr.end_at)) || + (slot.start.isBefore(sr.start_at) && slot.end.isAfter(sr.end_at)) + ); }); }); sameTimeReservations = _.union.apply(null, sameTimeReservations); diff --git a/app/views/api/members/show.json.jbuilder b/app/views/api/members/show.json.jbuilder index 6cbb9fd4a..2f4413fc2 100644 --- a/app/views/api/members/show.json.jbuilder +++ b/app/views/api/members/show.json.jbuilder @@ -70,19 +70,20 @@ json.all_projects @member.all_projects do |project| end end end -json.events_reservations @member.reservations.where(reservable_type: 'Event').joins(:slots).order('slots.start_at asc') do |r| - json.id r.id - json.start_at r.slots.first.start_at - json.end_at r.slots.first.end_at - json.nb_reserve_places r.nb_reserve_places - json.tickets r.tickets do |t| +json.events_reservations @member.reservations.where(reservable_type: 'Event').joins(:slots).order('slots.start_at asc').map(&:slots_reservations).flatten do |sr| + json.id sr.id + json.start_at sr.slot.start_at + json.end_at sr.slot.end_at + json.nb_reserve_places sr.reservation.nb_reserve_places + json.tickets sr.reservation.tickets do |t| json.booked t.booked json.price_category do json.name t.event_price_category.price_category.name end end - json.reservable r.reservable + json.reservable sr.reservation.reservable json.reservable_type 'Event' + json.canceled_at sr.canceled_at end json.invoices @member.invoices.order('reference DESC') do |i| json.id i.id From 07757e6adb326160c49fa4275e811711ca69d57d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 15:08:59 +0200 Subject: [PATCH 071/141] (bug) Erroneous "cancelation failed" message if elasticsearch was disabled --- CHANGELOG.md | 1 + app/services/slots_reservations_service.rb | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c3895556..bb2dc514c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fix a bug: Unable to change the group of a user - Fix a bug: As admin, unable to create a new member (#374) - Fix a bug: profile completion form is not shown is T&C were not filled +- Fix a bug: Erroneous "cancelation failed" message if elasticsearch was disabled ## v5.4.12 2022 July 06 diff --git a/app/services/slots_reservations_service.rb b/app/services/slots_reservations_service.rb index 1ef1846e2..2666ebddb 100644 --- a/app/services/slots_reservations_service.rb +++ b/app/services/slots_reservations_service.rb @@ -4,7 +4,7 @@ class SlotsReservationsService class << self def cancel(slot_reservation) - # first we mark ths slot reseravtion as cancelled in DB, to free a ticket + # first we mark ths slot reservation as cancelled in DB, to free a ticket slot_reservation.update_attributes(canceled_at: DateTime.current) # then we try to remove this reservation from ElasticSearch, to keep the statistics up-to-date @@ -18,6 +18,8 @@ class SlotsReservationsService conflicts: 'proceed', body: { query: { match: { reservationId: slot_reservation.reservation_id } } } ) + rescue Faraday::ConnectionFailed + warn 'Unable to update data in elasticsearch' end end end From eaaf3b9a73fe7d727f029989e39263402cd8d429 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 15:41:03 +0200 Subject: [PATCH 072/141] (bug) fix reservation reminder --- app/workers/reservation_reminder_worker.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/workers/reservation_reminder_worker.rb b/app/workers/reservation_reminder_worker.rb index 2967fa7a6..843990f29 100644 --- a/app/workers/reservation_reminder_worker.rb +++ b/app/workers/reservation_reminder_worker.rb @@ -15,7 +15,9 @@ class ReservationReminderWorker starting = DateTime.current.beginning_of_hour + delay ending = starting + 1.hour - Reservation.joins(:slots).where('slots.start_at >= ? AND slots.start_at <= ? AND slots.canceled_at IS NULL', starting, ending).each do |r| + Reservation.joins(slots_reservations: :slot) + .where('slots.start_at >= ? AND slots.start_at <= ? AND slots_reservations.canceled_at IS NULL', starting, ending) + .each do |r| already_sent = Notification.where( attached_object_type: Reservation.name, attached_object_id: r.id, From bb7eec924c94a278d43885c88bf772a2ae55a026 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 17:27:33 +0200 Subject: [PATCH 073/141] Usage of the rails logger instead of printing to standard output --- CHANGELOG.md | 1 + app/controllers/social_bot_controller.rb | 2 +- app/mailers/notifications_mailer.rb | 2 +- app/models/availability.rb | 4 ++-- app/models/invoice.rb | 11 +++++----- app/models/invoice_item.rb | 6 ++--- app/models/payment_schedule.rb | 5 +++-- app/models/plan.rb | 2 +- app/pdfs/pdf/invoice.rb | 22 +++++++++---------- app/pdfs/pdf/payment_schedule.rb | 2 +- app/services/cart_service.rb | 4 ++-- app/services/footprint_service.rb | 17 +++++++------- app/services/vat_export_service.rb | 4 ++-- app/workers/period_statistics_worker.rb | 2 +- app/workers/stripe_worker.rb | 22 +++++++++---------- app/workers/sync_objects_on_stripe_worker.rb | 20 ++++++++--------- ...09_migrate_profile_to_invoicing_profile.rb | 2 +- ...42_migrate_profile_to_statistic_profile.rb | 4 ++-- ...30_migrate_invoice_to_invoicing_profile.rb | 2 +- ...210521085710_add_object_to_invoice_item.rb | 4 ++-- ...5134018_create_payment_schedule_objects.rb | 4 ++-- ...e_payment_schedule_payment_method_check.rb | 2 +- lib/integrity/archive_helper.rb | 8 +++---- lib/tasks/fablab/auth.rake | 10 ++++----- 24 files changed, 80 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2dc514c..68760a25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Refactored and documented the availability-slot-reservation data model - Display bookers names to connected users now apply to all resources - Updated rails locales files +- Usage of the rails logger instead of printing to standard output - Fix a bug: prevent same slot booking feature ignores canceled reservations - Fix a bug: wrong currency on invoices files - Fix a bug: unable to reserve if user's subscription plan is disabled diff --git a/app/controllers/social_bot_controller.rb b/app/controllers/social_bot_controller.rb index e3da819ff..a0a4f1937 100644 --- a/app/controllers/social_bot_controller.rb +++ b/app/controllers/social_bot_controller.rb @@ -14,7 +14,7 @@ class SocialBotController < ActionController::Base @training = Training.friendly.find(Regexp.last_match(3).to_s) render :training, status: :ok else - puts "unknown bot request : #{request.original_url}" + Rails.logger.warn "unknown bot request : #{request.original_url}" end end end diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index e2de80d1e..119074177 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -25,7 +25,7 @@ class NotificationsMailer < NotifyWith::NotificationsMailer send(notification.notification_type) rescue StandardError => e - STDERR.puts "[NotificationsMailer] notification cannot be sent: #{e}" + Rails.logger.error "[NotificationsMailer] notification cannot be sent: #{e}" end def helpers diff --git a/app/models/availability.rb b/app/models/availability.rb index e737c6d03..29ca94313 100644 --- a/app/models/availability.rb +++ b/app/models/availability.rb @@ -74,7 +74,7 @@ class Availability < ApplicationRecord .joins(:slots) .where('slots.availability_id = ?', id) else - STDERR.puts "[safe_destroy] Availability with unknown type #{available_type}" + Rails.logger.warn "[safe_destroy] Availability with unknown type #{available_type}" reservations = [] end if reservations.size.zero? @@ -107,7 +107,7 @@ class Availability < ApplicationRecord when 'space' spaces.map(&:name).join(' - ') else - STDERR.puts "[title] Availability with unknown type #{available_type}" + Rails.logger.warn "[title] Availability with unknown type #{available_type}" '???' end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 47ab54b5a..34b738837 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -175,8 +175,9 @@ class Invoice < PaymentDocument return unless Setting.get('invoicing_module') unless Rails.env.test? - puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), main_item.object_id(#{main_item.object_id}), " \ - "main_item.object_type(#{main_item.object_type}), user_id(#{invoicing_profile.user_id})" + Rails.logger.info "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), " \ + "main_item.object_id(#{main_item.object_id}), " \ + "main_item.object_type(#{main_item.object_type}), user_id(#{invoicing_profile.user_id})" end InvoiceWorker.perform_async(id, user&.subscription&.expired_at) end @@ -185,9 +186,7 @@ class Invoice < PaymentDocument return if Rails.env.test? return unless changed? - puts "WARNING: Invoice update triggered [ id: #{id}, reference: #{reference} ]" - puts '---------- changes ----------' - puts changes - puts '---------------------------------' + Rails.logger.warn "Invoice update triggered [ id: #{id}, reference: #{reference} ]\n" \ + "---------- changes ----------#{changes}\n---------------------------------" end end diff --git a/app/models/invoice_item.rb b/app/models/invoice_item.rb index c9738ec8a..24db35a24 100644 --- a/app/models/invoice_item.rb +++ b/app/models/invoice_item.rb @@ -50,9 +50,7 @@ class InvoiceItem < Footprintable return if Rails.env.test? return unless changed? - puts "WARNING: InvoiceItem update triggered [ id: #{id}, invoice reference: #{invoice.reference} ]" - puts '---------- changes ----------' - puts changes - puts '---------------------------------' + Rails.logger.warn "InvoiceItem update triggered [ id: #{id}, invoice reference: #{invoice.reference} ]\n" \ + "---------- changes ----------\n#{changes}\n---------------------------------" end end diff --git a/app/models/payment_schedule.rb b/app/models/payment_schedule.rb index c79303c07..11646e8ff 100644 --- a/app/models/payment_schedule.rb +++ b/app/models/payment_schedule.rb @@ -100,8 +100,9 @@ class PaymentSchedule < PaymentDocument return unless Setting.get('invoicing_module') unless Rails.env.test? - puts "Creating an PaymentScheduleWorker job to generate the following payment schedule: id(#{id}), main_object.object_id(#{main_object.object_id}), " \ - "main_object.object_type(#{main_object.object_type}), user_id(#{invoicing_profile.user_id})" + Rails.logger.info "Creating an PaymentScheduleWorker job to generate the following payment schedule: id(#{id}), " \ + "main_object.object_id(#{main_object.object_id}), " \ + "main_object.object_type(#{main_object.object_type}), user_id(#{invoicing_profile.user_id})" end PaymentScheduleWorker.perform_async(id) end diff --git a/app/models/plan.rb b/app/models/plan.rb index 562192ac6..5a0150dd0 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -122,7 +122,7 @@ class Plan < ApplicationRecord if !stat_type.nil? && !stat_subtype.nil? StatisticTypeSubType.create!(statistic_type: stat_type, statistic_sub_type: stat_subtype) else - puts 'ERROR: Unable to create the statistics association for the new plan. ' \ + Rails.logger.error 'Unable to create the statistics association for the new plan. ' \ 'Possible causes: the type or the subtype were not created successfully.' end end diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index a63f65ce6..07de468a8 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -29,7 +29,7 @@ class PDF::Invoice < Prawn::Document begin image StringIO.new(Base64.decode64(img_b64)), fit: [415, 40] rescue StandardError => e - puts "Unable to decode invoice logo from base64: #{e}" + Rails.logger.error "Unable to decode invoice logo from base64: #{e}" end move_down 20 # the following line is a special comment to workaround RubyMine inspection problem @@ -120,7 +120,7 @@ class PDF::Invoice < Prawn::Document when 'StatisticProfilePrepaidPack' object = I18n.t('invoices.prepaid_pack') else - puts "ERROR : specified main_item.object_type type (#{invoice.main_item.object_type}) is unknown" + Rails.logger.error "specified main_item.object_type type (#{invoice.main_item.object_type}) is unknown" end end text I18n.t('invoices.object') + ' ' + object @@ -233,12 +233,14 @@ class PDF::Invoice < Prawn::Document # total verification total = invoice.total / 100.00 - puts "ERROR: totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total + Rails.logger.error "totals are NOT equals => expected: #{total}, computed: #{total_calc}" if total_calc != total # TVA vat_service = VatHistoryService.new vat_rate_group = vat_service.invoice_vat(invoice) - if total_vat != 0 + if total_vat.zero? + data += [[I18n.t('invoices.total_amount'), number_to_currency(total)]] + else data += [[I18n.t('invoices.total_including_all_taxes'), number_to_currency(total)]] vat_rate_group.each do |_type, rate| data += [[I18n.t('invoices.including_VAT_RATE', RATE: rate[:vat_rate], AMOUNT: number_to_currency(rate[:amount] / 100.00)), number_to_currency(rate[:total_vat] / 100.00)]] @@ -247,13 +249,11 @@ class PDF::Invoice < Prawn::Document data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]] # checking the round number - rounded = sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f - if rounded != sprintf('%.2f', total_calc).to_f - puts 'ERROR: rounding the numbers cause an invoice inconsistency. ' \ - "Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}" + rounded = sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00) + if rounded != sprintf('%.2f', total_calc) + Rails.logger.error 'rounding the numbers cause an invoice inconsistency. ' \ + "Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}" end - else - data += [[I18n.t('invoices.total_amount'), number_to_currency(total)]] end # display table @@ -305,7 +305,7 @@ class PDF::Invoice < Prawn::Document when 'none' payment_verbose = I18n.t('invoices.no_refund') else - puts "ERROR : specified refunding method (#{payment_verbose}) is unknown" + Rails.logger.error "specified refunding method (#{payment_verbose}) is unknown" end payment_verbose += ' ' + I18n.t('invoices.for_an_amount_of_AMOUNT', AMOUNT: number_to_currency(total)) else diff --git a/app/pdfs/pdf/payment_schedule.rb b/app/pdfs/pdf/payment_schedule.rb index 624949430..3157da62f 100644 --- a/app/pdfs/pdf/payment_schedule.rb +++ b/app/pdfs/pdf/payment_schedule.rb @@ -32,7 +32,7 @@ class PDF::PaymentSchedule < Prawn::Document begin image StringIO.new(Base64.decode64(img_b64)), fit: [415, 40] rescue StandardError => e - puts "Unable to decode invoice logo from base64: #{e}" + Rails.logger.error "Unable to decode invoice logo from base64: #{e}" end move_down 20 font('Open-Sans', size: 10) do diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index baa0e5b1b..1939dd82e 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -135,7 +135,7 @@ class CartService plan: plan_info[:plan], new_subscription: plan_info[:new_subscription]) else - STDERR.puts "WARNING: the reservable #{reservable} is not implemented" + Rails.logger.warn "the reservable #{reservable} is not implemented" raise NotImplementedError end end @@ -172,7 +172,7 @@ class CartService plan: plan, new_subscription: true) else - STDERR.puts "WARNING: the reservable #{reservable} is not implemented" + Rails.logger.warn "WARNING: the reservable #{reservable} is not implemented" raise NotImplementedError end end diff --git a/app/services/footprint_service.rb b/app/services/footprint_service.rb index 51373ce75..16425494f 100644 --- a/app/services/footprint_service.rb +++ b/app/services/footprint_service.rb @@ -51,19 +51,18 @@ class FootprintService return saved if Rails.env.test? if saved.nil? - puts "Debug data not found for #{klass} [ id: #{item.id} ]" + Rails.logger.debug { "Debug data not found for #{klass} [ id: #{item.id} ]" } else - puts "Debug footprint for #{klass} [ id: #{item.id} ]" - puts '-----------------------------------------' - puts "columns: [ #{columns.join(', ')} ]" - puts "current: #{current}" - puts " saved: #{saved.format_data(item.id)}" - puts '-----------------------------------------' + Rails.logger.debug do + "Debug footprint for #{klass} [ id: #{item.id} ]\n" \ + "-----------------------------------------\ncolumns: [ #{columns.join(', ')} ]\n" \ + "current: #{current}\n saved: #{saved.format_data(item.id)}\n" \ + '-----------------------------------------' + end item.footprint_children.map(&:debug_footprint) end others = FootprintDebug.where('klass = ? AND data LIKE ? AND id != ?', klass, "#{item.id}%", saved&.id) - puts "other possible matches IDs: #{others.map(&:id)}" - puts '-----------------------------------------' + Rails.logger.debug { "other possible matches IDs: #{others.map(&:id)}\n-----------------------------------------" } end private diff --git a/app/services/vat_export_service.rb b/app/services/vat_export_service.rb index a5a9ebcbf..1da48f745 100644 --- a/app/services/vat_export_service.rb +++ b/app/services/vat_export_service.rb @@ -62,7 +62,7 @@ class VatExportService vat_total = [] service = VatHistoryService.new invoices.each do |i| - puts "processing invoice #{i.id}..." unless Rails.env.test? + Rails.logger.info "processing invoice #{i.id}..." unless Rails.env.test? vat_total.push service.invoice_vat(i) end @@ -83,7 +83,7 @@ class VatExportService when 'amount' row << format_number(amount / 100.0) else - puts "Unsupported column: #{column}" + Rails.logger.warn "Unsupported column: #{column}" end row << separator end diff --git a/app/workers/period_statistics_worker.rb b/app/workers/period_statistics_worker.rb index b9cf062d6..a73a35004 100644 --- a/app/workers/period_statistics_worker.rb +++ b/app/workers/period_statistics_worker.rb @@ -8,7 +8,7 @@ class PeriodStatisticsWorker # @param period {String|Integer} date string or number of days until current date def perform(period) days = date_to_days(period) - puts "\n==> generating statistics for the last #{days} days <==\n" + Rails.logger.info "\n==> generating statistics for the last #{days} days <==\n" if days.zero? StatisticService.new.generate_statistic(start_date: DateTime.current.beginning_of_day, end_date: DateTime.current.end_of_day) else diff --git a/app/workers/stripe_worker.rb b/app/workers/stripe_worker.rb index 454be9412..b2f17deb3 100644 --- a/app/workers/stripe_worker.rb +++ b/app/workers/stripe_worker.rb @@ -30,18 +30,12 @@ class StripeWorker cpn = Stripe::Coupon.retrieve(coupon_code, api_key: Setting.get('stripe_secret_key')) cpn.delete rescue Stripe::InvalidRequestError => e - STDERR.puts "WARNING: Unable to delete the coupon on Stripe: #{e}" + warn "WARNING: Unable to delete the coupon on Stripe: #{e}" end def create_or_update_stp_product(class_name, id) object = class_name.constantize.find(id) - if !object.payment_gateway_object.nil? - Stripe::Product.update( - object.payment_gateway_object.gateway_object_id, - { name: object.name }, - { api_key: Setting.get('stripe_secret_key') } - ) - else + if object.payment_gateway_object.nil? product = Stripe::Product.create( { name: object.name, @@ -54,12 +48,18 @@ class StripeWorker pgo = PaymentGatewayObject.new(item: object) pgo.gateway_object = product pgo.save! - puts "Stripe product was created for the #{class_name} \##{id}" + Rails.logger.info "Stripe product was created for the #{class_name} \##{id}" + else + Stripe::Product.update( + object.payment_gateway_object.gateway_object_id, + { name: object.name }, + { api_key: Setting.get('stripe_secret_key') } + ) end rescue Stripe::InvalidRequestError obj_id = object.payment_gateway_object.gateway_object_id - STDERR.puts "WARNING: saved payment_gateway_object#id (#{obj_id}) does not match on Stripe, recreating..." + Rails.logger.warn "WARNING: saved payment_gateway_object#id (#{obj_id}) does not match on Stripe, recreating..." product = Stripe::Product.create( { name: object.name, @@ -72,6 +72,6 @@ class StripeWorker pgo = PaymentGatewayObject.new(item: object) pgo.gateway_object = product pgo.save! - puts "Stripe product was created for the #{class_name} \##{id}" + Rails.logger.info "Stripe product was created for the #{class_name} \##{id}" end end diff --git a/app/workers/sync_objects_on_stripe_worker.rb b/app/workers/sync_objects_on_stripe_worker.rb index b190f6828..62cf8826b 100644 --- a/app/workers/sync_objects_on_stripe_worker.rb +++ b/app/workers/sync_objects_on_stripe_worker.rb @@ -7,10 +7,10 @@ class SyncObjectsOnStripeWorker sidekiq_options lock: :until_executed, on_conflict: :reject, queue: :stripe def perform(notify_user_id = nil) - logger.debug 'We create all non-existing customers on stripe. This may take a while...' + Rails.logger.info 'We create all non-existing customers on stripe. This may take a while...' total = User.online_payers.count User.online_payers.each_with_index do |member, index| - logger.debug "#{index} / #{total}" + Rails.logger.info "#{index} / #{total}" begin stp_customer = member.payment_gateway_object&.gateway_object&.retrieve StripeWorker.new.create_stripe_customer(member.id) if stp_customer.nil? || stp_customer[:deleted] @@ -18,37 +18,37 @@ class SyncObjectsOnStripeWorker begin StripeWorker.new.create_stripe_customer(member.id) rescue Stripe::InvalidRequestError => e - puts "Unable to create the customer #{member.id} do to a Stripe error: #{e}" + Rails.logger.error "Unable to create the customer #{member.id} do to a Stripe error: #{e}" end end end - logger.debug 'We create all non-existing coupons on stripe. This may take a while...' + Rails.logger.info 'We create all non-existing coupons on stripe. This may take a while...' total = Coupon.all.count Coupon.all.each_with_index do |coupon, index| - logger.debug "#{index} / #{total}" + Rails.logger.info "#{index} / #{total}" Stripe::Coupon.retrieve(coupon.code, api_key: Setting.get('stripe_secret_key')) rescue Stripe::InvalidRequestError begin Stripe::Service.new.create_coupon(coupon.id) rescue Stripe::InvalidRequestError => e - logger.warn "Unable to create coupon #{coupon.code} on stripe: #{e}" + Rails.logger.error "Unable to create coupon #{coupon.code} on stripe: #{e}" end end w = StripeWorker.new [Machine, Training, Space, Plan].each do |klass| - logger.debug "We create all non-existing #{klass} on stripe. This may take a while..." + Rails.logger.info "We create all non-existing #{klass} on stripe. This may take a while..." total = klass.all.count klass.all.each_with_index do |item, index| - logger.debug "#{index} / #{total}" + Rails.logger.info "#{index} / #{total}" w.perform(:create_or_update_stp_product, klass.name, item.id) end end - logger.debug 'Sync is done' + Rails.logger.info 'Sync is done' return unless notify_user_id - logger.debug "Notify user #{notify_user_id}" + Rails.logger.info "Notify user #{notify_user_id}" user = User.find(notify_user_id) NotificationCenter.call type: :notify_admin_objects_stripe_sync, receiver: user, diff --git a/db/migrate/20190521124609_migrate_profile_to_invoicing_profile.rb b/db/migrate/20190521124609_migrate_profile_to_invoicing_profile.rb index 841ea79a2..08823dcab 100644 --- a/db/migrate/20190521124609_migrate_profile_to_invoicing_profile.rb +++ b/db/migrate/20190521124609_migrate_profile_to_invoicing_profile.rb @@ -4,7 +4,7 @@ class MigrateProfileToInvoicingProfile < ActiveRecord::Migration[4.2] def up User.all.each do |u| p = u.profile - puts "WARNING: User #{u.id} has no profile" and next unless p + Rails.logger.warn "User #{u.id} has no profile" and next unless p ip = InvoicingProfile.create!( user: u, diff --git a/db/migrate/20190521151142_migrate_profile_to_statistic_profile.rb b/db/migrate/20190521151142_migrate_profile_to_statistic_profile.rb index ac7292b40..9a5205ed5 100644 --- a/db/migrate/20190521151142_migrate_profile_to_statistic_profile.rb +++ b/db/migrate/20190521151142_migrate_profile_to_statistic_profile.rb @@ -4,7 +4,7 @@ class MigrateProfileToStatisticProfile < ActiveRecord::Migration[4.2] def up User.all.each do |u| p = u.profile - puts "WARNING: User #{u.id} has no profile" and next unless p + Rails.logger.warn "User #{u.id} has no profile" and next unless p StatisticProfile.create!( user: u, @@ -20,7 +20,7 @@ class MigrateProfileToStatisticProfile < ActiveRecord::Migration[4.2] def down StatisticProfile.all.each do |sp| p = sp.user.profile - puts "WARNING: User #{sp.user_id} has no profile" and next unless p + Rails.logger.warn "User #{sp.user_id} has no profile" and next unless p p.update_attributes( gender: sp.gender, diff --git a/db/migrate/20190522115230_migrate_invoice_to_invoicing_profile.rb b/db/migrate/20190522115230_migrate_invoice_to_invoicing_profile.rb index 45b263fd9..a7e44da64 100644 --- a/db/migrate/20190522115230_migrate_invoice_to_invoicing_profile.rb +++ b/db/migrate/20190522115230_migrate_invoice_to_invoicing_profile.rb @@ -12,7 +12,7 @@ class MigrateInvoiceToInvoicingProfile < ActiveRecord::Migration[4.2] # remove and save periods in memory periods = Integrity::ArchiveHelper.backup_and_remove_periods # migrate invoices - puts 'Migrating invoices. This may take a while...' + Rails.logger.info 'Migrating invoices. This may take a while...' Invoice.order(:id).all.each do |i| user = User.find(i.user_id) operator = User.find_by(id: i.operator_id) diff --git a/db/migrate/20210521085710_add_object_to_invoice_item.rb b/db/migrate/20210521085710_add_object_to_invoice_item.rb index c9187dd9c..b6b71ad7d 100644 --- a/db/migrate/20210521085710_add_object_to_invoice_item.rb +++ b/db/migrate/20210521085710_add_object_to_invoice_item.rb @@ -73,7 +73,7 @@ class AddObjectToInvoiceItem < ActiveRecord::Migration[5.2] Invoice.reset_column_information # chain records - puts 'Chaining all record. This may take a while...' + Rails.logger.info 'Chaining all record. This may take a while...' InvoiceItem.order(:id).all.each(&:chain_record) Invoice.order(:id).all.each(&:chain_record) @@ -121,7 +121,7 @@ class AddObjectToInvoiceItem < ActiveRecord::Migration[5.2] Invoice.reset_column_information # chain records - puts 'Chaining all record. This may take a while...' + Rails.logger.info 'Chaining all record. This may take a while...' InvoiceItem.order(:id).all.each(&:chain_record) Invoice.order(:id).all.each(&:chain_record) diff --git a/db/migrate/20210525134018_create_payment_schedule_objects.rb b/db/migrate/20210525134018_create_payment_schedule_objects.rb index 1a41b2277..bace65480 100644 --- a/db/migrate/20210525134018_create_payment_schedule_objects.rb +++ b/db/migrate/20210525134018_create_payment_schedule_objects.rb @@ -48,7 +48,7 @@ class CreatePaymentScheduleObjects < ActiveRecord::Migration[5.2] PaymentSchedule.reset_column_information # chain records - puts 'Chaining all record. This may take a while...' + Rails.logger.info 'Chaining all record. This may take a while...' PaymentScheduleItem.order(:id).all.each(&:chain_record) PaymentSchedule.order(:id).all.each(&:chain_record) end @@ -87,7 +87,7 @@ class CreatePaymentScheduleObjects < ActiveRecord::Migration[5.2] PaymentSchedule.reset_column_information # chain records - puts 'Chaining all record. This may take a while...' + Rails.logger.info 'Chaining all record. This may take a while...' PaymentScheduleItem.order(:id).all.each(&:chain_record) PaymentSchedule.order(:id).all.each(&:chain_record) end diff --git a/db/migrate/20220111134253_migrate_payment_schedule_payment_method_check.rb b/db/migrate/20220111134253_migrate_payment_schedule_payment_method_check.rb index 782d800c1..f06290324 100644 --- a/db/migrate/20220111134253_migrate_payment_schedule_payment_method_check.rb +++ b/db/migrate/20220111134253_migrate_payment_schedule_payment_method_check.rb @@ -18,7 +18,7 @@ class MigratePaymentSchedulePaymentMethodCheck < ActiveRecord::Migration[5.2] end # chain all records - puts 'Chaining all record. This may take a while...' + Rails.logger.info 'Chaining all record. This may take a while...' PaymentSchedule.order(:id).find_each(&:chain_record) # re-create all archives from the memory dump diff --git a/lib/integrity/archive_helper.rb b/lib/integrity/archive_helper.rb index 42695252a..a0b376982 100644 --- a/lib/integrity/archive_helper.rb +++ b/lib/integrity/archive_helper.rb @@ -11,7 +11,7 @@ class Integrity::ArchiveHelper def check_footprints if AccountingPeriod.count.positive? last_period = AccountingPeriod.order(start_at: :desc).first - puts "Checking invoices footprints from #{last_period.end_at}. This may take a while..." + Rails.logger.info "Checking invoices footprints from #{last_period.end_at}. This may take a while..." Invoice.where('created_at > ?', last_period.end_at).order(:id).each do |i| next if i.check_footprint @@ -19,7 +19,7 @@ class Integrity::ArchiveHelper raise "Invalid footprint for invoice #{i.id}" end else - puts 'Checking all invoices footprints. This may take a while...' + Rails.logger.info 'Checking all invoices footprints. This may take a while...' Invoice.order(:id).all.each do |i| next if i.check_footprint @@ -34,7 +34,7 @@ class Integrity::ArchiveHelper range_periods = get_periods(range_start: range_start, range_end: range_end) return [] unless range_periods.count.positive? - puts 'Removing accounting archives...' + Rails.logger.info 'Removing accounting archives...' # 1. remove protection for AccountingPeriods execute("DROP RULE IF EXISTS accounting_periods_del_protect ON #{AccountingPeriod.arel_table.name};") # 2. backup AccountingPeriods in memory @@ -58,7 +58,7 @@ class Integrity::ArchiveHelper return unless periods.size.positive? # 1. recreate AccountingPeriods - puts 'Recreating accounting archives. This may take a while...' + Rails.logger.info 'Recreating accounting archives. This may take a while...' periods.each do |p| AccountingPeriod.create!( start_at: p[:start_at], diff --git a/lib/tasks/fablab/auth.rake b/lib/tasks/fablab/auth.rake index c88053114..9269a6771 100644 --- a/lib/tasks/fablab/auth.rake +++ b/lib/tasks/fablab/auth.rake @@ -6,7 +6,7 @@ namespace :fablab do desc 'switch the active authentication provider' task :switch_provider, [:provider] => :environment do |_task, args| - providers = AuthProvider.all.inject('') { |str, item| str + item[:name] + ', ' } + providers = AuthProvider.all.inject('') { |str, item| "#{str}#{item[:name]}, " } unless args.provider puts "\e[0;31mERROR\e[0m: You must pass a provider name to activate. Available providers are: #{providers[0..-3]}" next @@ -32,14 +32,14 @@ namespace :fablab do AuthProvider.find_by(name: args.provider).update_attribute(:status, 'active') # migrate the current users. - if AuthProvider.active.providable_type != DatabaseProvider.name - # Concerns any providers except local database - User.all.each(&:generate_auth_migration_token) - else + if AuthProvider.active.providable_type == DatabaseProvider.name User.all.each do |user| # Concerns local database provider user.update_attribute(:auth_token, nil) end + else + # Concerns any providers except local database + User.all.each(&:generate_auth_migration_token) end # ask the user to restart the application From 56b254dffc2050a3e38eed34d96bbea0431e7130 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 17:38:33 +0200 Subject: [PATCH 074/141] print an error if the current invoice does not have a main_item --- app/models/invoice.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 34b738837..450a6b823 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -129,6 +129,11 @@ class Invoice < PaymentDocument def prevent_refund? return true if user.nil? + if main_item.nil? + Rails.logger.error "Invoice (id: #{id}) does not have a main_item and is probably in error" + return true + end + if main_item.object_type == 'Reservation' && main_item.object&.reservable_type == 'Training' user.trainings.include?(main_item.object.reservable_id) else From df0b5628b32f3736446e324d881989c6e384b696 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Jul 2022 17:55:45 +0200 Subject: [PATCH 075/141] (bug) canceled trainings are still shown on the public profile page --- CHANGELOG.md | 1 + app/frontend/src/javascript/filters/filters.js | 8 ++++++++ app/frontend/templates/shared/publicProfile.html.erb | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68760a25f..d93166a34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Display bookers names to connected users now apply to all resources - Updated rails locales files - Usage of the rails logger instead of printing to standard output +- Fix a bug: canceled trainings are still shown on the public profile page - Fix a bug: prevent same slot booking feature ignores canceled reservations - Fix a bug: wrong currency on invoices files - Fix a bug: unable to reserve if user's subscription plan is disabled diff --git a/app/frontend/src/javascript/filters/filters.js b/app/frontend/src/javascript/filters/filters.js index 832760e4a..293510366 100644 --- a/app/frontend/src/javascript/filters/filters.js +++ b/app/frontend/src/javascript/filters/filters.js @@ -191,6 +191,14 @@ Application.Filters.filter('humanReadablePlanName', ['$filter', function ($filte }; }]); +Application.Filters.filter('canceledReservationsFilter', [function () { + return function (elements) { + if (!angular.isUndefined(elements) && (elements != null)) { + return elements.filter(e => e.canceled_at === null); + } + }; +}]); + Application.Filters.filter('trainingReservationsFilter', [function () { return function (elements, selectedScope) { if (!angular.isUndefined(elements) && !angular.isUndefined(selectedScope) && (elements != null) && (selectedScope != null)) { diff --git a/app/frontend/templates/shared/publicProfile.html.erb b/app/frontend/templates/shared/publicProfile.html.erb index dc7ff7a4d..65fac1eb4 100644 --- a/app/frontend/templates/shared/publicProfile.html.erb +++ b/app/frontend/templates/shared/publicProfile.html.erb @@ -74,7 +74,7 @@

    {{ 'app.shared.public_profile.trainings' }}

    +
    +
    + {{ 'app.admin.settings.accounts_management' }} +
    +
    +
    +

    {{ 'app.admin.settings.members_list' }}

    +

    + {{ 'app.admin.settings.members_list_info' }} +

    +
    + + +
    +
    +
    +
    diff --git a/app/models/setting.rb b/app/models/setting.rb index 245b15446..a8bf421bf 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -147,7 +147,8 @@ class Setting < ApplicationRecord machines_module user_change_group user_validation_required - user_validation_required_list] } + user_validation_required_list + show_username_in_admin_list] } # WARNING: when adding a new key, you may also want to add it in: # - config/locales/en.yml#settings # - app/frontend/src/javascript/models/setting.ts#SettingName diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index d8b3581f7..d2687bec2 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1427,6 +1427,9 @@ en: enable_invoicing: "Enable invoicing" invoicing_module: "invoicing module" account_creation: "Account creation" + accounts_management: "Accounts management" + members_list: "Members list" + members_list_info: "You can customize the fields to display in the member management list" phone: "Phone" phone_is_required: "Phone required" phone_required_info: "You can define if the phone number should be required to register a new user on Fab-manager." @@ -1470,6 +1473,7 @@ en: extended_prices_info_html: "Spaces can have different prices depending on the cumulated duration of the booking. You can choose if this apply to all bookings or only to those starting within the same day." 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" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/en.yml b/config/locales/en.yml index 4ee542f12..fa90ed606 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -595,3 +595,4 @@ en: flickr: "flickr" machines_module: "Machines module" user_change_group: "Allow users to change their group" + show_username_in_admin_list: "Show the username in the admin's members list" diff --git a/db/seeds.rb b/db/seeds.rb index af31c3ed8..fc2b7a9fc 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -917,6 +917,8 @@ end Setting.set('extended_prices_in_same_day', false) unless Setting.find_by(name: 'extended_prices_in_same_day').try(:value) +Setting.set('show_username_in_admin_list', false) unless Setting.find_by(name: 'show_username_in_admin_list').try(:value) + if StatisticCustomAggregation.count.zero? # available reservations hours for machines machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2) From 7333a65839342bb327bc5d6c05fd983da5a2d9d9 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:05:47 +0200 Subject: [PATCH 104/141] updated changelog (#376) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 672f55de9..1c397ce12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - Fix a bug: As admin, unable to create a new member (#374) - Fix a bug: profile completion form is not shown is T&C were not filled - Fix a bug: Erroneous "cancelation failed" message if elasticsearch was disabled +- Fix a bug: admin group being replaced in SSO authentication (#376) - Fix a security issue: updated terser to 4.8.1 to fix [CVE-2022-25858](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-25858) - Fix a security issue: updated tzinfo to 1.2.10 to fix [CVE-2022-31163](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31163) - Fix a security issue: updated rails to 5.2.8.1 to fix [CVE-2022-32224](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-32224) From 986063b13468fa8c89283d31fe22b8d3b764dbef Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:09:05 +0200 Subject: [PATCH 105/141] updated changelog (#377) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c397ce12..b1a9695ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Fix a bug: profile completion form is not shown is T&C were not filled - Fix a bug: Erroneous "cancelation failed" message if elasticsearch was disabled - Fix a bug: admin group being replaced in SSO authentication (#376) +- Fix a bug: SSO data being overridden when it is empty and the user can change it (#377) - Fix a security issue: updated terser to 4.8.1 to fix [CVE-2022-25858](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-25858) - Fix a security issue: updated tzinfo to 1.2.10 to fix [CVE-2022-31163](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31163) - Fix a security issue: updated rails to 5.2.8.1 to fix [CVE-2022-32224](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-32224) From f943f68d7b366cc631feedcfac534f2b1bb60236 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:25 +0200 Subject: [PATCH 106/141] New translations app.admin.en.yml (Portuguese) --- config/locales/app.admin.pt.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index d81e9a73d..666aa1045 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -825,6 +825,7 @@ pt: search_for_an_user: "Buscar por usuário" add_a_new_member: "Adicionar novo membro" reservations: "Reservas" + username: "Username" surname: "Sobrenome" first_name: "Primeiro nome" email: "Email" @@ -1426,6 +1427,9 @@ pt: enable_invoicing: "Habilitar faturamento" invoicing_module: "módulo de faturamento" account_creation: "Criação de conta" + accounts_management: "Accounts management" + members_list: "Members list" + members_list_info: "You can customize the fields to display in the member management list" phone: "Telefone" phone_is_required: "Telefone é obrigatório" phone_required_info: "Você pode definir se o número de telefone deve ser exigido para registrar um novo usuário no Fab-manager." @@ -1469,6 +1473,7 @@ pt: extended_prices_info_html: "Os espaços podem ter preços diferentes dependendo da duração acumulada da reserva. Você pode escolher se isso se aplica a todas as reservas ou apenas àqueles que iniciam no mesmo dia." extended_prices_in_same_day: "Preços estendidos no mesmo dia" public_registrations: "Inscrições públicas" + show_username_in_admin_list: "Show the username in the list" overlapping_options: training_reservations: "Treinamentos" machine_reservations: "Máquinas" From fb2bd127e739e202cf54f8b7aa62e9bc3afd0718 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:28 +0200 Subject: [PATCH 107/141] New translations en.yml (French) --- config/locales/fr.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 4ef9125d7..98c759411 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -595,3 +595,4 @@ fr: flickr: "flickr" machines_module: "Module machines" user_change_group: "Permettre aux utilisateurs de changer leur groupe" + show_username_in_admin_list: "Show the username in the admin's members list" From 7542c52f5796ff4d3da3a94adf60fe1c002550c5 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:30 +0200 Subject: [PATCH 108/141] New translations en.yml (Spanish) --- config/locales/es.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/es.yml b/config/locales/es.yml index c148f5e92..2856baf08 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -595,3 +595,4 @@ es: flickr: "flickr" machines_module: "Machines module" user_change_group: "Allow users to change their group" + show_username_in_admin_list: "Show the username in the admin's members list" From 750a5b75b19c2db88f248f434a92f021970f0139 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:31 +0200 Subject: [PATCH 109/141] New translations en.yml (German) --- config/locales/de.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/de.yml b/config/locales/de.yml index db333594a..8c1abdc7d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -595,3 +595,4 @@ de: flickr: "flickr" machines_module: "Machines module" user_change_group: "Allow users to change their group" + show_username_in_admin_list: "Show the username in the admin's members list" From 72d7cdf801f75b52cf691cd8df58abd08fe981cf Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:32 +0200 Subject: [PATCH 110/141] New translations en.yml (Norwegian) --- config/locales/no.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/no.yml b/config/locales/no.yml index 2c6f03a26..7fe937f8e 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -595,3 +595,4 @@ flickr: "flickr" machines_module: "Machines module" user_change_group: "Allow users to change their group" + show_username_in_admin_list: "Show the username in the admin's members list" From 39042791f5af6b072f2d5b0cd598ec998c756eb5 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:33 +0200 Subject: [PATCH 111/141] New translations en.yml (Portuguese) --- config/locales/pt.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 4a4896619..45bff6a9b 100755 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -595,3 +595,4 @@ pt: flickr: "flickr" machines_module: "Módulo de Máquinas" user_change_group: "Permitir que os usuários mudem de grupo" + show_username_in_admin_list: "Show the username in the admin's members list" From 3f683fd0703b066482c02d06577746c677a54acb Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:34 +0200 Subject: [PATCH 112/141] New translations en.yml (Zulu) --- config/locales/zu.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/zu.yml b/config/locales/zu.yml index 120fb812f..ccb9dc4a0 100644 --- a/config/locales/zu.yml +++ b/config/locales/zu.yml @@ -595,3 +595,4 @@ zu: flickr: "crwdns23036:0crwdne23036:0" machines_module: "crwdns23038:0crwdne23038:0" user_change_group: "crwdns23040:0crwdne23040:0" + show_username_in_admin_list: "crwdns24016:0crwdne24016:0" From 87a4510f29bedc6fc2a45e10c65f85f1fadaea6d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:44 +0200 Subject: [PATCH 113/141] New translations app.admin.en.yml (French) --- config/locales/app.admin.fr.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 60c7fe649..94cbfae43 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -825,6 +825,7 @@ fr: search_for_an_user: "Recherchez un utilisateur" add_a_new_member: "Ajouter un nouveau membre" reservations: "Réservations" + username: "Username" surname: "Nom" first_name: "Prénom" email: "Courriel" @@ -1426,6 +1427,9 @@ fr: enable_invoicing: "Activer la facturation" invoicing_module: "module de facturation" account_creation: "Création de compte" + accounts_management: "Accounts management" + members_list: "Members list" + members_list_info: "You can customize the fields to display in the member management list" phone: "Téléphone" phone_is_required: "Téléphone requis" phone_required_info: "Vous pouvez définir si le numéro de téléphone doit être requis, lors de l'enregistrement d'un nouvel utilisateur sur Fab-manager." @@ -1469,6 +1473,7 @@ fr: extended_prices_info_html: "Les espaces peuvent avoir des prix différents selon la durée cumulée de la réservation. Vous pouvez choisir si cela s'applique à toutes les réservations ou seulement à celles qui commencent dans la même journée." extended_prices_in_same_day: "Prix étendus le même jour" public_registrations: "Inscriptions publiques" + show_username_in_admin_list: "Show the username in the list" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" From d3f308db2a8ce16000bd8e442379ba1f2b345f0a Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:47 +0200 Subject: [PATCH 114/141] New translations app.admin.en.yml (Spanish) --- config/locales/app.admin.es.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index 4dd4dc9ee..79202a048 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -825,6 +825,7 @@ es: search_for_an_user: "Buscar un usuario" add_a_new_member: "Añadir un nuevo miembro" reservations: "Reservas" + username: "Username" surname: "Last name" first_name: "First name" email: "Email" @@ -1426,6 +1427,9 @@ es: enable_invoicing: "Enable invoicing" invoicing_module: "invoicing module" account_creation: "Account creation" + accounts_management: "Accounts management" + members_list: "Members list" + members_list_info: "You can customize the fields to display in the member management list" phone: "Phone" phone_is_required: "Phone required" phone_required_info: "You can define if the phone number should be required to register a new user on Fab-manager." @@ -1469,6 +1473,7 @@ es: extended_prices_info_html: "Spaces can have different prices depending on the cumulated duration of the booking. You can choose if this apply to all bookings or only to those starting within the same day." 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" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" From ea31759df2a6ac54a1803eebb028f955a8902ae3 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:49 +0200 Subject: [PATCH 115/141] New translations app.admin.en.yml (German) --- config/locales/app.admin.de.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.admin.de.yml b/config/locales/app.admin.de.yml index 062cd5651..b82e840c0 100644 --- a/config/locales/app.admin.de.yml +++ b/config/locales/app.admin.de.yml @@ -825,6 +825,7 @@ de: search_for_an_user: "Nach einem Benutzer suchen" add_a_new_member: "Neues Mitglied hinzufügen" reservations: "Reservierungen" + username: "Username" surname: "Nachname" first_name: "Vorname" email: "E-Mail" @@ -1426,6 +1427,9 @@ de: enable_invoicing: "Rechnungsstellung aktivieren" invoicing_module: "Rechnungs-Modul" account_creation: "Account-Erstellung" + accounts_management: "Accounts management" + members_list: "Members list" + members_list_info: "You can customize the fields to display in the member management list" phone: "Telefon" phone_is_required: "Telefonummer erforderlich" phone_required_info: "Sie können festlegen, ob die Telefonnummer erforderlich sein soll, um einen neuen Benutzer auf Fab-Manager zu registrieren." @@ -1469,6 +1473,7 @@ de: extended_prices_info_html: "Räume können je nach Dauer der Buchung unterschiedliche Preise haben. Sie können wählen, ob dies für alle Buchungen oder nur für diejenigen gilt, die am selben Tag beginnen." 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" overlapping_options: training_reservations: "Schulungen" machine_reservations: "Maschinen" From 4d0958e5e88d3cbde3ba8007dfcdc489ec05819a Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:50 +0200 Subject: [PATCH 116/141] New translations app.admin.en.yml (Norwegian) --- config/locales/app.admin.no.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.admin.no.yml b/config/locales/app.admin.no.yml index e0954e55c..5c94b8ac1 100644 --- a/config/locales/app.admin.no.yml +++ b/config/locales/app.admin.no.yml @@ -825,6 +825,7 @@ search_for_an_user: "Søk etter bruker" add_a_new_member: "Legge til nytt medlem" reservations: "Reservasjoner" + username: "Username" surname: "Etternavn" first_name: "Fornavn" email: "E-post" @@ -1426,6 +1427,9 @@ enable_invoicing: "Enable invoicing" invoicing_module: "invoicing module" account_creation: "Account creation" + accounts_management: "Accounts management" + members_list: "Members list" + members_list_info: "You can customize the fields to display in the member management list" phone: "Phone" phone_is_required: "Phone required" phone_required_info: "You can define if the phone number should be required to register a new user on Fab-manager." @@ -1469,6 +1473,7 @@ extended_prices_info_html: "Spaces can have different prices depending on the cumulated duration of the booking. You can choose if this apply to all bookings or only to those starting within the same day." 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" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" From eb530a90f52935a549fb7dbc0a38643b1b354124 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:11:51 +0200 Subject: [PATCH 117/141] New translations app.admin.en.yml (Zulu) --- config/locales/app.admin.zu.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.admin.zu.yml b/config/locales/app.admin.zu.yml index 35a7c4e60..07f607958 100644 --- a/config/locales/app.admin.zu.yml +++ b/config/locales/app.admin.zu.yml @@ -825,6 +825,7 @@ zu: search_for_an_user: "crwdns7741:0crwdne7741:0" add_a_new_member: "crwdns7743:0crwdne7743:0" reservations: "crwdns7745:0crwdne7745:0" + username: "crwdns24006:0crwdne24006:0" surname: "crwdns7747:0crwdne7747:0" first_name: "crwdns7749:0crwdne7749:0" email: "crwdns7751:0crwdne7751:0" @@ -1426,6 +1427,9 @@ zu: enable_invoicing: "crwdns20674:0crwdne20674:0" invoicing_module: "crwdns20676:0crwdne20676:0" account_creation: "crwdns20678:0crwdne20678:0" + accounts_management: "crwdns24008:0crwdne24008:0" + members_list: "crwdns24010:0crwdne24010:0" + members_list_info: "crwdns24012:0crwdne24012:0" phone: "crwdns20680:0crwdne20680:0" phone_is_required: "crwdns20682:0crwdne20682:0" phone_required_info: "crwdns20684:0crwdne20684:0" @@ -1469,6 +1473,7 @@ zu: extended_prices_info_html: "crwdns22169:0crwdne22169:0" extended_prices_in_same_day: "crwdns22171:0crwdne22171:0" public_registrations: "crwdns22277:0crwdne22277:0" + show_username_in_admin_list: "crwdns24014:0crwdne24014:0" overlapping_options: training_reservations: "crwdns22111:0crwdne22111:0" machine_reservations: "crwdns22113:0crwdne22113:0" From 722e29224d1bfa162b591af7b3126bd72f34609d Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:13:12 +0200 Subject: [PATCH 118/141] New translations en.yml (French) --- config/locales/fr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 98c759411..99e63dd7e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -595,4 +595,4 @@ fr: flickr: "flickr" machines_module: "Module machines" user_change_group: "Permettre aux utilisateurs de changer leur groupe" - show_username_in_admin_list: "Show the username in the admin's members list" + show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste des membres de l'administrateur" From 52f91ab46563477f92d4beecf3f45fc0a43421a5 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 10:13:22 +0200 Subject: [PATCH 119/141] New translations app.admin.en.yml (French) --- config/locales/app.admin.fr.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 94cbfae43..f67cab2ff 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -825,7 +825,7 @@ fr: search_for_an_user: "Recherchez un utilisateur" add_a_new_member: "Ajouter un nouveau membre" reservations: "Réservations" - username: "Username" + username: "Pseudonyme" surname: "Nom" first_name: "Prénom" email: "Courriel" @@ -1427,9 +1427,9 @@ fr: enable_invoicing: "Activer la facturation" invoicing_module: "module de facturation" account_creation: "Création de compte" - accounts_management: "Accounts management" - members_list: "Members list" - members_list_info: "You can customize the fields to display in the member management list" + accounts_management: "Gestion des comptes" + members_list: "Liste des membres" + members_list_info: "Vous pouvez personnaliser les champs à afficher dans la liste de gestion des membres" phone: "Téléphone" phone_is_required: "Téléphone requis" phone_required_info: "Vous pouvez définir si le numéro de téléphone doit être requis, lors de l'enregistrement d'un nouvel utilisateur sur Fab-manager." @@ -1473,7 +1473,7 @@ fr: extended_prices_info_html: "Les espaces peuvent avoir des prix différents selon la durée cumulée de la réservation. Vous pouvez choisir si cela s'applique à toutes les réservations ou seulement à celles qui commencent dans la même journée." extended_prices_in_same_day: "Prix étendus le même jour" public_registrations: "Inscriptions publiques" - show_username_in_admin_list: "Show the username in the list" + show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" From 445e63b44c65f266ce33e67a7b0d408e99c0ebdd Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:04:17 +0200 Subject: [PATCH 120/141] (feature) Improved attached ICS file texts, in reservations emails --- CHANGELOG.md | 1 + app/mailers/notifications_mailer.rb | 2 +- app/models/concerns/i_calendar_concern.rb | 32 +++++++++++++++++++++-- app/pdfs/pdf/invoice.rb | 2 +- config/locales/en.yml | 10 +++---- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a9695ce..66f1fb5a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Updated rails locales files - Usage of the rails logger instead of printing to standard output - Optionnaly add a username column to the member list (#375) +- Improved attached ICS file texts, in reservations emails - Fix a bug: canceled trainings are still shown on the public profile page - Fix a bug: prevent same slot booking feature ignores canceled reservations - Fix a bug: wrong currency on invoices files diff --git a/app/mailers/notifications_mailer.rb b/app/mailers/notifications_mailer.rb index 119074177..370bd3e92 100644 --- a/app/mailers/notifications_mailer.rb +++ b/app/mailers/notifications_mailer.rb @@ -54,7 +54,7 @@ class NotificationsMailer < NotifyWith::NotificationsMailer end def notify_member_create_reservation - attachments[@attached_object.ics_filename] = @attached_object.to_ics.encode(Encoding::ISO_8859_15) + attachments[@attached_object.ics_filename] = @attached_object.to_ics.encode(Encoding::UTF_8) mail(to: @recipient.email, subject: t('notifications_mailer.notify_member_create_reservation.subject'), template_name: 'notify_member_create_reservation') diff --git a/app/models/concerns/i_calendar_concern.rb b/app/models/concerns/i_calendar_concern.rb index 708bd99a7..fc6c09949 100644 --- a/app/models/concerns/i_calendar_concern.rb +++ b/app/models/concerns/i_calendar_concern.rb @@ -24,8 +24,8 @@ module ICalendarConcern cal.event do |e| e.dtstart = start_time e.dtend = group_slots.last[:end_at] - e.summary = I18n.t('reservation_ics.summary', TYPE: I18n.t("reservation_ics.type.#{reservable.class.name}")) - e.description = I18n.t('reservation_ics.description', COUNT: group_slots.count, ITEM: reservable.name) + e.summary = title + e.description = I18n.t('reservation_ics.description_slot', COUNT: group_slots.count, ITEM: reservable.name) e.ip_class = 'PRIVATE' e.alarm do |a| @@ -38,5 +38,33 @@ module ICalendarConcern end cal end + + private + + def title + case reservable_type + when 'Machine', 'Training', 'Space' + reservable.name + when 'Event' + reservable.title + else + Rails.logger.warn "Unexpected reservable type #{reservable_type}" + reservable_type + end + end + + def description(group_slots) + case reservable_type + when 'Machine', 'Space' + I18n.t('reservation_ics.description_slot', COUNT: group_slots.count, ITEM: reservable.name) + when 'Training' + I18n.t('reservation_ics.description_training', TYPE: reservable.name) + when 'Event' + I18n.t('reservation_ics.description_event', NUMBER: nb_reserve_places + tickets.map(&:booked).reduce(:+)) + else + Rails.logger.warn "Unexpected reservable type #{reservable_type}" + I18n.t('reservation_ics.description_slot', COUNT: group_slots.count, ITEM: reservable_type) + end + end end end diff --git a/app/pdfs/pdf/invoice.rb b/app/pdfs/pdf/invoice.rb index 07de468a8..e2962d57e 100644 --- a/app/pdfs/pdf/invoice.rb +++ b/app/pdfs/pdf/invoice.rb @@ -249,7 +249,7 @@ class PDF::Invoice < Prawn::Document data += [[I18n.t('invoices.including_amount_payed_on_ordering'), number_to_currency(total)]] # checking the round number - rounded = sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00) + rounded = (sprintf('%.2f', total_vat / 100.00).to_f + sprintf('%.2f', total_ht / 100.00).to_f).to_s if rounded != sprintf('%.2f', total_calc) Rails.logger.error 'rounding the numbers cause an invoice inconsistency. ' \ "Total expected: #{sprintf('%.2f', total_calc)}, total computed: #{rounded}" diff --git a/config/locales/en.yml b/config/locales/en.yml index fa90ed606..e89a7ed57 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -245,13 +245,9 @@ en: reservations: "Reservations" available_seats: "Available seats" reservation_ics: - summary: "%{TYPE} reservation" - type: - Machine: "Machine" - Space: "Space" - Event: "Event" - Training: "Training" - description: "You have reserved %{COUNT} slots of %{ITEM}" + description_slot: "You have booked %{COUNT} slots of %{ITEM}" + description_training: "You have booked a %{TYPE} training" + description_event: "You have booked %{NUMBER} tickets for this event" alarm_summary: "Remind your reservation" roles: member: "Member" From fba9ce8d94fed5a2353c0781d244c50d253c0e19 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:06:48 +0200 Subject: [PATCH 121/141] New translations en.yml (French) --- config/locales/fr.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 99e63dd7e..409d39f9d 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -245,13 +245,9 @@ fr: reservations: "Réservations" available_seats: "Places disponibles" reservation_ics: - summary: "%{TYPE} réservation" - type: - Machine: "Machine" - Space: "Espace" - Event: "Événement" - Training: "Formation" - description: "Vous avez réservé %{COUNT} emplacements de %{ITEM}" + description_slot: "You have booked %{COUNT} slots of %{ITEM}" + description_training: "You have booked a %{TYPE} training" + description_event: "You have booked %{NUMBER} tickets for this event" alarm_summary: "Rappelez-vous votre réservation" roles: member: "Membre" From 0f8e4af6ff60cea7681518b3827d3bf12971619c Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:06:50 +0200 Subject: [PATCH 122/141] New translations en.yml (Spanish) --- config/locales/es.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/config/locales/es.yml b/config/locales/es.yml index 2856baf08..7813f93e0 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -245,13 +245,9 @@ es: reservations: "Reservas" available_seats: "Asientos disponibles" reservation_ics: - summary: "%{TYPE} reservation" - type: - Machine: "Machine" - Space: "Space" - Event: "Event" - Training: "Training" - description: "You have reserved %{COUNT} slots of %{ITEM}" + description_slot: "You have booked %{COUNT} slots of %{ITEM}" + description_training: "You have booked a %{TYPE} training" + description_event: "You have booked %{NUMBER} tickets for this event" alarm_summary: "Remind your reservation" roles: member: "Miembro" From dbe00672bcd6b2a140a54d2f984055b8a40e6bac Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:06:51 +0200 Subject: [PATCH 123/141] New translations en.yml (German) --- config/locales/de.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 8c1abdc7d..4336dc330 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -245,13 +245,9 @@ de: reservations: "Reservierungen" available_seats: "Verfügbare Plätze" reservation_ics: - summary: "%{TYPE} reservation" - type: - Machine: "Machine" - Space: "Space" - Event: "Event" - Training: "Training" - description: "You have reserved %{COUNT} slots of %{ITEM}" + description_slot: "You have booked %{COUNT} slots of %{ITEM}" + description_training: "You have booked a %{TYPE} training" + description_event: "You have booked %{NUMBER} tickets for this event" alarm_summary: "Remind your reservation" roles: member: "Mitglied" From 11e82a6a53021f73f3331bb215026c519591d765 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:06:52 +0200 Subject: [PATCH 124/141] New translations en.yml (Norwegian) --- config/locales/no.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/config/locales/no.yml b/config/locales/no.yml index 7fe937f8e..f7e2446f3 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -245,13 +245,9 @@ reservations: "Reservasjoner" available_seats: "Tilgjengelige plasser" reservation_ics: - summary: "%{TYPE} reservation" - type: - Machine: "Machine" - Space: "Space" - Event: "Event" - Training: "Training" - description: "You have reserved %{COUNT} slots of %{ITEM}" + description_slot: "You have booked %{COUNT} slots of %{ITEM}" + description_training: "You have booked a %{TYPE} training" + description_event: "You have booked %{NUMBER} tickets for this event" alarm_summary: "Remind your reservation" roles: member: "Medlem" From d68b40e3eadafd80b2e9bbff302cd1f9905cb2d6 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:06:53 +0200 Subject: [PATCH 125/141] New translations en.yml (Portuguese) --- config/locales/pt.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 45bff6a9b..c27e15ab0 100755 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -245,13 +245,9 @@ pt: reservations: "Reservas" available_seats: "Assentos disponíveis" reservation_ics: - summary: "%{TYPE} reserva" - type: - Machine: "Máquina" - Space: "Espaço" - Event: "Evento" - Training: "Treinamento" - description: "Você reservou %{COUNT} vagas de %{ITEM}" + description_slot: "You have booked %{COUNT} slots of %{ITEM}" + description_training: "You have booked a %{TYPE} training" + description_event: "You have booked %{NUMBER} tickets for this event" alarm_summary: "Lembrar a sua reserva" roles: member: "Membro" From 71b86fdeaa28c38423b16c126df9ed123c45b47b Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:06:55 +0200 Subject: [PATCH 126/141] New translations en.yml (Zulu) --- config/locales/zu.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/config/locales/zu.yml b/config/locales/zu.yml index ccb9dc4a0..5c0787148 100644 --- a/config/locales/zu.yml +++ b/config/locales/zu.yml @@ -245,13 +245,9 @@ zu: reservations: "crwdns3553:0crwdne3553:0" available_seats: "crwdns3555:0crwdne3555:0" reservation_ics: - summary: "crwdns22986:0%{TYPE}crwdne22986:0" - type: - Machine: "crwdns22988:0crwdne22988:0" - Space: "crwdns22990:0crwdne22990:0" - Event: "crwdns22992:0crwdne22992:0" - Training: "crwdns22994:0crwdne22994:0" - description: "crwdns22996:0%{COUNT}crwdnd22996:0%{ITEM}crwdne22996:0" + description_slot: "crwdns24018:0%{COUNT}crwdnd24018:0%{ITEM}crwdne24018:0" + description_training: "crwdns24020:0%{TYPE}crwdne24020:0" + description_event: "crwdns24022:0%{NUMBER}crwdne24022:0" alarm_summary: "crwdns22998:0crwdne22998:0" roles: member: "crwdns20446:0crwdne20446:0" From 055bfcbb66690d163018892460d7465863444833 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 12:08:24 +0200 Subject: [PATCH 127/141] New translations en.yml (French) --- config/locales/fr.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 409d39f9d..dde37f57b 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -245,9 +245,9 @@ fr: reservations: "Réservations" available_seats: "Places disponibles" reservation_ics: - description_slot: "You have booked %{COUNT} slots of %{ITEM}" - description_training: "You have booked a %{TYPE} training" - description_event: "You have booked %{NUMBER} tickets for this event" + description_slot: "Vous avez réservé %{COUNT} créneaux de %{ITEM}" + description_training: "Vous avez réservé une formation %{TYPE}" + description_event: "Vous avez réservé %{NUMBER} places pour cet événement" alarm_summary: "Rappelez-vous votre réservation" roles: member: "Membre" From 677b796d32a9c0b48659cacd98138c3fe32b00e8 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 27 Jul 2022 13:16:51 +0200 Subject: [PATCH 128/141] fix bug: user validation required alert is displayed and disappears instantly --- CHANGELOG.md | 1 + app/frontend/src/javascript/controllers/machines.js.erb | 1 + app/frontend/src/javascript/controllers/spaces.js.erb | 1 + app/frontend/src/javascript/controllers/trainings.js.erb | 1 + 4 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66f1fb5a4..ffaa74868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Usage of the rails logger instead of printing to standard output - Optionnaly add a username column to the member list (#375) - Improved attached ICS file texts, in reservations emails +- Fix a bug: user validation required alert is displayed and disappears instantly - Fix a bug: canceled trainings are still shown on the public profile page - Fix a bug: prevent same slot booking feature ignores canceled reservations - Fix a bug: wrong currency on invoices files diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index db407bf2c..04bcf9714 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -647,6 +647,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran }); if ($scope.currentUser.role !== 'admin') { + $scope.ctrl.member = $scope.currentUser; return Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; }); } }; diff --git a/app/frontend/src/javascript/controllers/spaces.js.erb b/app/frontend/src/javascript/controllers/spaces.js.erb index d14f08733..f46d2755a 100644 --- a/app/frontend/src/javascript/controllers/spaces.js.erb +++ b/app/frontend/src/javascript/controllers/spaces.js.erb @@ -558,6 +558,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi */ const initialize = function () { if ($scope.currentUser.role !== 'admin') { + $scope.ctrl.member = $scope.currentUser; Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; }); } // we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise diff --git a/app/frontend/src/javascript/controllers/trainings.js.erb b/app/frontend/src/javascript/controllers/trainings.js.erb index 5bfd46e64..113daec04 100644 --- a/app/frontend/src/javascript/controllers/trainings.js.erb +++ b/app/frontend/src/javascript/controllers/trainings.js.erb @@ -345,6 +345,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra */ const initialize = function () { if ($scope.currentUser.role !== 'admin') { + $scope.ctrl.member = $scope.currentUser; Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; }); } // we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise From 3f84c2dfa17a909d2cf72b9cd067bc37376eef5f Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 13:58:22 +0200 Subject: [PATCH 129/141] (bug) ics description not using new methdod --- app/models/concerns/i_calendar_concern.rb | 4 ++-- app/services/members/list_service.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/concerns/i_calendar_concern.rb b/app/models/concerns/i_calendar_concern.rb index fc6c09949..aa0253853 100644 --- a/app/models/concerns/i_calendar_concern.rb +++ b/app/models/concerns/i_calendar_concern.rb @@ -25,7 +25,7 @@ module ICalendarConcern e.dtstart = start_time e.dtend = group_slots.last[:end_at] e.summary = title - e.description = I18n.t('reservation_ics.description_slot', COUNT: group_slots.count, ITEM: reservable.name) + e.description = description(group_slots) e.ip_class = 'PRIVATE' e.alarm do |a| @@ -60,7 +60,7 @@ module ICalendarConcern when 'Training' I18n.t('reservation_ics.description_training', TYPE: reservable.name) when 'Event' - I18n.t('reservation_ics.description_event', NUMBER: nb_reserve_places + tickets.map(&:booked).reduce(:+)) + I18n.t('reservation_ics.description_event', NUMBER: nb_reserve_places + (tickets.map(&:booked).reduce(:+) || 0)) else Rails.logger.warn "Unexpected reservable type #{reservable_type}" I18n.t('reservation_ics.description_slot', COUNT: group_slots.count, ITEM: reservable_type) diff --git a/app/services/members/list_service.rb b/app/services/members/list_service.rb index d5033c645..955ac0ccd 100644 --- a/app/services/members/list_service.rb +++ b/app/services/members/list_service.rb @@ -42,7 +42,7 @@ class Members::ListService end def search(current_user, query, subscription, include_admins = 'false') - members = User.includes(:profile) + members = User.includes(:profile, :statistic_profile) .joins(:profile, :statistic_profile, :roles, From 8bc933162a4f06e0d1da2c14a68ba53e53a3719b Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 15:36:29 +0200 Subject: [PATCH 130/141] New translations app.admin.en.yml (Portuguese) --- config/locales/app.admin.pt.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index 666aa1045..732d6621a 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -825,7 +825,7 @@ pt: search_for_an_user: "Buscar por usuário" add_a_new_member: "Adicionar novo membro" reservations: "Reservas" - username: "Username" + username: "Nome de Usuário" surname: "Sobrenome" first_name: "Primeiro nome" email: "Email" @@ -1427,9 +1427,9 @@ pt: enable_invoicing: "Habilitar faturamento" invoicing_module: "módulo de faturamento" account_creation: "Criação de conta" - accounts_management: "Accounts management" - members_list: "Members list" - members_list_info: "You can customize the fields to display in the member management list" + accounts_management: "Gestão de contas" + members_list: "Lista de membros" + members_list_info: "Você pode personalizar os campos a serem exibidos na lista de gerenciamento de membros" phone: "Telefone" phone_is_required: "Telefone é obrigatório" phone_required_info: "Você pode definir se o número de telefone deve ser exigido para registrar um novo usuário no Fab-manager." @@ -1473,7 +1473,7 @@ pt: extended_prices_info_html: "Os espaços podem ter preços diferentes dependendo da duração acumulada da reserva. Você pode escolher se isso se aplica a todas as reservas ou apenas àqueles que iniciam no mesmo dia." extended_prices_in_same_day: "Preços estendidos no mesmo dia" public_registrations: "Inscrições públicas" - show_username_in_admin_list: "Show the username in the list" + show_username_in_admin_list: "Mostrar o nome de usuário na lista" overlapping_options: training_reservations: "Treinamentos" machine_reservations: "Máquinas" From 0d8914dd60c022b2b30e71b2027aa2715447bc1e Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 15:36:30 +0200 Subject: [PATCH 131/141] New translations mails.en.yml (Portuguese) --- config/locales/mails.pt.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/mails.pt.yml b/config/locales/mails.pt.yml index 40edeed84..7538fcd0a 100755 --- a/config/locales/mails.pt.yml +++ b/config/locales/mails.pt.yml @@ -31,7 +31,7 @@ pt: user_changed_group_html: "O usuário %{NAME} mudou de grupo." previous_group: "Grupo anterior:" new_group: "Novo grupo:" - user_invalidated: "The user's account was invalidated." + user_invalidated: "A conta do usuário foi invalidada." notify_admin_subscription_extended: subject: "Uma assinatura foi estendida" body: @@ -356,13 +356,13 @@ pt: user_update_proof_of_identity_file: "O membro %{NAME} modificou os documentos abaixo:" validate_user: "Por favor valide esta conta" notify_user_is_validated: - subject: "Account validated" + subject: "Conta validada" body: - account_validated: "Your account was validated. Now, you have access to booking features." + account_validated: "Sua conta foi validada. Agora, você tem acesso aos recursos do reserva." notify_user_is_invalidated: - subject: "Account invalidated" + subject: "Conta invalidada" body: - account_invalidated: "Your account was invalidated. You won't be able to book anymore, until your account is validated again." + account_invalidated: "Sua conta foi invalidada. Você não poderá mais reservar até que sua conta seja validada novamente." notify_user_proof_of_identity_refusal: subject: "Seus documentos foram recusados" body: From 5ceb06747ff368020eefe05cb6a751296eabb801 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 15:36:31 +0200 Subject: [PATCH 132/141] New translations en.yml (Portuguese) --- config/locales/pt.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/pt.yml b/config/locales/pt.yml index c27e15ab0..b8805ecd1 100755 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -245,9 +245,9 @@ pt: reservations: "Reservas" available_seats: "Assentos disponíveis" reservation_ics: - description_slot: "You have booked %{COUNT} slots of %{ITEM}" - description_training: "You have booked a %{TYPE} training" - description_event: "You have booked %{NUMBER} tickets for this event" + description_slot: "Você reservou %{COUNT} vaga(s) de %{ITEM}" + description_training: "Você reservou um treinamento de %{TYPE}" + description_event: "Você reservou %{NUMBER} ingresso(s) para este evento" alarm_summary: "Lembrar a sua reserva" roles: member: "Membro" @@ -591,4 +591,4 @@ pt: flickr: "flickr" machines_module: "Módulo de Máquinas" user_change_group: "Permitir que os usuários mudem de grupo" - show_username_in_admin_list: "Show the username in the admin's members list" + show_username_in_admin_list: "Mostrar o nome de usuário na lista de membros do administrador" From f09ea8446dc0374a865fd30d17ce7a7025fb1526 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 16:00:11 +0200 Subject: [PATCH 133/141] Version 5.4.13 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffaa74868..bcd029667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog Fab-manager -## next deploy +## v5.4.13 2022 July 27 - Improved calendars loading time - Refactored and documented the availability-slot-reservation data model diff --git a/package.json b/package.json index 92a5821c0..a8153e429 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.12", + "version": "5.4.13", "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 5f6a26e8263e5ee197dfc3069ad927158b903327 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Jul 2022 14:01:15 +0000 Subject: [PATCH 134/141] Bump moment from 2.29.2 to 2.29.4 Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4) --- updated-dependencies: - dependency-name: moment dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6bc98d8dc..82c4b0c6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5618,9 +5618,9 @@ moment-timezone@0.5: moment ">= 2.9.0" moment@2.29, "moment@>= 2.9.0", "moment@>=2.8.0 <3.0.0": - version "2.29.2" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" - integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== ms@2.0.0: version "2.0.0" From 9bc2d4f96c7eab54d48e4aa018f407fd682899c0 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Wed, 27 Jul 2022 17:14:15 +0200 Subject: [PATCH 135/141] improves file validation, validation is now based on content of the file in addition of the validation of the extension --- app/models/user_avatar.rb | 2 +- ...ntent_type_validation_from_file_content.rb | 26 +++++++++++++++++++ app/uploaders/custom_assets_uploader.rb | 1 + app/uploaders/event_file_uploader.rb | 1 + app/uploaders/event_image_uploader.rb | 1 + app/uploaders/machine_file_uploader.rb | 1 + app/uploaders/machine_image_uploader.rb | 3 ++- app/uploaders/plan_file_uploader.rb | 1 + app/uploaders/project_cao_uploader.rb | 21 +-------------- app/uploaders/project_image_uploader.rb | 1 + .../proof_of_identity_file_uploader.rb | 1 + app/uploaders/space_file_uploader.rb | 1 + app/uploaders/space_image_uploader.rb | 3 ++- app/uploaders/training_image_uploader.rb | 1 + ...ge_uploader.rb => user_avatar_uploader.rb} | 5 ++-- 15 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 app/uploaders/concerns/content_type_validation_from_file_content.rb rename app/uploaders/{profil_image_uploader.rb => user_avatar_uploader.rb} (92%) diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index 15e878841..73a8adca1 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -3,5 +3,5 @@ # UserAvatar is the profile picture for an User class UserAvatar < Asset include ImageValidatorConcern - mount_uploader :attachment, ProfilImageUploader + mount_uploader :attachment, UserAvatarUploader end diff --git a/app/uploaders/concerns/content_type_validation_from_file_content.rb b/app/uploaders/concerns/content_type_validation_from_file_content.rb new file mode 100644 index 000000000..c1232eccb --- /dev/null +++ b/app/uploaders/concerns/content_type_validation_from_file_content.rb @@ -0,0 +1,26 @@ +module ContentTypeValidationFromFileContent + extend ActiveSupport::Concern + + # overrides carrierwave methods to do a REAL mime type check based on content and not based on extension + + included do + private + def check_content_type_whitelist!(new_file) + content_type = Marcel::MimeType.for Pathname.new(new_file.file) + + if content_type_whitelist && content_type && !whitelisted_content_type?(content_type) + raise CarrierWave::IntegrityError, + I18n.translate(:'errors.messages.content_type_whitelist_error', + content_type: content_type, + allowed_types: Array(content_type_whitelist).join(', ')) + end + end + + def whitelisted_content_type?(content_type) + Array(content_type_whitelist).any? do |item| + item = Regexp.quote(item) if item.class != Regexp + content_type =~ /#{item}/ + end + end + end +end \ No newline at end of file diff --git a/app/uploaders/custom_assets_uploader.rb b/app/uploaders/custom_assets_uploader.rb index 5d61d91cd..3fc789cd7 100644 --- a/app/uploaders/custom_assets_uploader.rb +++ b/app/uploaders/custom_assets_uploader.rb @@ -8,6 +8,7 @@ class CustomAssetsUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick # include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/event_file_uploader.rb b/app/uploaders/event_file_uploader.rb index 97a74a01e..43752a117 100644 --- a/app/uploaders/event_file_uploader.rb +++ b/app/uploaders/event_file_uploader.rb @@ -6,6 +6,7 @@ class EventFileUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick # include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/event_image_uploader.rb b/app/uploaders/event_image_uploader.rb index 479ede225..90d8c965a 100644 --- a/app/uploaders/event_image_uploader.rb +++ b/app/uploaders/event_image_uploader.rb @@ -7,6 +7,7 @@ class EventImageUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/machine_file_uploader.rb b/app/uploaders/machine_file_uploader.rb index 8fc055c86..a7d212831 100644 --- a/app/uploaders/machine_file_uploader.rb +++ b/app/uploaders/machine_file_uploader.rb @@ -6,6 +6,7 @@ class MachineFileUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick # include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/machine_image_uploader.rb b/app/uploaders/machine_image_uploader.rb index 549254390..0141731e1 100644 --- a/app/uploaders/machine_image_uploader.rb +++ b/app/uploaders/machine_image_uploader.rb @@ -7,7 +7,8 @@ class MachineImageUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick include CarrierWave::MiniMagick include UploadHelper - + include ContentTypeValidationFromFileContent + # Choose what kind of storage to use for this uploader: storage :file after :remove, :delete_empty_dirs diff --git a/app/uploaders/plan_file_uploader.rb b/app/uploaders/plan_file_uploader.rb index 3b3210d67..680ef86af 100644 --- a/app/uploaders/plan_file_uploader.rb +++ b/app/uploaders/plan_file_uploader.rb @@ -6,6 +6,7 @@ class PlanFileUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick # include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/project_cao_uploader.rb b/app/uploaders/project_cao_uploader.rb index 3ad2ea3fa..0b060198f 100644 --- a/app/uploaders/project_cao_uploader.rb +++ b/app/uploaders/project_cao_uploader.rb @@ -4,6 +4,7 @@ # This file defines the parameters for these uploads class ProjectCaoUploader < CarrierWave::Uploader::Base include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file @@ -29,24 +30,4 @@ class ProjectCaoUploader < CarrierWave::Uploader::Base def content_type_whitelist Setting.get('allowed_cad_mime_types').split(' ') end - - private - - def check_content_type_whitelist!(new_file) - content_type = Marcel::MimeType.for Pathname.new(new_file.file) - - if content_type_whitelist && content_type && !whitelisted_content_type?(content_type) - raise CarrierWave::IntegrityError, - I18n.translate(:'errors.messages.content_type_whitelist_error', - content_type: content_type, - allowed_types: Array(content_type_whitelist).join(', ')) - end - end - - def whitelisted_content_type?(content_type) - Array(content_type_whitelist).any? do |item| - item = Regexp.quote(item) if item.class != Regexp - content_type =~ /#{item}/ - end - end end diff --git a/app/uploaders/project_image_uploader.rb b/app/uploaders/project_image_uploader.rb index d013b112b..96e9d5bf6 100644 --- a/app/uploaders/project_image_uploader.rb +++ b/app/uploaders/project_image_uploader.rb @@ -5,6 +5,7 @@ class ProjectImageUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/proof_of_identity_file_uploader.rb b/app/uploaders/proof_of_identity_file_uploader.rb index f7744cc82..d4e488fe9 100644 --- a/app/uploaders/proof_of_identity_file_uploader.rb +++ b/app/uploaders/proof_of_identity_file_uploader.rb @@ -8,6 +8,7 @@ class ProofOfIdentityFileUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick # include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/space_file_uploader.rb b/app/uploaders/space_file_uploader.rb index afecfaf50..1cdf2cf20 100644 --- a/app/uploaders/space_file_uploader.rb +++ b/app/uploaders/space_file_uploader.rb @@ -6,6 +6,7 @@ class SpaceFileUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick # include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/space_image_uploader.rb b/app/uploaders/space_image_uploader.rb index 5913d0a57..4c5b57a29 100644 --- a/app/uploaders/space_image_uploader.rb +++ b/app/uploaders/space_image_uploader.rb @@ -7,7 +7,8 @@ class SpaceImageUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick include CarrierWave::MiniMagick include UploadHelper - + include ContentTypeValidationFromFileContent + # Choose what kind of storage to use for this uploader: storage :file after :remove, :delete_empty_dirs diff --git a/app/uploaders/training_image_uploader.rb b/app/uploaders/training_image_uploader.rb index 321b7f8e4..8d87b4025 100644 --- a/app/uploaders/training_image_uploader.rb +++ b/app/uploaders/training_image_uploader.rb @@ -7,6 +7,7 @@ class TrainingImageUploader < CarrierWave::Uploader::Base # include CarrierWave::RMagick include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file diff --git a/app/uploaders/profil_image_uploader.rb b/app/uploaders/user_avatar_uploader.rb similarity index 92% rename from app/uploaders/profil_image_uploader.rb rename to app/uploaders/user_avatar_uploader.rb index 3494b6186..6c9f40ced 100644 --- a/app/uploaders/profil_image_uploader.rb +++ b/app/uploaders/user_avatar_uploader.rb @@ -2,11 +2,12 @@ # CarrierWave uploader for user's avatar. # This file defines the parameters for these uploads. -class ProfilImageUploader < CarrierWave::Uploader::Base +class UserAvatarUploader < CarrierWave::Uploader::Base # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick include UploadHelper + include ContentTypeValidationFromFileContent # Choose what kind of storage to use for this uploader: storage :file @@ -59,7 +60,7 @@ class ProfilImageUploader < CarrierWave::Uploader::Base end def content_type_whitelist - [%r{image/}] + %w[image/jpeg image/gif image/png] end # Override the filename of the uploaded files: From e4fb068c12bd8c514691ef0d84fdc331f7b864f1 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Wed, 27 Jul 2022 10:28:58 +0200 Subject: [PATCH 136/141] fix xss vulnerability in projects --- app/views/api/projects/show.json.jbuilder | 5 +++-- config/application.rb | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/views/api/projects/show.json.jbuilder b/app/views/api/projects/show.json.jbuilder index 5f79f3e98..5eff4564e 100644 --- a/app/views/api/projects/show.json.jbuilder +++ b/app/views/api/projects/show.json.jbuilder @@ -1,6 +1,7 @@ # frozen_string_literal: true -json.extract! @project, :id, :name, :description, :tags, :created_at, :updated_at, :licence_id, :slug +json.extract! @project, :id, :name, :tags, :created_at, :updated_at, :licence_id, :slug +json.description sanitize(@project.description) json.author_id @project.author.user_id json.project_image @project.project_image.attachment.large.url if @project.project_image json.project_full_image @project.project_image.attachment.url if @project.project_image @@ -56,7 +57,7 @@ json.project_users @project.project_users do |pu| end json.project_steps_attributes @project.project_steps.order('project_steps.step_nb ASC') do |s| json.id s.id - json.description s.description + json.description sanitize(s.description) json.title s.title json.project_step_images_attributes s.project_step_images.order('created_at ASC') do |si| json.id si.id diff --git a/config/application.rb b/config/application.rb index 2188891f5..1ae8adb38 100644 --- a/config/application.rb +++ b/config/application.rb @@ -74,6 +74,8 @@ module Fablab FabManager.activate_plugins! + config.action_view.sanitized_allowed_tags = %w(a acronym hr pre table b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p image iframe) + config.after_initialize do plugins = FabManager.plugins plugins&.each(&:notify_after_initialize) From a7290147c7d77062b30acb4c503376a1c13959d0 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Wed, 27 Jul 2022 15:59:22 +0200 Subject: [PATCH 137/141] adds missing tag style to action_view.sanitized_allowed_tags --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index 1ae8adb38..1a3dfda9c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -74,7 +74,7 @@ module Fablab FabManager.activate_plugins! - config.action_view.sanitized_allowed_tags = %w(a acronym hr pre table b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p image iframe) + config.action_view.sanitized_allowed_tags = %w(a acronym hr pre table b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p image iframe style) config.after_initialize do plugins = FabManager.plugins From a8d0df11d3931d2d4db2b35453e064f556fe7bf1 Mon Sep 17 00:00:00 2001 From: Nicolas Florentin Date: Wed, 27 Jul 2022 15:59:42 +0200 Subject: [PATCH 138/141] projects/index/json.jbuilder : sanitize project description --- app/views/api/projects/index.json.jbuilder | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/api/projects/index.json.jbuilder b/app/views/api/projects/index.json.jbuilder index 15b4e3f5c..616297a2a 100644 --- a/app/views/api/projects/index.json.jbuilder +++ b/app/views/api/projects/index.json.jbuilder @@ -1,7 +1,8 @@ # frozen_string_literal: true json.projects @projects do |project| - json.extract! project, :id, :name, :description, :licence_id, :slug, :state + json.extract! project, :id, :name, :licence_id, :slug, :state + json.description sanitize(project.description) json.author_id project.author.user_id json.project_image project.project_image.attachment.medium.url if project.project_image From acbd327f6db481772438988878d5b182a7acb101 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 27 Jul 2022 17:28:30 +0200 Subject: [PATCH 139/141] (test) Added a test for multiple reservations on the same space slot --- CHANGELOG.md | 1 + .../reservations/space_seats_test.rb | 218 +++++++ .../reservations_space_seats_user2.yml | 560 ++++++++++++++++++ .../reservations_space_seats_user3.yml | 118 ++++ 4 files changed, 897 insertions(+) create mode 100644 test/integration/reservations/space_seats_test.rb create mode 100644 test/vcr_cassettes/reservations_space_seats_user2.yml create mode 100644 test/vcr_cassettes/reservations_space_seats_user3.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index bcaa031eb..edb8d9556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## next release +- Added a test for multiple reservations on the same space slot - Fix a security issue: updated moment to 2.29.4 to fix [CVE-2022-31129](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31129) ## v5.4.13 2022 July 27 diff --git a/test/integration/reservations/space_seats_test.rb b/test/integration/reservations/space_seats_test.rb new file mode 100644 index 000000000..6afeff6b1 --- /dev/null +++ b/test/integration/reservations/space_seats_test.rb @@ -0,0 +1,218 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Reservations; end + +class Reservations::SpaceSeatsTest < ActionDispatch::IntegrationTest + setup do + @admin = User.admins.first + @user1 = User.find(5) + @user2 = User.find(2) + @user3 = User.find(4) + end + + test 'create a space availability and reserve it multiple times' do + login_as(@admin, scope: :user) + + space = Space.first + + date = (DateTime.current + 1.day).change(hour: 8, min: 0, sec: 0) + + post '/api/availabilities', + params: { + availability: { + start_at: date.iso8601, + end_at: (date + 1.hour).iso8601, + available_type: 'space', + tag_ids: [], + is_recurrent: false, + slot_duration: 60, + space_ids: [space.id], + nb_total_places: 2, + occurrences: [ + { start_at: date.iso8601, end_at: (date + 1.hour).iso8601 }] + } + } + + # Check response format & status + assert_equal 201, response.status + assert_equal Mime[:json], response.content_type + + # Check the availability + res = json_response(response.body) + availability = Availability.find(res[:id]) + + ### FIRST RESERVATION + + reservations_count = Reservation.count + invoice_count = Invoice.count + invoice_items_count = InvoiceItem.count + users_credit_count = UsersCredit.count + subscriptions_count = Subscription.count + + post '/api/local_payment/confirm_payment', + params: { + customer_id: @user1.id, + items: [ + { + reservation: { + reservable_id: space.id, + reservable_type: space.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + }.to_json, headers: default_headers + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 1, Reservation.count + assert_equal invoice_count + 1, Invoice.count + assert_equal invoice_items_count + 1, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count, Subscription.count + + # subscription assertions + assert_equal 0, @user1.subscriptions.count + assert_nil @user1.subscribed_plan + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items assertions + invoice_item = InvoiceItem.last + + assert_equal space.prices.find_by(group_id: @user1.group_id, plan_id: nil).amount, invoice_item.amount + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + + ### SECOND RESERVATION + login_as(@user2, scope: :user) + + VCR.use_cassette('reservations_space_seats_user2') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: space.id, + reservable_type: space.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 201, response.status + assert_equal reservations_count + 2, Reservation.count + assert_equal invoice_count + 2, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count, Subscription.count + + # subscription assertions + assert_equal 0, @user2.subscriptions.count + assert_nil @user2.subscribed_plan + + # reservation assertions + reservation = Reservation.last + + assert reservation.original_invoice + assert_equal 1, reservation.original_invoice.invoice_items.count + + # invoice_items assertions + invoice_item = InvoiceItem.last + + assert_equal space.prices.find_by(group_id: @user2.group_id, plan_id: nil).amount, invoice_item.amount + assert invoice_item.check_footprint + + # invoice assertions + item = InvoiceItem.find_by(object: reservation) + invoice = item.invoice + assert_invoice_pdf invoice + assert_not_nil invoice.debug_footprint + + assert_not invoice.payment_gateway_object.blank? + assert_not invoice.total.blank? + assert invoice.check_footprint + + # notification + assert_not_empty Notification.where(attached_object: reservation) + + ### THIRD RESERVATION + login_as(@user3, scope: :user) + + VCR.use_cassette('reservations_space_seats_user3') do + post '/api/stripe/confirm_payment', + params: { + payment_method_id: stripe_payment_method, + cart_items: { + items: [ + { + reservation: { + reservable_id: space.id, + reservable_type: space.class.name, + slots_reservations_attributes: [ + { + slot_id: availability.slots.first.id + } + ] + } + } + ] + } + }.to_json, headers: default_headers + end + + # general assertions + assert_equal 422, response.status + assert_equal reservations_count + 2, Reservation.count + assert_equal invoice_count + 2, Invoice.count + assert_equal invoice_items_count + 2, InvoiceItem.count + assert_equal users_credit_count, UsersCredit.count + assert_equal subscriptions_count, Subscription.count + + # subscription assertions + assert_equal 1, @user3.subscriptions.count + assert_not_nil @user3.subscribed_plan + + # assert nothing was created + reservation = Reservation.last + invoice = Invoice.last + invoice_item = InvoiceItem.last + + assert_not_equal reservation.user.id, @user3.id + assert_not_equal invoice.user.id, @user3.id + assert_not_equal space.prices.find_by(group_id: @user3.group_id, plan_id: nil).amount, invoice_item.amount + end +end diff --git a/test/vcr_cassettes/reservations_space_seats_user2.yml b/test/vcr_cassettes/reservations_space_seats_user2.yml new file mode 100644 index 000000000..9440dcd66 --- /dev/null +++ b/test/vcr_cassettes/reservations_space_seats_user2.yml @@ -0,0 +1,560 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/payment_methods + body: + encoding: UTF-8 + string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2023&card[cvc]=314 + headers: + User-Agent: + - Stripe/v1 RubyBindings/5.29.0 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + Stripe-Version: + - '2019-08-14' + X-Stripe-Client-User-Agent: + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 27 Jul 2022 15:17:01 GMT + Content-Type: + - application/json + Content-Length: + - '930' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Idempotency-Key: + - b2e1a527-8f47-4a44-9e30-ae875a15dbf2 + Original-Request: + - req_D8BOhF6zV284Qf + Request-Id: + - req_D8BOhF6zV284Qf + Stripe-Should-Retry: + - 'false' + Stripe-Version: + - '2019-08-14' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains; preload + body: + encoding: UTF-8 + string: |- + { + "id": "pm_1LQByr2sOmf47Nz9Hl3qCzNX", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "unchecked" + }, + "country": "US", + "exp_month": 4, + "exp_year": 2023, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1658935021, + "customer": null, + "livemode": false, + "metadata": {}, + "type": "card" + } + recorded_at: Wed, 27 Jul 2022 15:17:01 GMT +- request: + method: post + uri: https://api.stripe.com/v1/payment_intents + body: + encoding: UTF-8 + string: payment_method=pm_1LQByr2sOmf47Nz9Hl3qCzNX&amount=2000¤cy=usd&confirmation_method=manual&confirm=true&customer=cus_8Di1wjdVktv5kt + headers: + User-Agent: + - Stripe/v1 RubyBindings/5.29.0 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-Telemetry: + - '{"last_request_metrics":{"request_id":"req_D8BOhF6zV284Qf","request_duration_ms":1261}}' + Stripe-Version: + - '2019-08-14' + X-Stripe-Client-User-Agent: + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 27 Jul 2022 15:17:04 GMT + Content-Type: + - application/json + Content-Length: + - '4430' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Idempotency-Key: + - 74b3cf6c-69a2-4cec-abe4-69d6b170d7c3 + Original-Request: + - req_6Mz1jjCYQWTLMO + Request-Id: + - req_6Mz1jjCYQWTLMO + Stripe-Should-Retry: + - 'false' + Stripe-Version: + - '2019-08-14' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains; preload + body: + encoding: UTF-8 + string: |- + { + "id": "pi_3LQBys2sOmf47Nz90Bu55CC1", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 2000, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3LQBys2sOmf47Nz901xP6fgK", + "object": "charge", + "amount": 2000, + "amount_captured": 2000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3LQBys2sOmf47Nz90248q1tS", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "Stripe", + "captured": true, + "created": 1658935022, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "description": null, + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 58, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3LQBys2sOmf47Nz90Bu55CC1", + "payment_method": "pm_1LQByr2sOmf47Nz9Hl3qCzNX", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 4, + "exp_year": 2023, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "installments": null, + "last4": "4242", + "mandate": null, + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3LQBys2sOmf47Nz901xP6fgK/rcpt_M8SuF32X4OCCbk1wVP4b75lFFHrA5za", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3LQBys2sOmf47Nz901xP6fgK/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3LQBys2sOmf47Nz90Bu55CC1" + }, + "client_secret": "pi_3LQBys2sOmf47Nz90Bu55CC1_secret_RYLYFK1TOMVC69SeOlyczqhd1", + "confirmation_method": "manual", + "created": 1658935022, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "description": null, + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1LQByr2sOmf47Nz9Hl3qCzNX", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + recorded_at: Wed, 27 Jul 2022 15:17:04 GMT +- request: + method: post + uri: https://api.stripe.com/v1/payment_intents/pi_3LQBys2sOmf47Nz90Bu55CC1 + body: + encoding: UTF-8 + string: description=Invoice+reference%3A+2207002%2FVL + headers: + User-Agent: + - Stripe/v1 RubyBindings/5.29.0 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-Telemetry: + - '{"last_request_metrics":{"request_id":"req_6Mz1jjCYQWTLMO","request_duration_ms":2526}}' + Stripe-Version: + - '2019-08-14' + X-Stripe-Client-User-Agent: + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 27 Jul 2022 15:17:05 GMT + Content-Type: + - application/json + Content-Length: + - '4457' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Idempotency-Key: + - 6d5f0d63-ed5b-44f5-bf02-c62b655072bf + Original-Request: + - req_TLScQWYYL5dzB0 + Request-Id: + - req_TLScQWYYL5dzB0 + Stripe-Should-Retry: + - 'false' + Stripe-Version: + - '2019-08-14' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains; preload + body: + encoding: UTF-8 + string: |- + { + "id": "pi_3LQBys2sOmf47Nz90Bu55CC1", + "object": "payment_intent", + "amount": 2000, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 2000, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3LQBys2sOmf47Nz901xP6fgK", + "object": "charge", + "amount": 2000, + "amount_captured": 2000, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3LQBys2sOmf47Nz90248q1tS", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "calculated_statement_descriptor": "Stripe", + "captured": true, + "created": 1658935022, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "description": null, + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": {}, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 58, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3LQBys2sOmf47Nz90Bu55CC1", + "payment_method": "pm_1LQByr2sOmf47Nz9Hl3qCzNX", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 4, + "exp_year": 2023, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "installments": null, + "last4": "4242", + "mandate": null, + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/acct_103rE62sOmf47Nz9/ch_3LQBys2sOmf47Nz901xP6fgK/rcpt_M8SuF32X4OCCbk1wVP4b75lFFHrA5za", + "refunded": false, + "refunds": { + "object": "list", + "data": [], + "has_more": false, + "total_count": 0, + "url": "/v1/charges/ch_3LQBys2sOmf47Nz901xP6fgK/refunds" + }, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3LQBys2sOmf47Nz90Bu55CC1" + }, + "client_secret": "pi_3LQBys2sOmf47Nz90Bu55CC1_secret_RYLYFK1TOMVC69SeOlyczqhd1", + "confirmation_method": "manual", + "created": 1658935022, + "currency": "usd", + "customer": "cus_8Di1wjdVktv5kt", + "description": "Invoice reference: 2207002/VL", + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": {}, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1LQByr2sOmf47Nz9Hl3qCzNX", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + recorded_at: Wed, 27 Jul 2022 15:17:05 GMT +recorded_with: VCR 6.0.0 diff --git a/test/vcr_cassettes/reservations_space_seats_user3.yml b/test/vcr_cassettes/reservations_space_seats_user3.yml new file mode 100644 index 000000000..152d5bbac --- /dev/null +++ b/test/vcr_cassettes/reservations_space_seats_user3.yml @@ -0,0 +1,118 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.stripe.com/v1/payment_methods + body: + encoding: UTF-8 + string: type=card&card[number]=4242424242424242&card[exp_month]=4&card[exp_year]=2023&card[cvc]=314 + headers: + User-Agent: + - Stripe/v1 RubyBindings/5.29.0 + Authorization: + - Bearer sk_test_testfaketestfaketestfake + Content-Type: + - application/x-www-form-urlencoded + X-Stripe-Client-Telemetry: + - '{"last_request_metrics":{"request_id":"req_TLScQWYYL5dzB0","request_duration_ms":2}}' + Stripe-Version: + - '2019-08-14' + X-Stripe-Client-User-Agent: + - '{"bindings_version":"5.29.0","lang":"ruby","lang_version":"2.6.10 p210 (2022-04-12)","platform":"x86_64-linux","engine":"ruby","publisher":"stripe","uname":"Linux + version 5.18.10-arch1-1 (linux@archlinux) (gcc (GCC) 12.1.0, GNU ld (GNU Binutils) + 2.38) #1 SMP PREEMPT_DYNAMIC Thu, 07 Jul 2022 17:18:13 +0000","hostname":"Sylvain-laptop"}' + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Server: + - nginx + Date: + - Wed, 27 Jul 2022 15:21:15 GMT + Content-Type: + - application/json + Content-Length: + - '930' + Connection: + - keep-alive + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Methods: + - GET, POST, HEAD, OPTIONS, DELETE + Access-Control-Allow-Origin: + - "*" + Access-Control-Expose-Headers: + - Request-Id, Stripe-Manage-Version, X-Stripe-External-Auth-Required, X-Stripe-Privileged-Session-Required + Access-Control-Max-Age: + - '300' + Cache-Control: + - no-cache, no-store + Idempotency-Key: + - fb9a01ef-f2e0-4c17-aa2c-77565e5bef21 + Original-Request: + - req_ivPywFk7vszgb8 + Request-Id: + - req_ivPywFk7vszgb8 + Stripe-Should-Retry: + - 'false' + Stripe-Version: + - '2019-08-14' + Strict-Transport-Security: + - max-age=31556926; includeSubDomains; preload + body: + encoding: UTF-8 + string: |- + { + "id": "pm_1LQC2x2sOmf47Nz9bvxgcgS3", + "object": "payment_method", + "billing_details": { + "address": { + "city": null, + "country": null, + "line1": null, + "line2": null, + "postal_code": null, + "state": null + }, + "email": null, + "name": null, + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": null, + "address_postal_code_check": null, + "cvc_check": "unchecked" + }, + "country": "US", + "exp_month": 4, + "exp_year": 2023, + "fingerprint": "o52jybR7bnmNn6AT", + "funding": "credit", + "generated_from": null, + "last4": "4242", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1658935275, + "customer": null, + "livemode": false, + "metadata": {}, + "type": "card" + } + recorded_at: Wed, 27 Jul 2022 15:21:15 GMT +recorded_with: VCR 6.0.0 From be53adb5516aa9805afbea366e49a4b40aa91ec3 Mon Sep 17 00:00:00 2001 From: Cyril Date: Mon, 1 Aug 2022 17:44:36 +0200 Subject: [PATCH 140/141] Display the tag and theme field on the detail view of a project --- CHANGELOG.md | 1 + app/frontend/templates/projects/show.html | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edb8d9556..0d743431a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Added a test for multiple reservations on the same space slot - Fix a security issue: updated moment to 2.29.4 to fix [CVE-2022-31129](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31129) +- Display the tag and theme field on the detail view of a project ## v5.4.13 2022 July 27 diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index de9773310..4fe28d49e 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -84,9 +84,15 @@
    {{ 'app.public.projects_show.posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}} +
    + + {{theme.name}} + +
    +
    {{project.project_caos_attributes.length}} @@ -150,6 +156,15 @@
    +
    +
    +

    {{ 'app.shared.project.tags' }}

    +
    +
    +
    {{ project.tags }}
    +
    +
    +
    From da8fa6b996f25c59d408945d1772c1c1a3ee719b Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 1 Aug 2022 18:25:06 +0200 Subject: [PATCH 141/141] Version 5.4.14 --- CHANGELOG.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d743431a..20e5af9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,13 @@ ## next release +## v5.4.14 2022 August 1 + - Added a test for multiple reservations on the same space slot -- Fix a security issue: updated moment to 2.29.4 to fix [CVE-2022-31129](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31129) - Display the tag and theme field on the detail view of a project +- Improved file validation, validation is now based on content of the file in addition of the validation of the extension +- Fix a security issue: xss vulnerability in projects +- Fix a security issue: updated moment to 2.29.4 to fix [CVE-2022-31129](https://cve.mitre.org/cgi-bin/cvename.cgi?CVE-2022-31129) ## v5.4.13 2022 July 27 diff --git a/package.json b/package.json index a8153e429..3c6061ffd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fab-manager", - "version": "5.4.13", + "version": "5.4.14", "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",