1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(wip) refactoring Availabilities::AvailabilitiesService and Availabilities::StatusService

This commit is contained in:
Sylvain 2022-07-11 17:59:56 +02:00
parent 783e86d9cc
commit 5012912edd
12 changed files with 145 additions and 243 deletions

View File

@ -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

View File

@ -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'
});

View File

@ -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'
});
};

View File

@ -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'
});
};

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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