1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

(feat) places cache for slots

This commit is contained in:
Sylvain 2023-01-12 17:48:52 +01:00
parent 9f9a2e616f
commit 986a663c40
36 changed files with 838 additions and 180 deletions

View File

@ -57,6 +57,7 @@
- Fix a security issue: updated rack to 2.2.6.2 to fix [CVE-2022-44571](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-44571)
- Fix a security issue: updated globalid to 1.0.1 to fix [CVE-2023-22799](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-22799)
- [TODO DEPLOY] `rails fablab:fix:invoice_items_in_error` THEN `rails fablab:fix_invoice_items` THEN `rails db:migrate`
- [TODO DEPLOY] `rails fablab:fix_availabilities` THEN `rails fablab:setup:build_places_cache`
## v5.6.5 2023 January 9

View File

@ -116,7 +116,7 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
};
/**
* Change cart's customer by admin/manger
* Change cart's customer by admin/manager
*/
const handleChangeMember = (user: User): void => {
CartAPI.setCustomer(cart, user.id).then(setCart).catch(onError);

View File

@ -0,0 +1,74 @@
import React, { useEffect, useState } from 'react';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { Slot } from '../../models/slot';
import { ReservableType } from '../../models/reservation';
import { User } from '../../models/user';
import { MemberSelect } from '../user/member-select';
import CartAPI from '../../api/cart';
import useCart from '../../hooks/use-cart';
import { ReservationsSummary } from './reservations-summary';
import UserLib from '../../lib/user';
declare const Application: IApplication;
interface OngoingReservationPanelProps {
selectedSlot: Slot,
reservableId: number,
reservableType: ReservableType,
onError: (message: string) => void,
operator: User,
onSlotAdded: (slot: Slot) => void,
onSlotRemoved: (slot: Slot) => void,
}
/**
* Panel to manage the ongoing reservation (select the member, show the price, etc)
*/
export const OngoingReservationPanel: React.FC<OngoingReservationPanelProps> = ({ selectedSlot, reservableId, reservableType, onError, operator, onSlotAdded, onSlotRemoved }) => {
const { cart, setCart } = useCart(operator);
const [noMemberError, setNoMemberError] = useState<boolean>(false);
useEffect(() => {
if (selectedSlot && !cart.user) {
setNoMemberError(true);
onError('please select a member first');
}
}, [selectedSlot]);
/**
* The admin/manager can change the cart's customer
*/
const handleChangeMember = (user: User): void => {
CartAPI.setCustomer(cart, user.id).then(setCart).catch(onError);
};
return (
<div className="ongoing-reservation-panel">
{new UserLib(operator).hasPrivilegedRole() &&
<MemberSelect onSelected={handleChangeMember}
defaultUser={cart?.user as User}
hasError={noMemberError} />
}
<ReservationsSummary reservableId={reservableId}
reservableType={reservableType}
onError={onError}
cart={cart}
setCart={setCart}
customer={cart?.user as User}
onSlotAdded={onSlotAdded}
onSlotRemoved={onSlotRemoved}
slot={selectedSlot} />
</div>
);
};
const OngoingReservationPanelWrapper: React.FC<OngoingReservationPanelProps> = (props) => (
<Loader>
<OngoingReservationPanel {...props} />
</Loader>
);
Application.Components.component('ongoingReservationPanel', react2angular(OngoingReservationPanelWrapper, ['selectedSlot', 'reservableId', 'reservableType', 'onError', 'operator', 'onSlotRemoved', 'onSlotAdded']));

View File

@ -1,20 +1,18 @@
import { IApplication } from '../../models/application';
import React, { useEffect } from 'react';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import React, { ReactNode, useEffect, useState } from 'react';
import type { Slot } from '../../models/slot';
import { useImmer } from 'use-immer';
import FormatLib from '../../lib/format';
import { FabButton } from '../base/fab-button';
import { ShoppingCart } from 'phosphor-react';
import CartAPI from '../../api/cart';
import useCart from '../../hooks/use-cart';
import type { User } from '../../models/user';
import Switch from 'react-switch';
import { CartItemReservation } from '../../models/cart_item';
import { ReservableType } from '../../models/reservation';
declare const Application: IApplication;
import { Order } from '../../models/order';
import PriceAPI from '../../api/price';
import { PaymentMethod } from '../../models/payment';
import { ComputePriceResult } from '../../models/price';
interface ReservationsSummaryProps {
slot: Slot,
@ -22,15 +20,19 @@ interface ReservationsSummaryProps {
reservableId: number,
reservableType: ReservableType,
onError: (error: string) => void,
cart: Order,
setCart: (cart: Order) => void,
onSlotAdded: (slot: Slot) => void,
onSlotRemoved: (slot: Slot) => void,
}
/**
* Display a summary of the selected slots, and ask for confirmation before adding them to the cart
*/
const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, customer, reservableId, reservableType, onError }) => {
const { cart, setCart } = useCart(customer);
export const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, customer, reservableId, reservableType, onError, cart, setCart, onSlotAdded, onSlotRemoved }) => {
const [pendingSlots, setPendingSlots] = useImmer<Array<Slot>>([]);
const [offeredSlots, setOfferedSlots] = useImmer<Map<number, boolean>>(new Map());
const [price, setPrice] = useState<ComputePriceResult>(null);
const [reservation, setReservation] = useImmer<CartItemReservation>({
reservation: {
reservable_id: reservableId,
@ -42,18 +44,30 @@ const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, custome
useEffect(() => {
if (slot) {
if (pendingSlots.find(s => s.slot_id === slot.slot_id)) {
setPendingSlots(draft => draft.filter(s => s.slot_id !== slot.slot_id));
removeSlot(slot)();
} else {
setPendingSlots(draft => { draft.push(slot); });
addSlot(slot);
}
}
}, [slot]);
useEffect(() => {
if (customer && cart) {
CartAPI.setCustomer(cart, customer.id).then(setCart).catch(onError);
}
}, [customer]);
if (!customer) return;
PriceAPI.compute({
customer_id: customer.id,
items: [reservation],
payment_method: PaymentMethod.Other
}).then(setPrice).catch(onError);
}, [reservation]);
/**
* Add a new slot to the pending list
*/
const addSlot = (slot: Slot) => {
setPendingSlots(draft => { draft.push(slot); });
if (typeof onSlotAdded === 'function') onSlotAdded(slot);
};
/**
* Add the product to cart
@ -69,6 +83,33 @@ const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, custome
};
};
/**
* Check if the reservation contains the given slot
*/
const isSlotInReservation = (slot: Slot): boolean => {
return reservation.reservation.slots_reservations_attributes.filter(s => s.slot_id === slot.slot_id).length > 0;
};
/**
* Removes the given slot from the reservation (if applicable) and trigger the onSlotRemoved callback
*/
const removeSlot = (slot: Slot) => {
return () => {
if (isSlotInReservation(slot)) {
setReservation(draft => {
return {
reservation: {
...draft.reservation,
slots_reservations_attributes: draft.reservation.slots_reservations_attributes.filter(sr => sr.slot_id !== slot.slot_id)
}
};
});
}
setPendingSlots(draft => draft.filter(s => s.slot_id !== slot.slot_id));
if (typeof onSlotRemoved === 'function') onSlotRemoved(slot);
};
};
/**
* Build / validate the reservation at server-side, then add it to the cart.
*/
@ -93,26 +134,42 @@ const ReservationsSummary: React.FC<ReservationsSummaryProps> = ({ slot, custome
};
};
/**
* Return the price of the given slot, if known
*/
const slotPrice = (slot: Slot): string => {
if (!price) return '';
const slotPrice = price.details.slots.find(s => s.slot_id === slot.slot_id);
if (!slotPrice) return '';
return FormatLib.price(slotPrice.price);
};
/**
* Return the total price for the current reservation
*/
const total = (): ReactNode => {
if (!price || reservation.reservation.slots_reservations_attributes.length === 0) return '';
return <span>TOTAL: {FormatLib.price(price?.price)}</span>;
};
return (
<div>
<ul>{pendingSlots.map(slot => (
<li key={slot.slot_id}>
<span>{FormatLib.date(slot.start)} {FormatLib.time(slot.start)} - {FormatLib.time(slot.end)}</span>
<label>offered? <Switch checked={offeredSlots.get(slot.slot_id)} onChange={offerSlot(slot)} /></label>
<FabButton onClick={addSlotToReservation(slot)}>validate this slot</FabButton>
<label>offered? <Switch checked={offeredSlots.get(slot.slot_id) || false} onChange={offerSlot(slot)} /></label>
<span className="price">{slotPrice(slot)}</span>
{!isSlotInReservation(slot) && <FabButton onClick={addSlotToReservation(slot)}>Ajouter à la réservation</FabButton>}
<FabButton onClick={removeSlot(slot)}>Enlever</FabButton>
</li>
))}</ul>
{total()}
{reservation.reservation.slots_reservations_attributes.length > 0 && <div>
<FabButton onClick={addReservationToCart}><ShoppingCart size={24}/>Ajouter au panier</FabButton>
</div>}
</div>
);
};
const ReservationsSummaryWrapper: React.FC<ReservationsSummaryProps> = (props) => (
<Loader>
<ReservationsSummary {...props} />
</Loader>
);
Application.Components.component('reservationsSummary', react2angular(ReservationsSummaryWrapper, ['slot', 'customer', 'reservableId', 'reservableType', 'onError']));

View File

@ -15,7 +15,7 @@ interface MemberSelectProps {
}
/**
* This component renders the member select for manager.
* This component allows privileged users (managers/admins) to select a user on whose behalf to act.
*/
export const MemberSelect: React.FC<MemberSelectProps> = ({ defaultUser, value, onSelected, noHeader, hasError }) => {
const { t } = useTranslation('public');

View File

@ -469,21 +469,23 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
/**
* Change the last selected slot's appearance to looks like 'added to cart'
*/
$scope.markSlotAsAdded = function () {
$scope.selectedEvent.backgroundColor = FREE_SLOT_BORDER_COLOR;
$scope.selectedEvent.oldTitle = $scope.selectedEvent.title;
$scope.selectedEvent.title = _t('app.logged.machines_reserve.i_reserve');
updateEvents($scope.selectedEvent);
$scope.markSlotAsAdded = function (slot) {
let calSlot = { ...slot };
calSlot.backgroundColor = FREE_SLOT_BORDER_COLOR;
calSlot.oldTitle = $scope.selectedEvent.title;
calSlot.title = _t('app.logged.machines_reserve.i_reserve');
updateEvents(calSlot);
};
/**
* Change the last selected slot's appearance to looks like 'never added to cart'
*/
$scope.markSlotAsRemoved = function (slot) {
slot.backgroundColor = 'white';
slot.borderColor = FREE_SLOT_BORDER_COLOR;
slot.title = slot.oldTitle;
updateEvents(slot);
let calSlot = { ...slot };
calSlot.backgroundColor = 'white';
calSlot.borderColor = FREE_SLOT_BORDER_COLOR;
calSlot.title = calSlot.oldTitle;
updateEvents(calSlot);
};
/**

View File

@ -529,7 +529,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
}
// slot is not in the cart, so we add it
$scope.events.reserved.push($scope.slot);
if (typeof $scope.onSlotAddedToCart === 'function') { $scope.onSlotAddedToCart(); }
if (typeof $scope.onSlotAddedToCart === 'function') { $scope.onSlotAddedToCart($scope.slot); }
} else {
// slot is in the cart, remove it
$scope.removeSlot($scope.slot, index);

View File

@ -20,6 +20,13 @@ export default class UserLib {
return false;
};
/**
* Check if the current user has a privileged role
*/
hasPrivilegedRole = (): boolean => {
return (this.user?.role === 'admin' || this.user?.role === 'manager');
};
/**
* Filter social networks from the user's profile
*/

View File

@ -23,6 +23,7 @@ export interface ComputePriceResult {
price_without_coupon: number,
details?: {
slots: Array<{
slot_id: number,
start_at: TDateISO,
price: number,
promo: boolean

View File

@ -1,5 +1,6 @@
@import "variables/animations";
@import "variables/colors";
@import "variables/component";
@import "variables/decoration";
@import "variables/layout";
@import "variables/typography";

View File

@ -174,18 +174,9 @@
aside {
& > div {
margin-bottom: 3.2rem;
padding: 1.6rem;
background-color: var(--gray-soft-lightest);
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
h3,
.member-select-title {
margin: 0 0 2.4rem;
padding-bottom: 1.2rem;
border-bottom: 1px solid var(--gray-hard);
@include title-base;
color: var(--gray-hard-dark) !important;
@include component-border;
h3 {
@include component-title;
}
}
.checkout {

View File

@ -1,4 +1,8 @@
.member-select {
@include component-border;
&-title {
@include component-title;
}
&.error {
.select-input > div:first-of-type {
border-color: var(--alert);

View File

@ -0,0 +1,15 @@
@mixin component-border {
margin-bottom: 3.2rem;
padding: 1.6rem;
background-color: var(--gray-soft-lightest);
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
}
@mixin component-title {
margin: 0 0 2.4rem;
padding-bottom: 1.2rem;
border-bottom: 1px solid var(--gray-hard);
@include title-base;
color: var(--gray-hard-dark) !important;
}

View File

@ -31,9 +31,14 @@
<div class="col-sm-12 col-md-12 col-lg-3">
<div ng-if="isAuthorized(['admin', 'manager'])">
<select-member></select-member>
</div>
<ongoing-reservation-panel selected-slot="selectedEvent"
on-slot-added="markSlotAsAdded"
on-slot-removed="markSlotAsRemoved"
on-error="onError"
reservable-id="machine.id"
reservable-type="'Machine'"
operator="currentUser">
</ongoing-reservation-panel>
<packs-summary item="machine"
item-type="'Machine'"
@ -45,7 +50,9 @@
refresh="afterPaymentPromise">
</packs-summary>
<reservations-summary slot="selectedEvent" customer="ctrl.member" on-error="onError" reservable-id="machine.id" reservable-type="'Machine'"></reservations-summary>
<div ng-if="isAuthorized(['admin', 'manager'])">
<select-member></select-member>
</div>
<cart slot="selectedEvent"
slot-selection-time="selectionTime"

View File

@ -67,4 +67,8 @@ class CartItem::EventReservation < CartItem::Reservation
def type
'event'
end
def total_tickets
(normal_tickets || 0) + (cart_item_event_reservation_tickets.map(&:booked).reduce(:+) || 0)
end
end

View File

@ -2,6 +2,8 @@
# A generic reservation added to the shopping cart
class CartItem::Reservation < CartItem::BaseItem
self.table_name = 'cart_item_reservations'
MINUTES_PER_HOUR = 60.0
SECONDS_PER_MINUTE = 60.0
@ -104,20 +106,26 @@ class CartItem::Reservation < CartItem::BaseItem
0
end
##
# Group the slots by date, if the extended_prices_in_same_day option is set to true
##
# @return Hash{Symbol => Array<CartItem::ReservationSlot>}
def grouped_slots
return { all: cart_item_reservation_slots } unless Setting.get('extended_prices_in_same_day')
cart_item_reservation_slots.group_by { |slot| slot.slot[:start_at].to_date }
end
##
# Compute the price of a single slot, according to the list of applicable prices.
# @param prices {{ prices: Array<{price: Price, duration: number}> }} list of prices to use with the current reservation
# @see get_slot_price
##
# @param prices [Hash{Symbol => Array<Hash{Symbol => Number,Price}>}] list of prices to use with the current reservation,
# as returned by #get_slot_price
# @option prices [Array<Hash{Symbol => Number,Price}>}] :prices => {price: Price, duration: number}
# @param slot_reservation [CartItem::ReservationSlot]
# @param is_privileged [Boolean]
# @param options [Hash] optional parameters, allowing the following options:
# @option options [Array] :elements if provided the resulting price will be append into elements.slots
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
# @return [Float]
def get_slot_price_from_prices(prices, slot_reservation, is_privileged, options = {})
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
@ -134,19 +142,17 @@ class CartItem::Reservation < CartItem::BaseItem
real_price
end
##
# Compute the price of a single slot, according to the base price and the ability for an admin
# to offer the slot.
# @param hourly_rate {Number} base price of a slot
# @param slot_reservation {CartItem::ReservationSlot}
# @param is_privileged {Boolean} true if the current user has a privileged role (admin or manager)
# @param [options] {Hash} optional parameters, allowing the following options:
# - elements {Array} if provided the resulting price will be append into elements.slots
# - has_credits {Boolean} true if the user still has credits for the given slot, false if not provided
# - is_division {boolean} false if the slot covers a full availability, true if it is a subdivision (default)
# - prepaid_minutes {Number} number of remaining prepaid minutes for the customer
# @return {Number} price of the slot
##
# @param hourly_rate [Float] base price of a slot
# @param slot_reservation [CartItem::ReservationSlot]
# @param is_privileged [Boolean] true if the current user has a privileged role (admin or manager)
# @param options [Hash] optional parameters, allowing the following options:
# @option options [Array] :elements if provided the resulting price will be append into elements.slots
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
# @return [Float] price of the slot
def get_slot_price(hourly_rate, slot_reservation, is_privileged, options = {})
options = GET_SLOT_PRICE_DEFAULT_OPTS.merge(options)
@ -158,16 +164,12 @@ class CartItem::Reservation < CartItem::BaseItem
else
slot_rate
end
# subtract free minutes from prepaid packs
if real_price.positive? && options[:prepaid][:minutes]&.positive?
consumed = slot_minutes
consumed = options[:prepaid][:minutes] if slot_minutes > options[:prepaid][:minutes]
real_price = (Rational(slot_minutes - consumed) * (Rational(slot_rate) / Rational(MINUTES_PER_HOUR))).to_f
options[:prepaid][:minutes] -= consumed
end
real_price = handle_prepaid_pack_price(real_price, slot_minutes, slot_rate, options)
unless options[:elements].nil?
options[:elements][:slots].push(
slot_id: slot_reservation.slot_id,
start_at: slot_reservation.slot[:start_at],
price: real_price,
promo: (slot_rate != hourly_rate)
@ -176,15 +178,35 @@ class CartItem::Reservation < CartItem::BaseItem
real_price
end
# @param price [Float, Integer]
# @param slot_minutes [Number]
# @param options [Hash] optional parameters, allowing the following options:
# @option options [Array] :elements if provided the resulting price will be append into elements.slots
# @option options [Boolean] :has_credits true if the user still has credits for the given slot, false if not provided
# @option options [Boolean] :is_division false if the slot covers a full availability, true if it is a subdivision (default)
# @option options [Number] :prepaid_minutes number of remaining prepaid minutes for the customer
# @return [Float, Integer]
def handle_prepaid_pack_price(price, slot_minutes, slot_rate, options)
return price unless price.positive? && options[:prepaid][:minutes]&.positive?
# subtract free minutes from prepaid packs
consumed = slot_minutes
consumed = options[:prepaid][:minutes] if slot_minutes > options[:prepaid][:minutes]
options[:prepaid][:minutes] -= consumed
(Rational(slot_minutes - consumed) * (Rational(slot_rate) / Rational(MINUTES_PER_HOUR))).to_f
end
# We determine the list of prices applicable to current reservation
# The longest available price is always used in priority.
# Eg. If the reservation is for 12 hours, and there are prices for 3 hours, 7 hours,
# and the base price (1 hours), we use the 7 hours price, then 3 hours price, and finally the base price twice (7+3+1+1 = 12).
# All these prices are returned to be applied to the reservation.
# @param slots_reservations [Array<CartItem::ReservationSlot>]
# @return [Hash{Symbol => Array<Hash{Symbol => Number, Price}>}]
def applicable_prices(slots_reservations)
total_duration = slots_reservations.map do |slot|
(slot.slot[:end_at].to_time - slot.slot[:start_at].to_time) / SECONDS_PER_MINUTE
end.reduce(:+)
end.reduce(:+) || 0
rates = { prices: [] }
remaining_duration = total_duration

View File

@ -18,4 +18,39 @@ class CartItem::ReservationSlot < ApplicationRecord
belongs_to :slot
belongs_to :slots_reservation
after_create :add_to_places_cache
after_update :remove_from_places_cache, if: :canceled?
before_destroy :remove_from_places_cache
private
def add_to_places_cache
update_places_cache(:+)
end
def remove_from_places_cache
update_places_cache(:-)
end
# @param operation [Symbol] :+ or :-
def update_places_cache(operation)
user_method = operation == :+ ? :add_users : :remove_users
if cart_item_type == 'CartItem::EventReservation'
Slots::PlacesCacheService.change_places(slot, 'Event', cart_item.event_id, cart_item.total_tickets, operation)
Slots::PlacesCacheService.send(user_method,
slot,
'Event',
cart_item.event_id,
[cart_item.customer_profile.user_id])
else
Slots::PlacesCacheService.change_places(slot, cart_item.reservable_type, cart_item.reservable_id, 1, operation)
Slots::PlacesCacheService.send(user_method,
slot,
cart_item.reservable_type,
cart_item.reservable_id,
[cart_item.customer_profile.user_id])
end
end
end

View File

@ -22,6 +22,7 @@ class ShoppingCart
end
# compute the price details of the current shopping cart
# @return [Hash]
def total
total_amount = 0
all_elements = { slots: [] }

View File

@ -12,17 +12,25 @@ class Slot < ApplicationRecord
has_many :cart_item_reservation_slots, class_name: 'CartItem::ReservationSlot', dependent: :destroy
after_create_commit :create_places_cache
attr_accessor :is_reserved, :machine, :space, :title, :can_modify, :current_user_slots_reservations_ids, :current_user_pending_reservations_ids
# @param reservable [Machine, Space, Training, Event, NilClass]
def full?(reservable = nil)
availability_places = availability.available_places_per_slot(reservable)
return false if availability_places.nil?
if reservable.nil?
slots_reservations.where(canceled_at: nil).count >= availability_places
else
slots_reservations.includes(:reservation).where(canceled_at: nil).where('reservations.reservable': reservable).count >= availability_places
end
reserved_places = if reservable.nil?
places.pluck('reserved_places').reduce(:+)
else
rp = places.detect do |p|
p['reservable_type'] == reservable.class.name && p['reservable_id'] == reservable&.id
end
rp['reserved_places']
end
reserved_places >= availability_places
end
def empty?(reservable = nil)
@ -36,4 +44,10 @@ class Slot < ApplicationRecord
def duration
(end_at - start_at).seconds
end
private
def create_places_cache
Slots::PlacesCacheService.refresh(self)
end
end

View File

@ -7,11 +7,15 @@ class SlotsReservation < ApplicationRecord
belongs_to :reservation
has_one :cart_item_reservation_slot, class_name: 'CartItem::ReservationSlot', dependent: :nullify
after_create :add_to_places_cache
after_update :set_ex_start_end_dates_attrs, if: :slot_changed?
after_update :notify_member_and_admin_slot_is_modified, if: :slot_changed?
after_update :notify_member_and_admin_slot_is_canceled, if: :canceled?
after_update :update_event_nb_free_places, if: :canceled?
after_update :remove_from_places_cache, if: :canceled?
before_destroy :remove_from_places_cache
def set_ex_start_end_dates_attrs
update_columns(ex_start_at: previous_slot.start_at, ex_end_at: previous_slot.end_at) # rubocop:disable Rails/SkipsModelValidations
@ -37,6 +41,29 @@ class SlotsReservation < ApplicationRecord
reservation.update_event_nb_free_places
end
def add_to_places_cache
update_places_cache(:+)
Slots::PlacesCacheService.add_users(slot, reservation.reservable_type, reservation.reservable_id, [reservation.statistic_profile.user_id])
end
def remove_from_places_cache
update_places_cache(:-)
Slots::PlacesCacheService.remove_users(slot, reservation.reservable_type, reservation.reservable_id, [reservation.statistic_profile.user_id])
end
# @param operation [Symbol] :+ or :-
def update_places_cache(operation)
if reservation.reservable_type == 'Event'
Slots::PlacesCacheService.change_places(slot,
reservation.reservable_type,
reservation.reservable_id,
reservation.total_booked_seats,
operation)
else
Slots::PlacesCacheService.change_places(slot, reservation.reservable_type, reservation.reservable_id, 1, operation)
end
end
def notify_member_and_admin_slot_is_modified
NotificationCenter.call type: 'notify_member_slot_is_modified',
receiver: reservation.user,

View File

@ -2,6 +2,7 @@
# Provides helper methods checking reservation status of any availabilities
class Availabilities::StatusService
# @param current_user_role [String]
def initialize(current_user_role)
@current_user_role = current_user_role
@show_name = (%w[admin manager].include?(@current_user_role) || (current_user_role && Setting.get('display_name_enable')))
@ -10,7 +11,7 @@ class Availabilities::StatusService
# check that the provided slot is reserved for the given reservable (machine, training or space).
# Mark it accordingly for display in the calendar
# @param slot [Slot]
# @param user [User]
# @param user [User] the customer
# @param reservables [Array<Machine, Space, Training, Event>]
# @return [Slot]
def slot_reserved_status(slot, user, reservables)
@ -19,24 +20,24 @@ class Availabilities::StatusService
"#{reservables.map(&:class).map(&:name).uniq} , with slot #{slot.id}")
end
slots_reservations, user_slots_reservations = slots_reservations(slot.slots_reservations, reservables, user)
pending_reserv_slot_ids = slot.cart_item_reservation_slots.select('id').map(&:id)
pending_reservations, user_pending_reservations = pending_reservations(pending_reserv_slot_ids, reservables, user)
is_reserved = slots_reservations.count.positive? || pending_reservations.count.positive?
places = places(slot, reservables)
is_reserved = places.any? { |p| p['reserved_places'].positive? }
is_reserved_by_user = is_reserved && places.select { |p| p['user_ids'].include?(user.id) }.length.positive?
slot.is_reserved = is_reserved
slot.title = slot_title(slots_reservations, user_slots_reservations, user_pending_reservations, reservables)
slot.title = slot_title(slot, is_reserved, is_reserved_by_user, reservables)
slot.can_modify = true if %w[admin manager].include?(@current_user_role) || is_reserved
slot.current_user_slots_reservations_ids = user_slots_reservations.select('id').map(&:id)
slot.current_user_pending_reservations_ids = user_pending_reservations.select('id').map(&:id)
if is_reserved_by_user
user_reservations = Slots::ReservationsService.user_reservations(slot, user, reservables.first.class.name)
slot.current_user_slots_reservations_ids = user_reservations[:reservations].select('id').map(&:id)
slot.current_user_pending_reservations_ids = user_reservations[:pending].select('id').map(&:id)
end
slot
end
# check that the provided ability is reserved by the given user
# @param availability [Availability]
# @param user [User]
# @param user [User] the customer
# @param reservables [Array<Machine, Space, Training, Event>]
# @return [Availability]
def availability_reserved_status(availability, user, reservables)
@ -45,85 +46,54 @@ class Availabilities::StatusService
"#{reservables.map(&:class).map(&:name).uniq}, with availability #{availability.id}")
end
slots_reservations, user_slots_reservations = slots_reservations(availability.slots_reservations, reservables, user)
slots = availability.slots.map do |slot|
slot_reserved_status(slot, user, reservables)
end
pending_reserv_slot_ids = availability.joins(slots: :cart_item_reservation_slots)
.select('cart_item_reservation_slots.id as cirs_id')
pending_reservations, user_pending_reservations = pending_reservations(pending_reserv_slot_ids, reservables, user)
availability.is_reserved = slots_reservations.count.positive? || pending_reservations.count.positive?
availability.current_user_slots_reservations_ids = user_slots_reservations.select('id').map(&:id)
availability.current_user_pending_reservations_ids = user_pending_reservations.select('id').map(&:id)
availability.is_reserved = slots.any?(&:is_reserved)
availability.current_user_slots_reservations_ids = slots.map(&:current_user_slots_reservations_ids).flatten
availability.current_user_pending_reservations_ids = slots.map(&:current_user_pending_reservations_ids).flatten
availability
end
private
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
# @param user_slots_reservations [ActiveRecord::Relation<SlotsReservation>] same as slots_reservations but filtered by the current user
# @param user_pending_reservations [ActiveRecord::Relation<CartItem::ReservationSlot>]
# @param slot [Slot]
# @param reservables [Array<Machine, Space, Training, Event>]
def slot_title(slots_reservations, user_slots_reservations, user_pending_reservations, reservables)
# @return [Array<Hash>]
def places(slot, reservables)
places = []
reservables.each do |reservable|
places.push(slot.places.detect { |p| p['reservable_type'] == reservable.class.name && p['reservable_id'] == reservable.id })
end
places
end
# @param slot [Slot]
# @param is_reserved [Boolean]
# @param is_reserved_by_user [Boolean]
# @param reservables [Array<Machine, Space, Training, Event>]
def slot_title(slot, is_reserved, is_reserved_by_user, reservables)
name = reservables.map(&:name).join(', ')
if user_slots_reservations.count.zero? && slots_reservations.count.zero?
if !is_reserved && !is_reserved_by_user
name
elsif user_slots_reservations.count.zero? && slots_reservations.count.positive?
"#{name} #{@show_name ? "- #{slot_users_names(slots_reservations)}" : ''}"
elsif user_pending_reservations.count.positive?
"#{name} - #{I18n.t('availabilities.reserving')}"
elsif is_reserved && !is_reserved_by_user
"#{name} #{@show_name ? "- #{slot_users_names(slot, reservables)}" : ''}"
else
"#{name} - #{I18n.t('availabilities.i_ve_reserved')}"
end
end
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
# @param slot [Slot]
# @param reservables [Array<Machine, Space, Training, Event>]
# @return [String]
def slot_users_names(slots_reservations)
slots_reservations.map(&:reservation)
.map(&:user)
.map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') }
.join(', ')
end
# @param slot_ids [Array<number>]
# @param reservables [Array<Machine, Space, Training, Event>]
# @param user [User]
# @return [Array<ActiveRecord::Relation<CartItem::ReservationSlot>>]
def pending_reservations(slot_ids, reservables, user)
reservable_types = reservables.map(&:class).map(&:name).uniq
if reservable_types.size > 1
raise TypeError("[Availabilities::StatusService#pending_reservations] reservables have differents types: #{reservable_types}")
end
relation = "cart_item_#{reservable_types.first&.downcase}_reservation"
table = reservable_types.first == 'Event' ? 'cart_item_event_reservations' : 'cart_item_reservations'
pending_reservations = CartItem::ReservationSlot.where(id: slot_ids)
.includes(relation.to_sym)
.where(table => { reservable_type: reservable_types })
.where(table => { reservable_id: reservables.map(&:id) })
user_pending_reservations = pending_reservations.where(table => { customer_profile_id: user&.invoicing_profile&.id })
[pending_reservations, user_pending_reservations]
end
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
# @param reservables [Array<Machine, Space, Training, Event>]
# @param user [User]
# @return [Array<ActiveRecord::Relation<SlotsReservation>>]
def slots_reservations(slots_reservations, reservables, user)
reservable_types = reservables.map(&:class).map(&:name).uniq
if reservable_types.size > 1
raise TypeError("[Availabilities::StatusService#slot_reservations] reservables have differents types: #{reservable_types}")
end
reservations = slots_reservations.includes(:reservation)
.where('reservations.reservable_type': reservable_types)
.where('reservations.reservable_id': reservables.map(&:id))
.where('slots_reservations.canceled_at': nil)
user_slots_reservations = reservations.where('reservations.statistic_profile_id': user&.statistic_profile&.id)
[reservations, user_slots_reservations]
def slot_users_names(slot, reservables)
user_ids = slot.places
.select { |p| p['reservable_type'] == reservables.first.class.name && reservables.map(&:id).includes?(p['reservable_id']) }
.pluck('user_ids')
.flatten
User.where(id: user_ids).includes(:profile)
.map { |u| u&.profile&.full_name || I18n.t('availabilities.deleted_user') }
.join(', ')
end
end

View File

@ -0,0 +1,134 @@
# frozen_string_literal: true
# Services around slots
module Slots; end
# Maintain the cache of reserved places for a slot
class Slots::PlacesCacheService
class << self
# @param slot [Slot]
def refresh(slot)
return if slot.nil?
reservables = case slot.availability.available_type
when 'machines'
slot.availability.machines
when 'training'
slot.availability.trainings
when 'space'
slot.availability.spaces
when 'event'
Event.where(id: slot.availability.event.id)
else
Rails.logger.warn "[Slots::PlacesCacheService#update] Availability #{slot.availability_id} with unknown " \
"type #{slot.availability.available_type}"
[nil]
end
browser = reservables.respond_to?(:find_each) ? :find_each : :each
places = []
reservables.try(browser) do |reservable|
reservations = Slots::ReservationsService.reservations(slot.slots_reservations, [reservable])
pending = Slots::ReservationsService.pending_reservations(slot.cart_item_reservation_slots.map(&:id), [reservable])
places.push({
reservable_type: reservable.class.name,
reservable_id: reservable.try(&:id),
reserved_places: (reservations[:reservations].count || 0) + (pending[:reservations].count || 0),
user_ids: reservations[:user_ids] + pending[:user_ids]
})
end
slot.update(places: places)
end
# @param slot [Slot]
# @param reservable_type [String]
# @param reservable_id [Number]
# @param places [Number]
# @param operation [Symbol] :+ OR :-
def change_places(slot, reservable_type, reservable_id, places, operation = :+)
return if slot.nil?
ActiveRecord::Base.connection.execute <<-SQL.squish
with reservable_places as (
select ('{'||index-1||',reserved_places}')::text[] as path
,(place->>'reserved_places')::decimal as reserved_places
from slots
,jsonb_array_elements(places) with ordinality arr(place, index)
where place->>'reservable_type' = '#{reservable_type}'
and place->>'reservable_id' = '#{reservable_id}'
and id = #{slot.id}
)
update slots
set places = jsonb_set(places, reservable_places.path, (reservable_places.reserved_places #{operation} #{places})::varchar::jsonb, true)
from reservable_places
where id = #{slot.id};
SQL
end
# @param slot [Slot]
# @param reservable_type [String]
# @param reservable_id [Number]
# @param user_ids [Array<Number>]
def remove_users(slot, reservable_type, reservable_id, user_ids)
return if slot.nil?
ActiveRecord::Base.connection.execute <<-SQL.squish
with users as (
select ('{'||index-1||',user_ids}')::text[] as path
,place->>'user_ids' as user_ids
from slots
,jsonb_array_elements(places) with ordinality arr(place, index)
where place->>'reservable_type' = '#{reservable_type}'
and place->>'reservable_id' = '#{reservable_id}'
and id = #{slot.id}
),
all_users as (
select (ids.id)::text::int as all_ids
from users
,jsonb_array_elements(users.user_ids) with ordinality ids(id, index)
),
remaining_users as (
SELECT array_to_json(array(SELECT unnest(array_agg(all_users.all_ids)) EXCEPT SELECT unnest('{#{user_ids.to_s.gsub(/\]| |\[|/, '')}}'::int[])))::jsonb as ids
from all_users
)
update slots
set places = jsonb_set(places, users.path, remaining_users.ids, false)
from users, remaining_users
where id = #{slot.id};
SQL
end
# @param slot [Slot]
# @param reservable_type [String]
# @param reservable_id [Number]
# @param user_ids [Array<Number>]
def add_users(slot, reservable_type, reservable_id, user_ids)
return if slot.nil?
ActiveRecord::Base.connection.execute <<-SQL.squish
with users as (
select ('{'||index-1||',user_ids}')::text[] as path
,place->>'user_ids' as user_ids
from slots
,jsonb_array_elements(places) with ordinality arr(place, index)
where place->>'reservable_type' = '#{reservable_type}'
and place->>'reservable_id' = '#{reservable_id}'
and id = #{slot.id}
),
all_users as (
select (ids.id)::text::int as all_ids
from users
,jsonb_array_elements(users.user_ids::jsonb) with ordinality ids(id, index)
),
new_users as (
SELECT array_to_json(array_cat(array_agg(all_users.all_ids), '{#{user_ids.to_s.gsub(/\]| |\[|/, '')}}'::int[]))::jsonb as ids
from all_users
)
update slots
set places = jsonb_set(places, users.path, new_users.ids, false)
from users, new_users
where id = #{slot.id};
SQL
end
end
end

View File

@ -0,0 +1,93 @@
# frozen_string_literal: true
# Services around slots
module Slots; end
# Check the reservation status of a slot
class Slots::ReservationsService
class << self
# @param slots_reservations [ActiveRecord::Relation<SlotsReservation>]
# @param reservables [Array<Machine, Space, Training, Event, NilClass>]
# @return [Hash{Symbol=>ActiveRecord::Relation<SlotsReservation>,Array<Integer>}]
def reservations(slots_reservations, reservables)
reservable_types = reservables.map(&:class).map(&:name).uniq
if reservable_types.size > 1
raise TypeError("[Availabilities::StatusService#slot_reservations] reservables have differents types: #{reservable_types}")
end
reservations = slots_reservations.includes(:reservation)
.where('reservations.reservable_type': reservable_types)
.where('reservations.reservable_id': reservables.map { |r| r.try(:id) })
.where('slots_reservations.canceled_at': nil)
user_ids = reservations.includes(reservation: :statistic_profile)
.map(&:reservation)
.map(&:statistic_profile)
.map(&:user_id)
.filter { |id| !id.nil? }
{
reservations: reservations,
user_ids: user_ids
}
end
# @param cart_item_reservation_slot_ids [Array<number>]
# @param reservables [Array<Machine, Space, Training, Event, NilClass>]
# @return [Hash{Symbol=>ActiveRecord::Relation<CartItem::ReservationSlot>,Array<Integer>}]
def pending_reservations(cart_item_reservation_slot_ids, reservables)
reservable_types = reservables.map(&:class).map(&:name).uniq
if reservable_types.size > 1
raise TypeError("[Slots::StatusService#pending_reservations] reservables have differents types: #{reservable_types}")
end
relation = "cart_item_#{reservable_types.first&.downcase}_reservation".to_sym
pending_reservations = case reservable_types.first
when 'Event'
CartItem::ReservationSlot.where(id: cart_item_reservation_slot_ids)
.includes(relation)
.where(cart_item_event_reservations: { event_id: reservables.map(&:id) })
when 'NilClass'
[]
else
CartItem::ReservationSlot.where(id: cart_item_reservation_slot_ids)
.includes(relation)
.where(cart_item_reservations: { reservable_type: reservable_types })
.where(cart_item_reservations: { reservable_id: reservables.map { |r| r.try(:id) } })
end
user_ids = if reservable_types.first == 'NilClass'
[]
else
pending_reservations.includes(relation => :customer_profile)
.map(&:cart_item)
.map(&:customer_profile)
.map(&:user_id)
.filter { |id| !id.nil? }
end
{
reservations: pending_reservations,
user_ids: user_ids
}
end
# @param slot [Slot]
# @param user [User]
# @param reservable_type [String] 'Machine' | 'Space' | 'Training' | 'Event'
# @return [Hash{Symbol=>ActiveRecord::Relation<SlotsReservation>,ActiveRecord::Relation<CartItem::ReservationSlot>}]
def user_reservations(slot, user, reservable_type)
reservations = SlotsReservation.includes(:reservation)
.where(slot_id: slot.id, reservations: { statistic_profile_id: user.statistic_profile.id })
relation = "cart_item_#{reservable_type&.downcase}_reservation".to_sym
table = (reservable_type == 'Event' ? 'cart_item_event_reservations' : 'cart_item_reservations').to_sym
pending = CartItem::ReservationSlot.includes(relation)
.where(slot_id: slot.id, table => { customer_profile_id: user.invoicing_profile.id })
{
reservations: reservations,
pending: pending
}
end
end
end

View File

@ -5,6 +5,7 @@ json.price_without_coupon @amount[:before_coupon] / 100.00
if @amount[:elements]
json.details do
json.slots @amount[:elements][:slots] do |slot|
json.slot_id slot[:slot_id]
json.start_at slot[:start_at]
json.price slot[:price] / 100.00
json.promo slot[:promo]

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
# From this migration, ths object_type and object_id columns in InvoiceItem won't be able to be null anymore
# From this migration, the object_type and object_id columns in InvoiceItem won't be able to be null anymore
# This will prevent issues while building the accounting data, and ensure data integrity
class AddNotNullToInvoiceItemsObject < ActiveRecord::Migration[5.2]
def change

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
# From this migration, we will save the number of reserved places for each slots, for each reservable
# This will improved performance because of computing the number of reserved seats on each request in very resource demanding
#
# The places field is a jsonb object, with the following structure:
# {reservable_type: string, reservable_id: number, reserved_places: number, user_ids: number[]}
class AddPlacesCacheToSlot < ActiveRecord::Migration[5.2]
def change
add_column :slots, :places, :jsonb, null: false, default: []
add_index :slots, :places, using: :gin
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
# From this migration, the slot_id and reservation_id of the SlotsReservation won't be allowed to be null,
# otherwise this could result in error
class AddNotNullToSlotsReservation < ActiveRecord::Migration[5.2]
def change
change_column_null :slots_reservations, :slot_id, false
change_column_null :slots_reservations, :reservation_id, false
change_column_null :slots, :availability_id, false
end
end

View File

@ -19,8 +19,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
enable_extension "unaccent"
create_table "abuses", id: :serial, force: :cascade do |t|
t.integer "signaled_id"
t.string "signaled_type"
t.integer "signaled_id"
t.string "first_name"
t.string "last_name"
t.string "email"
@ -68,8 +68,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
t.string "locality"
t.string "country"
t.string "postal_code"
t.integer "placeable_id"
t.string "placeable_type"
t.integer "placeable_id"
t.datetime "created_at"
t.datetime "updated_at"
end
@ -93,8 +93,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
end
create_table "assets", id: :serial, force: :cascade do |t|
t.integer "viewable_id"
t.string "viewable_type"
t.integer "viewable_id"
t.string "attachment"
t.string "type"
t.datetime "created_at"
@ -281,8 +281,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
end
create_table "credits", id: :serial, force: :cascade do |t|
t.integer "creditable_id"
t.string "creditable_type"
t.integer "creditable_id"
t.integer "plan_id"
t.integer "hours"
t.datetime "created_at"
@ -524,15 +524,15 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
create_table "notifications", id: :serial, force: :cascade do |t|
t.integer "receiver_id"
t.integer "attached_object_id"
t.string "attached_object_type"
t.integer "attached_object_id"
t.integer "notification_type_id"
t.boolean "is_read", default: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "receiver_type"
t.boolean "is_send", default: false
t.jsonb "meta_data", default: {}
t.jsonb "meta_data", default: "{}"
t.index ["notification_type_id"], name: "index_notifications_on_notification_type_id"
t.index ["receiver_id"], name: "index_notifications_on_receiver_id"
end
@ -772,8 +772,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
create_table "prices", id: :serial, force: :cascade do |t|
t.integer "group_id"
t.integer "plan_id"
t.integer "priceable_id"
t.string "priceable_type"
t.integer "priceable_id"
t.integer "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@ -976,8 +976,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "reservable_id"
t.string "reservable_type"
t.integer "reservable_id"
t.integer "nb_reserve_places"
t.integer "statistic_profile_id"
t.index ["reservable_type", "reservable_id"], name: "index_reservations_on_reservable_type_and_reservable_id"
@ -986,8 +986,8 @@ ActiveRecord::Schema.define(version: 2023_01_31_104958) do
create_table "roles", id: :serial, force: :cascade do |t|
t.string "name"
t.integer "resource_id"
t.string "resource_type"
t.integer "resource_id"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id"

View File

@ -82,3 +82,4 @@ This is currently not supported, because of some PostgreSQL specific instruction
- `db/migrate/20200623141305_update_search_vector_of_projects.rb` defines a PL/pgSQL function (`fill_search_vector_for_project()`) and create an SQL trigger for this function;
- `db/migrate/20200629123011_update_pg_trgm.rb` is using [ALTER EXTENSION](https://www.postgresql.org/docs/10/sql-alterextension.html);
- `db/migrate/20201027101809_create_payment_schedule_items.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);
- `db/migrate/20230112151631_add_places_cache_to_slot.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html);

View File

@ -0,0 +1,88 @@
# frozen_string_literal: true
# This take will ensure data integrity for availbilities.
# For an unknown reason, some slots are associated with unexisting availabilities. This script will try to re-create them
namespace :fablab do
desc 'regenerate missing availabilities'
task fix_availabilities: :environment do |_task, _args|
ActiveRecord::Base.transaction do
Slot.find_each do |slot|
next unless slot.availability.nil?
other_slots = Slot.where(availability_id: slot.availability_id)
reservations = SlotsReservation.where(slot_id: other_slots.map(&:id))
a = Availability.new(
id: slot.availability_id,
start_at: other_slots.group('id').select('min(start_at) as min').first[:min],
end_at: other_slots.group('id').select('max(end_at) as max').first[:max],
available_type: available_type(reservations),
machine_ids: machines_ids(reservations, slot.availability_id),
space_ids: space_ids(reservations, slot.availability_id),
training_ids: training_ids(reservations, slot.availability_id)
)
raise StandardError, "unable to save availability for slot #{slot.id}: #{a.errors.full_messages}" unless a.save(validate: false)
end
end
end
private
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
def available_type(reservations)
return 'unknown' if reservations.count.zero?
type = reservations.first&.reservation&.reservable_type
case type
when 'Training', 'Space', 'Event'
type&.downcase
else
'machines'
end
end
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
# @param availability_id [Number]
def machines_ids(reservations, availability_id)
type = reservations.first&.reservation&.reservable_type
return [] unless type == 'Machine'
ma = MachinesAvailability.where(availability_id: availability_id).map(&:machine_id)
return ma unless ma.empty?
rv = reservations.map(&:reservation).map(&:reservable_id)
return rv unless rv.empty?
[]
end
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
# @param availability_id [Number]
def space_ids(reservations, availability_id)
type = reservations.first&.reservation&.reservable_type
return [] unless type == 'Space'
sa = SpacesAvailability.where(availability_id: availability_id).map(&:machine_id)
return sa unless sa.empty?
rv = reservations.map(&:reservation).map(&:reservable_id)
return rv unless rv.empty?
[]
end
# @param reservations [ActiveRecord::Relation<SlotsReservation>]
# @param availability_id [Number]
def training_ids(reservations, availability_id)
type = reservations.first&.reservation&.reservable_type
return [] unless type == 'Training'
ta = TrainingsAvailability.where(availability_id: availability_id).map(&:machine_id)
return ta unless ta.empty?
rv = reservations.map(&:reservation).map(&:reservable_id)
return rv unless rv.empty?
[]
end
end

View File

@ -134,6 +134,17 @@ namespace :fablab do
puts '-> Done'
end
desc 'build the reserved places cache for all slots'
task build_places_cache: :environment do
puts 'Builing the places cache. This may take some time...'
total = Slot.maximum(:id)
Slot.order(id: :asc).find_each do |slot|
puts "#{slot.id} / #{total}"
Slots::PlacesCacheService.refresh(slot)
end
puts '-> Done'
end
def select_group(groups)
groups.each do |g|
print "#{g.id}) #{g.name}\n"

View File

@ -5,7 +5,7 @@ reservation_1:
message:
created_at: 2012-03-12 11:03:31.651441000 Z
updated_at: 2012-03-12 11:03:31.651441000 Z
reservable_id: 2
reservable_id: 3
reservable_type: Training
nb_reserve_places:
@ -15,6 +15,6 @@ reservation_2:
message:
created_at: 2015-06-10 11:20:01.341130000 Z
updated_at: 2015-06-10 11:20:01.341130000 Z
reservable_id: 4
reservable_id: 2
reservable_type: Machine
nb_reserve_places:

View File

@ -6,6 +6,7 @@ slot_1:
created_at: '2012-03-12 13:40:22.342717'
updated_at: '2012-03-12 13:40:22.342717'
availability_id: 12
places: [{"user_ids": [7], "reservable_id": 3, "reservable_type": "Training", "reserved_places": 1}]
slot_2:
id: 2
@ -14,6 +15,7 @@ slot_2:
created_at: '2015-06-10 11:20:01.341130'
updated_at: '2015-06-10 11:20:01.341130'
availability_id: 13
places: [{"user_ids": [3], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 1}]
slot_9:
id: 9
@ -22,6 +24,7 @@ slot_9:
created_at: '2022-07-12 15:18:43.880751'
updated_at: '2022-07-12 15:18:43.880751'
availability_id: 3
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_10:
id: 10
@ -30,6 +33,7 @@ slot_10:
created_at: '2022-07-12 15:18:43.882957'
updated_at: '2022-07-12 15:18:43.882957'
availability_id: 3
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_11:
id: 11
@ -38,6 +42,7 @@ slot_11:
created_at: '2022-07-12 15:18:43.884691'
updated_at: '2022-07-12 15:18:43.884691'
availability_id: 3
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_12:
id: 12
@ -46,6 +51,7 @@ slot_12:
created_at: '2022-07-12 15:18:43.886431'
updated_at: '2022-07-12 15:18:43.886431'
availability_id: 3
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_13:
id: 13
@ -54,6 +60,7 @@ slot_13:
created_at: '2022-07-12 15:18:43.888074'
updated_at: '2022-07-12 15:18:43.888074'
availability_id: 3
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_14:
id: 14
@ -62,6 +69,7 @@ slot_14:
created_at: '2022-07-12 15:18:43.889691'
updated_at: '2022-07-12 15:18:43.889691'
availability_id: 3
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_15:
id: 15
@ -70,6 +78,7 @@ slot_15:
created_at: '2022-07-12 15:18:43.893096'
updated_at: '2022-07-12 15:18:43.893096'
availability_id: 4
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_16:
id: 16
@ -78,6 +87,7 @@ slot_16:
created_at: '2022-07-12 15:18:43.894777'
updated_at: '2022-07-12 15:18:43.894777'
availability_id: 4
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_17:
id: 17
@ -86,6 +96,7 @@ slot_17:
created_at: '2022-07-12 15:18:43.896423'
updated_at: '2022-07-12 15:18:43.896423'
availability_id: 4
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_18:
id: 18
@ -94,6 +105,7 @@ slot_18:
created_at: '2022-07-12 15:18:43.898021'
updated_at: '2022-07-12 15:18:43.898021'
availability_id: 4
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_19:
id: 19
@ -102,6 +114,7 @@ slot_19:
created_at: '2022-07-12 15:18:43.899592'
updated_at: '2022-07-12 15:18:43.899592'
availability_id: 4
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_20:
id: 20
@ -110,6 +123,7 @@ slot_20:
created_at: '2022-07-12 15:18:43.900938'
updated_at: '2022-07-12 15:18:43.900938'
availability_id: 4
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_21:
id: 21
@ -118,6 +132,7 @@ slot_21:
created_at: '2022-07-12 15:18:43.904013'
updated_at: '2022-07-12 15:18:43.904013'
availability_id: 5
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_22:
id: 22
@ -126,6 +141,7 @@ slot_22:
created_at: '2022-07-12 15:18:43.905470'
updated_at: '2022-07-12 15:18:43.905470'
availability_id: 5
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_23:
id: 23
@ -134,6 +150,7 @@ slot_23:
created_at: '2022-07-12 15:18:43.907030'
updated_at: '2022-07-12 15:18:43.907030'
availability_id: 5
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_24:
id: 24
@ -142,6 +159,7 @@ slot_24:
created_at: '2022-07-12 15:18:43.908585'
updated_at: '2022-07-12 15:18:43.908585'
availability_id: 5
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_25:
id: 25
@ -150,6 +168,7 @@ slot_25:
created_at: '2022-07-12 15:18:43.910138'
updated_at: '2022-07-12 15:18:43.910138'
availability_id: 5
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_26:
id: 26
@ -158,6 +177,7 @@ slot_26:
created_at: '2022-07-12 15:18:43.911643'
updated_at: '2022-07-12 15:18:43.911643'
availability_id: 5
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_27:
id: 27
@ -166,6 +186,7 @@ slot_27:
created_at: '2022-07-12 15:18:43.914664'
updated_at: '2022-07-12 15:18:43.914664'
availability_id: 6
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_28:
id: 28
@ -174,6 +195,7 @@ slot_28:
created_at: '2022-07-12 15:18:43.916047'
updated_at: '2022-07-12 15:18:43.916047'
availability_id: 6
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_29:
id: 29
@ -182,6 +204,7 @@ slot_29:
created_at: '2022-07-12 15:18:43.917304'
updated_at: '2022-07-12 15:18:43.917304'
availability_id: 6
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_30:
id: 30
@ -190,6 +213,7 @@ slot_30:
created_at: '2022-07-12 15:18:43.918798'
updated_at: '2022-07-12 15:18:43.918798'
availability_id: 6
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_31:
id: 31
@ -198,6 +222,7 @@ slot_31:
created_at: '2022-07-12 15:18:43.920194'
updated_at: '2022-07-12 15:18:43.920194'
availability_id: 6
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_32:
id: 32
@ -206,6 +231,7 @@ slot_32:
created_at: '2022-07-12 15:18:43.921662'
updated_at: '2022-07-12 15:18:43.921662'
availability_id: 6
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Machine", "reserved_places": 0}]
slot_33:
id: 33
@ -214,6 +240,7 @@ slot_33:
created_at: '2022-07-12 15:18:43.924285'
updated_at: '2022-07-12 15:18:43.924285'
availability_id: 7
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_34:
id: 34
@ -222,6 +249,7 @@ slot_34:
created_at: '2022-07-12 15:18:43.925669'
updated_at: '2022-07-12 15:18:43.925669'
availability_id: 7
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_35:
id: 35
@ -230,6 +258,7 @@ slot_35:
created_at: '2022-07-12 15:18:43.927038'
updated_at: '2022-07-12 15:18:43.927038'
availability_id: 7
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_36:
id: 36
@ -238,6 +267,7 @@ slot_36:
created_at: '2022-07-12 15:18:43.928407'
updated_at: '2022-07-12 15:18:43.928407'
availability_id: 7
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_37:
id: 37
@ -246,6 +276,7 @@ slot_37:
created_at: '2022-07-12 15:18:43.929907'
updated_at: '2022-07-12 15:18:43.929907'
availability_id: 7
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_38:
id: 38
@ -254,6 +285,7 @@ slot_38:
created_at: '2022-07-12 15:18:43.931295'
updated_at: '2022-07-12 15:18:43.931295'
availability_id: 7
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 4, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 5, "reservable_type": "Machine", "reserved_places": 0}, {"user_ids": [], "reservable_id": 6, "reservable_type": "Machine", "reserved_places": 0}]
slot_39:
id: 39
@ -262,6 +294,7 @@ slot_39:
created_at: '2022-07-12 15:18:43.934465'
updated_at: '2022-07-12 15:18:43.934465'
availability_id: 13
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_40:
id: 40
@ -270,6 +303,7 @@ slot_40:
created_at: '2022-07-12 15:18:43.935716'
updated_at: '2022-07-12 15:18:43.935716'
availability_id: 13
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_41:
id: 41
@ -278,6 +312,7 @@ slot_41:
created_at: '2022-07-12 15:18:43.937025'
updated_at: '2022-07-12 15:18:43.937025'
availability_id: 13
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_42:
id: 42
@ -286,6 +321,7 @@ slot_42:
created_at: '2022-07-12 15:18:43.938379'
updated_at: '2022-07-12 15:18:43.938379'
availability_id: 13
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_43:
id: 43
@ -294,6 +330,7 @@ slot_43:
created_at: '2022-07-12 15:18:43.939737'
updated_at: '2022-07-12 15:18:43.939737'
availability_id: 13
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_44:
id: 44
@ -302,6 +339,7 @@ slot_44:
created_at: '2022-07-12 15:18:43.942392'
updated_at: '2022-07-12 15:18:43.942392'
availability_id: 14
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_45:
id: 45
@ -310,6 +348,7 @@ slot_45:
created_at: '2022-07-12 15:18:43.943779'
updated_at: '2022-07-12 15:18:43.943779'
availability_id: 14
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_46:
id: 46
@ -318,6 +357,7 @@ slot_46:
created_at: '2022-07-12 15:18:43.945154'
updated_at: '2022-07-12 15:18:43.945154'
availability_id: 14
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_47:
id: 47
@ -326,6 +366,7 @@ slot_47:
created_at: '2022-07-12 15:18:43.946515'
updated_at: '2022-07-12 15:18:43.946515'
availability_id: 14
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_48:
id: 48
@ -334,6 +375,7 @@ slot_48:
created_at: '2022-07-12 15:18:43.949178'
updated_at: '2022-07-12 15:18:43.949178'
availability_id: 15
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_49:
id: 49
@ -342,6 +384,7 @@ slot_49:
created_at: '2022-07-12 15:18:43.950348'
updated_at: '2022-07-12 15:18:43.950348'
availability_id: 15
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_50:
id: 50
@ -350,6 +393,7 @@ slot_50:
created_at: '2022-07-12 15:18:43.951535'
updated_at: '2022-07-12 15:18:43.951535'
availability_id: 15
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_51:
id: 51
@ -358,6 +402,7 @@ slot_51:
created_at: '2022-07-12 15:18:43.952864'
updated_at: '2022-07-12 15:18:43.952864'
availability_id: 15
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_52:
id: 52
@ -366,6 +411,7 @@ slot_52:
created_at: '2022-07-12 15:18:43.955443'
updated_at: '2022-07-12 15:18:43.955443'
availability_id: 16
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_53:
id: 53
@ -374,6 +420,7 @@ slot_53:
created_at: '2022-07-12 15:18:43.956657'
updated_at: '2022-07-12 15:18:43.956657'
availability_id: 16
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_54:
id: 54
@ -382,6 +429,7 @@ slot_54:
created_at: '2022-07-12 15:18:43.957811'
updated_at: '2022-07-12 15:18:43.957811'
availability_id: 16
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_55:
id: 55
@ -390,6 +438,7 @@ slot_55:
created_at: '2022-07-12 15:18:43.959063'
updated_at: '2022-07-12 15:18:43.959063'
availability_id: 16
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Machine", "reserved_places": 0}]
slot_56:
id: 56
@ -398,6 +447,7 @@ slot_56:
created_at: '2022-07-12 15:18:43.961319'
updated_at: '2022-07-12 15:18:43.961319'
availability_id: 17
places: [{"user_ids": [], "reservable_id": 4, "reservable_type": "Event", "reserved_places": 0}]
slot_112:
id: 112
@ -406,6 +456,7 @@ slot_112:
created_at: '2022-07-12 15:18:44.038089'
updated_at: '2022-07-12 15:18:44.038089'
availability_id: 18
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
slot_113:
id: 113
@ -414,6 +465,7 @@ slot_113:
created_at: '2022-07-12 15:18:44.039392'
updated_at: '2022-07-12 15:18:44.039392'
availability_id: 18
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
slot_114:
id: 114
@ -422,6 +474,7 @@ slot_114:
created_at: '2022-07-12 15:18:44.040522'
updated_at: '2022-07-12 15:18:44.040522'
availability_id: 18
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
slot_115:
id: 115
@ -430,6 +483,7 @@ slot_115:
created_at: '2022-07-12 15:18:44.041937'
updated_at: '2022-07-12 15:18:44.041937'
availability_id: 18
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
slot_116:
id: 116
@ -438,6 +492,7 @@ slot_116:
created_at: '2022-07-12 15:18:44.044421'
updated_at: '2022-07-12 15:18:44.044421'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_117:
id: 117
@ -446,6 +501,7 @@ slot_117:
created_at: '2022-07-12 15:18:44.045689'
updated_at: '2022-07-12 15:18:44.045689'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_118:
id: 118
@ -454,6 +510,7 @@ slot_118:
created_at: '2022-07-12 15:18:44.047009'
updated_at: '2022-07-12 15:18:44.047009'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_119:
id: 119
@ -462,6 +519,7 @@ slot_119:
created_at: '2022-07-12 15:18:44.048272'
updated_at: '2022-07-12 15:18:44.048272'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_120:
id: 120
@ -470,6 +528,7 @@ slot_120:
created_at: '2022-07-12 15:18:44.049599'
updated_at: '2022-07-12 15:18:44.049599'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_121:
id: 121
@ -478,6 +537,7 @@ slot_121:
created_at: '2022-07-12 15:18:44.050947'
updated_at: '2022-07-12 15:18:44.050947'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_122:
id: 122
@ -486,6 +546,7 @@ slot_122:
created_at: '2022-07-12 15:18:44.052817'
updated_at: '2022-07-12 15:18:44.052817'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_123:
id: 123
@ -494,6 +555,7 @@ slot_123:
created_at: '2022-07-12 15:18:44.054966'
updated_at: '2022-07-12 15:18:44.054966'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_124:
id: 124
@ -502,6 +564,7 @@ slot_124:
created_at: '2022-07-12 15:18:44.057217'
updated_at: '2022-07-12 15:18:44.057217'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_125:
id: 125
@ -510,6 +573,7 @@ slot_125:
created_at: '2022-07-12 15:18:44.059135'
updated_at: '2022-07-12 15:18:44.059135'
availability_id: 19
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Machine", "reserved_places": 0}]
slot_126:
id: 126
@ -518,6 +582,7 @@ slot_126:
created_at: '2022-07-12 15:18:44.061887'
updated_at: '2022-07-12 15:18:44.061887'
availability_id: 1
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Training", "reserved_places": 0}]
slot_127:
id: 127
@ -526,6 +591,7 @@ slot_127:
created_at: '2022-07-12 15:18:44.063528'
updated_at: '2022-07-12 15:18:44.063528'
availability_id: 2
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Training", "reserved_places": 0}]
slot_128:
id: 128
@ -534,6 +600,7 @@ slot_128:
created_at: '2022-07-12 15:18:44.065114'
updated_at: '2022-07-12 15:18:44.065114'
availability_id: 8
places: [{"user_ids": [], "reservable_id": 4, "reservable_type": "Training", "reserved_places": 0}]
slot_129:
id: 129
@ -542,6 +609,7 @@ slot_129:
created_at: '2022-07-12 15:18:44.066837'
updated_at: '2022-07-12 15:18:44.066837'
availability_id: 9
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Event", "reserved_places": 0}]
slot_130:
id: 130
@ -550,6 +618,7 @@ slot_130:
created_at: '2022-07-12 15:18:44.068259'
updated_at: '2022-07-12 15:18:44.068259'
availability_id: 10
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Event", "reserved_places": 0}]
slot_131:
id: 131
@ -558,6 +627,7 @@ slot_131:
created_at: '2022-07-12 15:18:44.069870'
updated_at: '2022-07-12 15:18:44.069870'
availability_id: 11
places: [{"user_ids": [], "reservable_id": 3, "reservable_type": "Event", "reserved_places": 0}]
slot_132:
id: 132
@ -566,6 +636,7 @@ slot_132:
created_at: '2022-07-18 12:38:21.616510'
updated_at: '2022-07-18 12:38:21.616510'
availability_id: 20
places: [{"user_ids": [], "reservable_id": 2, "reservable_type": "Training", "reserved_places": 0}]
slot_133:
id: 133
@ -574,6 +645,7 @@ slot_133:
created_at: '2022-12-14 12:01:26.165110'
updated_at: '2022-12-14 12:01:26.165110'
availability_id: 21
places: [{"user_ids": [], "reservable_id": 1, "reservable_type": "Space", "reserved_places": 0}]
slot_134:
id: 134
@ -582,3 +654,4 @@ slot_134:
created_at: 2023-01-24 13:34:43.841240000 Z
updated_at: 2023-01-24 13:34:43.841240000 Z
availability_id: 22
places: [{"user_ids": [], "reservable_id": 4, "reservable_type": "Training", "reserved_places": 0}]

View File

@ -79,9 +79,9 @@ class MachinesTest < ActionDispatch::IntegrationTest
end
test 'soft delete a machine' do
machine = Machine.find(4)
machine = Machine.find(2)
assert_not machine.destroyable?
delete '/api/machines/4', headers: default_headers
delete "/api/machines/#{machine.id}", headers: default_headers
assert_response :success
assert_empty response.body

View File

@ -53,13 +53,13 @@ class OpenApi::MachinesTest < ActionDispatch::IntegrationTest
end
test 'soft delete a machine' do
assert_not Machine.find(4).destroyable?
delete '/open_api/v1/machines/4', headers: open_api_headers(@token)
assert_not Machine.find(2).destroyable?
delete '/open_api/v1/machines/2', headers: open_api_headers(@token)
assert_response :success
get '/open_api/v1/machines/4', headers: open_api_headers(@token)
get '/open_api/v1/machines/2', headers: open_api_headers(@token)
assert_response :not_found
get '/open_api/v1/machines', headers: open_api_headers(@token)
machines = json_response(response.body)
assert_not(machines[:machines].any? { |m| m[:id] == 4 })
assert_not(machines[:machines].any? { |m| m[:id] == 2 })
end
end

View File

@ -57,13 +57,13 @@ class OpenApi::ReservationsTest < ActionDispatch::IntegrationTest
assert_equal ['Machine'], reservations[:reservations].pluck(:reservable_type).uniq
end
test 'list all machine 4 reservations' do
get '/open_api/v1/reservations?reservable_type=Machine&reservable_id=4', headers: open_api_headers(@token)
test 'list all machine 2 reservations' do
get '/open_api/v1/reservations?reservable_type=Machine&reservable_id=2', headers: open_api_headers(@token)
assert_response :success
assert_equal Mime[:json], response.content_type
reservations = json_response(response.body)
assert_not_empty reservations[:reservations]
assert_equal [4], reservations[:reservations].pluck(:reservable_id).uniq
assert_equal [2], reservations[:reservations].pluck(:reservable_id).uniq
end
end