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:
parent
9f9a2e616f
commit
986a663c40
@ -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
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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']));
|
@ -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']));
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -23,6 +23,7 @@ export interface ComputePriceResult {
|
||||
price_without_coupon: number,
|
||||
details?: {
|
||||
slots: Array<{
|
||||
slot_id: number,
|
||||
start_at: TDateISO,
|
||||
price: number,
|
||||
promo: boolean
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import "variables/animations";
|
||||
@import "variables/colors";
|
||||
@import "variables/component";
|
||||
@import "variables/decoration";
|
||||
@import "variables/layout";
|
||||
@import "variables/typography";
|
||||
|
@ -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 {
|
||||
|
@ -1,4 +1,8 @@
|
||||
.member-select {
|
||||
@include component-border;
|
||||
&-title {
|
||||
@include component-title;
|
||||
}
|
||||
&.error {
|
||||
.select-input > div:first-of-type {
|
||||
border-color: var(--alert);
|
||||
|
15
app/frontend/src/stylesheets/variables/component.scss
Normal file
15
app/frontend/src/stylesheets/variables/component.scss
Normal 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;
|
||||
}
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: [] }
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
134
app/services/slots/places_cache_service.rb
Normal file
134
app/services/slots/places_cache_service.rb
Normal 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
|
93
app/services/slots/reservations_service.rb
Normal file
93
app/services/slots/reservations_service.rb
Normal 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
|
@ -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]
|
||||
|
@ -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
|
||||
|
13
db/migrate/20230112151631_add_places_cache_to_slot.rb
Normal file
13
db/migrate/20230112151631_add_places_cache_to_slot.rb
Normal 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
|
@ -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
|
18
db/schema.rb
18
db/schema.rb
@ -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"
|
||||
|
@ -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);
|
||||
|
88
lib/tasks/fablab/fix_availabilities.rake
Normal file
88
lib/tasks/fablab/fix_availabilities.rake
Normal 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
|
@ -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"
|
||||
|
4
test/fixtures/reservations.yml
vendored
4
test/fixtures/reservations.yml
vendored
@ -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:
|
||||
|
73
test/fixtures/slots.yml
vendored
73
test/fixtures/slots.yml
vendored
@ -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}]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user