1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-22 11:52:21 +01:00

Merge branch 'spaces-machines-tree' into staging

This commit is contained in:
Du Peng 2023-08-24 15:03:46 +02:00
commit b5f330ea5a
45 changed files with 700 additions and 95 deletions

View File

@ -151,3 +151,5 @@ gem 'sentry-rails'
gem 'sentry-ruby'
gem "reverse_markdown"
gem "ancestry"

View File

@ -76,6 +76,8 @@ GEM
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
afm (0.2.2)
ancestry (4.3.3)
activerecord (>= 5.2.6)
ansi (1.5.0)
api-pagination (4.8.2)
apipie-rails (0.5.17)
@ -536,6 +538,7 @@ DEPENDENCIES
aasm
active_record_query_trace
acts_as_list
ancestry
api-pagination
apipie-rails
awesome_print

View File

@ -7,7 +7,9 @@ class API::SpacesController < API::APIController
respond_to :json
def index
@spaces = Space.includes(:space_image).where(deleted_at: nil)
@spaces = Space.includes(:space_image, :machines).where(deleted_at: nil)
@spaces_indexed_with_parent = @spaces.index_with { |space| @spaces.find { |s| s.id == space.parent_id } }
@spaces_grouped_by_parent_id = @spaces.group_by(&:parent_id)
end
def show
@ -20,6 +22,7 @@ class API::SpacesController < API::APIController
authorize Space
@space = Space.new(space_params)
if @space.save
update_space_children(@space, params[:space][:child_ids])
render :show, status: :created, location: @space
else
render json: @space.errors, status: :unprocessable_entity
@ -29,6 +32,7 @@ class API::SpacesController < API::APIController
def update
authorize @space
if @space.update(space_params)
update_space_children(@space, params[:space][:child_ids])
render :show, status: :ok, location: @space
else
render json: @space.errors, status: :unprocessable_entity
@ -50,8 +54,18 @@ class API::SpacesController < API::APIController
def space_params
params.require(:space).permit(:name, :description, :characteristics, :default_places, :disabled,
machine_ids: [],
space_image_attributes: %i[id attachment],
space_files_attributes: %i[id attachment _destroy],
advanced_accounting_attributes: %i[code analytical_section])
end
def update_space_children(parent_space, child_ids)
Space.transaction do
parent_space.children.each { |child| child.update!(parent: nil) }
child_ids.to_a.select(&:present?).each do |child_id|
Space.find(child_id).update!(parent: parent_space)
end
end
end
end

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,73 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<defs><style> .nc-icon-wrapper { display: none } .nc-icon-wrapper:target { display: inline } </style></defs>
<svg viewBox="0 0 24 24">
<g id="lastfm" class="nc-icon-wrapper">
<path d="M20.25 1.5H3.75A2.25 2.25 0 0 0 1.5 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h16.5a2.25 2.25 0 0 0 2.25-2.25V3.75a2.25 2.25 0 0 0-2.25-2.25zm-4.322 14.667c-2.972 0-4.003-1.34-4.551-3.005-.764-2.39-1.008-3.951-2.954-3.951-1.05 0-2.114.755-2.114 2.869 0 1.65.844 2.68 2.03 2.68 1.34 0 2.231-.998 2.231-.998l.549 1.496s-.928.91-2.869.91c-2.405 0-3.745-1.412-3.745-4.023 0-2.714 1.34-4.312 3.867-4.312 3.445 0 3.787 1.94 4.725 4.776.412 1.257 1.134 2.166 2.869 2.166 1.167 0 1.786-.258 1.786-.895 0-.933-1.022-1.032-2.34-1.34-1.425-.343-1.992-1.084-1.992-2.25 0-1.876 1.514-2.457 3.057-2.457 1.753 0 2.817.637 2.953 2.184l-1.72.206c-.07-.74-.516-1.05-1.341-1.05-.755 0-1.219.343-1.219.929 0 .515.225.825.98.998 1.533.333 3.365.563 3.365 2.695.005 1.72-1.439 2.372-3.567 2.372z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="instagram" class="nc-icon-wrapper">
<path d="M12 9.5a2.5 2.5 0 1 0 .002 5 2.5 2.5 0 0 0-.002-5zm5.846-1.922a2.532 2.532 0 0 0-1.426-1.426c-.984-.388-3.328-.301-4.42-.301s-3.434-.09-4.42.301a2.531 2.531 0 0 0-1.426 1.426c-.388.984-.302 3.33-.302 4.421 0 1.092-.086 3.435.304 4.423a2.531 2.531 0 0 0 1.425 1.425c.984.389 3.328.302 4.42.302 1.094 0 3.434.09 4.421-.302a2.532 2.532 0 0 0 1.426-1.425c.391-.985.301-3.33.301-4.422 0-1.091.09-3.434-.301-4.422h-.002zM12 15.844a3.844 3.844 0 1 1 0-7.689 3.844 3.844 0 0 1 0 7.689zm4.002-6.952a.897.897 0 1 1 .002.002l-.002-.002zM20.25 1.5H3.75A2.25 2.25 0 0 0 1.5 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h16.5a2.25 2.25 0 0 0 2.25-2.25V3.75a2.25 2.25 0 0 0-2.25-2.25zm-.802 13.594c-.061 1.201-.335 2.266-1.212 3.14-.877.875-1.94 1.155-3.14 1.212-1.239.07-4.95.07-6.188 0-1.202-.06-2.263-.335-3.141-1.212-.878-.876-1.155-1.941-1.212-3.14-.07-1.239-.07-4.95 0-6.188.06-1.201.332-2.266 1.212-3.14.88-.875 1.944-1.152 3.14-1.209 1.239-.07 4.95-.07 6.188 0 1.202.06 2.266.335 3.14 1.212.876.876 1.155 1.941 1.213 3.143.07 1.234.07 4.942 0 6.182z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="github" class="nc-icon-wrapper">
<path d="M20.25 1.5H3.75C2.50781 1.5 1.5 2.50781 1.5 3.75V20.25C1.5 21.4922 2.50781 22.5 3.75 22.5H20.25C21.4922 22.5 22.5 21.4922 22.5 20.25V3.75C22.5 2.50781 21.4922 1.5 20.25 1.5ZM14.4984 19.4859C14.1047 19.5563 13.9594 19.3125 13.9594 19.1109C13.9594 18.8578 13.9688 17.5641 13.9688 16.5187C13.9688 15.7875 13.725 15.3234 13.4391 15.0797C15.1734 14.8875 17.0016 14.6484 17.0016 11.6531C17.0016 10.8 16.6969 10.3734 16.2 9.825C16.2797 9.62344 16.5469 8.79375 16.1203 7.71563C15.4687 7.51406 13.9781 8.55469 13.9781 8.55469C13.3594 8.38125 12.6891 8.29219 12.0281 8.29219C11.3672 8.29219 10.6969 8.38125 10.0781 8.55469C10.0781 8.55469 8.5875 7.51406 7.93594 7.71563C7.50938 8.78906 7.77188 9.61875 7.85625 9.825C7.35938 10.3734 7.125 10.8 7.125 11.6531C7.125 14.6344 8.87344 14.8875 10.6078 15.0797C10.3828 15.2813 10.1812 15.6281 10.1109 16.125C9.66562 16.3266 8.52656 16.6734 7.84687 15.4734C7.42031 14.7328 6.65156 14.6719 6.65156 14.6719C5.89219 14.6625 6.6 15.15 6.6 15.15C7.10625 15.3844 7.4625 16.2844 7.4625 16.2844C7.91719 17.6766 10.0922 17.2078 10.0922 17.2078C10.0922 17.8594 10.1016 18.9188 10.1016 19.1109C10.1016 19.3125 9.96094 19.5563 9.5625 19.4859C6.46875 18.45 4.30312 15.5062 4.30312 12.0656C4.30312 7.7625 7.59375 4.49531 11.8969 4.49531C16.2 4.49531 19.6875 7.7625 19.6875 12.0656C19.6922 15.5062 17.5922 18.4547 14.4984 19.4859ZM9.9 16.6219C9.81094 16.6406 9.72656 16.6031 9.71719 16.5422C9.70781 16.4719 9.76875 16.4109 9.85781 16.3922C9.94688 16.3828 10.0312 16.4203 10.0406 16.4813C10.0547 16.5422 9.99375 16.6031 9.9 16.6219ZM9.45469 16.5797C9.45469 16.6406 9.38437 16.6922 9.29062 16.6922C9.1875 16.7016 9.11719 16.65 9.11719 16.5797C9.11719 16.5188 9.1875 16.4672 9.28125 16.4672C9.37031 16.4578 9.45469 16.5094 9.45469 16.5797ZM8.8125 16.5281C8.79375 16.5891 8.7 16.6172 8.62031 16.5891C8.53125 16.5703 8.47031 16.5 8.48906 16.4391C8.50781 16.3781 8.60156 16.35 8.68125 16.3688C8.775 16.3969 8.83594 16.4672 8.8125 16.5281ZM8.23594 16.275C8.19375 16.3266 8.10469 16.3172 8.03437 16.2469C7.96406 16.1859 7.94531 16.0969 7.99219 16.0547C8.03437 16.0031 8.12344 16.0125 8.19375 16.0828C8.25469 16.1438 8.27812 16.2375 8.23594 16.275ZM7.80938 15.8484C7.76719 15.8766 7.6875 15.8484 7.63594 15.7781C7.58437 15.7078 7.58437 15.6281 7.63594 15.5953C7.6875 15.5531 7.76719 15.5859 7.80938 15.6562C7.86094 15.7266 7.86094 15.8109 7.80938 15.8484V15.8484ZM7.50469 15.3937C7.4625 15.4359 7.39219 15.4125 7.34062 15.3656C7.28906 15.3047 7.27969 15.2344 7.32187 15.2016C7.36406 15.1594 7.43437 15.1828 7.48594 15.2297C7.5375 15.2906 7.54688 15.3609 7.50469 15.3937ZM7.19063 15.0469C7.17188 15.0891 7.11094 15.0984 7.05937 15.0656C6.99844 15.0375 6.97031 14.9859 6.98906 14.9437C7.00781 14.9156 7.05938 14.9016 7.12031 14.925C7.18125 14.9578 7.20938 15.0094 7.19063 15.0469Z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="echosciences" class="nc-icon-wrapper">
<path d="M3.5 1.5C2.39543 1.5 1.5 2.39543 1.5 3.5V20.5C1.5 21.6046 2.39543 22.5 3.5 22.5H20.5C21.6046 22.5 22.5 21.6046 22.5 20.5V3.5C22.5 2.39543 21.6046 1.5 20.5 1.5H3.5ZM15.099 5H16.5742L16.6848 5.14204C18.2776 7.18714 19.1861 9.48429 19.1838 11.9303C19.1837 14.433 18.2315 16.7805 16.5685 18.8616L16.4578 19H14.9691L15.4785 18.3944C17.1447 16.4138 18.0564 14.2227 18.0564 11.93C18.0579 9.6857 17.1869 7.54197 15.5936 5.60226L15.099 5ZM4.81616 8.48674V5.73684H13.5809V8.48674H4.81616ZM12.7682 13.3182H7.98311V15.5169H13.5964V18.2632H4.83164V10.5757H12.7682V13.3182Z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="flickr" class="nc-icon-wrapper">
<path d="M20.25 1.5H3.75A2.25 2.25 0 0 0 1.5 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h16.5a2.25 2.25 0 0 0 2.25-2.25V3.75a2.25 2.25 0 0 0-2.25-2.25zM8.273 14.953a2.975 2.975 0 0 1-2.976-2.976A2.975 2.975 0 0 1 8.273 9a2.975 2.975 0 0 1 2.977 2.977 2.975 2.975 0 0 1-2.977 2.976zm7.454 0a2.975 2.975 0 0 1-2.977-2.976A2.975 2.975 0 0 1 15.727 9a2.975 2.975 0 0 1 2.976 2.977 2.975 2.975 0 0 1-2.976 2.976z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="facebook" class="nc-icon-wrapper">
<path d="M20.25 1.5H3.75C3.15326 1.5 2.58097 1.73705 2.15901 2.15901C1.73705 2.58097 1.5 3.15326 1.5 3.75L1.5 20.25C1.5 20.8467 1.73705 21.419 2.15901 21.841C2.58097 22.2629 3.15326 22.5 3.75 22.5H10.1836V15.3605H7.23047V12H10.1836V9.43875C10.1836 6.52547 11.918 4.91625 14.5744 4.91625C15.8466 4.91625 17.1769 5.14313 17.1769 5.14313V8.0025H15.7111C14.2669 8.0025 13.8164 8.89875 13.8164 9.81797V12H17.0405L16.5248 15.3605H13.8164V22.5H20.25C20.8467 22.5 21.419 22.2629 21.841 21.841C22.2629 21.419 22.5 20.8467 22.5 20.25V3.75C22.5 3.15326 22.2629 2.58097 21.841 2.15901C21.419 1.73705 20.8467 1.5 20.25 1.5V1.5Z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="youtube" class="nc-icon-wrapper">
<path d="M10.2563 9.47344L14.7188 12.0094L10.2563 14.5453V9.47344ZM22.5 3.75V20.25C22.5 21.4922 21.4922 22.5 20.25 22.5H3.75C2.50781 22.5 1.5 21.4922 1.5 20.25V3.75C1.5 2.50781 2.50781 1.5 3.75 1.5H20.25C21.4922 1.5 22.5 2.50781 22.5 3.75ZM20.5312 12.0141C20.5312 12.0141 20.5312 9.22031 20.175 7.87969C19.9781 7.13906 19.4016 6.55781 18.6656 6.36094C17.3391 6 12 6 12 6C12 6 6.66094 6 5.33438 6.36094C4.59844 6.55781 4.02187 7.13906 3.825 7.87969C3.46875 9.21563 3.46875 12.0141 3.46875 12.0141C3.46875 12.0141 3.46875 14.8078 3.825 16.1484C4.02187 16.8891 4.59844 17.4469 5.33438 17.6437C6.66094 18 12 18 12 18C12 18 17.3391 18 18.6656 17.6391C19.4016 17.4422 19.9781 16.8844 20.175 16.1437C20.5312 14.8078 20.5312 12.0141 20.5312 12.0141V12.0141Z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="vimeo" class="nc-icon-wrapper">
<path d="M20.4 1.5H3.6c-1.158 0-2.1.942-2.1 2.1v16.8c0 1.158.942 2.1 2.1 2.1h16.8c1.158 0 2.1-.942 2.1-2.1V3.6c0-1.158-.942-2.1-2.1-2.1zm-1.228 6.975c-.066 1.477-1.097 3.502-3.094 6.066-2.062 2.68-3.81 4.021-5.236 4.021-.886 0-1.631-.815-2.245-2.451-1.195-4.373-1.706-6.938-2.69-6.938-.113 0-.512.24-1.191.713l-.713-.919C5.752 7.43 7.42 5.723 8.466 5.63c1.18-.113 1.907.693 2.18 2.423.97 6.15 1.4 7.078 3.168 4.294.633-1.003.975-1.767 1.022-2.292.164-1.557-1.214-1.449-2.147-1.05.745-2.443 2.17-3.628 4.275-3.563 1.561.042 2.297 1.055 2.208 3.033z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="vimeo" class="nc-icon-wrapper">
<path d="M20.4 1.5H3.6c-1.158 0-2.1.942-2.1 2.1v16.8c0 1.158.942 2.1 2.1 2.1h16.8c1.158 0 2.1-.942 2.1-2.1V3.6c0-1.158-.942-2.1-2.1-2.1zm-1.228 6.975c-.066 1.477-1.097 3.502-3.094 6.066-2.062 2.68-3.81 4.021-5.236 4.021-.886 0-1.631-.815-2.245-2.451-1.195-4.373-1.706-6.938-2.69-6.938-.113 0-.512.24-1.191.713l-.713-.919C5.752 7.43 7.42 5.723 8.466 5.63c1.18-.113 1.907.693 2.18 2.423.97 6.15 1.4 7.078 3.168 4.294.633-1.003.975-1.767 1.022-2.292.164-1.557-1.214-1.449-2.147-1.05.745-2.443 2.17-3.628 4.275-3.563 1.561.042 2.297 1.055 2.208 3.033z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="viadeo" class="nc-icon-wrapper">
<path d="M20.25 1.5H3.75A2.25 2.25 0 0 0 1.5 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h16.5a2.25 2.25 0 0 0 2.25-2.25V3.75a2.25 2.25 0 0 0-2.25-2.25zm-5.592 16.369c-1.988 2.165-5.625 2.184-7.613 0-3.187-3.45-.928-9.192 3.807-9.192.623 0 1.246.098 1.832.314a3.749 3.749 0 0 0-.393 1.27 3.696 3.696 0 0 0-1.44-.281c-2.287 0-3.965 1.954-3.965 4.167 0 2.016 1.336 3.689 3.258 4.026 2.883-1.125 3.417-5.512 3.417-8.203 0-.342 0-.693-.028-1.036-.525-1.542-1.247-3.028-2.072-4.43 1.27.859 1.964 2.93 2.072 4.412v.018a10.15 10.15 0 0 1 .553 3.282c0 2.536-1.027 4.64-3.202 6.009l-.112.01c2.344.046 4.04-1.81 4.04-4.088a4.29 4.29 0 0 0-.323-1.674 3.763 3.763 0 0 0 1.238-.492 5.554 5.554 0 0 1-1.07 5.888zm1.326-6.914c-.623 0-1.176-.333-1.612-.755 1.026-.563 2.325-1.44 2.92-2.484.07-.141.192-.404.211-.563-.586 1.308-2.072 2.335-3.464 2.658a2.092 2.092 0 0 1-.351-1.14c0-.482.243-1.129.604-1.48 1.013-.961 2.485-.399 3.394-2.344 1.523 2.165.614 6.108-1.702 6.108z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="twitter" class="nc-icon-wrapper">
<path d="M20.25 1.5H3.75A2.25 2.25 0 0 0 1.5 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h16.5a2.25 2.25 0 0 0 2.25-2.25V3.75a2.25 2.25 0 0 0-2.25-2.25zm-2.292 7.444c.01.131.01.267.01.398 0 4.064-3.095 8.747-8.748 8.747a8.706 8.706 0 0 1-4.72-1.378c.248.028.487.037.74.037 1.44 0 2.762-.487 3.816-1.312a3.078 3.078 0 0 1-2.873-2.133c.473.07.9.07 1.387-.056a3.075 3.075 0 0 1-2.46-3.019v-.037c.407.23.885.37 1.387.389a3.068 3.068 0 0 1-1.369-2.56c0-.572.15-1.097.417-1.551a8.73 8.73 0 0 0 6.338 3.215c-.436-2.086 1.125-3.778 3-3.778.886 0 1.683.37 2.245.97a6.024 6.024 0 0 0 1.95-.74 3.066 3.066 0 0 1-1.35 1.692A6.117 6.117 0 0 0 19.5 7.35a6.471 6.471 0 0 1-1.542 1.594z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="pinterest" class="nc-icon-wrapper">
<path d="M22.5 3.75V20.25C22.5 21.4922 21.4922 22.5 20.25 22.5H8.7375C9.19687 21.7313 9.7875 20.625 10.0219 19.7203C10.1625 19.1813 10.7391 16.9828 10.7391 16.9828C11.1141 17.7 12.2109 18.3047 13.3781 18.3047C16.8516 18.3047 19.35 15.1125 19.35 11.1469C19.35 7.34531 16.2469 4.5 12.2531 4.5C7.28437 4.5 4.65 7.83281 4.65 11.4656C4.65 13.1531 5.55 15.2531 6.98438 15.9234C7.20469 16.0266 7.31719 15.9797 7.36875 15.7687C7.40625 15.6094 7.60313 14.8266 7.6875 14.4656C7.71563 14.3484 7.70156 14.25 7.60781 14.1375C7.13438 13.5609 6.75 12.5016 6.75 11.5125C6.75 8.97188 8.67187 6.51562 11.9484 6.51562C14.775 6.51562 16.7578 8.44219 16.7578 11.1984C16.7578 14.3109 15.1875 16.4672 13.1391 16.4672C12.0094 16.4672 11.1656 15.5344 11.4328 14.3859C11.7562 13.0172 12.3844 11.5406 12.3844 10.5516C12.3844 8.06719 8.84531 8.40938 8.84531 11.7234C8.84531 12.7406 9.1875 13.4344 9.1875 13.4344C7.71563 19.6594 7.49531 19.7391 7.8 22.4625L7.90313 22.5H3.75C2.50781 22.5 1.5 21.4922 1.5 20.25V3.75C1.5 2.50781 2.50781 1.5 3.75 1.5H20.25C21.4922 1.5 22.5 2.50781 22.5 3.75Z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="linkedin" class="nc-icon-wrapper">
<path d="M21 1.5H2.995C2.17 1.5 1.5 2.18 1.5 3.014v17.972c0 .834.67 1.514 1.495 1.514H21c.825 0 1.5-.68 1.5-1.514V3.014A1.51 1.51 0 0 0 21 1.5zm-13.153 18H4.734V9.478h3.118V19.5h-.005zM6.29 8.11a1.805 1.805 0 0 1 0-3.61c.993 0 1.804.81 1.804 1.805 0 .998-.806 1.804-1.804 1.804zM19.514 19.5h-3.112v-4.875c0-1.162-.024-2.658-1.618-2.658-1.622 0-1.87 1.266-1.87 2.574V19.5H9.802V9.478h2.986v1.369h.042c.417-.788 1.434-1.617 2.948-1.617 3.15 0 3.736 2.076 3.736 4.776V19.5z" fill="currentColor"></path>
</g>
</svg>
<svg viewBox="0 0 24 24">
<g id="dailymotion" class="nc-icon-wrapper">
<path d="M15.512 12.516C15.1636 12.3201 14.7697 12.2197 14.37 12.225C13.75 12.225 13.228 12.432 12.803 12.847C12.378 13.261 12.165 13.783 12.165 14.413C12.165 15.074 12.373 15.615 12.788 16.035C13.203 16.455 13.725 16.665 14.354 16.665C14.995 16.665 15.528 16.45 15.954 16.019C16.379 15.589 16.592 15.053 16.594 14.413C16.5962 14.0277 16.4976 13.6485 16.308 13.313C16.1215 12.9788 15.8459 12.7029 15.512 12.516ZM3.5 1.5C2.39543 1.5 1.5 2.39543 1.5 3.5V20.5C1.5 21.6046 2.39543 22.5 3.5 22.5H20.5C21.6046 22.5 22.5 21.6046 22.5 20.5V3.5C22.5 2.39543 21.6046 1.5 20.5 1.5H3.5ZM19.064 18.997H16.576V17.877H16.544C16.051 18.696 15.179 19.105 13.929 19.105C13.069 19.105 12.304 18.9 11.638 18.491C10.9765 18.0875 10.4416 17.5065 10.094 16.814C9.731 16.105 9.55 15.31 9.55 14.429C9.55 13.567 9.734 12.783 10.102 12.075C10.452 11.385 10.986 10.804 11.645 10.397C12.307 9.988 13.053 9.784 13.882 9.783C14.3579 9.77641 14.8308 9.85952 15.276 10.028C15.691 10.19 16.082 10.456 16.448 10.824V7.172L19.063 6.605L19.065 18.997H19.064Z" fill="currentColor"></path>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,36 @@
import * as React from 'react';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import Icons from '../../../../images/icons.svg';
declare const Application: IApplication;
interface FabBadgeProps {
icon: string,
iconWidth: string,
className?: string,
}
/**
* Renders a badge (parent needs to be position: relative)
*/
export const FabBadge: React.FC<FabBadgeProps> = ({ icon, iconWidth, className }) => {
return (
<div className={`fab-badge ${className || ''}`}>
<svg viewBox="0 0 24 24" width={iconWidth}>
<use href={`${Icons}#${icon}`}/>
</svg>
</div>
);
};
const FabBadgeWrapper: React.FC<FabBadgeProps> = ({ icon, iconWidth, className }) => {
return (
<Loader>
<FabBadge icon={icon} iconWidth={iconWidth} className={className} />
</Loader>
);
};
Application.Components.component('fabBadge', react2angular(FabBadgeWrapper, ['icon', 'iconWidth', 'className']));

View File

@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Loader } from '../base/loader';
import { ReserveButton } from './reserve-button';
import { User } from '../../models/user';
import { FabBadge } from '../base/fab-badge';
interface MachineCardProps {
user?: User,
@ -57,6 +58,7 @@ const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine,
return (
<div className={`machine-card ${loading ? 'loading' : ''} ${machine.disabled ? 'disabled' : ''} ${!machine.reservable ? 'unreservable' : ''}`}>
{machinePicture()}
{machine.space && user.role === 'admin' && <FabBadge icon='pin-map' iconWidth='3rem' /> }
<div className="machine-name">
{machine.name}
</div>

View File

@ -3,7 +3,7 @@ import { FormState, UseFormRegister, UseFormSetValue } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { User } from '../../models/user';
import { SocialNetwork } from '../../models/social-network';
import Icons from '../../../../images/social-icons.svg';
import Icons from '../../../../images/icons.svg';
import { FormInput } from '../form/form-input';
import { Trash } from 'phosphor-react';
import { useTranslation } from 'react-i18next';

View File

@ -8,7 +8,7 @@ import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { SettingName } from '../../models/setting';
import Icons from '../../../../images/social-icons.svg';
import Icons from '../../../../images/icons.svg';
import { Trash } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';

View File

@ -14,9 +14,13 @@ import { FormSwitch } from '../form/form-switch';
import { FormMultiFileUpload } from '../form/form-multi-file-upload';
import { FabButton } from '../base/fab-button';
import { Space } from '../../models/space';
import { Machine } from '../../models/machine';
import { AdvancedAccountingForm } from '../accounting/advanced-accounting-form';
import SettingAPI from '../../api/setting';
import { FabAlert } from '../base/fab-alert';
import MachineAPI from '../../api/machine';
import { FormMultiSelect } from '../form/form-multi-select';
import { SelectOption } from '../../models/select';
declare const Application: IApplication;
@ -41,6 +45,41 @@ export const SpaceForm: React.FC<SpaceFormProps> = ({ action, space, onError, on
SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).catch(onError);
}, []);
/**
* Asynchronously load the full list of machines to display in the drop-down select field
*/
const loadMachines = (inputValue: string, callback: (options: Array<SelectOption<number>>) => void): void => {
MachineAPI.index().then(data => {
callback(data.map(m => machineToOption(m)));
}).catch(error => onError(error));
};
/**
* Convert a machine to an option usable by react-select
*/
const machineToOption = (machine: Machine): SelectOption<number> => {
return { value: machine.id, label: machine.name };
};
/**
* Asynchronously load the full list of spaces to display in the drop-down select field
*/
const loadSpaces = (inputValue: string, callback: (options: Array<SelectOption<number>>) => void): void => {
SpaceAPI.index().then(data => {
if (space) {
data = data.filter((d) => d.id !== space.id);
}
callback(data.map(m => spaceToOption(m)));
}).catch(error => onError(error));
};
/**
* Convert a space to an option usable by react-select
*/
const spaceToOption = (space: Space): SelectOption<number> => {
return { value: space.id, label: space.name };
};
/**
* Callback triggered when the user validates the machine form: handle create or update
*/
@ -106,6 +145,29 @@ export const SpaceForm: React.FC<SpaceFormProps> = ({ action, space, onError, on
</div>
</section>
<section>
<header>
<p className="title">
{t('app.admin.space_form.associated_objects')}
</p>
<p className="description">
{t('app.admin.space_form.associated_objects_warning')}
</p>
</header>
<div className="content">
<FormMultiSelect control={control}
id="child_ids"
formState={formState}
label={t('app.admin.space_form.children_spaces')}
loadOptions={loadSpaces} />
<FormMultiSelect control={control}
id="machine_ids"
formState={formState}
label={t('app.admin.space_form.associated_machines')}
loadOptions={loadMachines} />
</div>
</section>
<section>
<header>
<p className="title">{t('app.admin.space_form.attachments')}</p>

View File

@ -33,5 +33,8 @@ export interface Machine {
slug: string,
}>,
advanced_accounting_attributes?: AdvancedAccounting,
machine_category_id?: number
machine_category_id?: number,
space: {
name: string
}
}

View File

@ -8,6 +8,7 @@ export interface Slot {
end: TDateISO,
is_reserved: boolean,
is_completed: boolean,
is_blocked?: boolean,
backgroundColor: 'white',
availability_id: number,

View File

@ -27,6 +27,7 @@
@import "modules/base/edit-destroy-buttons";
@import "modules/base/editorial-block";
@import "modules/base/fab-alert";
@import "modules/base/fab-badge";
@import "modules/base/fab-button";
@import "modules/base/fab-input";
@import "modules/base/fab-modal";

View File

@ -0,0 +1,15 @@
.fab-badge {
position: absolute;
top: 0;
right: 1.5rem;
padding: 0.8rem;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--secondary);
color: var(--secondary-text-color);
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
z-index: 1;
pointer-events: none;
}

View File

@ -67,12 +67,12 @@
@include colorVariant(var(--information), var(--gray-soft-lightest));
}
&.is-secondary {
@include colorVariant(var(--secondary), var(--gray-hard-darkest));
@include colorVariant(var(--secondary), var(--secondary-text-color));
}
&.is-black {
@include colorVariant(var(--gray-hard-darkest), var(--gray-soft-lightest));
}
&.is-main {
@include colorVariant(var(--main), var(--gray-soft-lightest));
@include colorVariant(var(--main), var(--main-text-color));
}
}

View File

@ -11,6 +11,94 @@
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 3.2rem;
.panel { margin-bottom: 0; }
.panel {
position: relative;
margin-bottom: 0;
}
}
&-relations {
padding: 1.6rem;
display: flex;
flex-direction: column;
align-items: flex-start;
row-gap: 1.6rem;
background-color: var(--gray-soft-light);
border-radius: var(--border-radius);
.space-parent {
@include text-lg(500);
color: var(--gray-hard-light);
margin: 0;
}
.space-current {
display: flex;
align-items: center;
gap: 0.8rem;
&.has-parent::before {
content: "";
display: block;
width: 1rem;
height: 1rem;
border-bottom: 1px solid var(--gray-hard-lightest);
border-left: 1px solid var(--gray-hard-lightest);
}
&-name {
padding: 0.8rem;
display: flex;
align-items: center;
gap: 0.8rem;
@include text-lg(600);
color: var(--main);
background-color: var(--gray-soft-lightest);
border-radius: var(--border-radius-sm);
svg { color: var(--gray-hard-darkest); }
}
}
.related-machines,
.related-spaces {
margin: 0;
display: flex;
flex-direction: column;
gap: 0.8rem;
list-style-type: none;
}
.related-spaces {
position: relative;
padding-inline-start: 2.6rem;
@include text-lg(500);
color: var(--gray-hard-light);
&::before {
position: absolute;
top: 0.5rem;
left: 0.8rem;
content: "";
display: block;
width: 1rem;
height: 1rem;
border-bottom: 1px solid var(--gray-hard-lightest);
border-left: 1px solid var(--gray-hard-lightest);
}
}
.related-machines {
position: relative;
&::before {
position: absolute;
top: 0.4rem;
left: 1.6rem;
content: "";
display: block;
width: 0.6rem;
height: 0.6rem;
border-bottom: 1px solid var(--gray-hard-lightest);
border-left: 1px solid var(--gray-hard-lightest);
}
}
}
}

View File

@ -48,6 +48,7 @@
<div class="spaces-grid">
<div ng-class="{'disabled-reservable' : space.disabled && spaceFiltering === 'all'}" ng-repeat="space in spaces | filterDisabled:spaceFiltering">
<div class="widget panel panel-default">
<fab-badge ng-if="isAuthorized('admin') && (space.parent || space.children.length)" icon="'pin-map'" icon-width="'3rem'"></fab-badge>
<div class="panel-heading picture" ng-if="!space.space_image_attributes" ng-click="showSpace(space)">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:'Font Awesome 5 Free'/icon" bs-holder class="img-responsive">
</div>

View File

@ -41,6 +41,23 @@
</div>
<div class="col-sm-12 col-md-12 col-lg-4">
<div class="spaces-relations m" ng-show="space.parent || space.children.length || space.machines.length">
<p ng-show="space.parent" class="space-parent">{{ space.parent.name }}</p>
<div class="space-current" ng-class="{'has-parent': space.parent}">
<span class="space-current-name">
<svg viewBox="0 0 24 24" width="3rem">
<use href="../../images/icons.svg#pin-map"/>
</svg>
{{ space.name }}
</span>
</div>
<ul ng-show="space.machines.length" class="related-machines">
<li ng-repeat="machine in space.machines" class="">{{ machine.name }}</li>
</ul>
<ul ng-show="space.children.length" class="related-spaces">
<li ng-repeat="child_space in space.children" class="">{{ child_space.name }}</li>
</ul>
</div>
<div class="widget panel b-a m m-t-lg" ng-show="space.characteristics">
<div class="panel-heading b-b small">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Space>]
# @param slots [ActiveRecord::Relation<Slot>]
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
Ancestry.default_ancestry_format = :materialized_path2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
class AddSpaceIdToMachines < ActiveRecord::Migration[7.0]
def change
add_reference :machines, :space, foreign_key: true, index: true
end
end

View File

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

View File

@ -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: '/'

View File

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

View File

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

View File

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