diff --git a/app/helpers/availability_helper.rb b/app/helpers/availability_helper.rb
index eb817f1c6..4163c5a50 100644
--- a/app/helpers/availability_helper.rb
+++ b/app/helpers/availability_helper.rb
@@ -8,6 +8,7 @@ module AvailabilityHelper
EVENT_COLOR = '#dd7e6b'
IS_RESERVED_BY_CURRENT_USER = '#b2e774'
IS_FULL = '#eeeeee'
+ IS_BLOCKED = '#b2e774' # same color as IS_RESERVED_BY_CURRENT_USER for simplicity
def availability_border_color(availability)
case availability.available_type
@@ -38,6 +39,8 @@ module AvailabilityHelper
IS_RESERVED_BY_CURRENT_USER
elsif slot.full?
IS_FULL
+ elsif slot.is_blocked
+ IS_BLOCKED
else
SPACE_COLOR
end
diff --git a/app/models/cart_item/reservation.rb b/app/models/cart_item/reservation.rb
index 68e424a5e..507916cbe 100644
--- a/app/models/cart_item/reservation.rb
+++ b/app/models/cart_item/reservation.rb
@@ -283,6 +283,26 @@ class CartItem::Reservation < CartItem::BaseItem
return false
end
+ unless operator.privileged?
+ if reservable_type == "Space"
+ space = reservable
+ slot = reservation_slot.slot
+ if Slots::InterblockingService.new.blocked_slots_for_spaces([space], [slot]).any?
+ errors.add(:slot, I18n.t('cart_item_validation.blocked_by_another_reservation'))
+ return false
+ end
+ end
+
+ if reservable_type == "Machine"
+ machine = reservable
+ slot = reservation_slot.slot
+ if Slots::InterblockingService.new.blocked_slots_for_machines([machine], [slot]).any?
+ errors.add(:slot, I18n.t('cart_item_validation.blocked_by_another_reservation'))
+ return false
+ end
+ end
+ end
+
true
end
end
diff --git a/app/models/machine.rb b/app/models/machine.rb
index 029ebfdfe..6e7d58ac7 100644
--- a/app/models/machine.rb
+++ b/app/models/machine.rb
@@ -44,6 +44,8 @@ class Machine < ApplicationRecord
has_many :plan_limitations, dependent: :destroy, inverse_of: :machine, foreign_type: 'limitable_type', as: :limitable
+ belongs_to :space
+
after_create :create_statistic_subtype
after_create :create_machine_prices
after_create :update_gateway_product
diff --git a/app/models/slot.rb b/app/models/slot.rb
index 673b6a306..c4e83e038 100644
--- a/app/models/slot.rb
+++ b/app/models/slot.rb
@@ -14,6 +14,8 @@ class Slot < ApplicationRecord
after_create_commit :create_places_cache
+ attr_accessor :is_blocked
+
# @param reservable [Machine,Space,Training,Event,NilClass]
# @return [Integer] the total number of reserved places
def reserved_places(reservable = nil)
diff --git a/app/models/space.rb b/app/models/space.rb
index 765eef8a0..d6bd1ca2f 100644
--- a/app/models/space.rb
+++ b/app/models/space.rb
@@ -6,6 +6,7 @@
class Space < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
+ has_ancestry cache_depth: true
validates :name, :default_places, presence: true
@@ -34,6 +35,8 @@ class Space < ApplicationRecord
has_many :cart_item_space_reservations, class_name: 'CartItem::SpaceReservation', dependent: :destroy, inverse_of: :reservable,
foreign_type: 'reservable_type', as: :reservable
+ has_many :machines, dependent: :nullify
+
after_create :create_statistic_subtype
after_create :create_space_prices
after_create :update_gateway_product
diff --git a/app/services/availabilities/availabilities_service.rb b/app/services/availabilities/availabilities_service.rb
index d2fcd3f96..8df177504 100644
--- a/app/services/availabilities/availabilities_service.rb
+++ b/app/services/availabilities/availabilities_service.rb
@@ -39,7 +39,10 @@ class Availabilities::AvailabilitiesService
availabilities = availabilities(ma_availabilities, 'machines', user, window[:start], window[:end])
if @level == 'slot'
- availabilities.map(&:slots).flatten
+ slots = availabilities.map(&:slots).flatten
+
+ blocked_slots = Slots::InterblockingService.new.blocked_slots_for_machines(machines, slots)
+ flag_or_remove_blocked_slots(slots, blocked_slots, @current_user)
else
availabilities
end
@@ -57,7 +60,10 @@ class Availabilities::AvailabilitiesService
availabilities = availabilities(sp_availabilities, 'space', user, window[:start], window[:end])
if @level == 'slot'
- availabilities.map(&:slots).flatten
+ slots = availabilities.map(&:slots).flatten
+
+ blocked_slots = Slots::InterblockingService.new.blocked_slots_for_spaces(spaces, slots)
+ flag_or_remove_blocked_slots(slots, blocked_slots, @current_user)
else
availabilities
end
@@ -133,4 +139,15 @@ class Availabilities::AvailabilitiesService
qry
end
+
+ def flag_or_remove_blocked_slots(slots, blocked_slots, user)
+ if user.admin? || user.manager?
+ blocked_slots.each do |slot|
+ slot.is_blocked = true
+ end
+ else
+ slots -= blocked_slots
+ end
+ slots
+ end
end
diff --git a/app/services/machine_service.rb b/app/services/machine_service.rb
index 61d2e563c..49827abbd 100644
--- a/app/services/machine_service.rb
+++ b/app/services/machine_service.rb
@@ -9,9 +9,9 @@ class MachineService
def list(filters)
sort_by = Setting.get('machines_sort_by') || 'default'
machines = if sort_by == 'default'
- Machine.includes(:machine_image, :plans)
+ Machine.includes(:machine_image, :plans, :space)
else
- Machine.includes(:machine_image, :plans).order(sort_by)
+ Machine.includes(:machine_image, :plans, :space).order(sort_by)
end
# do not include soft destroyed
machines = machines.where(deleted_at: nil)
diff --git a/app/services/slots/interblocking_service.rb b/app/services/slots/interblocking_service.rb
new file mode 100644
index 000000000..1026371cf
--- /dev/null
+++ b/app/services/slots/interblocking_service.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+# Services around slots
+module Slots; end
+
+# Check the reservation status of a slot
+class Slots::InterblockingService
+ # returns an array of slots
+ # @param spaces [ActiveRecord::Relation]
+ # @param slots [ActiveRecord::Relation]
+ def blocked_slots_for_spaces(spaces, slots)
+ blocking_slots_start_at_end_at = []
+ spaces.each do |space|
+ parent_and_child_space_ids = [space.parent_id, space.child_ids].flatten.compact
+ blocking_slots_start_at_end_at << Slot.joins(slots_reservations: :reservation)
+ .where(slots_reservations: { canceled_at: nil },
+ reservations: { reservable_type: 'Space',
+ reservable_id: parent_and_child_space_ids })
+ .pluck(:start_at, :end_at)
+ .map { |d| %i[start_at end_at].zip(d).to_h }
+ child_machine_ids = Machine.where(space_id: [space.id, parent_and_child_space_ids].flatten)
+ blocking_slots_start_at_end_at << Slot.joins(slots_reservations: :reservation)
+ .where(slots_reservations: { canceled_at: nil },
+ reservations: { reservable_type: 'Machine',
+ reservable_id: child_machine_ids })
+ .pluck(:start_at, :end_at)
+ .map { |d| %i[start_at end_at].zip(d).to_h }
+ end
+ blocking_slots_start_at_end_at = blocking_slots_start_at_end_at.flatten&.uniq || []
+
+ blocked_slots(slots, blocking_slots_start_at_end_at)
+ end
+
+ def blocked_slots_for_machines(machines, slots)
+ blocking_slots_start_at_end_at = []
+ machines.each do |machine|
+ parent_space_ids = machine.space&.path_ids
+ next unless parent_space_ids&.any?
+
+ blocking_slots_start_at_end_at << Slot.joins(slots_reservations: :reservation)
+ .where(slots_reservations: { canceled_at: nil }, reservations: { reservable_type: 'Space',
+ reservable_id: parent_space_ids })
+ .pluck(:start_at, :end_at)
+ .map { |d| %i[start_at end_at].zip(d).to_h }
+ end
+ blocking_slots_start_at_end_at = blocking_slots_start_at_end_at.flatten&.uniq || []
+
+ blocked_slots(slots, blocking_slots_start_at_end_at)
+ end
+
+ private
+
+ def blocked_slots(slots, blocking_slots)
+ slots.select do |slot|
+ blocking_slots.find do |blocking_slot|
+ blocking_slot[:start_at] < slot.end_at && slot.start_at < blocking_slot[:end_at]
+ end
+ end
+ end
+end
diff --git a/app/services/slots/title_service.rb b/app/services/slots/title_service.rb
index 33a453f48..f37b9dc82 100644
--- a/app/services/slots/title_service.rb
+++ b/app/services/slots/title_service.rb
@@ -15,12 +15,16 @@ class Slots::TitleService
is_reserved_by_user = slot.reserved_by?(@user&.id, 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)}" : ''}"
+ if !slot.is_blocked
+ 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
else
- "#{name} - #{I18n.t('availabilities.i_ve_reserved')}"
+ "#{name} - #{I18n.t('availabilities.blocked')}"
end
end
diff --git a/app/views/api/availabilities/_slot.json.jbuilder b/app/views/api/availabilities/_slot.json.jbuilder
index b9afa9923..eed3ddc68 100644
--- a/app/views/api/availabilities/_slot.json.jbuilder
+++ b/app/views/api/availabilities/_slot.json.jbuilder
@@ -7,6 +7,7 @@ json.start slot.start_at.iso8601
json.end slot.end_at.iso8601
json.is_reserved slot.reserved?(reservable)
json.is_completed slot.full?(reservable)
+json.is_blocked slot.is_blocked
json.backgroundColor 'white'
json.availability_id slot.availability_id
diff --git a/app/views/api/machines/_machine.json.jbuilder b/app/views/api/machines/_machine.json.jbuilder
index 7969b3a7a..75c9de678 100644
--- a/app/views/api/machines/_machine.json.jbuilder
+++ b/app/views/api/machines/_machine.json.jbuilder
@@ -15,3 +15,9 @@ if machine.advanced_accounting
json.partial! 'api/advanced_accounting/advanced_accounting', advanced_accounting: machine.advanced_accounting
end
end
+
+if machine.space_id
+ json.space do
+ json.name machine.space.name
+ end
+end
\ No newline at end of file
diff --git a/app/views/api/spaces/_space.json.jbuilder b/app/views/api/spaces/_space.json.jbuilder
index 6daf5c976..82a1a8e9e 100644
--- a/app/views/api/spaces/_space.json.jbuilder
+++ b/app/views/api/spaces/_space.json.jbuilder
@@ -14,3 +14,7 @@ if space.advanced_accounting
json.partial! 'api/advanced_accounting/advanced_accounting', advanced_accounting: space.advanced_accounting
end
end
+
+json.machines space.machines do |machine|
+ json.name machine.name
+end
diff --git a/app/views/api/spaces/index.json.jbuilder b/app/views/api/spaces/index.json.jbuilder
index cb524287f..6fdc2f7e6 100644
--- a/app/views/api/spaces/index.json.jbuilder
+++ b/app/views/api/spaces/index.json.jbuilder
@@ -2,4 +2,15 @@
json.array!(@spaces) do |space|
json.partial! 'api/spaces/space', space: space
+
+ parent = @spaces_indexed_with_parent[space]
+ if parent
+ json.parent do
+ json.name parent.name
+ end
+ end
+
+ json.children @spaces_grouped_by_parent_id[space.id] do |child|
+ json.name child.name
+ end
end
diff --git a/app/views/api/spaces/show.json.jbuilder b/app/views/api/spaces/show.json.jbuilder
index 7127304e0..ffffcccf4 100644
--- a/app/views/api/spaces/show.json.jbuilder
+++ b/app/views/api/spaces/show.json.jbuilder
@@ -1,9 +1,18 @@
# frozen_string_literal: true
json.partial! 'api/spaces/space', space: @space
-json.extract! @space, :characteristics, :created_at, :updated_at
+json.extract! @space, :characteristics, :machine_ids, :child_ids, :created_at, :updated_at
json.space_files_attributes @space.space_files do |f|
json.id f.id
json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_url
end
+
+if @space.parent
+ json.parent do
+ json.name @space.parent.name
+ end
+end
+json.children @space.children do |child|
+ json.name child.name
+end
diff --git a/config/initializers/ancestry.rb b/config/initializers/ancestry.rb
new file mode 100644
index 000000000..47f56693c
--- /dev/null
+++ b/config/initializers/ancestry.rb
@@ -0,0 +1 @@
+Ancestry.default_ancestry_format = :materialized_path2
\ No newline at end of file
diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml
index 7260b74a9..39657676c 100644
--- a/config/locales/app.admin.en.yml
+++ b/config/locales/app.admin.en.yml
@@ -111,6 +111,10 @@ en:
save: "Save"
create_success: "The space was created successfully"
update_success: "The space was updated successfully"
+ associated_machines: "Included machines"
+ children_spaces: "Included spaces"
+ associated_objects: "Associated objects"
+ associated_objects_warning: "Only use these fields if you want interblocking reservation between spaces, child spaces and machines. If you want machine and space reservations to remain independent, please leave the following fields blank."
event_form:
ACTION_title: "{ACTION, select, create{New} other{Update the}} event"
title: "Title"
diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml
index 4312c953b..637bc910b 100644
--- a/config/locales/app.admin.fr.yml
+++ b/config/locales/app.admin.fr.yml
@@ -111,6 +111,10 @@ fr:
save: "Enregistrer"
create_success: "L'espace a bien été créé"
update_success: "L'espace a bien été mis à jour"
+ associated_machines: "Machines"
+ children_spaces: "Espaces"
+ associated_objects: "Machines et sous-espaces"
+ associated_objects_warning: "Utilisez ces champs uniquement si vous souhaitez que la réservation de l'espace bloque la réservation des machines associées et des sous-espaces (et vice-versa). Si vous souhaitez que les réservations restent indépendantes, veuillez laisser les champs suivants vides."
event_form:
ACTION_title: "{ACTION, select, create{Nouvel } other{Mettre à jour l''}}événement"
title: "Titre"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index e6e49ea61..e613bf9ef 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -66,6 +66,7 @@ en:
not_available: "Not available"
reserving: "I'm reserving"
i_ve_reserved: "I've reserved"
+ blocked: "Blocked"
length_must_be_slot_multiple: "must be at least %{MIN} minutes after the start date"
must_be_associated_with_at_least_1_machine: "must be associated with at least 1 machine"
deleted_user: "Deleted user"
@@ -562,6 +563,7 @@ en:
space: "This space is disabled"
machine: "This machine is disabled"
reservable: "This machine is not reservable"
+ blocked_by_another_reservation: "This slot is blocked by another reservation"
cart_validation:
select_user: "Please select a user before continuing"
settings:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 278fc2ec7..992e4937f 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -66,6 +66,7 @@ fr:
not_available: "Non disponible"
reserving: "Je réserve"
i_ve_reserved: "J'ai réservé"
+ blocked: "Bloquée"
length_must_be_slot_multiple: "doit être au moins %{MIN} minutes après la date de début"
must_be_associated_with_at_least_1_machine: "doit être associé avec au moins 1 machine"
deleted_user: "Utilisateur supprimé"
@@ -562,6 +563,7 @@ fr:
space: "Cet espace est désactivé"
machine: "Cette machine est désactivée"
reservable: "Cette machine n'est pas réservable"
+ blocked_by_another_reservation: "Ce créneau est bloqué par une autre réservation"
cart_validation:
select_user: "Veuillez sélectionner un utilisateur avant de continuer"
settings:
diff --git a/db/migrate/20140610153123_add_stp_customer_id_to_users.rb b/db/migrate/20140610153123_add_stp_customer_id_to_users.rb
index f06fcaa44..42787d54a 100644
--- a/db/migrate/20140610153123_add_stp_customer_id_to_users.rb
+++ b/db/migrate/20140610153123_add_stp_customer_id_to_users.rb
@@ -3,6 +3,7 @@
class AddStpCustomerIdToUsers < ActiveRecord::Migration[4.2]
def up
add_column :users, :stp_customer_id, :string
+ User.reset_column_information
User.all.each do |user|
if user.stp_customer_id.blank?
user.send(:create_stripe_customer)
diff --git a/db/migrate/20230728072726_add_ancestry_to_spaces.rb b/db/migrate/20230728072726_add_ancestry_to_spaces.rb
new file mode 100644
index 000000000..c816fdd89
--- /dev/null
+++ b/db/migrate/20230728072726_add_ancestry_to_spaces.rb
@@ -0,0 +1,9 @@
+class AddAncestryToSpaces < ActiveRecord::Migration[7.0]
+ def change
+ add_column :spaces, :ancestry, :string, collation: 'C'
+ Space.update_all(ancestry: '/')
+ change_column_null(:spaces, :ancestry, false)
+ add_column :spaces, :ancestry_depth, :integer, default: 0
+ add_index :spaces, :ancestry
+ end
+end
diff --git a/db/migrate/20230728090257_add_space_id_to_machines.rb b/db/migrate/20230728090257_add_space_id_to_machines.rb
new file mode 100644
index 000000000..4a9a88239
--- /dev/null
+++ b/db/migrate/20230728090257_add_space_id_to_machines.rb
@@ -0,0 +1,5 @@
+class AddSpaceIdToMachines < ActiveRecord::Migration[7.0]
+ def change
+ add_reference :machines, :space, foreign_key: true, index: true
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index aba1558d6..1585e16e3 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1736,7 +1736,8 @@ CREATE TABLE public.machines (
disabled boolean,
deleted_at timestamp without time zone,
machine_category_id bigint,
- reservable boolean DEFAULT true
+ reservable boolean DEFAULT true,
+ space_id bigint
);
@@ -3351,7 +3352,9 @@ CREATE TABLE public.spaces (
updated_at timestamp without time zone NOT NULL,
characteristics text,
disabled boolean,
- deleted_at timestamp without time zone
+ deleted_at timestamp without time zone,
+ ancestry character varying NOT NULL COLLATE pg_catalog."C",
+ ancestry_depth integer DEFAULT 0
);
@@ -6853,6 +6856,13 @@ CREATE INDEX index_machines_on_machine_category_id ON public.machines USING btre
CREATE UNIQUE INDEX index_machines_on_slug ON public.machines USING btree (slug);
+--
+-- Name: index_machines_on_space_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_machines_on_space_id ON public.machines USING btree (space_id);
+
+
--
-- Name: index_notification_preferences_on_notification_type_id; Type: INDEX; Schema: public; Owner: -
--
@@ -7392,6 +7402,13 @@ CREATE INDEX index_spaces_availabilities_on_availability_id ON public.spaces_ava
CREATE INDEX index_spaces_availabilities_on_space_id ON public.spaces_availabilities USING btree (space_id);
+--
+-- Name: index_spaces_on_ancestry; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_spaces_on_ancestry ON public.spaces USING btree (ancestry);
+
+
--
-- Name: index_spaces_on_deleted_at; Type: INDEX; Schema: public; Owner: -
--
@@ -8451,6 +8468,14 @@ ALTER TABLE ONLY public.statistic_profile_prepaid_packs
ADD CONSTRAINT fk_rails_b0251cdfcf FOREIGN KEY (prepaid_pack_id) REFERENCES public.prepaid_packs(id);
+--
+-- Name: machines fk_rails_b2e37688bb; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.machines
+ ADD CONSTRAINT fk_rails_b2e37688bb FOREIGN KEY (space_id) REFERENCES public.spaces(id);
+
+
--
-- Name: orders fk_rails_b33ed6c672; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -9154,7 +9179,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230626103314'),
('20230626122844'),
('20230626122947'),
-('20230710072403');
+('20230710072403'),
('20230718133636'),
('20230718134350'),
-('20230720085857');
+('20230720085857'),
+('20230728072726'),
+('20230728090257');
diff --git a/test/fixtures/spaces.yml b/test/fixtures/spaces.yml
index 242041286..7dcb23747 100644
--- a/test/fixtures/spaces.yml
+++ b/test/fixtures/spaces.yml
@@ -7,3 +7,4 @@ space_1:
created_at: 2017-02-15 15:55:04.123928000 Z
updated_at: 2017-02-15 15:55:04.123928000 Z
characteristics: Scie à chantourner, rabot, dégauchisseuse, chanfreineuse et pyrograveur
+ ancestry: '/'
diff --git a/test/models/space_test.rb b/test/models/space_test.rb
index 008de7e84..92e54663a 100644
--- a/test/models/space_test.rb
+++ b/test/models/space_test.rb
@@ -31,4 +31,43 @@ class SpaceTest < ActiveSupport::TestCase
assert_nil Space.find_by(slug: slug)
assert_nil StatisticSubType.find_by(key: slug)
end
+
+ test "space can be associated with spaces in a tree-like structure" do
+ space_1 = Space.create!(name: "space 1", default_places: 2)
+ space_1_1 = Space.create!(name: "space 1_1", default_places: 2, parent: space_1)
+ space_1_2 = Space.create!(name: "space 1_2", default_places: 2, parent: space_1)
+ space_1_2_1 = Space.create!(name: "space 1_2_1", default_places: 2, parent: space_1_2)
+ space_other = Space.create!(name: "space other", default_places: 2)
+
+ assert_equal [space_1_1, space_1_2], space_1.children
+ assert_equal [], space_1_1.children
+ assert_equal [space_1_2_1], space_1_2.children
+
+ assert_equal [space_1, space_1_2], space_1_2_1.ancestors
+ assert_equal [space_1], space_1_2.ancestors
+ assert_equal [space_1], space_1_1.ancestors
+ assert_equal [], space_1.ancestors
+
+ assert_equal [space_1_1, space_1_2, space_1_2_1], space_1.descendants
+ assert_equal [], space_1_1.descendants
+ assert_equal [space_1_2_1], space_1_2.descendants
+ assert_equal [], space_1_2_1.descendants
+
+ assert_equal [], space_other.descendants
+ assert_equal [], space_other.ancestors
+ end
+
+ test "space can be associated with machines" do
+ space = spaces(:space_1)
+ machine_1 = machines(:machine_1)
+ machine_2 = machines(:machine_2)
+
+ space.machines << machine_1
+ space.machines << machine_2
+
+ assert_equal 2, space.machines.count
+
+ assert_equal space, machine_1.space
+ assert_equal space, machine_2.space
+ end
end
diff --git a/test/services/availabilities/availabilities_service_test.rb b/test/services/availabilities/availabilities_service_test.rb
index cb3827029..06a15d469 100644
--- a/test/services/availabilities/availabilities_service_test.rb
+++ b/test/services/availabilities/availabilities_service_test.rb
@@ -66,6 +66,50 @@ class Availabilities::AvailabilitiesServiceTest < ActiveSupport::TestCase
assert_equal availability.end_at, slots.max_by(&:end_at).end_at
end
+ test '[member] machines availabilities with blocked slots' do
+ space = Space.find(1)
+ machine = Machine.find(1).tap { |m| m.update!(space: space) }
+ reservation = Reservation.create!(reservable: space, statistic_profile: statistic_profiles(:jdupont))
+ machine_availability = Availability.create!(availabilities(:availability_7).slice(:start_at, :end_at, :slot_duration)
+ .merge(available_type: 'machines', machine_ids: [machine.id]))
+
+ machine_slot = availabilities(:availability_7).slots.first
+ slot = Slot.create!(availability: machine_availability, start_at: machine_slot.start_at, end_at: machine_slot.end_at)
+
+ opts = { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }
+ service = Availabilities::AvailabilitiesService.new(@no_subscription)
+ slots = service.machines([machine], @no_subscription, opts)
+ assert_equal 7, slots.count
+
+ SlotsReservation.create!(reservation: reservation, slot: slot)
+
+ slots = service.machines([machine], @no_subscription, opts)
+ assert_equal 5, slots.count
+ end
+
+ test '[admin] machines availabilities with blocked slots' do
+ space = Space.find(1)
+ machine = Machine.find(1).tap { |m| m.update!(space: space) }
+ reservation = Reservation.create!(reservable: space, statistic_profile: statistic_profiles(:jdupont))
+ machine_availability = Availability.create!(availabilities(:availability_7).slice(:start_at, :end_at, :slot_duration)
+ .merge(available_type: 'machines', machine_ids: [machine.id]))
+
+ machine_slot = availabilities(:availability_7).slots.first
+ slot = Slot.create!(availability: machine_availability, start_at: machine_slot.start_at, end_at: machine_slot.end_at)
+
+ opts = { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }
+ service = Availabilities::AvailabilitiesService.new(@admin)
+ slots = service.machines([machine], @admin, opts)
+ assert_equal 7, slots.count
+
+ SlotsReservation.create!(reservation: reservation, slot: slot)
+
+ slots = service.machines([machine], @admin, opts)
+ assert_equal 7, slots.count
+
+ assert_equal 2, slots.count(&:is_blocked)
+ end
+
test 'spaces availabilities' do
service = Availabilities::AvailabilitiesService.new(@no_subscription)
slots = service.spaces([Space.find(1)], @no_subscription, { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day })
@@ -77,6 +121,50 @@ class Availabilities::AvailabilitiesServiceTest < ActiveSupport::TestCase
assert_equal availability.end_at, slots.max_by(&:end_at).end_at
end
+ test '[member] spaces availabilities with blocked slots' do
+ space = Space.find(1)
+ machine = machines(:machine_1).tap { |m| m.update!(space: space) }
+ reservation = Reservation.create!(reservable: machine, statistic_profile: statistic_profiles(:jdupont))
+ machine_availability = Availability.create!(availabilities(:availability_18).slice(:start_at, :end_at, :slot_duration)
+ .merge(available_type: 'machines', machine_ids: [machine.id]))
+
+ space_slot = availabilities(:availability_18).slots.first
+ slot = Slot.create!(availability: machine_availability, start_at: space_slot.start_at, end_at: space_slot.end_at)
+
+ opts = { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }
+ service = Availabilities::AvailabilitiesService.new(@no_subscription)
+ slots = service.spaces([space], @no_subscription, opts)
+ assert_equal 4, slots.count
+
+ SlotsReservation.create!(reservation: reservation, slot: slot)
+
+ slots = service.spaces([space], @no_subscription, opts)
+ assert_equal 3, slots.count
+ end
+
+ test '[admin] spaces availabilities with blocked slots' do
+ space = Space.find(1)
+ machine = machines(:machine_1).tap { |m| m.update!(space: space) }
+ reservation = Reservation.create!(reservable: machine, statistic_profile: statistic_profiles(:jdupont))
+ machine_availability = Availability.create!(availabilities(:availability_18).slice(:start_at, :end_at, :slot_duration)
+ .merge(available_type: 'machines', machine_ids: [machine.id]))
+
+ space_slot = availabilities(:availability_18).slots.first
+ slot = Slot.create!(availability: machine_availability, start_at: space_slot.start_at, end_at: space_slot.end_at)
+
+ opts = { start: 2.days.from_now.beginning_of_day, end: 4.days.from_now.end_of_day }
+ service = Availabilities::AvailabilitiesService.new(@admin)
+ slots = service.spaces([space], @admin, opts)
+ assert_equal 4, slots.count
+
+ SlotsReservation.create!(reservation: reservation, slot: slot)
+
+ slots = service.spaces([space], @admin, opts)
+ assert_equal 4, slots.count
+
+ assert_equal 1, slots.count(&:is_blocked)
+ end
+
test 'trainings availabilities' do
service = Availabilities::AvailabilitiesService.new(@no_subscription)
trainings = [Training.find(1), Training.find(2)]
diff --git a/test/services/slots/interblocking_service_test.rb b/test/services/slots/interblocking_service_test.rb
new file mode 100644
index 000000000..1415a66c2
--- /dev/null
+++ b/test/services/slots/interblocking_service_test.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Slots::InterblockingServiceTest < ActiveSupport::TestCase
+ setup do
+ @parent_space = spaces(:space_1)
+ @child_space = Space.create!(name: 'space 1-1', default_places: 2, parent: @parent_space)
+ @space_availability = availabilities(:availability_18)
+ @space_slots = @space_availability.slots
+ @machine_availability = availabilities(:availability_7)
+ @machine_slots = @machine_availability.slots
+ @machine = machines(:machine_1).tap { |m| m.update!(space: @child_space) }
+ end
+
+ test '#blocked_slots_for_spaces : no reservation' do
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_spaces([@parent_space], @space_slots)
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_spaces([@child_space], @space_slots)
+ end
+
+ test '#blocked_slots_for_spaces : reservation on parent space' do
+ reservation = Reservation.create!(reservable: @parent_space, statistic_profile: statistic_profiles(:jdupont))
+ SlotsReservation.create!(reservation: reservation, slot: @space_slots.first)
+
+ assert_equal [@space_slots.first], Slots::InterblockingService.new.blocked_slots_for_spaces([@child_space], @space_slots)
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_spaces([@parent_space], @space_slots)
+ end
+
+ test '#blocked_slots_for_spaces : reservation on child space' do
+ reservation = Reservation.create!(reservable: @child_space, statistic_profile: statistic_profiles(:jdupont))
+ SlotsReservation.create!(reservation: reservation, slot: @space_slots.first)
+
+ assert_equal [@space_slots.first], Slots::InterblockingService.new.blocked_slots_for_spaces([@parent_space], @space_slots)
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_spaces([@child_space], @space_slots)
+ end
+
+ test '#blocked_slots_for_spaces : reservation on child machine' do
+ reservation = Reservation.create!(reservable: @machine, statistic_profile: statistic_profiles(:jdupont))
+ machine_availability = Availability.create!(@space_availability.slice(:start_at, :end_at, :slot_duration)
+ .merge(available_type: 'machines', machine_ids: [@machine.id]))
+
+ slot = Slot.create!(availability: machine_availability, start_at: @space_slots.first.start_at, end_at: @space_slots.first.end_at)
+ SlotsReservation.create!(reservation: reservation, slot: slot)
+
+ assert_equal [@space_slots.first], Slots::InterblockingService.new.blocked_slots_for_spaces([@parent_space], @space_slots)
+ assert_equal [@space_slots.first], Slots::InterblockingService.new.blocked_slots_for_spaces([@child_space], @space_slots)
+
+ slot.update!(start_at: slot.start_at - 15.minutes, end_at: slot.end_at - 15.minutes)
+
+ # still match when overlapping
+ assert_equal [@space_slots.first], Slots::InterblockingService.new.blocked_slots_for_spaces([@parent_space], @space_slots)
+ assert_equal [@space_slots.first], Slots::InterblockingService.new.blocked_slots_for_spaces([@child_space], @space_slots)
+
+ slot.update!(start_at: slot.start_at - 45.minutes, end_at: slot.end_at - 45.minutes)
+
+ # not overlapping anymore
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_spaces([@parent_space], @space_slots)
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_spaces([@child_space], @space_slots)
+ end
+
+ test '#blocked_slots_for_machines : no reservation' do
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_machines([@machine], @machine_slots)
+ end
+
+ test '#blocked_slots_for_machines : reservation on parent space' do
+ reservation = Reservation.create!(reservable: @parent_space, statistic_profile: statistic_profiles(:jdupont))
+
+ space_availability = Availability.create!(@machine_availability.slice(:start_at, :end_at, :slot_duration)
+ .merge(available_type: 'space', space_ids: [@parent_space.id]))
+
+ slot = Slot.create!(availability: space_availability, start_at: @machine_slots.first.start_at, end_at: @machine_slots.first.end_at)
+ SlotsReservation.create!(reservation: reservation, slot: slot)
+
+ assert_equal [@machine_slots.first], Slots::InterblockingService.new.blocked_slots_for_machines([@machine], @machine_slots)
+
+ slot.update!(start_at: slot.start_at - 15.minutes, end_at: slot.end_at - 15.minutes)
+
+ # still match when overlapping
+ assert_equal [@machine_slots.first], Slots::InterblockingService.new.blocked_slots_for_machines([@machine], @machine_slots)
+
+ slot.update!(start_at: slot.start_at - 45.minutes, end_at: slot.end_at - 45.minutes)
+
+ # not overlapping anymore
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_machines([@machine], @machine_slots)
+ end
+
+ test '#blocked_slots_for_machines : reservation on child space' do
+ reservation = Reservation.create!(reservable: @child_space, statistic_profile: statistic_profiles(:jdupont))
+
+ space_availability = Availability.create!(@machine_availability.slice(:start_at, :end_at, :slot_duration)
+ .merge(available_type: 'space', space_ids: [@child_space.id]))
+
+ slot = Slot.create!(availability: space_availability, start_at: @machine_slots.first.start_at, end_at: @machine_slots.first.end_at)
+ SlotsReservation.create!(reservation: reservation, slot: slot)
+
+ assert_equal [@machine_slots.first], Slots::InterblockingService.new.blocked_slots_for_machines([@machine], @machine_slots)
+
+ slot.update!(start_at: slot.start_at - 15.minutes, end_at: slot.end_at - 15.minutes)
+
+ # still match when overlapping
+ assert_equal [@machine_slots.first], Slots::InterblockingService.new.blocked_slots_for_machines([@machine], @machine_slots)
+
+ slot.update!(start_at: slot.start_at - 45.minutes, end_at: slot.end_at - 45.minutes)
+
+ # not overlapping anymore
+ assert_empty Slots::InterblockingService.new.blocked_slots_for_machines([@machine], @machine_slots)
+ end
+end