mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(wip) refactoring slots to be unique per availability time-slot
This commit is contained in:
parent
05a297ab3f
commit
8be2425275
@ -79,7 +79,10 @@ class Event < ApplicationRecord
|
||||
if nb_total_places.nil?
|
||||
self.nb_free_places = nil
|
||||
else
|
||||
reserved_places = reservations.joins(:slots).where('slots.canceled_at': nil).map(&:total_booked_seats).inject(0) { |sum, t| sum + t }
|
||||
reserved_places = reservations.joins(:slots_reservations)
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
.map(&:total_booked_seats)
|
||||
.inject(0) { |sum, t| sum + t }
|
||||
self.nb_free_places = (nb_total_places - reserved_places)
|
||||
end
|
||||
end
|
||||
|
@ -35,12 +35,17 @@ class Reservation < ApplicationRecord
|
||||
# @param canceled if true, count the number of seats for this reservation, including canceled seats
|
||||
def total_booked_seats(canceled: false)
|
||||
# cases:
|
||||
# - machine/training/space reservation => 1 slot = 1 seat (currently not covered by this function)
|
||||
# - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices)
|
||||
return 0 if slots.first.canceled_at && !canceled
|
||||
if reservable_type == 'Event'
|
||||
# - event reservation => seats = nb_reserve_place (normal price) + tickets.booked (other prices)
|
||||
total = nb_reserve_places
|
||||
total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?
|
||||
|
||||
total = nb_reserve_places
|
||||
total += tickets.map(&:booked).map(&:to_i).reduce(:+) if tickets.count.positive?
|
||||
total = 0 unless slots_reservations.first&.canceled_at.nil?
|
||||
else
|
||||
# - machine/training/space reservation => 1 slot_reservation = 1 seat
|
||||
total = slots_reservations.count
|
||||
total -= slots_reservations.where.not(canceled_at: nil).count unless canceled
|
||||
end
|
||||
|
||||
total
|
||||
end
|
||||
|
@ -1,12 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A Time range.
|
||||
# Slots have two functions:
|
||||
# - slicing an Availability
|
||||
# > Slots duration are defined globally by Setting.get('slot_duration') but can be overridden per availability.
|
||||
# > These slots are not persisted in database and instantiated on the fly, if needed.
|
||||
# - hold detailed data about a Reservation.
|
||||
# > Persisted slots (in DB) represents booked slots and stores data about a time range that have been reserved.
|
||||
# A Time range, slicing an Availability.
|
||||
# Slots duration are defined globally by Setting.get('slot_duration') but can be
|
||||
# overridden per availability.
|
||||
class Slot < ApplicationRecord
|
||||
include NotifyWith::NotificationAttachedObject
|
||||
|
||||
@ -16,62 +12,7 @@ class Slot < ApplicationRecord
|
||||
|
||||
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :is_reserved_by_current_user
|
||||
|
||||
after_update :set_ex_start_end_dates_attrs, if: :dates_were_modified?
|
||||
after_update :notify_member_and_admin_slot_is_modified, if: :dates_were_modified?
|
||||
|
||||
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
|
||||
after_update :update_event_nb_free_places, if: :canceled?
|
||||
|
||||
# for backward compatibility
|
||||
def reservation
|
||||
reservations.first
|
||||
end
|
||||
|
||||
def destroy
|
||||
update_column(:destroying, true)
|
||||
super
|
||||
end
|
||||
|
||||
def complete?
|
||||
reservations.length >= availability.nb_total_places
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_member_and_admin_slot_is_modified
|
||||
NotificationCenter.call type: 'notify_member_slot_is_modified',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_modified',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def notify_member_and_admin_slot_is_canceled
|
||||
NotificationCenter.call type: 'notify_member_slot_is_canceled',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_canceled',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def dates_were_modified?
|
||||
saved_change_to_start_at? || saved_change_to_end_at?
|
||||
end
|
||||
|
||||
def canceled?
|
||||
saved_change_to_canceled_at?
|
||||
end
|
||||
|
||||
def set_ex_start_end_dates_attrs
|
||||
update_columns(ex_start_at: start_at_before_last_save, ex_end_at: end_at_before_last_save)
|
||||
end
|
||||
|
||||
def update_event_nb_free_places
|
||||
return unless reservation.reservable_type == 'Event'
|
||||
raise NotImplementedError if reservations.count > 1
|
||||
|
||||
reservation.update_event_nb_free_places
|
||||
end
|
||||
end
|
||||
|
@ -1,16 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# SlotsReservation is the relation table between a Slot and a Reservation.
|
||||
# It holds detailed data about a Reservation for the attached Slot.
|
||||
class SlotsReservation < ApplicationRecord
|
||||
belongs_to :slot
|
||||
belongs_to :reservation
|
||||
after_destroy :cleanup_slots
|
||||
|
||||
# when the SlotsReservation is deleted (from Reservation destroy cascade), we delete the
|
||||
# corresponding slot
|
||||
def cleanup_slots
|
||||
return unless slot.destroying
|
||||
after_update :set_ex_start_end_dates_attrs, if: :slot_changed?
|
||||
after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed?
|
||||
|
||||
slot.destroy
|
||||
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
|
||||
after_update :update_event_nb_free_places, if: :canceled?
|
||||
|
||||
def set_ex_start_end_dates_attrs
|
||||
update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def slot_changed?
|
||||
saved_change_to_slot_id?
|
||||
end
|
||||
|
||||
def previous_slot
|
||||
Slot.find(slot_id_before_last_save)
|
||||
end
|
||||
|
||||
def canceled?
|
||||
saved_change_to_canceled_at?
|
||||
end
|
||||
|
||||
def update_event_nb_free_places
|
||||
return unless reservation.reservable_type == 'Event'
|
||||
|
||||
reservation.update_event_nb_free_places
|
||||
end
|
||||
|
||||
def notify_member_and_admin_slot_is_modified
|
||||
NotificationCenter.call type: 'notify_member_slot_is_modified',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_modified',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
|
||||
def notify_member_and_admin_slot_is_canceled
|
||||
NotificationCenter.call type: 'notify_member_slot_is_canceled',
|
||||
receiver: reservation.user,
|
||||
attached_object: self
|
||||
NotificationCenter.call type: 'notify_admin_slot_is_canceled',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
end
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
<p><%= t('.body.member_cancelled', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
|
||||
<p><%= t('.body.item_details',
|
||||
START: I18n.l(@attached_object.start_at, format: :long),
|
||||
END: I18n.l(@attached_object.end_at, format: :hour_minute),
|
||||
START: I18n.l(@attached_object.slot.start_at, format: :long),
|
||||
END: I18n.l(@attached_object.slot.end_at, format: :hour_minute),
|
||||
RESERVABLE: @attached_object.reservation.reservable.name) %>
|
||||
</p>
|
||||
<p><%= t('.body.generate_refund') %></p>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p><%= t('.body.slot_modified', NAME: @attached_object.reservation.user&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
|
||||
<p><%= t('.body.new_date') %> <%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %></p>
|
||||
<p><%= t('.body.new_date') %> <%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %></p>
|
||||
<p><small><%= t('.body.old_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %></small></p>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p><%= t('.body.reservation_canceled', RESERVABLE: @attached_object.reservation.reservable.name ) %></p>
|
||||
<p><%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %></p>
|
||||
<p><%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %></p>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p><%= t('.body.reservation_changed_to') %></p>
|
||||
<p><%= "#{I18n.l(@attached_object.start_at, format: :long)} - #{I18n.l(@attached_object.end_at, format: :hour_minute)}" %></p>
|
||||
<p><%= "#{I18n.l(@attached_object.slot.start_at, format: :long)} - #{I18n.l(@attached_object.slot.end_at, format: :hour_minute)}" %></p>
|
||||
<p><small><%= t('.body.previous_date') %> <%= "#{I18n.l(@attached_object.ex_start_at, format: :long)} - #{I18n.l(@attached_object.ex_end_at, format: :hour_minute)}" %></small></p>
|
||||
|
@ -0,0 +1,104 @@
|
||||
# Previously, the Slot table was holding data about reservations.
|
||||
# This was a wrong assumption that leads to a bug.
|
||||
# An Availability should have many slots but a slot can be related to multiple Reservations,
|
||||
# so a slot must not hold data about a single reservation (like `offered`),these data
|
||||
# should be stored in SlotsReservation instead.
|
||||
class AddReservationFieldsToSlotsReservations < ActiveRecord::Migration[5.2]
|
||||
def up
|
||||
add_column :slots_reservations, :ex_start_at, :datetime
|
||||
add_column :slots_reservations, :ex_end_at, :datetime
|
||||
add_column :slots_reservations, :canceled_at, :datetime
|
||||
add_column :slots_reservations, :offered, :boolean
|
||||
|
||||
execute %(
|
||||
UPDATE slots_reservations
|
||||
SET
|
||||
ex_start_at=slots.ex_start_at,
|
||||
ex_end_at=slots.ex_end_at,
|
||||
canceled_at=slots.canceled_at,
|
||||
offered=slots.offered
|
||||
FROM slots
|
||||
WHERE slots_reservations.slot_id = slots.id
|
||||
)
|
||||
|
||||
remove_column :slots, :ex_start_at
|
||||
remove_column :slots, :ex_end_at
|
||||
remove_column :slots, :canceled_at
|
||||
remove_column :slots, :offered
|
||||
remove_column :slots, :destroying
|
||||
|
||||
# we gonna keep only only one slot (remove duplicates) because data is now hold in slots_reservations
|
||||
|
||||
# update slots_reservation.slot_id
|
||||
execute %(
|
||||
UPDATE slots_reservations
|
||||
SET slot_id=r.kept
|
||||
FROM (
|
||||
SELECT count(*), start_at, end_at, availability_id, min(id) AS kept, array_agg(id) AS all_ids
|
||||
FROM slots
|
||||
GROUP BY start_at, end_at, availability_id
|
||||
HAVING count(*) > 1) as r
|
||||
WHERE slot_id = ANY(r.all_ids);
|
||||
)
|
||||
|
||||
# remove useless slots
|
||||
execute %q(
|
||||
WITH same_slots AS (
|
||||
SELECT count(*), start_at, end_at, availability_id, min(id) AS kept, array_agg(id) AS all_ids
|
||||
FROM slots
|
||||
GROUP BY start_at, end_at, availability_id
|
||||
HAVING count(*) > 1
|
||||
)
|
||||
DELETE FROM slots
|
||||
WHERE id IN (SELECT unnest(all_ids) FROM same_slots)
|
||||
AND id NOT IN (SELECT kept FROM same_slots);
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
## FIXME, more than one row returned by a subquery used as an expression
|
||||
## in UPDATE slots_reservations, slot_slots return all inserted ids
|
||||
#
|
||||
# WITH same_slots AS (
|
||||
# SELECT count(*), array_agg(id) AS all_ids, slot_id
|
||||
# FROM slots_reservations
|
||||
# GROUP BY slot_id
|
||||
# HAVING count(*) > 1
|
||||
# ), slot AS (
|
||||
# SELECT *
|
||||
# FROM slots
|
||||
# WHERE id IN (SELECT slot_id FROM same_slots)
|
||||
# ), insert_slot AS (
|
||||
# INSERT INTO slots (start_at, end_at, created_at, updated_at, availability_id)
|
||||
# SELECT start_at, end_at, now(), now(), availability_id
|
||||
# FROM slot
|
||||
# RETURNING id
|
||||
# )
|
||||
# UPDATE slots_reservations
|
||||
# SET slot_id=(SELECT id FROM insert_slot)
|
||||
# WHERE id IN (SELECT unnest(all_ids) FROM same_slots);
|
||||
#
|
||||
|
||||
add_column :slots, :ex_start_at, :datetime
|
||||
add_column :slots, :ex_end_at, :datetime
|
||||
add_column :slots, :canceled_at, :datetime
|
||||
add_column :slots, :offered, :boolean
|
||||
add_column :slots, :destroying, :boolean, default: false
|
||||
|
||||
execute %(
|
||||
UPDATE slots
|
||||
SET
|
||||
ex_start_at=slots_reservations.ex_start_at,
|
||||
ex_end_at=slots_reservations.ex_end_at,
|
||||
canceled_at=slots_reservations.canceled_at,
|
||||
offered=slots_reservations.offered
|
||||
FROM slots_reservations
|
||||
WHERE slots_reservations.slot_id = slots.id
|
||||
)
|
||||
|
||||
remove_column :slots_reservations, :ex_start_at
|
||||
remove_column :slots_reservations, :ex_end_at
|
||||
remove_column :slots_reservations, :canceled_at
|
||||
remove_column :slots_reservations, :offered
|
||||
end
|
||||
end
|
11
db/schema.rb
11
db/schema.rb
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2022_05_31_160223) do
|
||||
ActiveRecord::Schema.define(version: 2022_07_04_084929) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "fuzzystrmatch"
|
||||
@ -760,17 +760,16 @@ ActiveRecord::Schema.define(version: 2022_05_31_160223) do
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "availability_id"
|
||||
t.datetime "ex_start_at"
|
||||
t.datetime "ex_end_at"
|
||||
t.datetime "canceled_at"
|
||||
t.boolean "offered", default: false
|
||||
t.boolean "destroying", default: false
|
||||
t.index ["availability_id"], name: "index_slots_on_availability_id"
|
||||
end
|
||||
|
||||
create_table "slots_reservations", id: :serial, force: :cascade do |t|
|
||||
t.integer "slot_id"
|
||||
t.integer "reservation_id"
|
||||
t.datetime "ex_start_at"
|
||||
t.datetime "ex_end_at"
|
||||
t.datetime "canceled_at"
|
||||
t.boolean "offered"
|
||||
t.index ["reservation_id"], name: "index_slots_reservations_on_reservation_id"
|
||||
t.index ["slot_id"], name: "index_slots_reservations_on_slot_id"
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user