diff --git a/CHANGELOG.md b/CHANGELOG.md index f0f667cc4..6b1b27fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog Fab-manager - Report user's prepaid packs in the dashboard - Ability to buy a new prepaid pack from the user's dashboard +- Improved public calendar loading time +- [TODO DEPLOY] `rails fablab:fix_availabilities` THEN `rails fablab:setup:build_places_cache` - Use Time instead of DateTime objects - Fix a bug: missing statististics subtypes @@ -57,7 +59,6 @@ - Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-44571) - Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799) - [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate` -- [TODO DEPLOY] `rails fablab:fix_availabilities` THEN `rails fablab:setup:build_places_cache` ## v5.6.5 2023 January 9 diff --git a/app/frontend/src/javascript/controllers/calendar.js b/app/frontend/src/javascript/controllers/calendar.js index 8917cbdea..a4f4ac162 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', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise', 'machineCategoriesPromise', - function ($scope, $state, $aside, moment, Availability, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise, machineCategoriesPromise) { +Application.Controllers.controller('CalendarController', ['$scope', '$state', '$aside', '$uibModal', 'moment', 'Availability', 'Setting', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', '_t', 'uiCalendarConfig', 'CalendarConfig', 'trainingsPromise', 'machinesPromise', 'spacesPromise', 'iCalendarPromise', 'machineCategoriesPromise', + function ($scope, $state, $aside, $uibModal, moment, Availability, Setting, growl, dialogs, bookingWindowStart, bookingWindowEnd, _t, uiCalendarConfig, CalendarConfig, trainingsPromise, machinesPromise, spacesPromise, iCalendarPromise, machineCategoriesPromise) { /* PRIVATE STATIC CONSTANTS */ let currentMachineEvent = null; machinesPromise.forEach(m => m.checked = true); @@ -263,8 +263,33 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$ $state.go('app.public.training_show', { id: event.training_id }); } else { 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] }); + if (event.machine_ids.length === 1) { + $state.go('app.public.machines_show', { id: event.machine_ids[0] }); + } else { + // open a modal to ask the user to select the machine to show + const modalInstance = $uibModal.open({ + animation: true, + templateUrl: '/calendar/chooseMachine.html', + size: 'md', + controller: ['$scope', 'machinesPromise', '$uibModalInstance', function ($scope, machinesPromise, $uibModalInstance) { + $scope.machines = machinesPromise.filter(m => event.machine_ids.includes(m.id)); + $scope.selectMachine = function (machineId) { + $uibModalInstance.close(machineId); + }; + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + }], + resolve: { + machinesPromise: ['Machine', function (Machine) { + return Machine.query().$promise; + }] + } + }); + modalInstance.result.then(function (res) { + $state.go('app.public.machines_show', { id: res }); + }); + } } else if (event.space_id) { $state.go('app.public.space_show', { id: event.space_id }); } diff --git a/app/frontend/src/stylesheets/app.utilities.scss b/app/frontend/src/stylesheets/app.utilities.scss index d2b4c58cc..999d0ee4d 100644 --- a/app/frontend/src/stylesheets/app.utilities.scss +++ b/app/frontend/src/stylesheets/app.utilities.scss @@ -791,6 +791,10 @@ p, .widget p { margin-right: 40px; } +.m-auto { + margin: auto; +} + .media-xs { min-width: 50px; } diff --git a/app/frontend/templates/calendar/chooseMachine.html b/app/frontend/templates/calendar/chooseMachine.html new file mode 100644 index 000000000..df99985c0 --- /dev/null +++ b/app/frontend/templates/calendar/chooseMachine.html @@ -0,0 +1,11 @@ + + + diff --git a/app/helpers/availability_helper.rb b/app/helpers/availability_helper.rb index ec49741d0..c5b193101 100644 --- a/app/helpers/availability_helper.rb +++ b/app/helpers/availability_helper.rb @@ -22,16 +22,16 @@ module AvailabilityHelper end end - def machines_slot_border_color(slot) - if slot.is_reserved - slot.current_user_slots_reservations_ids.empty? ? IS_FULL : IS_RESERVED_BY_CURRENT_USER + def machines_slot_border_color(slot, customer = nil) + if slot.reserved? + slot.reserved_users.include?(customer&.id) ? IS_RESERVED_BY_CURRENT_USER : IS_FULL else MACHINE_COLOR end end def space_slot_border_color(slot) - if slot.is_reserved + if slot.reserved? IS_RESERVED_BY_CURRENT_USER elsif slot.full? IS_FULL @@ -41,7 +41,7 @@ module AvailabilityHelper end def trainings_events_border_color(availability) - if availability.is_reserved + if availability.reserved? IS_RESERVED_BY_CURRENT_USER elsif availability.full? IS_FULL diff --git a/app/models/slot.rb b/app/models/slot.rb index 7299ed744..68e040c78 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -14,8 +14,6 @@ class Slot < ApplicationRecord after_create_commit :create_places_cache - attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids, :current_user_pending_reservations_ids - # @param reservable [Machine,Space,Training,Event,NilClass] # @return [Integer] the total number of reserved places def reserved_places(reservable = nil) @@ -29,7 +27,7 @@ class Slot < ApplicationRecord # @param reservables [Array,NilClass] # @return [Array] Collection of User's IDs def reserved_users(reservables = nil) - if reservable.nil? + if reservables.nil? places.pluck('user_ids').flatten else r_places = places.select do |p| diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index b819a8bfc..e173080b2 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -11,7 +11,6 @@ class Availabilities::AvailabilitiesService other: Setting.get('visibility_others').to_i.months.since } @minimum_visibility = Setting.get('reservation_deadline').to_i.minutes.since - @service = Availabilities::StatusService.new(current_user&.role) @level = level end @@ -39,16 +38,15 @@ class Availabilities::AvailabilitiesService # @param window [Hash] the time window the look through: {start: xxx, end: xxx} # @option window [ActiveSupport::TimeWithZone] :start the beginning of the time window # @option window [ActiveSupport::TimeWithZone] :end the end of the time window - # @param no_status [Boolean] should the availability/slot reservation status be computed? - def machines(machines, user, window, no_status: false) + def machines(machines, user, window) ma_availabilities = Availability.includes(:machines_availabilities) .where('machines_availabilities.machine_id': machines.map(&:id)) availabilities = availabilities(ma_availabilities, 'machines', user, window[:start], window[:end]) if @level == 'slot' - availabilities.map(&:slots).flatten.map { |s| no_status ? s : @service.slot_reserved_status(s, user, (machines & s.availability.machines)) } + availabilities.map(&:slots).flatten else - no_status ? availabilities : availabilities.map { |a| @service.availability_reserved_status(a, user, (machines & a.machines)) } + availabilities end end @@ -58,16 +56,15 @@ class Availabilities::AvailabilitiesService # @param window [Hash] the time window the look through: {start: xxx, end: xxx} # @option window [ActiveSupport::TimeWithZone] :start # @option window [ActiveSupport::TimeWithZone] :end - # @param no_status [Boolean] should the availability/slot reservation status be computed? - def spaces(spaces, user, window, no_status: false) + def spaces(spaces, user, window) sp_availabilities = Availability.includes('spaces_availabilities') .where('spaces_availabilities.space_id': spaces.map(&:id)) availabilities = availabilities(sp_availabilities, 'space', user, window[:start], window[:end]) if @level == 'slot' - availabilities.map(&:slots).flatten.map { |s| no_status ? s : @service.slot_reserved_status(s, user, (spaces & s.availability.spaces)) } + availabilities.map(&:slots).flatten else - no_status ? availabilities : availabilities.map { |a| @service.availability_reserved_status(a, user, (spaces & a.spaces)) } + availabilities end end @@ -77,15 +74,15 @@ class Availabilities::AvailabilitiesService # @param window [Hash] the time window the look through: {start: xxx, end: xxx} # @option window [ActiveSupport::TimeWithZone] :start # @option window [ActiveSupport::TimeWithZone] :end - def trainings(trainings, user, window, no_status: false) + 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]) if @level == 'slot' - availabilities.map(&:slots).flatten.map { |s| no_status ? s : @service.slot_reserved_status(s, user, (trainings & s.availability.trainings)) } + availabilities.map(&:slots).flatten else - no_status ? availabilities : availabilities.map { |a| @service.availability_reserved_status(a, user, (trainings & a.trainings)) } + availabilities end end @@ -95,14 +92,14 @@ class Availabilities::AvailabilitiesService # @param window [Hash] the time window the look through: {start: xxx, end: xxx} # @option window [ActiveSupport::TimeWithZone] :start # @option window [ActiveSupport::TimeWithZone] :end - def events(events, user, window, no_status: false) + 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| no_status ? s : @service.slot_reserved_status(s, user, [s.availability.event]) } + availabilities.map(&:slots).flatten else - no_status ? availabilities : availabilities.map { |a| @service.availability_reserved_status(a, user, [a.event]) } + availabilities end end diff --git a/app/services/availabilities/public_availabilities_service.rb b/app/services/availabilities/public_availabilities_service.rb index 5e5167826..bb0485ec3 100644 --- a/app/services/availabilities/public_availabilities_service.rb +++ b/app/services/availabilities/public_availabilities_service.rb @@ -4,7 +4,6 @@ class Availabilities::PublicAvailabilitiesService def initialize(current_user) @current_user = current_user - @service = Availabilities::StatusService.new('public') end def public_availabilities(window, ids, events: false) @@ -12,17 +11,17 @@ class Availabilities::PublicAvailabilitiesService service = Availabilities::AvailabilitiesService.new(@current_user, level) machines_slots = if Setting.get('machines_module') - service.machines(Machine.where(id: ids[:machines]), @current_user, window, no_status: true) + service.machines(Machine.where(id: ids[:machines]), @current_user, window) else [] end - spaces_slots = Setting.get('spaces_module') ? service.spaces(Space.where(id: ids[:spaces]), @current_user, window, no_status: true) : [] + spaces_slots = Setting.get('spaces_module') ? service.spaces(Space.where(id: ids[:spaces]), @current_user, window) : [] trainings_slots = if Setting.get('trainings_module') - service.trainings(Training.where(id: ids[:trainings]), @current_user, window, no_status: true) + service.trainings(Training.where(id: ids[:trainings]), @current_user, window) else [] end - events_slots = events ? service.events(Event.all, @current_user, window, no_status: true) : [] + events_slots = events ? service.events(Event.all, @current_user, window) : [] [].concat(trainings_slots).concat(events_slots).concat(machines_slots).concat(spaces_slots) end diff --git a/app/services/availabilities/status_service.rb b/app/services/availabilities/status_service.rb deleted file mode 100644 index e58594ccc..000000000 --- a/app/services/availabilities/status_service.rb +++ /dev/null @@ -1,99 +0,0 @@ -# frozen_string_literal: true -# TODO, remove this -# Provides helper methods checking reservation status of any availabilities -class Availabilities::StatusService - # @param current_user_role [String] - def initialize(current_user_role) - @current_user_role = current_user_role - @show_name = (%w[admin manager].include?(@current_user_role) || (current_user_role && Setting.get('display_name_enable'))) - end - - # check that the provided slot is reserved for the given reservable (machine, training or space). - # Mark it accordingly for display in the calendar - # @param slot [Slot] - # @param user [User] the customer - # @param reservables [Array] - # @return [Slot] - def slot_reserved_status(slot, user, reservables) - if reservables.map(&:class).map(&:name).uniq.size > 1 - raise TypeError('[Availabilities::StatusService#slot_reserved_status] reservables have differents types: ' \ - "#{reservables.map(&:class).map(&:name).uniq} , with slot #{slot.id}") - end - - places = places(slot, reservables) - is_reserved = places.any? { |p| p['reserved_places'].positive? } - is_reserved_by_user = is_reserved && user && places.select { |p| p['user_ids'].include?(user.id) }.length.positive? - slot.is_reserved = is_reserved - slot.title = slot_title(slot, is_reserved, is_reserved_by_user, reservables) - slot.can_modify = true if %w[admin manager].include?(@current_user_role) || is_reserved - if is_reserved_by_user - user_reservations = Slots::ReservationsService.user_reservations(slot, user, reservables.first.class.name) - - slot.current_user_slots_reservations_ids = user_reservations[:reservations].select('id').map(&:id) - slot.current_user_pending_reservations_ids = user_reservations[:pending].select('id').map(&:id) - end - slot - end - - # check that the provided ability is reserved by the given user - # @param availability [Availability] - # @param user [User] the customer - # @param reservables [Array] - # @return [Availability] - def availability_reserved_status(availability, user, reservables) - if reservables.map(&:class).map(&:name).uniq.size > 1 - raise TypeError('[Availabilities::StatusService#availability_reserved_status] reservables have differents types: ' \ - "#{reservables.map(&:class).map(&:name).uniq}, with availability #{availability.id}") - end - - slots = availability.slots.map do |slot| - slot_reserved_status(slot, user, reservables) - end - - availability.is_reserved = slots.any?(&:is_reserved) - availability.current_user_slots_reservations_ids = slots.map(&:current_user_slots_reservations_ids).flatten - availability.current_user_pending_reservations_ids = slots.map(&:current_user_pending_reservations_ids).flatten - availability - end - - private - - # @param slot [Slot] - # @param reservables [Array] - # @return [Array] - def places(slot, reservables) - places = [] - reservables.each do |reservable| - places.push(slot.places.detect { |p| p['reservable_type'] == reservable.class.name && p['reservable_id'] == reservable.id }) - end - places - end - - # @param slot [Slot] - # @param is_reserved [Boolean] - # @param is_reserved_by_user [Boolean] - # @param reservables [Array] - def slot_title(slot, is_reserved, is_reserved_by_user, reservables) - name = reservables.map(&:name).join(', ') - if !is_reserved && !is_reserved_by_user - name - elsif is_reserved && !is_reserved_by_user - "#{name} #{@show_name ? "- #{slot_users_names(slot, reservables)}" : ''}" - else - "#{name} - #{I18n.t('availabilities.i_ve_reserved')}" - end - end - - # @param slot [Slot] - # @param reservables [Array] - # @return [String] - def slot_users_names(slot, reservables) - user_ids = slot.places - .select { |p| p['reservable_type'] == reservables.first.class.name && reservables.map(&:id).includes?(p['reservable_id']) } - .pluck('user_ids') - .flatten - User.where(id: user_ids).includes(:profile) - .map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') } - .join(', ') - end -end diff --git a/app/services/slots/reservations_service.rb b/app/services/slots/reservations_service.rb index 97b2328a0..272c34cd7 100644 --- a/app/services/slots/reservations_service.rb +++ b/app/services/slots/reservations_service.rb @@ -12,7 +12,7 @@ class Slots::ReservationsService def reservations(slots_reservations, reservables) reservable_types = reservables.map(&:class).map(&:name).uniq if reservable_types.size > 1 - raise TypeError("[Availabilities::StatusService#slot_reservations] reservables have differents types: #{reservable_types}") + raise TypeError("[Availabilities::ReservationsService#reservations] reservables have differents types: #{reservable_types}") end reservations = slots_reservations.includes(:reservation) @@ -75,14 +75,26 @@ class Slots::ReservationsService # @param slot [Slot] # @param user [User] # @param reservable_type [String] 'Machine' | 'Space' | 'Training' | 'Event' + # @param reservable_id [Integer] # @return [Hash{Symbol=>ActiveRecord::Relation,ActiveRecord::Relation}] - def user_reservations(slot, user, reservable_type) + def user_reservations(slot, user, reservable_type, reservable_id) + return { reservations: [], pending: [] } if user.nil? + reservations = SlotsReservation.includes(:reservation) - .where(slot_id: slot.id, reservations: { statistic_profile_id: user.statistic_profile.id }) + .where(slot_id: slot.id, reservations: { + statistic_profile_id: user.statistic_profile.id, + reservable_type: reservable_type, + reservable_id: reservable_id + }) relation = "cart_item_#{reservable_type&.downcase}_reservation".to_sym table = (reservable_type == 'Event' ? 'cart_item_event_reservations' : 'cart_item_reservations').to_sym + id_key = (reservable_type == 'Event' ? 'event_id' : 'reservable_id').to_sym + type_key = (reservable_type == 'Event' ? {} : { reservable_type: reservable_type }) pending = CartItem::ReservationSlot.includes(relation) - .where(slot_id: slot.id, table => { customer_profile_id: user.invoicing_profile.id }) + .where(slot_id: slot.id, table => { + customer_profile_id: user.invoicing_profile.id, + id_key => reservable_id + }.merge!(type_key)) { reservations: reservations, diff --git a/app/services/slots/title_service.rb b/app/services/slots/title_service.rb index 9444aa670..1e5b19bbd 100644 --- a/app/services/slots/title_service.rb +++ b/app/services/slots/title_service.rb @@ -9,15 +9,16 @@ class Slots::TitleService # @param slot [Slot] # @param reservables [Array] - def slot_title(slot, reservables) + def call(slot, reservables = nil) + reservables = all_reservables(slot) if reservables.nil? is_reserved = slot.reserved? - is_reserved_by_user = slot.reserved_users(reservables).include?(@user.id) + is_reserved_by_user = slot.reserved_users(reservables).include?(@user&.id) name = reservables.map(&:name).join(', ') if !is_reserved && !is_reserved_by_user name elsif is_reserved && !is_reserved_by_user - "#{name} #{@show_name ? "- #{Slots::TitleService.slot_users_names(slot, reservables)}" : ''}" + "#{name} #{@show_name ? "- #{slot_users_names(slot, reservables)}" : ''}" else "#{name} - #{I18n.t('availabilities.i_ve_reserved')}" end @@ -34,4 +35,9 @@ class Slots::TitleService .map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') } .join(', ') end + + # @param slot [Slot] + def all_reservables(slot) + slot.places.pluck('reservable_type', 'reservable_id').map { |r| r[0].classify.constantize.find(r[1]) } + end end diff --git a/app/views/api/availabilities/_slot.json.jbuilder b/app/views/api/availabilities/_slot.json.jbuilder index 396932eda..63ce58b7a 100644 --- a/app/views/api/availabilities/_slot.json.jbuilder +++ b/app/views/api/availabilities/_slot.json.jbuilder @@ -1,8 +1,8 @@ # frozen_string_literal: true json.slot_id slot.id -json.can_modify slot.modifiable?(operator_role, @user.id, reservable) -json.title Slots::TitleService.new(operator_role, @user).slot_title(slot, [reservable]) +json.can_modify slot.modifiable?(operator_role, @user&.id, reservable) +json.title Slots::TitleService.new(operator_role, @user).call(slot, [reservable]) json.start slot.start_at.iso8601 json.end slot.end_at.iso8601 json.is_reserved slot.reserved?(reservable) @@ -10,7 +10,7 @@ json.is_completed slot.full?(reservable) json.backgroundColor 'white' json.availability_id slot.availability_id -json.slots_reservations_ids slot.current_user_slots_reservations_ids #TODO, move this out of attr_accessor +json.slots_reservations_ids Slots::ReservationsService.user_reservations(slot, @user, reservable&.class&.name, reservable&.id)[:reservations] json.tag_ids slot.availability.tag_ids json.tags slot.availability.tags do |t| diff --git a/app/views/api/availabilities/machine.json.jbuilder b/app/views/api/availabilities/machine.json.jbuilder index 6296ee385..9bc639cc2 100644 --- a/app/views/api/availabilities/machine.json.jbuilder +++ b/app/views/api/availabilities/machine.json.jbuilder @@ -2,7 +2,7 @@ json.array!(@slots) do |slot| json.partial! 'api/availabilities/slot', slot: slot, operator_role: @operator_role, reservable: @machine - json.borderColor machines_slot_border_color(slot) + json.borderColor machines_slot_border_color(slot, @customer) json.machine do json.id @machine.id diff --git a/app/views/api/availabilities/public.json.jbuilder b/app/views/api/availabilities/public.json.jbuilder index f272266c0..a2d0a3bbd 100644 --- a/app/views/api/availabilities/public.json.jbuilder +++ b/app/views/api/availabilities/public.json.jbuilder @@ -32,27 +32,28 @@ json.array!(@availabilities) do |availability| # slot object ( here => availability = slot ), for daily view elsif availability.instance_of? Slot - json.title availability.title - json.tag_ids availability.availability.tag_ids - json.tags availability.availability.tags do |t| + slot = availability + json.title Slots::TitleService.new(@user.role, @user).call(slot) + json.tag_ids slot.availability.tag_ids + json.tags slot.availability.tags do |t| json.id t.id json.name t.name end - json.is_reserved availability.reserved? - json.is_completed availability.full? - case availability.availability.available_type + json.is_reserved slot.reserved? + json.is_completed slot.full? + case slot.availability.available_type when 'machines' - json.machine_ids availability.availability.machines.map(&:id) - json.borderColor machines_slot_border_color(availability) + json.machine_ids slot.availability.machines.map(&:id) + json.borderColor machines_slot_border_color(slot) when 'space' - json.space_id availability.availability.spaces.first.id - json.borderColor space_slot_border_color(availability) + json.space_id slot.availability.spaces.first.id + json.borderColor space_slot_border_color(slot) when 'training' - json.training_id availability.availability.trainings.first.id - json.borderColor trainings_events_border_color(availability.availability) + json.training_id slot.availability.trainings.first.id + json.borderColor trainings_events_border_color(slot.availability) when 'event' - json.event_id availability.availability.event.id - json.borderColor trainings_events_border_color(availability.availability) + json.event_id slot.availability.event.id + json.borderColor trainings_events_border_color(slot.availability) else json.title 'Unknown slot' end diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 4e23e6abf..b1ba54b64 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -366,6 +366,8 @@ en: spaces: "Spaces" events: "Events" externals: "Other calendars" + choose_a_machine: "Choose a machine" + cancel: "Cancel" #list of spaces spaces_list: the_spaces: "The spaces"