From 74cc69d1bd13032311e955890f27041b7945689a Mon Sep 17 00:00:00 2001 From: Du Peng Date: Fri, 7 Feb 2020 17:37:00 +0100 Subject: [PATCH 1/7] Ability to configure reservation slot restrict for plans --- CHANGELOG.md | 15 ++-- .../controllers/admin/calendar.js.erb | 68 ++++++++++++++++++- .../javascripts/controllers/machines.js.erb | 2 + app/assets/javascripts/directives/cart.js.erb | 38 +++++++++++ .../admin/calendar/eventModal.html.erb | 35 +++++++++- .../templates/machines/reserve.html.erb | 2 + app/assets/templates/shared/_cart.html.erb | 9 +++ .../api/availabilities_controller.rb | 4 +- app/models/availability.rb | 8 ++- app/models/plan.rb | 1 - app/models/plans_availability.rb | 6 ++ .../api/availabilities/machine.json.jbuilder | 13 ++-- .../api/availabilities/spaces.json.jbuilder | 13 ++-- .../availabilities/trainings.json.jbuilder | 3 + config/locales/app.admin.en.yml | 3 + config/locales/app.admin.es.yml | 3 + config/locales/app.admin.fr.yml | 3 + config/locales/app.admin.pt.yml | 3 + config/locales/app.shared.en.yml | 2 + config/locales/app.shared.es.yml | 2 + config/locales/app.shared.fr.yml | 2 + config/locales/app.shared.pt.yml | 2 + ...00206132857_create_plans_availabilities.rb | 10 +++ db/schema.rb | 12 +++- 24 files changed, 232 insertions(+), 27 deletions(-) create mode 100644 app/models/plans_availability.rb create mode 100644 db/migrate/20200206132857_create_plans_availabilities.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ebfbbb7..183d8b00c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Changelog Fab Manager +- Ability to configure reservation slot restrict for plans - Ability to create and delete periodic calendar availabilities (recurrence) - Ability to fully customize the home page - Automated setup assistant @@ -140,7 +141,7 @@ ## v4.0.4 2019 August 14 - Fix a bug: #140 VAT rate is erroneous in invoices. - Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if + Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st (if - [TODO DEPLOY] `rake fablab:maintenance:regenerate_invoices[2019,8]` ## v4.0.3 2019 August 01 @@ -184,7 +185,7 @@ - Refactored user's profile to keep invoicing data after an user was deleted - Refactored user's profile to keep statistical data after an user was deleted - Ability to delete an user (fixes #129 and #120) -- Ask user acceptance before deposing analytics cookies +- Ask user acceptance before deposing analytics cookies - Fix a bug: (spanish) some translations are not loaded correctly - Fix a bug: some users may not appear in the admin's general listing - Fix a bug: Availabilities export report an erroneous number of reservations for machine availabilities (#131) @@ -381,8 +382,8 @@ - Fix a security issue: sprockets < 2.12.5 has a security vulnerability as described in [CVE-2018-3760](https://nvd.nist.gov/vuln/detail/CVE-2018-3760) - Ensure elasticSearch indices are started with green status on new installations - Refactored User.to_json to remove code duplication -- Fixed syntax and typos in README -- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch. +- Fixed syntax and typos in README +- [TODO DEPLOY] **IMPORTANT** Please read [elastic_upgrade.md](doc/elastic_upgrade.md) for instructions on upgrading ElasticSearch. - [TODO DEPLOY] `rake fablab:fix:categories_slugs` - [TODO DEPLOY] -> (only dev) `bundle install` - [TODO DEPLOY] `rake db:seed` @@ -395,7 +396,7 @@ - Set Stripe API version, all fab-managers has to use this version because codebase relies on it - Fix a security issue: OmniAuth < 1.3.2 has a security vulnerability described in [CVE-2017-18076](https://nvd.nist.gov/vuln/detail/CVE-2017-18076) - Fix a security issue: rack-protection < 1.5.5 has a security vulnerability described in [CVE-2018-1000119](https://nvd.nist.gov/vuln/detail/CVE-2018-1000119) -- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency +- Fix a security issue: http gem < 0.7.3 has a security vulnerability described in [CVE-2015-1828](https://nvd.nist.gov/vuln/detail/CVE-2015-1828), updates twitter gem as a dependency ## v2.6.3 2018 January 2 @@ -451,12 +452,12 @@ ## v2.5.13 2017 September 11 -- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted +- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with recursive events which the initial event was deleted ## v2.5.12 2017 September 11 - Fix a bug: Long words overflow from homepage's events blocks -- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events +- Fix a bug: ActiveRecord::RecordNotFound when running rake task fix:recursive_events_over_DST with non-recursive events ## v2.5.11 2017 September 7 diff --git a/app/assets/javascripts/controllers/admin/calendar.js.erb b/app/assets/javascripts/controllers/admin/calendar.js.erb index d822d5521..4244e7878 100644 --- a/app/assets/javascripts/controllers/admin/calendar.js.erb +++ b/app/assets/javascripts/controllers/admin/calendar.js.erb @@ -297,7 +297,9 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], trainingsPromise: ['Training', function (Training) { return Training.query().$promise; }], spacesPromise: ['Space', function (Space) { return Space.query().$promise; }], - tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }] + tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], + plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] } }); // when the modal is closed, we send the slot to the server for saving modalInstance.result.then( @@ -394,8 +396,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state /** * Controller used in the slot creation modal window */ -Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'growl', '_t', - function ($scope, $uibModalInstance, $sce, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, growl, _t) { +Application.Controllers.controller('CreateEventModalController', ['$scope', '$uibModalInstance', '$sce', 'moment', 'start', 'end', 'machinesPromise', 'Availability', 'trainingsPromise', 'spacesPromise', 'tagsPromise', 'plansPromise', 'groupsPromise', 'growl', '_t', + function ($scope, $uibModalInstance, $sce, moment, start, end, machinesPromise, Availability, trainingsPromise, spacesPromise, tagsPromise, plansPromise, groupsPromise, growl, _t) { // $uibModal parameter $scope.start = start; @@ -414,6 +416,21 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui // all tags list $scope.tags = tagsPromise; + $scope.isOnlySubscriptions = false; + $scope.selectedPlans = []; + $scope.selectedPlansBinding = {}; + // list of plans, classified by group + $scope.plansClassifiedByGroup = []; + for (let group of Array.from(groupsPromise)) { + const groupObj = { id: group.id, name: group.name, plans: [] }; + for (let plan of Array.from(plansPromise)) { + if (plan.group_id === group.id) { groupObj.plans.push(plan); } + } + if (groupObj.plans.length > 0) { + $scope.plansClassifiedByGroup.push(groupObj); + } + } + // machines associated with the created slot $scope.selectedMachines = []; $scope.selectedMachinesBinding = {}; @@ -463,6 +480,9 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui // localized name(s) of the selected tag(s) $scope.tagsName = ''; + // localized name(s) of the selected plan(s) + $scope.plansName = ''; + /** * Adds or removes the provided machine from the current slot * @param machine {Object} @@ -491,6 +511,34 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui } } + /** + * Adds or removes the provided plan from the current slot + * @param plan {Object} + */ + $scope.toggleSelectPlan = function (plan) { + const index = $scope.selectedPlans.indexOf(plan); + if (index > -1) { + return $scope.selectedMachines.splice(index, 1); + } else { + return $scope.selectedMachines.push(machine); + } + }; + + /** + * Select/unselect all the plans + */ + $scope.toggleAllPlans = function() { + const count = $scope.selectedPlans.length; + $scope.selectedPlans = []; + $scope.selectedPlansBinding = {}; + if (count == 0) { + plansPromise.forEach(function (plan) { + $scope.selectedPlans.push(plan); + $scope.selectedPlansBinding[plan.id] = true; + }) + } + }; + /** * Callback for the modal window validation: save the slot and closes the modal */ @@ -510,6 +558,9 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui if ($scope.availability.is_recurrent) { $scope.availability.occurrences = $scope.occurrences; } + if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) { + $scope.availability.plan_ids = $scope.selectedPlans.map(function (p) { return p.id; }); + } return Availability.save( { availability: $scope.availability }, function (availability) { $uibModalInstance.close(availability); } @@ -560,6 +611,14 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui $scope.selectedSpace = $scope.spaces[0]; } + // when disable is only subscriptions option, reset all selected plans + $scope.$watch('isOnlySubscriptions', function(value) { + if (!value) { + $scope.selectedPlans = []; + $scope.selectedPlansBinding = {}; + } + }); + // When we configure a machine/space availability, do not let the user change the end time, as the total // time must be dividable by Fablab.slotDuration minutes (base slot duration). For training availabilities, the user // can configure any duration as it does not matters. @@ -685,6 +744,9 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui return $scope.availability.tag_ids.indexOf(t.id) > -1; }) $scope.tagsName = localizedList(tags); + if ($scope.isOnlySubscriptions && $scope.selectedPlans.length > 0) { + $scope.plansName = localizedList($scope.selectedPlans); + } } const localizedList = function (items) { diff --git a/app/assets/javascripts/controllers/machines.js.erb b/app/assets/javascripts/controllers/machines.js.erb index 733b1dca1..2ae5aebca 100644 --- a/app/assets/javascripts/controllers/machines.js.erb +++ b/app/assets/javascripts/controllers/machines.js.erb @@ -367,6 +367,8 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat $scope.settings = settingsPromise; // list of plans, classified by group + $scope.groups = groupsPromise; + $scope.plans = plansPromise; $scope.plansClassifiedByGroup = []; for (let group of Array.from(groupsPromise)) { const groupObj = { id: group.id, name: group.name, plans: [] }; diff --git a/app/assets/javascripts/directives/cart.js.erb b/app/assets/javascripts/directives/cart.js.erb index ff2c4a810..cbe5a6980 100644 --- a/app/assets/javascripts/directives/cart.js.erb +++ b/app/assets/javascripts/directives/cart.js.erb @@ -23,6 +23,8 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', plan: '=', planSelectionTime: '=', settings: '=', + plans: '=', + groups: '=', onSlotAddedToCart: '=', onSlotRemovedFromCart: '=', onSlotStartToModify: '=', @@ -134,6 +136,27 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', $scope.payCart = function () { // first, we check that a user was selected if (Object.keys($scope.user).length > 0) { + + // check user was selected a plan if slot is restricted for subcriptions + const slotHasPlan = []; + $scope.events.reserved.forEach(slot => { + if (slot.plan_ids.length > 0) { + if ( + ($scope.selectedPlan && _.include(slot.plan_ids, $scope.selectedPlan.id)) || + ($scope.user.subscribed_plan && _.include(slot.plan_ids, $scope.user.subscribed_plan.id)) + ) { + slotHasPlan.push(true); + } else { + slotHasPlan.push(false); + } + } + }); + const hasPlanForSlot = slotHasPlan.every(a => a); + if (!hasPlanForSlot) { + return growl.error(_t('app.shared.cart.slot_restrict_subcriptions_must_select_plan')); + } + + const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan); return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) { @@ -262,6 +285,21 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', */ var slotSelectionChanged = function () { if ($scope.slot) { + // build a list of plans if this slot is restricted for subcriptions + if ($scope.slot.plan_ids.length > 0) { + const _plans = _.filter($scope.plans, p => _.include($scope.slot.plan_ids, p.id)); + $scope.slot.plansGrouped = []; + for (let group of Array.from($scope.groups)) { + const groupObj = { id: group.id, name: group.name, plans: [] }; + for (let plan of Array.from(_plans)) { + if (plan.group_id === group.id) { groupObj.plans.push(plan); } + } + if (groupObj.plans.length > 0) { + $scope.slot.plansGrouped.push(groupObj); + } + } + } + if (!$scope.slot.is_reserved && !$scope.events.modifiable && !$scope.slot.is_completed) { // slot is not reserved and we are not currently modifying a slot // -> can be added to cart or removed if already present diff --git a/app/assets/templates/admin/calendar/eventModal.html.erb b/app/assets/templates/admin/calendar/eventModal.html.erb index 3b7f245c1..6773e1e78 100644 --- a/app/assets/templates/admin/calendar/eventModal.html.erb +++ b/app/assets/templates/admin/calendar/eventModal.html.erb @@ -93,12 +93,41 @@ +
+

{{ 'app.admin.calendar.restrict_this_slot_for_subcriptions_optional' }}

+
+
+ + +
+
+

{{ 'app.admin.calendar.select_some_plans' | translate }}

+ +
+ +
+
{{::group.name}}
+ +
+
+
+
+
+
+
{{ 'app.shared.cart.slot_restrict_plans' }}
+
+
{{::group.name}}
+
    +
  • {{::plan.name}}
  • +
+
+
diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index 405f8e443..cec87315d 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -148,8 +148,8 @@ class API::AvailabilitiesController < API::ApiController def availability_params params.require(:availability).permit(:start_at, :end_at, :available_type, :machine_ids, :training_ids, :nb_total_places, :is_recurrent, :period, :nb_periods, :end_date, - machine_ids: [], training_ids: [], space_ids: [], tag_ids: [], - machines_attributes: %i[id _destroy]) + machine_ids: [], training_ids: [], space_ids: [], tag_ids: [], plan_ids: [], + machines_attributes: %i[id _destroy], plans_attributes: %i[id _destroy]) end def lock_params diff --git a/app/models/availability.rb b/app/models/availability.rb index 13d48d253..0a01d0da7 100644 --- a/app/models/availability.rb +++ b/app/models/availability.rb @@ -4,7 +4,6 @@ # Eg. a 3D printer will be reservable on thursday from 9 to 11 pm # Availabilities may be subdivided into Slots (of 1h), for some types of reservables (eg. Machine) class Availability < ActiveRecord::Base - # elastic initialisations include Elasticsearch::Model index_name 'fablab' @@ -29,6 +28,10 @@ class Availability < ActiveRecord::Base has_many :tags, through: :availability_tags accepts_nested_attributes_for :tags, allow_destroy: true + has_many :plans_availabilities, dependent: :destroy + has_many :plans, through: :plans_availabilities + accepts_nested_attributes_for :plans, allow_destroy: true + scope :machines, -> { where(available_type: 'machines') } scope :trainings, -> { includes(:trainings).where(available_type: 'training') } scope :spaces, -> { includes(:spaces).where(available_type: 'space') } @@ -86,7 +89,7 @@ class Availability < ActiveRecord::Base def available_space_places return unless available_type == 'space' - ((end_at - start_at)/ApplicationHelper::SLOT_DURATION.minutes).to_i * nb_total_places + ((end_at - start_at) / ApplicationHelper::SLOT_DURATION.minutes).to_i * nb_total_places end def title(filter = {}) @@ -166,5 +169,4 @@ class Availability < ActiveRecord::Base errors.add(:machine_ids, I18n.t('availabilities.must_be_associated_with_at_least_1_machine')) end - end diff --git a/app/models/plan.rb b/app/models/plan.rb index 94d760994..a4d1b0d7a 100644 --- a/app/models/plan.rb +++ b/app/models/plan.rb @@ -25,7 +25,6 @@ class Plan < ActiveRecord::Base after_create :create_statistic_type after_create :set_name - validates :amount, :group, :base_name, presence: true validates :interval_count, numericality: { only_integer: true, greater_than_or_equal_to: 1 } validates :interval_count, numericality: { less_than: 13 }, if: proc { |plan| plan.interval == 'month' } diff --git a/app/models/plans_availability.rb b/app/models/plans_availability.rb new file mode 100644 index 000000000..baa30c111 --- /dev/null +++ b/app/models/plans_availability.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class PlansAvailability < ActiveRecord::Base + belongs_to :plan + belongs_to :availability +end diff --git a/app/views/api/availabilities/machine.json.jbuilder b/app/views/api/availabilities/machine.json.jbuilder index 255b9153b..7c62843a9 100644 --- a/app/views/api/availabilities/machine.json.jbuilder +++ b/app/views/api/availabilities/machine.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(@slots) do |slot| json.id slot.id if slot.id json.can_modify slot.can_modify @@ -14,13 +16,16 @@ json.array!(@slots) do |slot| json.name slot.machine.name end # the user who booked the slot ... - json.user do - json.id slot.reservation.user.id - json.name slot.reservation.user.profile.full_name - end if @current_user_role == 'admin' and slot.reservation # ... if the slot was reserved + if (@current_user_role == 'admin') && slot.reservation + json.user do + json.id slot.reservation.user.id + json.name slot.reservation.user.profile.full_name + end + end # ... if the slot was reserved 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/spaces.json.jbuilder b/app/views/api/availabilities/spaces.json.jbuilder index f8a0b417c..0528284fb 100644 --- a/app/views/api/availabilities/spaces.json.jbuilder +++ b/app/views/api/availabilities/spaces.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(@slots) do |slot| json.id slot.id if slot.id json.can_modify slot.can_modify @@ -15,13 +17,16 @@ json.array!(@slots) do |slot| json.name slot.space.name end # the user who booked the slot ... - json.user do - json.id slot.reservation.user.id - json.name slot.reservation.user.profile.full_name - end if @current_user_role == 'admin' and slot.reservation # ... if the slot was reserved + if (@current_user_role == 'admin') && slot.reservation + json.user do + json.id slot.reservation.user.id + json.name slot.reservation.user.profile.full_name + end + end # ... if the slot was reserved 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 cf76193cd..36782c8fb 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(@availabilities) do |a| json.id a.slot_id if a.slot_id if a.is_reserved @@ -31,4 +33,5 @@ json.array!(@availabilities) do |a| json.id t.id json.name t.name end + json.plan_ids slot.availability.plan_ids end diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index a29b730cc..0f6766af0 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -46,6 +46,9 @@ en: adjust_the_opening_hours: "Adjust the opening hours" to_time: "to" # context: time. eg. 'from 18:00 to 21:00' restrict_this_slot_with_labels_optional: "Restrict this slot with labels (optional)" + restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)" + select_some_plans: "Select some plans" + plans: "Plan(s):" recurrence: "Recurrence" enabled: "Enabled" period: "Period" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index 016570414..263a58454 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -44,6 +44,9 @@ es: adjust_the_opening_hours: "Ajustar el horario de apertura" to_time: a restrict_this_slot_with_labels_optional: "Restringir este horario con etiquetas (opcional)" + restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)" + select_some_plans: "Select some plans" + plans: "Plan(s):" recurrence: "Recurrencia" enabled: "Activa" period: "Período" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 94c46c09b..b4024cdbf 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -46,6 +46,9 @@ fr: adjust_the_opening_hours: "Ajuster l'horaire" to_time: "à" # context: time. eg. 'from 18:00 to 21:00' restrict_this_slot_with_labels_optional: "Restreindre ce créneau avec des étiquettes (optionnel)" + restrict_this_slot_for_subcriptions_optional: "Restreindre ce créneau pour les abonnements (optionnel)" + select_some_plans: "Sélectionnez des formules d'abonnement" + plans: "Abonnement(s):" recurrence: "Récurrence" enabled: "Activée" period: "Période" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index 2ba804e25..cdd97fe9e 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -46,6 +46,9 @@ pt: adjust_the_opening_hours: "Ajustar o horário de funcionamento" to_time: "ás" # context: time. eg. 'from 18:00 to 21:00' restrict_this_slot_with_labels_optional: "Restrinja este slot com etiquetas (opcional)" + restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)" + select_some_plans: "Select some plans" + plans: "Plan(s):" recurrence: "Recurrence" enabled: "Enabled" period: "Period" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index dc22183b4..38e729b3b 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -439,3 +439,5 @@ en: a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later." none: "None" online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly." + slot_restrict_plans: "This slot is restricted for the plans below:" + slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first." diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index 3e79b184c..76428ab9d 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -416,3 +416,5 @@ es: a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later." none: "Ninguno" online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab." + slot_restrict_plans: "This slot is restricted for the plans below:" + slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first." diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index b0fe0c49f..77fb1dc5f 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -439,3 +439,5 @@ fr: a_problem_occurred_during_the_payment_process_please_try_again_later: "Il y a eu un problème lors de la procédure de paiement. Veuillez réessayer plus tard." none: "Aucune" online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab." + slot_restrict_plans: "Ce créneau est restreint pour les formules d'abonnement ci-desous:" + slot_restrict_subcriptions_must_select_plan: "Le créneau est restreint pour les abonnements, Veuillez tout d'abord sélectionner un formule d'abonnement" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index f3e770b8e..197ba6c55 100755 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -439,3 +439,5 @@ pt: a_problem_occurred_during_the_payment_process_please_try_again_later: "Um problema ocorreu durante o processo de pagamento. Por favor tente novamente mais tarde." none: "Vazio" online_payment_disabled: "O pagamento online não está disponível. Entre em contato diretamente com a recepção do Fablab." + slot_restrict_plans: "This slot is restricted for the plans below:" + slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first." diff --git a/db/migrate/20200206132857_create_plans_availabilities.rb b/db/migrate/20200206132857_create_plans_availabilities.rb new file mode 100644 index 000000000..740d5d98e --- /dev/null +++ b/db/migrate/20200206132857_create_plans_availabilities.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CreatePlansAvailabilities < ActiveRecord::Migration + def change + create_table :plans_availabilities do |t| + t.belongs_to :plan, index: true + t.belongs_to :availability, index: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 286c27881..ee59f3f76 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,12 +11,12 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20200127111404) do +ActiveRecord::Schema.define(version: 20200206132857) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - enable_extension "pg_trgm" enable_extension "unaccent" + enable_extension "pg_trgm" create_table "abuses", force: :cascade do |t| t.integer "signaled_id" @@ -465,6 +465,14 @@ ActiveRecord::Schema.define(version: 20200127111404) do add_index "plans", ["group_id"], name: "index_plans_on_group_id", using: :btree + create_table "plans_availabilities", force: :cascade do |t| + t.integer "plan_id" + t.integer "availability_id" + end + + add_index "plans_availabilities", ["availability_id"], name: "index_plans_availabilities_on_availability_id", using: :btree + add_index "plans_availabilities", ["plan_id"], name: "index_plans_availabilities_on_plan_id", using: :btree + create_table "price_categories", force: :cascade do |t| t.string "name" t.text "conditions" From 427725f9274efa786a0d23a82a9a64f75c40400a Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 10 Feb 2020 11:52:57 +0100 Subject: [PATCH 2/7] fix bug: select plans error --- app/assets/javascripts/controllers/admin/calendar.js.erb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/controllers/admin/calendar.js.erb b/app/assets/javascripts/controllers/admin/calendar.js.erb index 4244e7878..92d116512 100644 --- a/app/assets/javascripts/controllers/admin/calendar.js.erb +++ b/app/assets/javascripts/controllers/admin/calendar.js.erb @@ -518,9 +518,9 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui $scope.toggleSelectPlan = function (plan) { const index = $scope.selectedPlans.indexOf(plan); if (index > -1) { - return $scope.selectedMachines.splice(index, 1); + return $scope.selectedPlans.splice(index, 1); } else { - return $scope.selectedMachines.push(machine); + return $scope.selectedPlans.push(plan); } }; @@ -750,6 +750,7 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui } const localizedList = function (items) { + console.log(items) if (items.length === 0) return `${_t("app.admin.calendar.none")}`; const names = items.map(function (i) { return $sce.trustAsHtml(`${i.name}`); }); From d9676b604c40aa5bcb909df7fc5b1a561f218cc3 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 10 Feb 2020 11:53:20 +0100 Subject: [PATCH 3/7] remove debug info --- app/assets/javascripts/controllers/admin/calendar.js.erb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/controllers/admin/calendar.js.erb b/app/assets/javascripts/controllers/admin/calendar.js.erb index 92d116512..7d37a33c9 100644 --- a/app/assets/javascripts/controllers/admin/calendar.js.erb +++ b/app/assets/javascripts/controllers/admin/calendar.js.erb @@ -750,7 +750,6 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui } const localizedList = function (items) { - console.log(items) if (items.length === 0) return `${_t("app.admin.calendar.none")}`; const names = items.map(function (i) { return $sce.trustAsHtml(`${i.name}`); }); From 372d7f936b26af8a3b855f7a636ca1fe240f5c22 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 11 Feb 2020 11:46:40 +0100 Subject: [PATCH 4/7] typos & syntax --- CHANGELOG.md | 2 +- app/assets/javascripts/directives/cart.js.erb | 12 ++++++------ .../templates/admin/calendar/eventModal.html.erb | 2 +- app/assets/templates/plans/index.html.erb | 2 +- config/locales/app.admin.en.yml | 2 +- config/locales/app.admin.es.yml | 2 +- config/locales/app.admin.fr.yml | 2 +- config/locales/app.admin.pt.yml | 2 +- config/locales/app.public.ach.yml | 2 +- config/locales/app.public.en.yml | 2 +- config/locales/app.public.es.yml | 2 +- config/locales/app.public.fr.yml | 2 +- config/locales/app.public.pt.yml | 2 +- config/locales/app.shared.en.yml | 2 +- config/locales/app.shared.es.yml | 2 +- config/locales/app.shared.fr.yml | 2 +- config/locales/app.shared.pt.yml | 2 +- db/schema.rb | 2 +- 18 files changed, 23 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 183d8b00c..98877aa7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog Fab Manager -- Ability to configure reservation slot restrict for plans +- Ability to configure reservation slot restricted for plan subscribers - Ability to create and delete periodic calendar availabilities (recurrence) - Ability to fully customize the home page - Automated setup assistant diff --git a/app/assets/javascripts/directives/cart.js.erb b/app/assets/javascripts/directives/cart.js.erb index cbe5a6980..6ea6805a8 100644 --- a/app/assets/javascripts/directives/cart.js.erb +++ b/app/assets/javascripts/directives/cart.js.erb @@ -137,9 +137,9 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', // first, we check that a user was selected if (Object.keys($scope.user).length > 0) { - // check user was selected a plan if slot is restricted for subcriptions + // check user was selected a plan if slot is restricted for subscriptions const slotHasPlan = []; - $scope.events.reserved.forEach(slot => { + $scope.events.reserved.forEach(function (slot) { if (slot.plan_ids.length > 0) { if ( ($scope.selectedPlan && _.include(slot.plan_ids, $scope.selectedPlan.id)) || @@ -151,9 +151,9 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', } } }); - const hasPlanForSlot = slotHasPlan.every(a => a); + const hasPlanForSlot = slotHasPlan.every(function (a) { return a }); if (!hasPlanForSlot) { - return growl.error(_t('app.shared.cart.slot_restrict_subcriptions_must_select_plan')); + return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan')); } @@ -285,9 +285,9 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', */ var slotSelectionChanged = function () { if ($scope.slot) { - // build a list of plans if this slot is restricted for subcriptions + // build a list of plans if this slot is restricted for subscriptions if ($scope.slot.plan_ids.length > 0) { - const _plans = _.filter($scope.plans, p => _.include($scope.slot.plan_ids, p.id)); + const _plans = _.filter($scope.plans, function (p) { return _.include($scope.slot.plan_ids, p.id) }); $scope.slot.plansGrouped = []; for (let group of Array.from($scope.groups)) { const groupObj = { id: group.id, name: group.name, plans: [] }; diff --git a/app/assets/templates/admin/calendar/eventModal.html.erb b/app/assets/templates/admin/calendar/eventModal.html.erb index 6773e1e78..065a01241 100644 --- a/app/assets/templates/admin/calendar/eventModal.html.erb +++ b/app/assets/templates/admin/calendar/eventModal.html.erb @@ -94,7 +94,7 @@
-

{{ 'app.admin.calendar.restrict_this_slot_for_subcriptions_optional' }}

+

{{ 'app.admin.calendar.restrict_this_slot_for_subscriptions_optional' }}

diff --git a/app/assets/templates/plans/index.html.erb b/app/assets/templates/plans/index.html.erb index 69c2ac6c3..090eb9822 100644 --- a/app/assets/templates/plans/index.html.erb +++ b/app/assets/templates/plans/index.html.erb @@ -7,7 +7,7 @@
-

{{ 'app.public.plans.subcriptions' }}

+

{{ 'app.public.plans.subscriptions' }}

diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 0f6766af0..3fda351c6 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -46,7 +46,7 @@ en: adjust_the_opening_hours: "Adjust the opening hours" to_time: "to" # context: time. eg. 'from 18:00 to 21:00' restrict_this_slot_with_labels_optional: "Restrict this slot with labels (optional)" - restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)" + restrict_this_slot_for_subscriptions_optional: "Restrict this slot for subscription users (optional)" select_some_plans: "Select some plans" plans: "Plan(s):" recurrence: "Recurrence" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index 263a58454..965d0138f 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -44,7 +44,7 @@ es: adjust_the_opening_hours: "Ajustar el horario de apertura" to_time: a restrict_this_slot_with_labels_optional: "Restringir este horario con etiquetas (opcional)" - restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)" + restrict_this_slot_for_subscriptions_optional: "Restrict this slot for subscription users (optional)" select_some_plans: "Select some plans" plans: "Plan(s):" recurrence: "Recurrencia" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index b4024cdbf..bdb5ba723 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -46,7 +46,7 @@ fr: adjust_the_opening_hours: "Ajuster l'horaire" to_time: "à" # context: time. eg. 'from 18:00 to 21:00' restrict_this_slot_with_labels_optional: "Restreindre ce créneau avec des étiquettes (optionnel)" - restrict_this_slot_for_subcriptions_optional: "Restreindre ce créneau pour les abonnements (optionnel)" + restrict_this_slot_for_subscriptions_optional: "Restreindre ce créneau pour les abonnements (optionnel)" select_some_plans: "Sélectionnez des formules d'abonnement" plans: "Abonnement(s):" recurrence: "Récurrence" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index cdd97fe9e..d0473aa6c 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -46,7 +46,7 @@ pt: adjust_the_opening_hours: "Ajustar o horário de funcionamento" to_time: "ás" # context: time. eg. 'from 18:00 to 21:00' restrict_this_slot_with_labels_optional: "Restrinja este slot com etiquetas (opcional)" - restrict_this_slot_for_subcriptions_optional: "Restrict this slot for subscription users (optional)" + restrict_this_slot_for_subscriptions_optional: "Restrict this slot for subscription users (optional)" select_some_plans: "Select some plans" plans: "Plan(s):" recurrence: "Recurrence" diff --git a/config/locales/app.public.ach.yml b/config/locales/app.public.ach.yml index 6e8290c80..bc623609c 100644 --- a/config/locales/app.public.ach.yml +++ b/config/locales/app.public.ach.yml @@ -229,7 +229,7 @@ ach: the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users: "crwdns9167:0crwdne9167:0" plans: #summary of the subscriptions - subcriptions: "crwdns9169:0crwdne9169:0" + subscriptions: "crwdns9169:0crwdne9169:0" i_choose_that_plan: "crwdns9171:0crwdne9171:0" i_subscribe_online: "crwdns9173:0crwdne9173:0" i_already_subscribed: "crwdns9175:0crwdne9175:0" diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 4122457c7..ee71749c6 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -253,7 +253,7 @@ en: plans: # summary of the subscriptions - subcriptions: "Subscriptions" + subscriptions: "Subscriptions" i_choose_that_plan: "I choose that plan" i_subscribe_online: "I subscribe online" i_already_subscribed: "I already subscribed" diff --git a/config/locales/app.public.es.yml b/config/locales/app.public.es.yml index b1150d2ea..5524a1134 100644 --- a/config/locales/app.public.es.yml +++ b/config/locales/app.public.es.yml @@ -229,7 +229,7 @@ es: the_training_cant_be_deleted_because_it_is_already_reserved_by_some_users: "El curso no puede borrarse porque ya ha sido reservado por algún usuario." plans: #summary of the subscriptions - subcriptions: "Suscripciones" + subscriptions: "Suscripciones" i_choose_that_plan: "Elijo este plan" i_subscribe_online: "Suscribirme online" i_already_subscribed: "Ya me he suscrito" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 6a969ff36..679caf697 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -253,7 +253,7 @@ fr: plans: # page récapitulative des abonnements - subcriptions: "Les abonnements" + subscriptions: "Les abonnements" i_choose_that_plan: "Je choisis cette formule" i_subscribe_online: "Je m'abonne en ligne" i_already_subscribed: "Je suis déjà abonné" diff --git a/config/locales/app.public.pt.yml b/config/locales/app.public.pt.yml index 671d8ff3d..8c935f5ca 100755 --- a/config/locales/app.public.pt.yml +++ b/config/locales/app.public.pt.yml @@ -253,7 +253,7 @@ pt: plans: # summary of the subscriptions - subcriptions: "Assinaturas" + subscriptions: "Assinaturas" i_choose_that_plan: "Eu escolho esse plano" i_subscribe_online: "Me inscrever online" i_already_subscribed: "Eu já estou inscrito" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 38e729b3b..e4524982e 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -440,4 +440,4 @@ en: none: "None" online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly." slot_restrict_plans: "This slot is restricted for the plans below:" - slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first." + slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first." diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index 76428ab9d..2da7aeab8 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -417,4 +417,4 @@ es: none: "Ninguno" online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab." slot_restrict_plans: "This slot is restricted for the plans below:" - slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first." + slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first." diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 77fb1dc5f..bc0fc98cf 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -440,4 +440,4 @@ fr: none: "Aucune" online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab." slot_restrict_plans: "Ce créneau est restreint pour les formules d'abonnement ci-desous:" - slot_restrict_subcriptions_must_select_plan: "Le créneau est restreint pour les abonnements, Veuillez tout d'abord sélectionner un formule d'abonnement" + slot_restrict_subscriptions_must_select_plan: "Le créneau est restreint pour les abonnés. Veuillez tout d'abord sélectionner une formule d'abonnement" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index 197ba6c55..1456ec5f1 100755 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -440,4 +440,4 @@ pt: none: "Vazio" online_payment_disabled: "O pagamento online não está disponível. Entre em contato diretamente com a recepção do Fablab." slot_restrict_plans: "This slot is restricted for the plans below:" - slot_restrict_subcriptions_must_select_plan: "The slot is restricted for subscriptions, please select a plan first." + slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first." diff --git a/db/schema.rb b/db/schema.rb index ee59f3f76..5990b14f6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -15,8 +15,8 @@ ActiveRecord::Schema.define(version: 20200206132857) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - enable_extension "unaccent" enable_extension "pg_trgm" + enable_extension "unaccent" create_table "abuses", force: :cascade do |t| t.integer "signaled_id" From 760109de058f4b6e8f7f8e8350b7263b9c99e1f7 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 11 Feb 2020 13:21:25 +0100 Subject: [PATCH 5/7] fix bug: cant reserve formation slot --- app/assets/javascripts/controllers/spaces.js.erb | 2 ++ app/assets/javascripts/controllers/trainings.js.erb | 2 ++ app/assets/templates/spaces/reserve.html.erb | 2 ++ app/assets/templates/trainings/reserve.html.erb | 2 ++ app/views/api/availabilities/trainings.json.jbuilder | 2 +- 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/controllers/spaces.js.erb b/app/assets/javascripts/controllers/spaces.js.erb index a60d0fce0..edf49d36d 100644 --- a/app/assets/javascripts/controllers/spaces.js.erb +++ b/app/assets/javascripts/controllers/spaces.js.erb @@ -243,6 +243,8 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$stateP // list of plans, classified by group $scope.plansClassifiedByGroup = []; + $scope.groups = groupsPromise; + $scope.plans = plansPromise; for (let group of Array.from(groupsPromise)) { const groupObj = { id: group.id, name: group.name, plans: [] }; for (let plan of Array.from(plansPromise)) { diff --git a/app/assets/javascripts/controllers/trainings.js.erb b/app/assets/javascripts/controllers/trainings.js.erb index 22ed4e5dd..dfecc48af 100644 --- a/app/assets/javascripts/controllers/trainings.js.erb +++ b/app/assets/javascripts/controllers/trainings.js.erb @@ -111,6 +111,8 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$sta { member: {} }; // list of plans, classified by group + $scope.groups = groupsPromise; + $scope.plans = plansPromise; $scope.plansClassifiedByGroup = []; for (let group of Array.from(groupsPromise)) { const groupObj = { id: group.id, name: group.name, plans: [] }; diff --git a/app/assets/templates/spaces/reserve.html.erb b/app/assets/templates/spaces/reserve.html.erb index 145c74a5c..81a57c380 100644 --- a/app/assets/templates/spaces/reserve.html.erb +++ b/app/assets/templates/spaces/reserve.html.erb @@ -35,6 +35,8 @@ plan="selectedPlan" plan-selection-time="planSelectionTime" settings="settings" + plans="plans" + groups="groups" on-slot-added-to-cart="markSlotAsAdded" on-slot-removed-from-cart="markSlotAsRemoved" on-slot-start-to-modify="markSlotAsModifying" diff --git a/app/assets/templates/trainings/reserve.html.erb b/app/assets/templates/trainings/reserve.html.erb index 4d1994bba..c30037fa3 100644 --- a/app/assets/templates/trainings/reserve.html.erb +++ b/app/assets/templates/trainings/reserve.html.erb @@ -47,6 +47,8 @@ plan="selectedPlan" plan-selection-time="planSelectionTime" settings="settings" + plans="plans" + groups="groups" on-slot-added-to-cart="markSlotAsAdded" on-slot-removed-from-cart="markSlotAsRemoved" on-slot-start-to-modify="markSlotAsModifying" diff --git a/app/views/api/availabilities/trainings.json.jbuilder b/app/views/api/availabilities/trainings.json.jbuilder index 36782c8fb..42bcd2b35 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -33,5 +33,5 @@ json.array!(@availabilities) do |a| json.id t.id json.name t.name end - json.plan_ids slot.availability.plan_ids + json.plan_ids a.plan_ids end From 096a658bacc195f83448a2021c048bb8ba7062cb Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 11 Feb 2020 13:22:20 +0100 Subject: [PATCH 6/7] add a validator to check reservation's slot is or not restrict for subscriptions --- app/models/reservation.rb | 3 +++ .../reservation_slot_subscription_validator.rb | 15 +++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 app/validators/reservation_slot_subscription_validator.rb diff --git a/app/models/reservation.rb b/app/models/reservation.rb index 1536531be..d95c8060e 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Reservation < ActiveRecord::Base include NotifyWith::NotificationAttachedObject @@ -17,6 +19,7 @@ class Reservation < ActiveRecord::Base validates_presence_of :reservable_id, :reservable_type validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) } validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) } + validates_with ReservationSlotSubscriptionValidator attr_accessor :plan_id, :subscription diff --git a/app/validators/reservation_slot_subscription_validator.rb b/app/validators/reservation_slot_subscription_validator.rb new file mode 100644 index 000000000..8815bae12 --- /dev/null +++ b/app/validators/reservation_slot_subscription_validator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ReservationSlotSubscriptionValidator < ActiveModel::Validator + def validate(record) + record.slots.each do |s| + unless s.availability.plan_ids.empty? + if record.user.subscribed_plan && s.availability.plan_ids.include?(record.user.subscribed_plan.id) + elsif s.availability.plan_ids.include?(record.plan_id) + else + record.errors[:slots] << 'slot is restrict for subscriptions' + end + end + end + end +end From b559d10b8773df2082e4ff879bf92441c21cc9b3 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 12 Feb 2020 12:58:17 +0100 Subject: [PATCH 7/7] improve slot is only subscription --- .../controllers/admin/calendar.js.erb | 66 ++++++++++- app/assets/javascripts/directives/cart.js.erb | 108 ++++++++++++++---- app/assets/javascripts/router.js.erb | 4 +- .../admin/calendar/calendar.html.erb | 19 +++ app/assets/templates/shared/_cart.html.erb | 21 ++-- .../shared/_reserve_slot_without_plan.html | 22 ++++ .../api/availabilities/index.json.jbuilder | 3 + .../api/availabilities/show.json.jbuilder | 3 +- config/locales/app.admin.en.yml | 2 + config/locales/app.admin.es.yml | 2 + config/locales/app.admin.fr.yml | 2 + config/locales/app.admin.pt.yml | 2 + config/locales/app.shared.en.yml | 4 + config/locales/app.shared.es.yml | 4 + config/locales/app.shared.fr.yml | 4 + config/locales/app.shared.pt.yml | 4 + 16 files changed, 234 insertions(+), 36 deletions(-) create mode 100644 app/assets/templates/shared/_reserve_slot_without_plan.html diff --git a/app/assets/javascripts/controllers/admin/calendar.js.erb b/app/assets/javascripts/controllers/admin/calendar.js.erb index 7d37a33c9..c1ac7a1e9 100644 --- a/app/assets/javascripts/controllers/admin/calendar.js.erb +++ b/app/assets/javascripts/controllers/admin/calendar.js.erb @@ -18,8 +18,8 @@ * Controller used in the calendar management page */ -Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', - function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) { +Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', + function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, _t, uiCalendarConfig, CalendarConfig) { /* PRIVATE STATIC CONSTANTS */ // The calendar is divided in slots of 30 minutes @@ -164,6 +164,44 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state } }; + /** + * Open a confirmation modal to remove a plan for the currently selected availability, + * @param plan {Object} must contain the machine ID and name + */ + $scope.removePlan = function (plan) { + // open a confirmation dialog + return dialogs.confirm({ + resolve: { + object () { + return { + title: _t('app.admin.calendar.confirmation_required'), + msg: _t('app.admin.calendar.do_you_really_want_to_remove_PLAN_from_this_slot', { GENDER: getGender($scope.currentUser), PLAN: plan.name }) + ' ' + + _t('app.admin.calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + '
' + + _t('app.admin.calendar.beware_this_cannot_be_reverted') + '' + }; + } + } + } + , function () { + // the admin has confirmed, remove the plan + const plans = _.drop($scope.availability.plan_ids, plan.id); + + return Availability.update({ id: $scope.availability.id }, { availability: { plans_attributes: [{ id: plan.id, _destroy: true }] } } + , function (data, status) { // success + // update the plan_ids attribute + $scope.availability.plan_ids = data.plan_ids; + $scope.availability.plans = availabilityPlans(); + uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents'); + // notify the admin + return growl.success(_t('app.admin.calendar.the_plan_was_successfully_removed_from_the_slot')); + } + , function (data, status) { // failed + growl.error(_t('app.admin.calendar.deletion_failed')); + } + ); + }); + }; + /** * Callback to alert the admin that the export request was acknowledged and is * processing right now. @@ -265,6 +303,26 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state } else { return 'other'; } }; + /** + * Return a list of plans classified by group + * + * @returns {array} + */ + var availabilityPlans = function() { + const plansClassifiedByGroup = []; + const _plans = _.filter(plansPromise, function (p) { return _.include($scope.availability.plan_ids, p.id) }); + for (let group of Array.from(groupsPromise)) { + const groupObj = { id: group.id, name: group.name, plans: [] }; + for (let plan of Array.from(_plans)) { + if (plan.group_id === group.id) { groupObj.plans.push(plan); } + } + if (groupObj.plans.length > 0) { + plansClassifiedByGroup.push(groupObj); + } + } + return plansClassifiedByGroup; + }; + // Triggered when the admin drag on the agenda to create a new reservable slot. // @see http://fullcalendar.io/docs/selection/select_callback/ // @@ -316,7 +374,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state borderColor: availability.borderColor, tag_ids: availability.tag_ids, tags: availability.tags, - machine_ids: availability.machine_ids + machine_ids: availability.machine_ids, + plan_ids: availability.plan_ids }, true ); @@ -333,6 +392,7 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state */ var calendarEventClickCb = function (event, jsEvent, view) { $scope.availability = event; + $scope.availability.plans = availabilityPlans(); if ($scope.availabilityDom) { $scope.availabilityDom.classList.remove("fc-selected") diff --git a/app/assets/javascripts/directives/cart.js.erb b/app/assets/javascripts/directives/cart.js.erb index 6ea6805a8..ccfb10405 100644 --- a/app/assets/javascripts/directives/cart.js.erb +++ b/app/assets/javascripts/directives/cart.js.erb @@ -138,41 +138,53 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', if (Object.keys($scope.user).length > 0) { // check user was selected a plan if slot is restricted for subscriptions - const slotHasPlan = []; + const slotValidations = []; + let slotNotValid; + let slotNotValidError; $scope.events.reserved.forEach(function (slot) { if (slot.plan_ids.length > 0) { if ( ($scope.selectedPlan && _.include(slot.plan_ids, $scope.selectedPlan.id)) || ($scope.user.subscribed_plan && _.include(slot.plan_ids, $scope.user.subscribed_plan.id)) ) { - slotHasPlan.push(true); + slotValidations.push(true); } else { - slotHasPlan.push(false); + slotNotValid = slot; + if ($scope.selectedPlan && !_.include(slot.plan_ids, $scope.selectedPlan.id)) { + slotNotValidError = 'selectedPlanError'; + } + if ($scope.user.subscribed_plan && !_.include(slot.plan_ids, $scope.user.subscribed_plan.id)) { + slotNotValidError = 'userPlanError'; + } + if (!$scope.selectedPlan || !$scope.user.subscribed_plan) { + slotNotValidError = 'noPlanError'; + } + slotValidations.push(false); } } }); - const hasPlanForSlot = slotHasPlan.every(function (a) { return a }); + const hasPlanForSlot = slotValidations.every(function (a) { return a; }); if (!hasPlanForSlot) { - return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan')); - } - - - const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan); - - return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) { - const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); - if (!$scope.isAdmin() && (amountToPay > 0)) { - if ($rootScope.fablabWithoutOnlinePayment) { - growl.error(_t('app.shared.cart.online_payment_disabled')); - } else { - return payByStripe(reservation); - } + if (!$scope.isAdmin()) { + return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan')); } else { - if ($scope.isAdmin() || (amountToPay === 0)) { - return payOnSite(reservation); - } + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '<%= asset_path "shared/_reserve_slot_without_plan.html" %>', + size: 'md', + controller: 'ReserveSlotWithoutPlanController', + resolve: { + slot: function() { return slotNotValid; }, + slotNotValidError: function() { return slotNotValidError; }, + } + }); + modalInstance.result.then(function(res) { + return paySlots(); + }); } - }); + } else { + return paySlots(); + } } else { // otherwise we alert, this error musn't occur when the current user is not admin return growl.error(_t('app.shared.cart.please_select_a_member_first')); @@ -289,15 +301,21 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', if ($scope.slot.plan_ids.length > 0) { const _plans = _.filter($scope.plans, function (p) { return _.include($scope.slot.plan_ids, p.id) }); $scope.slot.plansGrouped = []; + $scope.slot.group_ids = []; for (let group of Array.from($scope.groups)) { const groupObj = { id: group.id, name: group.name, plans: [] }; for (let plan of Array.from(_plans)) { if (plan.group_id === group.id) { groupObj.plans.push(plan); } } if (groupObj.plans.length > 0) { - $scope.slot.plansGrouped.push(groupObj); + if ($scope.isAdmin()) { + $scope.slot.plansGrouped.push(groupObj); + } else if ($scope.user.group_id === groupObj.id) { + $scope.slot.plansGrouped.push(groupObj); + } } } + $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) { @@ -646,9 +664,53 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', return $scope.selectedPlan = null; }; + /** + * Actions to pay slots + */ + var paySlots = function() { + const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan); + + return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) { + const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount); + if (!$scope.isAdmin() && (amountToPay > 0)) { + if ($rootScope.fablabWithoutOnlinePayment) { + growl.error(_t('app.shared.cart.online_payment_disabled')); + } else { + return payByStripe(reservation); + } + } else { + if ($scope.isAdmin() || (amountToPay === 0)) { + return payOnSite(reservation); + } + } + }); + }; + // !!! MUST BE CALLED AT THE END of the directive return initialize(); } }); } ]); + +/** + * Controller used to alert admin reserve slot without plan + */ +Application.Controllers.controller('ReserveSlotWithoutPlanController', ['$scope', '$uibModalInstance', 'slot', 'slotNotValidError', 'growl', '_t', + function ($scope, $uibModalInstance, slot, slotNotValidError, growl, _t) { + $scope.slot = slot; + $scope.slotNotValidError = slotNotValidError; + /** + * Confirmation callback + */ + $scope.ok = function () { + $uibModalInstance.close({}); + } + /** + * Cancellation callback + */ + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + } + } +]); diff --git a/app/assets/javascripts/router.js.erb b/app/assets/javascripts/router.js.erb index ff921a190..1ab7df6f7 100644 --- a/app/assets/javascripts/router.js.erb +++ b/app/assets/javascripts/router.js.erb @@ -584,7 +584,9 @@ angular.module('application.router', ['ui.router']) resolve: { bookingWindowStart: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_start' }).$promise; }], bookingWindowEnd: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_end' }).$promise; }], - machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }] + machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }], + plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }], + groupsPromise: ['Group', function (Group) { return Group.query().$promise; }] } }) .state('app.admin.calendar.icalendar', { diff --git a/app/assets/templates/admin/calendar/calendar.html.erb b/app/assets/templates/admin/calendar/calendar.html.erb index fdc394926..342255566 100644 --- a/app/assets/templates/admin/calendar/calendar.html.erb +++ b/app/assets/templates/admin/calendar/calendar.html.erb @@ -84,6 +84,25 @@
+
+
+

{{ 'app.admin.calendar.plans' }}

+
+
+
    +
  • +
    {{::g.name}}
    +
      +
    • + {{::plan.name}} + +
    • +
    +
  • +
+
+
+

{{ 'app.admin.calendar.actions' }}

diff --git a/app/assets/templates/shared/_cart.html.erb b/app/assets/templates/shared/_cart.html.erb index 3c1628043..ae6dc7365 100644 --- a/app/assets/templates/shared/_cart.html.erb +++ b/app/assets/templates/shared/_cart.html.erb @@ -28,17 +28,22 @@ switch-animate="true" switch-readonly="{{slot.isValid}}"/>
-
-
{{ 'app.shared.cart.slot_restrict_plans' }}
-
-
{{::group.name}}
-
    -
  • {{::plan.name}}
  • -
+
+
+
{{ 'app.shared.cart.slot_restrict_plans' }}
+
+
{{::group.name}}
+
    +
  • {{::plan.name}}
  • +
+
+
+
+
{{ 'app.shared.cart.slot_restrict_plans_of_others_groups' }}
-
+
diff --git a/app/assets/templates/shared/_reserve_slot_without_plan.html b/app/assets/templates/shared/_reserve_slot_without_plan.html new file mode 100644 index 000000000..671c26fb7 --- /dev/null +++ b/app/assets/templates/shared/_reserve_slot_without_plan.html @@ -0,0 +1,22 @@ + + + diff --git a/app/views/api/availabilities/index.json.jbuilder b/app/views/api/availabilities/index.json.jbuilder index e6e86f563..e98ad286c 100644 --- a/app/views/api/availabilities/index.json.jbuilder +++ b/app/views/api/availabilities/index.json.jbuilder @@ -1,3 +1,5 @@ +# frozen_string_literal: true + json.array!(@availabilities) do |availability| json.id availability.id json.title availability.title @@ -15,4 +17,5 @@ json.array!(@availabilities) do |availability| json.name t.name end json.lock availability.lock + json.plan_ids availability.plan_ids end diff --git a/app/views/api/availabilities/show.json.jbuilder b/app/views/api/availabilities/show.json.jbuilder index df5b2d734..a3788e23b 100644 --- a/app/views/api/availabilities/show.json.jbuilder +++ b/app/views/api/availabilities/show.json.jbuilder @@ -5,10 +5,11 @@ json.start_at @availability.start_at.iso8601 json.end_at @availability.end_at.iso8601 json.available_type @availability.available_type json.machine_ids @availability.machine_ids +json.plan_ids @availability.plan_ids json.backgroundColor 'white' json.borderColor availability_border_color(@availability) json.tag_ids @availability.tag_ids json.tags @availability.tags do |t| json.id t.id json.name t.name -end \ No newline at end of file +end diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 3fda351c6..bb7f5c964 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -32,6 +32,8 @@ en: beware_this_cannot_be_reverted: "Beware: this cannot be reverted." the_machine_was_successfully_removed_from_the_slot: "The machine was successfully removed from the slot." deletion_failed: "Deletion failed." + do_you_really_want_to_remove_PLAN_from_this_slot: "Do you really want to remove \"{PLAN}\" from this slot?" + the_plan_was_successfully_removed_from_the_slot: "The plan was successfully removed from the slot." DATE_slot: "{DATE} slot:" what_kind_of_slot_do_you_want_to_create: "What kind of slot do you want to create?" training: "Training" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index 965d0138f..7f9f8923f 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -30,6 +30,8 @@ es: beware_this_cannot_be_reverted: "Beware: esto no puede ser revertido." the_machine_was_successfully_removed_from_the_slot: "La máquina se eliminó correctamente de la ranura." deletion_failed: "Fallo al borrar." + do_you_really_want_to_remove_PLAN_from_this_slot: "Do you really want to remove \"{PLAN}\" from this slot?" + the_plan_was_successfully_removed_from_the_slot: "The plan was successfully removed from the slot." DATE_slot: "{DATE} espacio:" what_kind_of_slot_do_you_want_to_create: "¿Qué tipo de horario desea crear?" training: "Formación" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index bdb5ba723..ba5fcc8c4 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -32,6 +32,8 @@ fr: beware_this_cannot_be_reverted: "Attention : ceci n'est pas réversible." the_machine_was_successfully_removed_from_the_slot: "La machine a bien été supprimée du créneau." deletion_failed: "La suppression a échouée." + do_you_really_want_to_remove_PLAN_from_this_slot: "Êtes-vous {GENDER, select, female{sûre} other{sûr}} de vouloir retirer \"{PLAN}\" de ce créneau ?" + the_plan_was_successfully_removed_from_the_slot: "Le formule d'abonnement a bien été supprimée du créneau." DATE_slot: "Créneau du {DATE} :" what_kind_of_slot_do_you_want_to_create: "Quel type de créneau voulez-vous créer ?" training: "Formation" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index d0473aa6c..5dca4207c 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -32,6 +32,8 @@ pt: beware_this_cannot_be_reverted: "Cuidado: isso não pode ser revertido." the_machine_was_successfully_removed_from_the_slot: "A máquina foi removida com sucesso desse slot." deletion_failed: "Falha ao deletar." + do_you_really_want_to_remove_PLAN_from_this_slot: "Do you really want to remove \"{PLAN}\" from this slot?" + the_plan_was_successfully_removed_from_the_slot: "The plan was successfully removed from the slot." DATE_slot: "{DATE} slot:" what_kind_of_slot_do_you_want_to_create: "Qual tipo de slot você deseja criar?" training: "Treinamento" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index e4524982e..318855aa5 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -441,3 +441,7 @@ en: online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly." slot_restrict_plans: "This slot is restricted for the plans below:" slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first." + slot_restrict_plans_of_others_groups: "The slot is restricted for the subscribers of others groups." + selected_plan_dont_match_slot: "Selected plan dont match this slot" + user_plan_dont_match_slot: "User subscribed plan dont match this slot" + no_plan_match_slot: "You dont have any plan to match this slot" diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index 2da7aeab8..60ad98aec 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -418,3 +418,7 @@ es: online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab." slot_restrict_plans: "This slot is restricted for the plans below:" slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first." + slot_restrict_plans_of_others_groups: "The slot is restricted for the subscribers of others groups." + selected_plan_dont_match_slot: "Selected plan dont match this slot" + user_plan_dont_match_slot: "User subscribed plan dont match this slot" + no_plan_match_slot: "You dont have any plan to match this slot" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index bc0fc98cf..136deb71e 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -441,3 +441,7 @@ fr: online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab." slot_restrict_plans: "Ce créneau est restreint pour les formules d'abonnement ci-desous:" slot_restrict_subscriptions_must_select_plan: "Le créneau est restreint pour les abonnés. Veuillez tout d'abord sélectionner une formule d'abonnement" + slot_restrict_plans_of_others_groups: "Ce créneau est restreint pour les abonnés d'autres groupes." + selected_plan_dont_match_slot: "L'abonnement sélectionné ne correspondent pas ce créneau" + user_plan_dont_match_slot: "L'abonnement du membre ne correspondent pas ce créneau" + no_plan_match_slot: "Aucun abonnement correspondent ce créneau" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index 1456ec5f1..a566ce067 100755 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -441,3 +441,7 @@ pt: online_payment_disabled: "O pagamento online não está disponível. Entre em contato diretamente com a recepção do Fablab." slot_restrict_plans: "This slot is restricted for the plans below:" slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first." + slot_restrict_plans_of_others_groups: "The slot is restricted for the subscribers of others groups." + selected_plan_dont_match_slot: "Selected plan dont match this slot" + user_plan_dont_match_slot: "User subscribed plan dont match this slot" + no_plan_match_slot: "You dont have any plan to match this slot"