diff --git a/app/controllers/api/availabilities_controller.rb b/app/controllers/api/availabilities_controller.rb index fbcd58746..65be3f7bd 100644 --- a/app/controllers/api/availabilities_controller.rb +++ b/app/controllers/api/availabilities_controller.rb @@ -4,15 +4,15 @@ class API::AvailabilitiesController < API::ApiController before_action :authenticate_user!, except: [:public] before_action :set_availability, only: %i[show update reservations lock] - before_action :define_max_visibility, only: %i[machine trainings spaces] + before_action :set_operator_role, only: %i[machine spaces] + before_action :set_customer, only: %i[machine spaces trainings] respond_to :json def index authorize Availability - start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) - end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day + display_window = window @availabilities = Availability.includes(:machines, :tags, :trainings, :spaces) - .where('start_at >= ? AND end_at <= ?', start_date, end_date) + .where('start_at >= ? AND end_at <= ?', display_window[:start], display_window[:end]) @availabilities = @availabilities.where.not(available_type: 'event') unless Setting.get('events_in_calendar') @@ -20,17 +20,17 @@ class API::AvailabilitiesController < API::ApiController end def public - start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start]) - end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day + # FIXME, use AvailabilitiesService + display_window = window @reservations = Reservation.includes(:slots, :statistic_profile) .references(:slots) - .where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date) + .where('slots.start_at >= ? AND slots.end_at <= ?', display_window[:start], display_window[:end]) machine_ids = params[:m] || [] service = Availabilities::PublicAvailabilitiesService.new(current_user) @availabilities = service.public_availabilities( - start_date, - end_date, + display_window[:start], + display_window[:end], @reservations, machines: machine_ids, spaces: params[:s] ) @@ -78,22 +78,25 @@ class API::AvailabilitiesController < API::ApiController end def machine - @current_user_role = current_user.role - - service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year) - @slots = service.machines(params[:machine_id], user) + service = Availabilities::AvailabilitiesService.new(current_user) + @machine = Machine.friendly.find(params[:machine_id]) + @slots = service.machines(@machine, @customer, window) end def trainings - service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year) - @availabilities = service.trainings(params[:training_id], user) + service = Availabilities::AvailabilitiesService.new(current_user) + @trainings = if training_id.is_number? || (training_id.length.positive? && training_id != 'all') + [Training.friendly.find(training_id)] + else + Training.all + end + @slots = service.trainings(@trainings, @customer, window) end def spaces - @current_user_role = current_user.role - - service = Availabilities::AvailabilitiesService.new(current_user, other: @visi_max_other, year: @visi_max_year) - @slots = service.spaces(params[:space_id], user) + service = Availabilities::AvailabilitiesService.new(current_user) + @space = Space.friendly.find(space_id) + @slots = service.spaces(@space, @customer, window) end def reservations @@ -133,12 +136,22 @@ class API::AvailabilitiesController < API::ApiController private - def user - if params[:member_id] - User.find(params[:member_id]) - else - current_user - end + def window + start_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:start]) + end_date = ActiveSupport::TimeZone[params[:timezone]]&.parse(params[:end])&.end_of_day + { start: start_date, end: end_date } + end + + def set_customer + @customer = if params[:member_id] + User.find(params[:member_id]) + else + current_user + end + end + + def set_operator_role + @current_user_role = current_user.role end def set_availability @@ -191,9 +204,4 @@ class API::AvailabilitiesController < API::ApiController def remove_full?(availability) params[:dispo] == 'false' && (availability.is_reserved || (availability.try(:full?) && availability.full?)) end - - def define_max_visibility - @visi_max_year = Setting.get('visibility_yearly').to_i.months.since - @visi_max_other = Setting.get('visibility_others').to_i.months.since - end end diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index 68ba37ae7..496199cd9 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -677,11 +677,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran */ const initialize = function () { $scope.eventSources.push({ - events: function (start, end, timezone, callback) { - Availability.machine({ machineId: $transition$.params().id }, function (availabilities) { - callback(availabilities); - }); - }, + url: `/api/availabilities/machines/${$transition$.params().id}`, textColor: 'black' }); diff --git a/app/frontend/src/javascript/controllers/spaces.js.erb b/app/frontend/src/javascript/controllers/spaces.js.erb index d3be7749c..7fa10da71 100644 --- a/app/frontend/src/javascript/controllers/spaces.js.erb +++ b/app/frontend/src/javascript/controllers/spaces.js.erb @@ -607,11 +607,7 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi // we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise // in the router because this allows to refetchEvents from fullCalendar API. $scope.eventSources.push({ - events: function (start, end, timezone, callback) { - Availability.spaces({ spaceId: $transition$.params().id }, function (availabilities) { - callback(availabilities); - }); - }, + url: `/api/availabilities/spaces/${$transition$.params().id}`, textColor: 'black' }); }; diff --git a/app/frontend/src/javascript/controllers/trainings.js.erb b/app/frontend/src/javascript/controllers/trainings.js.erb index ec381ac13..7750cfe93 100644 --- a/app/frontend/src/javascript/controllers/trainings.js.erb +++ b/app/frontend/src/javascript/controllers/trainings.js.erb @@ -397,11 +397,7 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra // we load the availabilities from a callback function of the $scope.eventSources, instead of resolving a promise // in the router because this allows to refetchEvents from fullCalendar API. $scope.eventSources.push({ - events: function (start, end, timezone, callback) { - Availability.trainings({ trainingId: $transition$.params().id }, function (availabilities) { - callback(availabilities); - }); - }, + url: `/api/availabilities/trainings/${$transition$.params().id}`, textColor: 'black' }); }; diff --git a/app/helpers/availability_helper.rb b/app/helpers/availability_helper.rb index e1b106704..59f07352b 100644 --- a/app/helpers/availability_helper.rb +++ b/app/helpers/availability_helper.rb @@ -25,7 +25,7 @@ module AvailabilityHelper def machines_slot_border_color(slot) if slot.is_reserved - slot.is_reserved_by_current_user ? IS_RESERVED_BY_CURRENT_USER : IS_COMPLETED + slot.current_user_slots_reservations_ids.empty? ? IS_COMPLETED : IS_RESERVED_BY_CURRENT_USER else MACHINE_COLOR end diff --git a/app/models/slot.rb b/app/models/slot.rb index 7e6f84a16..f30202d47 100644 --- a/app/models/slot.rb +++ b/app/models/slot.rb @@ -10,7 +10,7 @@ class Slot < ApplicationRecord has_many :reservations, through: :slots_reservations belongs_to :availability - attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :is_reserved_by_current_user + attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids def complete? reservations.length >= availability.nb_total_places diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb index 4342e2623..39ac38341 100644 --- a/app/services/availabilities/availabilities_service.rb +++ b/app/services/availabilities/availabilities_service.rb @@ -3,121 +3,68 @@ # Provides helper methods for Availability resources and properties class Availabilities::AvailabilitiesService - def initialize(current_user, maximum_visibility = {}) + def initialize(current_user) @current_user = current_user - @maximum_visibility = maximum_visibility - @service = Availabilities::StatusService.new(current_user.role) + @maximum_visibility = { + year: Setting.get('visibility_yearly').to_i.months.since, + other: Setting.get('visibility_others').to_i.months.since + } + @service = Availabilities::StatusService.new(current_user&.role) end - # list all slots for the given machine, with reservations info, relatives to the given user - def machines(machine_id, user) - machine = Machine.friendly.find(machine_id) - reservations = reservations(machine, user) - availabilities = availabilities(machine, 'machines', user) + # list all slots for the given machine, with visibility relative to the given user + def machines(machine, user, window) + availabilities = availabilities(machine.availabilities, 'machines', user, window[:start], window[:end]) - slots = [] - availabilities.each do |a| - a.slots.each do |slot| - slot.machine = machine - slot.title = '' - slot = @service.machine_reserved_status(slot, reservations, @current_user) - slots << slot - end - end - slots + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, machine) } end - # list all slots for the given space, with reservations info, relatives to the given user - def spaces(space_id, user) - space = Space.friendly.find(space_id) - reservations = reservations(space, user) - availabilities = availabilities(space, 'space', user) + # list all slots for the given space, with visibility relative to the given user + def spaces(space, user, window) + availabilities = availabilities(space.availabilities, 'space', user, window[:start], window[:end]) - slots = [] - availabilities.each do |a| - a.slots.each do |slot| - slot.space = space - slot.title = '' - slot = @service.space_reserved_status(slot, reservations, user) - slots << slot - end - end - slots.each do |s| - s.title = I18n.t('availabilities.not_available') if s.complete? && !s.is_reserved - end - slots + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, space) } end - # list all slots for the given training, with reservations info, relatives to the given user - def trainings(training_id, user) - # first, we get the already-made reservations - reservations = user.reservations.where("reservable_type = 'Training'") - reservations = reservations.where('reservable_id = :id', id: training_id.to_i) if training_id.is_number? - reservations = reservations.joins(slots_reservations: :slots).where('slots.start_at > ?', @current_user.admin? ? 1.month.ago : DateTime.current) + # list all slots for the given training, with visibility relative to the given user + def trainings(trainings, user, window) + tr_availabilities = Availability.includes('trainings_availabilities') + .where('trainings_availabilities.training_id': trainings.map(&:id)) + availabilities = availabilities(tr_availabilities, 'training', user, window[:start], window[:end]) - # visible availabilities depends on multiple parameters - availabilities = training_availabilities(training_id, user) - - # finally, we merge the availabilities with the reservations - availabilities.each do |a| - a = @service.training_event_reserved_status(a, reservations, user) - end + availabilities.map(&:slots).flatten.map { |s| @service.slot_reserved_status(s, user, trainings) } end private def subscription_year?(user) - user.subscription && user.subscription.plan.interval == 'year' && user.subscription.expired_at >= DateTime.current + user&.subscription && user.subscription.plan.interval == 'year' && user.subscription.expired_at >= DateTime.current end - # member must have validated at least 1 training and must have a valid yearly subscription. - def show_extended_slots?(user) + # members must have validated at least 1 training and must have a valid yearly subscription to view + # the trainings further in the futur. This is used to prevent users with a rolling subscription to take + # their first training in a very long delay. + def show_more_trainings?(user) user.trainings.size.positive? && subscription_year?(user) end - def reservations(reservable, user) - Reservation.where('reservable_type = ? and reservable_id = ?', reservable.class.name, reservable.id) - .includes(:slots, statistic_profile: [user: [:profile]]) - .references(:slots, :user) - .where('slots.start_at > ?', user.admin? ? 1.month.ago : DateTime.current) - end - - def availabilities(reservable, type, user) - if user.admin? || user.manager? - reservable.availabilities - .includes(:tags, :plans) - .where('end_at > ? AND available_type = ?', 1.month.ago, type) - .where(lock: false) - else - end_at = @maximum_visibility[:other] - end_at = @maximum_visibility[:year] if subscription_year?(user) - reservable.availabilities - .includes(:tags, :plans) - .where('end_at > ? AND end_at < ? AND available_type = ?', DateTime.current, end_at, type) - .where('availability_tags.tag_id' => user.tag_ids.concat([nil])) - .where(lock: false) - end - end - - def training_availabilities(training_id, user) - availabilities = if training_id.is_number? || (training_id.length.positive? && training_id != 'all') - Training.friendly.find(training_id).availabilities - else - Availability.trainings - end - + def availabilities(availabilities, type, user, range_start, range_end) # who made the request? - # 1) an admin (he can see all availabilities of 1 month ago and future) - if @current_user.admin? - availabilities.includes(:tags, :slots, :plans, trainings: [:machines]) - .where('availabilities.start_at > ?', 1.month.ago) + # 1) an admin (he can see all availabilities from 1 month ago to anytime in the future) + if @current_user&.admin? || @current_user&.manager? + window_start = [range_start, 1.month.ago].max + availabilities.includes(:tags, :plans) + .where('start_at <= ? AND end_at >= ? AND available_type = ?', range_end, window_start, type) .where(lock: false) - # 2) an user (he cannot see availabilities further than 1 (or 3) months) + # 2) an user (he cannot see past availabilities neither those further than 1 (or 3) months in the future) else end_at = @maximum_visibility[:other] - end_at = @maximum_visibility[:year] if show_extended_slots?(user) - availabilities.includes(:tags, :slots, :availability_tags, :plans, trainings: [:machines]) - .where('availabilities.start_at > ? AND availabilities.start_at < ?', DateTime.current, end_at) + end_at = @maximum_visibility[:year] if subscription_year?(user) && type != 'training' + end_at = @maximum_visibility[:year] if show_more_trainings?(user) && type == 'training' + window_end = [end_at, range_end].min + window_start = [range_start, DateTime.current].max + availabilities.includes(:tags, :plans) + .where('start_at < ? AND end_at > ? AND available_type = ?', window_end, window_start, type) .where('availability_tags.tag_id' => user.tag_ids.concat([nil])) .where(lock: false) end diff --git a/app/services/availabilities/public_availabilities_service.rb b/app/services/availabilities/public_availabilities_service.rb index efee253c7..ca3b77861 100644 --- a/app/services/availabilities/public_availabilities_service.rb +++ b/app/services/availabilities/public_availabilities_service.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true # Provides helper methods for public calendar of Availability +# FIXME, Availabilities::StatusService was refactored +# TODO, Use Availabilities::AvailabilitiesService class Availabilities::PublicAvailabilitiesService def initialize(current_user) @current_user = current_user diff --git a/app/services/availabilities/status_service.rb b/app/services/availabilities/status_service.rb index c48ac6d58..71376245d 100644 --- a/app/services/availabilities/status_service.rb +++ b/app/services/availabilities/status_service.rb @@ -7,75 +7,27 @@ class Availabilities::StatusService @show_name = (%w[admin manager].include?(@current_user_role) || Setting.get('display_name_enable')) end - # check that the provided machine slot is reserved or not and modify it accordingly - def machine_reserved_status(slot, reservations, user) + # check that the provided slot is reserved for the given reservable (machine, training or space). + # Mark it accordingly for display in the calendar + def slot_reserved_status(slot, user, reservables) statistic_profile_id = user&.statistic_profile&.id - reservations.each do |r| - r.slots.each do |s| - next unless slot.machine.id == r.reservable_id - next unless s.start_at == slot.start_at && s.canceled_at.nil? + slots_reservations = slot.slots_reservations + .includes(:reservation) + .where('reservations.reservable_type': reservables.map(&:class).map(&:name)) + .where('reservations.reservable_id': reservables.map(&:id)) + .where('slots_reservations.canceled_at': nil) - slot.id = s.id - slot.is_reserved = true - user_name = r.user ? r.user&.profile&.full_name : I18n.t('availabilities.deleted_user'); - slot.title = "#{slot.machine.name} - #{@show_name ? user_name : I18n.t('availabilities.deleted_user')}" - slot.can_modify = true if %w[admin manager].include?(@current_user_role) - slot.reservations.push r + user_slots_reservations = slots_reservations.where('reservations.statistic_profile_id': statistic_profile_id) - next unless r.statistic_profile_id == statistic_profile_id + slot.is_reserved = !slots_reservations.empty? + slot.title = slot_title(slots_reservations, user_slots_reservations, reservables) + slot.can_modify = true if %w[admin manager].include?(@current_user_role) || !user_slots_reservations.empty? + slot.current_user_slots_reservations_ids = user_slots_reservations.map(&:id) - slot.title = "#{slot.machine.name} - #{I18n.t('availabilities.i_ve_reserved')}" - slot.can_modify = true - slot.is_reserved_by_current_user = true - end - end slot end - # check that the provided space slot is reserved or not and modify it accordingly - def space_reserved_status(slot, reservations, user) - statistic_profile_id = user&.statistic_profile&.id - reservations.each do |r| - r.slots.each do |s| - next unless slot.space.id == r.reservable_id - - next unless s.start_at == slot.start_at && s.canceled_at.nil? - - slot.can_modify = true if %w[admin manager].include?(@current_user_role) - slot.reservations.push r - - next unless r.statistic_profile_id == statistic_profile_id - - slot.id = s.id - slot.title = I18n.t('availabilities.i_ve_reserved') - slot.can_modify = true - slot.is_reserved = true - end - end - slot - end - - # check that the provided availability (training or event) is reserved or not and modify it accordingly - def training_event_reserved_status(availability, reservations, user) - statistic_profile_id = user&.statistic_profile&.id - reservations.each do |r| - r.slots.each do |s| - next unless ( - (availability.available_type == 'training' && availability.trainings.first.id == r.reservable_id) || - (availability.available_type == 'event' && availability.event.id == r.reservable_id) - ) && s.start_at == availability.start_at && s.canceled_at.nil? - - availability.slot_id = s.id - if r.statistic_profile_id == statistic_profile_id - availability.is_reserved = true - availability.can_modify = true - end - end - end - availability - end - # check that the provided ability is reserved by the given user def reserved_availability?(availability, user) if user @@ -88,4 +40,21 @@ class Availabilities::StatusService false end end + + private + + def slot_title(slots_reservations, user_slots_reservations, reservables) + name = reservables.map(&:name).join(', ') + if user_slots_reservations.empty? && slots_reservations.empty? + name + elsif user_slots_reservations.empty? && !slots_reservations.empty? + user_names = slots_reservations.map(&:reservation) + .map(&:user) + .map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') } + .join(', ') + "#{name} - #{@show_name ? user_names : I18n.t('availabilities.not_available')}" + else + "#{name} - #{I18n.t('availabilities.i_ve_reserved')}" + end + end end diff --git a/app/views/api/availabilities/_slot.json.jbuilder b/app/views/api/availabilities/_slot.json.jbuilder new file mode 100644 index 000000000..da2ff0ce0 --- /dev/null +++ b/app/views/api/availabilities/_slot.json.jbuilder @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +json.slot_id slot.id +json.can_modify slot.can_modify +json.title slot.title +json.start slot.start_at.iso8601 +json.end slot.end_at.iso8601 +json.is_reserved slot.is_reserved +json.backgroundColor 'white' + +json.availability_id slot.availability_id +json.slots_reservations_ids slot.current_user_slots_reservations_ids + +json.tag_ids slot.availability.tag_ids +json.tags slot.availability.tags do |t| + json.id t.id + json.name t.name +end +json.plan_ids slot.availability.plan_ids diff --git a/app/views/api/availabilities/machine.json.jbuilder b/app/views/api/availabilities/machine.json.jbuilder index ab4879c08..d42a92fc4 100644 --- a/app/views/api/availabilities/machine.json.jbuilder +++ b/app/views/api/availabilities/machine.json.jbuilder @@ -1,19 +1,12 @@ # frozen_string_literal: true json.array!(@slots) do |slot| - json.slot_id slot.id if slot.id - json.can_modify slot.can_modify - json.title slot.title - json.start slot.start_at.iso8601 - json.end slot.end_at.iso8601 - json.is_reserved slot.is_reserved - json.backgroundColor 'white' + json.partial! 'api/availabilities/slot', slot: slot json.borderColor machines_slot_border_color(slot) - json.availability_id slot.availability_id json.machine do - json.id slot.machine.id - json.name slot.machine.name + json.id @machine.id + json.name @machine.name end # the user who booked the slot, if the slot was reserved if (%w[admin manager].include? @current_user_role) && slot.reservation @@ -22,10 +15,4 @@ json.array!(@slots) do |slot| json.name slot.reservation.user&.profile&.full_name end end - json.tag_ids slot.availability.tag_ids - json.tags slot.availability.tags do |t| - json.id t.id - json.name t.name - end - json.plan_ids slot.availability.plan_ids end diff --git a/app/views/api/availabilities/trainings.json.jbuilder b/app/views/api/availabilities/trainings.json.jbuilder index 9030dd6e4..9c9974dc6 100644 --- a/app/views/api/availabilities/trainings.json.jbuilder +++ b/app/views/api/availabilities/trainings.json.jbuilder @@ -1,37 +1,19 @@ # frozen_string_literal: true -json.array!(@availabilities) do |a| - json.slot_id a.slot_id if a.slot_id - if a.is_reserved - json.is_reserved true - json.title "#{a.trainings[0].name}' - #{t('trainings.i_ve_reserved')}" - elsif a.full? - json.is_completed true - json.title "#{a.trainings[0].name} - #{t('trainings.completed')}" - else - json.title a.trainings[0].name - end - json.borderColor trainings_events_border_color(a) - json.start a.start_at.iso8601 - json.end a.end_at.iso8601 - json.backgroundColor 'white' - json.can_modify a.can_modify - json.nb_total_places a.nb_total_places - json.availability_id a.id +json.array!(@slots) do |slot| + json.partial! 'api/availabilities/slot', slot: slot + json.borderColor trainings_events_border_color(slot) + + json.is_completed slot.full? + json.nb_total_places slot.nb_total_places json.training do - json.id a.trainings.first.id - json.name a.trainings.first.name - json.description a.trainings.first.description - json.machines a.trainings.first.machines do |m| + json.id slot.availability.trainings.first.id + json.name slot.availability.trainings.first.name + json.description slot.availability.trainings.first.description + json.machines slot.availability.trainings.first.machines do |m| json.id m.id json.name m.name end end - json.tag_ids a.tag_ids - json.tags a.tags do |t| - json.id t.id - json.name t.name - end - json.plan_ids a.plan_ids end