mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-22 11:52:21 +01:00
Merge branch 'reservation-context' into staging
This commit is contained in:
commit
c0df547293
@ -1,8 +1,15 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
- adds reservation context feature (for machine, training, space)
|
||||
- [TODO DEPLOY] `rails fablab:es:build_stats`
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:regenerate_statistics[2014,1]`
|
||||
|
||||
## v6.0.10 2023 July 13
|
||||
|
||||
- Fix a bug: unable to confirm payment of store for admin
|
||||
- Fix a bug: unable to update payment schedule item
|
||||
|
||||
- Fix a bug: event reserved places compute error
|
||||
- [TODO DEPLOY] `rails fablab:setup:build_places_cache`
|
||||
|
||||
## v6.0.9 2023 July 07
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
class API::ProjectCategoriesController < ApplicationController
|
||||
before_action :set_project_category, only: %i[update destroy]
|
||||
before_action :authenticate_user!, only: %i[create update destroy]
|
||||
|
||||
def index
|
||||
@project_categories = ProjectCategory.all
|
||||
end
|
||||
|
58
app/controllers/api/reservation_contexts_controller.rb
Normal file
58
app/controllers/api/reservation_contexts_controller.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type ReservationContext
|
||||
class API::ReservationContextsController < API::APIController
|
||||
before_action :authenticate_user!, except: [:index]
|
||||
before_action :set_reservation_context, only: %i[show update destroy]
|
||||
|
||||
def index
|
||||
@reservation_contexts = ReservationContext.all
|
||||
@reservation_contexts = @reservation_contexts.applicable_on(params[:applicable_on]) if params[:applicable_on].present?
|
||||
@reservation_contexts = @reservation_contexts.order(:created_at)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
authorize ReservationContext
|
||||
@reservation_context = ReservationContext.new(reservation_context_params)
|
||||
if @reservation_context.save
|
||||
render :show, status: :created, location: @reservation_context
|
||||
else
|
||||
render json: @reservation_context.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize ReservationContext
|
||||
if @reservation_context.update(reservation_context_params)
|
||||
render :show, status: :ok, location: @reservation_context
|
||||
else
|
||||
render json: @reservation_context.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize ReservationContext
|
||||
if @reservation_context.safe_destroy
|
||||
head :no_content
|
||||
else
|
||||
render json: @reservation_context.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def applicable_on_values
|
||||
authorize ReservationContext
|
||||
render json: ReservationContext::APPLICABLE_ON, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_reservation_context
|
||||
@reservation_context = ReservationContext.find(params[:id])
|
||||
end
|
||||
|
||||
def reservation_context_params
|
||||
params.require(:reservation_context).permit(:name, applicable_on: [])
|
||||
end
|
||||
end
|
10
app/frontend/src/javascript/api/reservation_context.ts
Normal file
10
app/frontend/src/javascript/api/reservation_context.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { ReservationContext } from '../models/reservation';
|
||||
|
||||
export default class ReservationContextAPI {
|
||||
static async index (): Promise<Array<ReservationContext>> {
|
||||
const res: AxiosResponse<Array<ReservationContext>> = await apiClient.get('/api/reservation_contexts');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
@ -12,8 +12,8 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('SettingsController', ['$scope', '$rootScope', '$filter', '$uibModal', 'dialogs', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t', 'Member', 'uiTourService',
|
||||
function ($scope, $rootScope, $filter, $uibModal, dialogs, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t, Member, uiTourService) {
|
||||
Application.Controllers.controller('SettingsController', ['$scope', '$rootScope', '$filter', '$uibModal', 'dialogs', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'reservationContextsPromise', 'reservationContextApplicableOnValuesPromise', 'CSRF', '_t', 'Member', 'uiTourService', 'ReservationContext',
|
||||
function ($scope, $rootScope, $filter, $uibModal, dialogs, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, reservationContextsPromise, reservationContextApplicableOnValuesPromise, CSRF, _t, Member, uiTourService, ReservationContext) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// timepickers steps configuration
|
||||
@ -76,6 +76,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color };
|
||||
$scope.secondColorSetting = { name: 'secondary_color', value: settingsPromise.secondary_color };
|
||||
$scope.nameGenre = { name: 'name_genre', value: settingsPromise.name_genre };
|
||||
$scope.reservationContextFeature = { name: 'reservation_context_feature', value: settingsPromise.reservation_context_feature };
|
||||
$scope.cguFile = cguFile.custom_asset;
|
||||
$scope.cgvFile = cgvFile.custom_asset;
|
||||
$scope.customLogo = logoFile.custom_asset;
|
||||
@ -83,6 +84,11 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
$scope.customFavicon = faviconFile.custom_asset;
|
||||
$scope.profileImage = profileImageFile.custom_asset;
|
||||
|
||||
// necessary initialisation for reservation context
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
$scope.reservationContextApplicableOnValues = reservationContextApplicableOnValuesPromise;
|
||||
$scope.newReservationContext = null;
|
||||
|
||||
// By default, we display the currently published privacy policy
|
||||
$scope.privacyPolicy = {
|
||||
version: null,
|
||||
@ -314,21 +320,6 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
$scope.codeMirrorEditor = editor;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a success message forwarded from a child react component
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
console.error(message);
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for allow/prevent book overlapping slots: which kind of slots are used in the overlapping computation
|
||||
*/
|
||||
@ -476,6 +467,63 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
// Functions for reservation context feature
|
||||
|
||||
$scope.onReservationContextFeatureChange = function (value) {
|
||||
$scope.reservationContextFeature.value = value;
|
||||
};
|
||||
|
||||
$scope.saveReservationContext = function (data, id) {
|
||||
if (id != null) {
|
||||
return ReservationContext.update({ id }, data);
|
||||
} else {
|
||||
return ReservationContext.save(data, function (resp) { $scope.reservationContexts[$scope.reservationContexts.length - 1].id = resp.id; });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeReservationContext = function (index) {
|
||||
if ($scope.reservationContexts[index].related_to > 0) {
|
||||
growl.error(_t('app.admin.settings.unable_to_delete_reservation_context_already_related_to_reservations'));
|
||||
return false;
|
||||
}
|
||||
return dialogs.confirm({
|
||||
resolve: {
|
||||
object () {
|
||||
return {
|
||||
title: _t('app.admin.settings.confirmation_required'),
|
||||
msg: _t('app.admin.settings.do_you_really_want_to_delete_this_reservation_context')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
, function () { // delete confirmed
|
||||
ReservationContext.delete($scope.reservationContexts[index], null, function () { $scope.reservationContexts.splice(index, 1); }
|
||||
, function () { growl.error(_t('app.admin.settings.unable_to_delete_reservation_context_an_error_occured')); });
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addReservationContext = function () {
|
||||
$scope.newReservationContext = {
|
||||
name: '',
|
||||
related_to: 0
|
||||
};
|
||||
return $scope.reservationContexts.push($scope.newReservationContext);
|
||||
};
|
||||
|
||||
$scope.cancelReservationContext = function (rowform, index) {
|
||||
if ($scope.reservationContexts[index].id != null) {
|
||||
return rowform.$cancel();
|
||||
} else {
|
||||
return $scope.reservationContexts.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.translateApplicableOnValue = function (value) {
|
||||
if (!value) { return; }
|
||||
if (angular.isArray(value)) { return value.map(v => _t(`app.admin.reservation_contexts.${v}`)).join(', '); }
|
||||
return _t(`app.admin.reservation_contexts.${value}`);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
|
@ -15,8 +15,8 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$transitions', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', 'uiTourService', 'settingsPromise',
|
||||
function ($scope, $state, $transitions, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise, uiTourService, settingsPromise) {
|
||||
Application.Controllers.controller('StatisticsController', ['$scope', '$state', '$transitions', '$rootScope', '$uibModal', 'es', 'Member', '_t', 'membersPromise', 'statisticsPromise', 'uiTourService', 'settingsPromise', 'reservationContextsPromise', 'reservationContextApplicableOnValuesPromise',
|
||||
function ($scope, $state, $transitions, $rootScope, $uibModal, es, Member, _t, membersPromise, statisticsPromise, uiTourService, settingsPromise, reservationContextsPromise, reservationContextApplicableOnValuesPromise) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// search window size
|
||||
@ -133,6 +133,18 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reservationContextFeatureEnabled = settingsPromise.reservation_context_feature === 'true';
|
||||
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
|
||||
$scope.reservationContexts = reservationContextsPromise.filter(rc => rc.applicable_on.length > 0);
|
||||
|
||||
$scope.reservationContextApplicableOnValues = reservationContextApplicableOnValuesPromise;
|
||||
|
||||
$scope.reservationContextIsApplicable = function (esTypeKey) {
|
||||
return $scope.reservationContextApplicableOnValues.includes(esTypeKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a localized name for the given field
|
||||
*/
|
||||
@ -220,17 +232,9 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
|
||||
*/
|
||||
$scope.formatDate = function (date) { return moment(date).format('LL'); };
|
||||
|
||||
/**
|
||||
* Parse the sex and return a user-friendly string
|
||||
* @param sex {string} 'male' | 'female'
|
||||
*/
|
||||
$scope.formatSex = function (sex) {
|
||||
if (sex === 'male') {
|
||||
return _t('app.admin.statistics.man');
|
||||
}
|
||||
if (sex === 'female') {
|
||||
return _t('app.admin.statistics.woman');
|
||||
}
|
||||
$scope.formatReservationContext = function (id) {
|
||||
if (id === null || id === undefined) { return; }
|
||||
return $scope.reservationContexts.find(rc => rc.id === id).name;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -645,6 +649,15 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state',
|
||||
{ key: 'ca', label: _t('app.admin.statistics.revenue'), values: ['input_number'] }
|
||||
];
|
||||
|
||||
if ($scope.reservationContextFeatureEnabled && $scope.reservationContextIsApplicable($scope.selectedIndex.es_type_key)) {
|
||||
const reservationContextValues = $scope.reservationContexts.map((rc) => {
|
||||
return { key: rc.id, label: rc.name };
|
||||
});
|
||||
$scope.filters.push({
|
||||
key: 'reservationContextId', label: _t('app.admin.statistics.reservation_context'), values: reservationContextValues
|
||||
});
|
||||
}
|
||||
|
||||
// if no plans were created, there's no types for statisticIndex=subscriptions
|
||||
if ($scope.type.active) {
|
||||
$scope.filters.splice(4, 0, { key: 'subType', label: _t('app.admin.statistics.type'), values: $scope.type.active.subtypes });
|
||||
|
@ -390,8 +390,8 @@ Application.Controllers.controller('ShowMachineController', ['$scope', '$state',
|
||||
* This controller workflow is pretty similar to the trainings reservation controller.
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveMachineController', ['$scope', '$transition$', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'growl', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, uiCalendarConfig, CalendarConfig, Reservation, growl, helpers, AuthService) {
|
||||
Application.Controllers.controller('ReserveMachineController', ['$scope', '$transition$', '_t', 'moment', 'Auth', '$timeout', 'Member', 'Availability', 'plansPromise', 'groupsPromise', 'machinePromise', 'settingsPromise', 'reservationContextsPromise', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'growl', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, _t, moment, Auth, $timeout, Member, Availability, plansPromise, groupsPromise, machinePromise, settingsPromise, reservationContextsPromise, uiCalendarConfig, CalendarConfig, Reservation, growl, helpers, AuthService) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Slot free to be booked
|
||||
@ -445,6 +445,13 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
// will be set to a Promise and resolved after the payment is sone
|
||||
$scope.afterPaymentPromise = null;
|
||||
|
||||
// reservation contexts stuff
|
||||
if (settingsPromise.reservation_context_feature === 'true') {
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
} else {
|
||||
$scope.reservationContexts = [];
|
||||
}
|
||||
|
||||
// fullCalendar (v2) configuration
|
||||
$scope.calendarConfig = CalendarConfig({
|
||||
minTime: moment.duration(moment.utc(settingsPromise.booking_window_start.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
|
@ -328,8 +328,8 @@ Application.Controllers.controller('ShowSpaceController', ['$scope', '$state', '
|
||||
* per slots.
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transition$', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers, AuthService) {
|
||||
Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transition$', 'Auth', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'spacePromise', 'reservationContextsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers', 'AuthService',
|
||||
function ($scope, $transition$, Auth, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, spacePromise, reservationContextsPromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers, AuthService) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Color of the selected event backgound
|
||||
@ -407,6 +407,13 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
// Global config: is the user validation required ?
|
||||
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
|
||||
|
||||
// reservation contexts stuff
|
||||
if (settingsPromise.reservation_context_feature === 'true') {
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
} else {
|
||||
$scope.reservationContexts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearance to looks like 'added to cart'
|
||||
*/
|
||||
|
@ -98,8 +98,8 @@ Application.Controllers.controller('ShowTrainingController', ['$scope', '$state'
|
||||
* training can be reserved during the reservation process (the shopping cart may contain only one training and a subscription).
|
||||
*/
|
||||
|
||||
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$transition$', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers',
|
||||
function ($scope, $transition$, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers) {
|
||||
Application.Controllers.controller('ReserveTrainingController', ['$scope', '$transition$', 'Auth', 'AuthService', '$timeout', 'Availability', 'Member', 'plansPromise', 'groupsPromise', 'settingsPromise', 'trainingPromise', 'reservationContextsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig', 'Reservation', 'helpers',
|
||||
function ($scope, $transition$, Auth, AuthService, $timeout, Availability, Member, plansPromise, groupsPromise, settingsPromise, trainingPromise, reservationContextsPromise, _t, uiCalendarConfig, CalendarConfig, Reservation, helpers) {
|
||||
/* PRIVATE STATIC CONSTANTS */
|
||||
|
||||
// Color of the selected event backgound
|
||||
@ -180,6 +180,13 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
// Global config: is the user validation required ?
|
||||
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
|
||||
|
||||
// reservation contexts stuff
|
||||
if (settingsPromise.reservation_context_feature === 'true') {
|
||||
$scope.reservationContexts = reservationContextsPromise;
|
||||
} else {
|
||||
$scope.reservationContexts = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the last selected slot's appearance to looks like 'added to cart'
|
||||
*/
|
||||
|
@ -36,7 +36,8 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
reservableId: '@',
|
||||
reservableType: '@',
|
||||
reservableName: '@',
|
||||
limitToOneSlot: '@'
|
||||
limitToOneSlot: '@',
|
||||
reservationContexts: '='
|
||||
},
|
||||
templateUrl: '/shared/_cart.html',
|
||||
link ($scope, element, attributes) {
|
||||
@ -88,6 +89,11 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
// currently logged-in user
|
||||
$scope.currentUser = $rootScope.currentUser;
|
||||
|
||||
$scope.reservationContextsData = {
|
||||
options: $scope.reservationContexts || [],
|
||||
selected: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the provided slot to the shopping cart (state transition from free to 'about to be reserved')
|
||||
* and increment the total amount of the cart if needed.
|
||||
@ -162,6 +168,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
* Validates the shopping chart and redirect the user to the payment step
|
||||
*/
|
||||
$scope.payCart = function () {
|
||||
if ($scope.reservationContextsData.options.length && angular.isUndefinedOrNull($scope.reservationContextsData.selected)) {
|
||||
return growl.error(_t('app.shared.cart.please_select_a_reservation_context'));
|
||||
}
|
||||
// first, we check that a user was selected
|
||||
if (Object.keys($scope.user).length > 0) {
|
||||
// check selected user has a subscription, if any slot is restricted for subscriptions
|
||||
@ -725,6 +734,9 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
||||
reservable_type: $scope.reservableType,
|
||||
slots_reservations_attributes: []
|
||||
};
|
||||
if (!angular.isUndefinedOrNull($scope.reservationContextsData.selected)) {
|
||||
reservation.reservation_context_id = $scope.reservationContextsData.selected.id;
|
||||
}
|
||||
angular.forEach(slots, function (slot) {
|
||||
reservation.slots_reservations_attributes.push({
|
||||
offered: slot.offered || false,
|
||||
|
@ -80,3 +80,9 @@ export interface ReservationIndexFilter extends ApiFilter {
|
||||
reservable_type?: ReservableType | Array<ReservableType>,
|
||||
user_id?: number
|
||||
}
|
||||
|
||||
export interface ReservationContext {
|
||||
id?: number,
|
||||
name: string,
|
||||
related_to?: number
|
||||
}
|
||||
|
@ -88,7 +88,8 @@ export const bookingSettings = [
|
||||
'display_name_enable',
|
||||
'book_overlapping_slots',
|
||||
'slot_duration',
|
||||
'overlapping_categories'
|
||||
'overlapping_categories',
|
||||
'reservation_context_feature'
|
||||
] as const;
|
||||
|
||||
export const themeSettings = [
|
||||
|
@ -429,9 +429,11 @@ angular.module('application.router', ['ui.router'])
|
||||
return Setting.query({
|
||||
names: "['machine_explications_alert', 'booking_window_start', 'booking_window_end', 'booking_move_enable', " +
|
||||
"'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
|
||||
"'online_payment_module', 'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list']"
|
||||
"'online_payment_module', 'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list', " +
|
||||
"'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}]
|
||||
}],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query({ applicable_on: 'machine' }).$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.machines_edit', {
|
||||
@ -518,9 +520,10 @@ angular.module('application.router', ['ui.router'])
|
||||
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
|
||||
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
|
||||
"'space_explications_alert', 'online_payment_module', 'payment_gateway', 'overlapping_categories', " +
|
||||
"'user_validation_required', 'user_validation_required_list']"
|
||||
"'user_validation_required', 'user_validation_required_list', 'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}]
|
||||
}],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query({ applicable_on: 'space' }).$promise; }]
|
||||
}
|
||||
})
|
||||
|
||||
@ -572,9 +575,11 @@ angular.module('application.router', ['ui.router'])
|
||||
names: "['booking_window_start', 'booking_window_end', 'booking_move_enable', 'booking_move_delay', " +
|
||||
"'booking_cancel_enable', 'booking_cancel_delay', 'subscription_explications_alert', " +
|
||||
"'training_explications_alert', 'training_information_message', 'online_payment_module', " +
|
||||
"'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list']"
|
||||
"'payment_gateway', 'overlapping_categories', 'user_validation_required', 'user_validation_required_list', " +
|
||||
"'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}]
|
||||
}],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query({ applicable_on: 'training' }).$promise; }]
|
||||
}
|
||||
})
|
||||
// notifications
|
||||
@ -1152,7 +1157,9 @@ angular.module('application.router', ['ui.router'])
|
||||
resolve: {
|
||||
membersPromise: ['Member', function (Member) { return Member.mapping().$promise; }],
|
||||
statisticsPromise: ['Statistics', function (Statistics) { return Statistics.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display']" }).$promise; }]
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['feature_tour_display', 'reservation_context_feature']" }).$promise; }],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query().$promise; }],
|
||||
reservationContextApplicableOnValuesPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.applicableOnValues().$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.stats_graphs', {
|
||||
@ -1193,14 +1200,16 @@ angular.module('application.router', ['ui.router'])
|
||||
"'extended_prices_in_same_day', 'recaptcha_site_key', 'recaptcha_secret_key', 'user_validation_required', " +
|
||||
"'user_validation_required_list', 'machines_module', 'user_change_group', 'show_username_in_admin_list', " +
|
||||
"'store_module', 'machine_reservation_deadline', 'training_reservation_deadline', 'event_reservation_deadline', " +
|
||||
"'space_reservation_deadline']"
|
||||
"'space_reservation_deadline', 'reservation_context_feature']"
|
||||
}).$promise;
|
||||
}],
|
||||
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
|
||||
cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }],
|
||||
cgvFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgv-file' }).$promise; }],
|
||||
faviconFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'favicon-file' }).$promise; }],
|
||||
profileImageFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'profile-image-file' }).$promise; }]
|
||||
profileImageFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'profile-image-file' }).$promise; }],
|
||||
reservationContextsPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.query().$promise; }],
|
||||
reservationContextApplicableOnValuesPromise: ['ReservationContext', function (ReservationContext) { return ReservationContext.applicableOnValues().$promise; }]
|
||||
}
|
||||
})
|
||||
|
||||
|
16
app/frontend/src/javascript/services/reservation_context.js
Normal file
16
app/frontend/src/javascript/services/reservation_context.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('ReservationContext', ['$resource', function ($resource) {
|
||||
return $resource('/api/reservation_contexts/:id',
|
||||
{ id: '@id' }, {
|
||||
update: {
|
||||
method: 'PUT'
|
||||
},
|
||||
applicableOnValues: {
|
||||
method: 'GET',
|
||||
url: '/api/reservation_contexts/applicable_on_values',
|
||||
isArray: true
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -250,3 +250,74 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default m-t-lg">
|
||||
<div class="panel-heading">
|
||||
<span class="font-sbold" translate>{{ 'app.admin.settings.reservation_context_feature_title' }}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.reservation_context_feature_info' | translate"></p>
|
||||
<boolean-setting name="'reservation_context_feature'"
|
||||
label="'app.admin.settings.reservation_context_feature' | translate"
|
||||
class-name="'m-l'"
|
||||
on-success="onSuccess"
|
||||
on-error="onError"
|
||||
on-change="onReservationContextFeatureChange">
|
||||
</boolean-setting>
|
||||
</div>
|
||||
<div class="row p-l p-r" ng-if="reservationContextFeature.value === 'true'">
|
||||
<h3 class="" translate>{{ 'app.admin.settings.reservation_context_options' }}</h3>
|
||||
|
||||
<button type="button" class="btn btn-warning m-b m-t" ng-click="addReservationContext()" translate>{{ 'app.admin.settings.add_a_reservation_context' }}</button>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40%" translate>{{ 'app.admin.reservation_contexts.name' }}</th>
|
||||
<th style="width:40%" translate>{{ 'app.admin.reservation_contexts.applicable_on' }}</th>
|
||||
<th style="width:20%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="reservationContext in reservationContexts">
|
||||
<td>
|
||||
<span editable-text="reservationContext.name" e-cols="100" e-name="name" e-form="rowform" e-required>
|
||||
{{ reservationContext.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div editable-ui-select="reservationContext.applicable_on" e-multiple e-form="rowform" e-name="applicable_on" e-ng-model="reservationContext.applicable_on" e-style="min-width: 200px">
|
||||
{{ translateApplicableOnValue(reservationContext.applicable_on) }}
|
||||
<editable-ui-select-match placeholder="">
|
||||
{{ translateApplicableOnValue($item) }}
|
||||
</editable-ui-select-match>
|
||||
<editable-ui-select-choices repeat="ao in reservationContextApplicableOnValues | filter: $select.search track by $index">
|
||||
{{ translateApplicableOnValue(ao) }}
|
||||
</editable-ui-select-choices>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<!-- form -->
|
||||
<form editable-form name="rowform" onbeforesave="saveReservationContext($data, reservationContext.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="newReservationContext == reservationContext">
|
||||
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelReservationContext(rowform, $index)" class="btn btn-default">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</form>
|
||||
<div class="buttons" ng-show="!rowform.$visible">
|
||||
<button class="btn btn-default" ng-click="rowform.$show()">
|
||||
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="removeReservationContext($index)">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -257,7 +257,9 @@
|
||||
<th ng-if="['booking', 'hour'].includes(type.active.key)" translate>{{ 'app.admin.statistics.reservation_date' }}</th>
|
||||
<th ng-if="!['booking', 'hour'].includes(type.active.key)" translate>{{ 'app.admin.statistics.date' }}</th>
|
||||
<th translate>{{ 'app.admin.statistics.user' }}</th>
|
||||
<th translate>{{ 'app.admin.statistics.gender' }}</th>
|
||||
<th ng-if="reservationContextFeatureEnabled && reservationContextIsApplicable(selectedIndex.es_type_key)">
|
||||
{{ 'app.admin.statistics.reservation_context' | translate }}
|
||||
</th>
|
||||
<th translate>{{ 'app.admin.statistics.age' }}</th>
|
||||
<th translate>{{ 'app.admin.statistics.type' }}</th>
|
||||
<th ng-if="!type.active.simple">{{type.active.label}}</th>
|
||||
@ -280,7 +282,9 @@
|
||||
<a ng-show="datum._source.userId" ui-sref="app.admin.members_edit({id:datum._source.userId})">{{getUserNameFromId(datum._source.userId)}}</a>
|
||||
<span class="text-gray text-italic" ng-hide="datum._source.userId" translate>{{ 'app.admin.statistics.deleted_user' }}</span>
|
||||
</td>
|
||||
<td>{{formatSex(datum._source.gender)}}</td>
|
||||
<td ng-if="reservationContextFeatureEnabled && reservationContextIsApplicable(selectedIndex.es_type_key)">
|
||||
{{ formatReservationContext(datum._source.reservationContextId) }}
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="datum._source.age">{{datum._source.age}} {{ 'app.admin.statistics.years_old' | translate }}</span>
|
||||
<span ng-if="!datum._source.age" translate>{{ 'app.admin.statistics.unknown' }}</span>
|
||||
|
@ -75,7 +75,8 @@
|
||||
after-payment="afterPayment"
|
||||
reservable-id="{{machine.id}}"
|
||||
reservable-type="Machine"
|
||||
reservable-name="{{machine.name}}"></cart>
|
||||
reservable-name="{{machine.name}}"
|
||||
reservation-contexts="reservationContexts"></cart>
|
||||
|
||||
<uib-alert type="warning m">
|
||||
<p class="text-sm">
|
||||
|
@ -18,6 +18,10 @@
|
||||
</div>
|
||||
|
||||
<div class="widget-content no-bg auto wrapper" ng-if="events.reserved.length > 0">
|
||||
<div ng-if="reservationContextsData.options.length > 0" class="m-b">
|
||||
<div class="font-sbold m-b-sm " translate>{{ 'app.shared.cart.select_the_reservation_context' }}</div>
|
||||
<select ng-model="reservationContextsData.selected" class="form-control m-t-sm" ng-options="o.name for o in reservationContextsData.options"></select>
|
||||
</div>
|
||||
|
||||
<div class="font-sbold m-b-sm " translate>{{ 'app.shared.cart.you_ve_just_selected_the_slot' }}</div>
|
||||
|
||||
|
@ -47,7 +47,8 @@
|
||||
after-payment="afterPayment"
|
||||
reservable-id="{{space.id}}"
|
||||
reservable-type="Space"
|
||||
reservable-name="{{space.name}}"></cart>
|
||||
reservable-name="{{space.name}}"
|
||||
reservation-contexts="reservationContexts"></cart>
|
||||
|
||||
|
||||
<uib-alert type="warning m" ng-show="spaceExplicationsAlert">
|
||||
|
@ -60,7 +60,8 @@
|
||||
reservable-id="{{training.id}}"
|
||||
reservable-type="Training"
|
||||
reservable-name="{{training.name}}"
|
||||
limit-to-one-slot="true"></cart>
|
||||
limit-to-one-slot="true"
|
||||
reservation-contexts="reservationContexts"></cart>
|
||||
|
||||
|
||||
<uib-alert type="info m">
|
||||
|
@ -47,6 +47,20 @@ module ExcelHelper
|
||||
[data, styles, types]
|
||||
end
|
||||
|
||||
def add_hardcoded_cells(index, hit, data, styles, types)
|
||||
if index.concerned_by_reservation_context?
|
||||
add_reservation_context_cell(hit, data, styles, types)
|
||||
end
|
||||
end
|
||||
|
||||
def add_reservation_context_cell(hit, data, styles, types)
|
||||
reservation_contexts = ReservationContext.pluck(:id, :name).to_h
|
||||
|
||||
data.push reservation_contexts[hit['_source']['reservationContextId']]
|
||||
styles.push nil
|
||||
types.push :text
|
||||
end
|
||||
|
||||
# append a cell containing the CA amount
|
||||
def add_ca_cell(index, hit, data, styles, types)
|
||||
return unless index.ca
|
||||
|
@ -202,6 +202,7 @@ module SettingsHelper
|
||||
projects_list_date_filters_presence
|
||||
project_categories_filter_placeholder
|
||||
project_categories_wording
|
||||
reservation_context_feature
|
||||
].freeze
|
||||
end
|
||||
# rubocop:enable Metrics/ModuleLength
|
||||
|
@ -13,6 +13,8 @@ class CartItem::MachineReservation < CartItem::Reservation
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
def type
|
||||
'machine'
|
||||
end
|
||||
|
@ -83,6 +83,7 @@ class CartItem::Reservation < CartItem::BaseItem
|
||||
::Reservation.new(
|
||||
reservable_id: reservable_id,
|
||||
reservable_type: reservable_type,
|
||||
reservation_context_id: reservation_context_id,
|
||||
slots_reservations_attributes: slots_params,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: customer).id
|
||||
)
|
||||
|
@ -13,6 +13,8 @@ class CartItem::SpaceReservation < CartItem::Reservation
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
def type
|
||||
'space'
|
||||
end
|
||||
|
@ -13,6 +13,8 @@ class CartItem::TrainingReservation < CartItem::Reservation
|
||||
|
||||
belongs_to :plan
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
def price
|
||||
base_amount = reservable&.amount_by_group(customer.group_id)&.amount
|
||||
is_privileged = operator.admin? || (operator.manager? && operator.id != customer.id)
|
||||
|
@ -6,6 +6,7 @@ module StatReservationConcern
|
||||
|
||||
included do
|
||||
attribute :reservationId, Integer
|
||||
attribute :reservationContextId, Integer
|
||||
attribute :ca, Float
|
||||
attribute :name, String
|
||||
end
|
||||
|
@ -26,6 +26,8 @@ class Reservation < ApplicationRecord
|
||||
has_many :booking_users, dependent: :destroy
|
||||
accepts_nested_attributes_for :booking_users, allow_destroy: true
|
||||
|
||||
belongs_to :reservation_context
|
||||
|
||||
validates :reservable_id, :reservable_type, presence: true
|
||||
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
||||
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
|
||||
|
27
app/models/reservation_context.rb
Normal file
27
app/models/reservation_context.rb
Normal file
@ -0,0 +1,27 @@
|
||||
class ReservationContext < ApplicationRecord
|
||||
has_many :reservations
|
||||
has_many :cart_item_reservations, dependent: :nullify, class_name: "CartItem::Reservation"
|
||||
|
||||
APPLICABLE_ON = %w[machine space training]
|
||||
|
||||
scope :applicable_on, ->(applicable_on) { where("applicable_on @> ?", "{#{applicable_on.presence_in(APPLICABLE_ON)}}")}
|
||||
|
||||
validates :name, presence: true
|
||||
validate :validate_applicable_on
|
||||
|
||||
def safe_destroy
|
||||
if reservations.count.zero?
|
||||
destroy
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_applicable_on
|
||||
return if applicable_on.all? { |applicable_on| applicable_on.in? APPLICABLE_ON }
|
||||
|
||||
errors.add(:applicable_on, :invalid)
|
||||
end
|
||||
end
|
@ -2,4 +2,11 @@ class StatisticIndex < ApplicationRecord
|
||||
has_many :statistic_types
|
||||
has_many :statistic_fields
|
||||
has_one :statistic_graph
|
||||
|
||||
def concerned_by_reservation_context?
|
||||
return false unless es_type_key.in? ReservationContext::APPLICABLE_ON
|
||||
return false unless Setting.get('reservation_context_feature')
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
7
app/policies/reservation_context_policy.rb
Normal file
7
app/policies/reservation_context_policy.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class ReservationContextPolicy < ApplicationPolicy
|
||||
%w(create update destroy applicable_on_values).each do |action|
|
||||
define_method "#{action}?" do
|
||||
user.admin?
|
||||
end
|
||||
end
|
||||
end
|
@ -47,7 +47,7 @@ class SettingPolicy < ApplicationPolicy
|
||||
machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label
|
||||
trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label
|
||||
events_banner_cta_url projects_list_member_filter_presence projects_list_date_filters_presence
|
||||
project_categories_filter_placeholder project_categories_wording family_account child_validation_required]
|
||||
project_categories_filter_placeholder project_categories_wording family_account child_validation_required reservation_context_feature]
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -157,14 +157,16 @@ class CartService
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
new_subscription: plan_info[:new_subscription],
|
||||
reservation_context_id: cart_item[:reservation_context_id])
|
||||
when Training
|
||||
CartItem::TrainingReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
new_subscription: plan_info[:new_subscription],
|
||||
reservation_context_id: cart_item[:reservation_context_id])
|
||||
when Event
|
||||
CartItem::EventReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
@ -179,7 +181,8 @@ class CartService
|
||||
reservable: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
plan: plan_info[:plan],
|
||||
new_subscription: plan_info[:new_subscription])
|
||||
new_subscription: plan_info[:new_subscription],
|
||||
reservation_context_id: cart_item[:reservation_context_id])
|
||||
else
|
||||
Rails.logger.warn "the reservable #{reservable} is not implemented"
|
||||
raise NotImplementedError
|
||||
|
@ -17,7 +17,9 @@ class Statistics::Builders::ReservationsBuilderService
|
||||
subType: r["#{category}_type".to_sym],
|
||||
ca: r[:ca],
|
||||
name: r["#{category}_name".to_sym],
|
||||
reservationId: r[:reservation_id] }.merge(user_info_stat(r)))
|
||||
reservationId: r[:reservation_id],
|
||||
reservationContextId: r[:reservation_context_id]
|
||||
}.merge(user_info_stat(r)))
|
||||
stat[:stat] = (type == 'booking' ? 1 : r[:nb_hours])
|
||||
stat["#{category}Id".to_sym] = r["#{category}_id".to_sym]
|
||||
|
||||
|
@ -46,7 +46,7 @@ class Statistics::FetcherService
|
||||
Reservation
|
||||
.where("reservable_type = 'Machine' AND slots_reservations.canceled_at IS NULL AND " \
|
||||
'reservations.created_at >= :start_date AND reservations.created_at <= :end_date', options)
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, statistic_profile: [:group])
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, :reservation_context, statistic_profile: [:group])
|
||||
.find_each do |r|
|
||||
next unless r.reservable
|
||||
|
||||
@ -58,7 +58,9 @@ class Statistics::FetcherService
|
||||
machine_name: r.reservable.name,
|
||||
slot_dates: r.slots.map(&:start_at).map(&:to_date),
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_f,
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
ca: calcul_ca(r.original_invoice),
|
||||
reservation_context_id: r.reservation_context_id
|
||||
}.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
end
|
||||
@ -70,7 +72,7 @@ class Statistics::FetcherService
|
||||
Reservation
|
||||
.where("reservable_type = 'Space' AND slots_reservations.canceled_at IS NULL AND " \
|
||||
'reservations.created_at >= :start_date AND reservations.created_at <= :end_date', options)
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, statistic_profile: [:group])
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, :reservation_context, statistic_profile: [:group])
|
||||
.find_each do |r|
|
||||
next unless r.reservable
|
||||
|
||||
@ -82,7 +84,9 @@ class Statistics::FetcherService
|
||||
space_type: r.reservable.slug,
|
||||
slot_dates: r.slots.map(&:start_at).map(&:to_date),
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_f,
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
ca: calcul_ca(r.original_invoice),
|
||||
reservation_context_id: r.reservation_context_id
|
||||
}.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
end
|
||||
@ -94,7 +98,7 @@ class Statistics::FetcherService
|
||||
Reservation
|
||||
.where("reservable_type = 'Training' AND slots_reservations.canceled_at IS NULL AND " \
|
||||
'reservations.created_at >= :start_date AND reservations.created_at <= :end_date', options)
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, statistic_profile: [:group])
|
||||
.eager_load(:slots, :slots_reservations, :invoice_items, :reservation_context, statistic_profile: [:group])
|
||||
.find_each do |r|
|
||||
next unless r.reservable
|
||||
|
||||
@ -107,7 +111,9 @@ class Statistics::FetcherService
|
||||
training_name: r.reservable.name,
|
||||
training_date: slot.start_at.to_date,
|
||||
nb_hours: difference_in_hours(slot.start_at, slot.end_at),
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
ca: calcul_ca(r.original_invoice),
|
||||
reservation_context_id: r.reservation_context_id
|
||||
}.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
end
|
||||
|
6
app/views/api/reservation_contexts/index.json.jbuilder
Normal file
6
app/views/api/reservation_contexts/index.json.jbuilder
Normal file
@ -0,0 +1,6 @@
|
||||
user_is_admin = (current_user and current_user.admin?)
|
||||
|
||||
json.array!(@reservation_contexts) do |reservation_context|
|
||||
json.extract! reservation_context, :id, :name, :applicable_on
|
||||
json.related_to reservation_context.reservations.count if user_is_admin
|
||||
end
|
1
app/views/api/reservation_contexts/show.json.jbuilder
Normal file
1
app/views/api/reservation_contexts/show.json.jbuilder
Normal file
@ -0,0 +1 @@
|
||||
json.extract! @reservation_context, :id, :name, :applicable_on
|
@ -26,7 +26,9 @@ wb.add_worksheet(name: ExcelService.name_safe(index.label)) do |sheet|
|
||||
fields.each do |f|
|
||||
columns.push f.label
|
||||
end
|
||||
columns.push t('export.reservation_context') if index.concerned_by_reservation_context?
|
||||
columns.push t('export.revenue') if index.ca
|
||||
|
||||
sheet.add_row columns, style: header
|
||||
|
||||
# data rows
|
||||
@ -37,6 +39,7 @@ wb.add_worksheet(name: ExcelService.name_safe(index.label)) do |sheet|
|
||||
fields.each do |f|
|
||||
format_xlsx_cell(hit['_source'][f.key], data, styles, types, source_data_type: f.data_type, date_format: date)
|
||||
end
|
||||
add_hardcoded_cells(index, hit, data, styles, types)
|
||||
add_ca_cell(index, hit, data, styles, types)
|
||||
|
||||
sheet.add_row data, style: styles, types: types
|
||||
|
@ -35,6 +35,7 @@ indices.each do |index|
|
||||
index.statistic_fields.each do |f|
|
||||
format_xlsx_cell(hit['_source'][f.key], data, styles, types, source_data_type: f.data_type, date_format: date)
|
||||
end
|
||||
add_hardcoded_cells(index, hit, data, styles, types)
|
||||
# proceed the 'ca' field if requested
|
||||
add_ca_cell(index, hit, data, styles, types)
|
||||
|
||||
|
@ -1596,6 +1596,7 @@ en:
|
||||
create_plans_to_start: "Start by creating new subscription plans."
|
||||
click_here: "Click here to create your first one."
|
||||
average_cart: "Average cart:"
|
||||
reservation_context: Reservation context
|
||||
#statistics graphs
|
||||
stats_graphs:
|
||||
statistics: "Statistics"
|
||||
@ -1850,6 +1851,15 @@ en:
|
||||
enable_family_account: "Enable the Family Account option"
|
||||
child_validation_required: "the account validation option for children"
|
||||
child_validation_required_label: "Activate the account validation option for children"
|
||||
reservation_context_feature_title: Reservation context
|
||||
reservation_context_feature_info: "If you enable this feature, members will have to enter the context of their reservation when reserving."
|
||||
reservation_context_feature: "Enable the feature \"Reservation context\""
|
||||
reservation_context_options: Reservation context options
|
||||
add_a_reservation_context: Add a new context
|
||||
confirmation_required: Confirmation required
|
||||
do_you_really_want_to_delete_this_reservation_context: "Do you really want to delete this context?"
|
||||
unable_to_delete_reservation_context_already_related_to_reservations: "Unable to delete this context because it is already associated to a reservation"
|
||||
unable_to_delete_reservation_context_an_error_occured: "Unable to delete: an error occurred"
|
||||
overlapping_options:
|
||||
training_reservations: "Trainings"
|
||||
machine_reservations: "Machines"
|
||||
@ -2524,3 +2534,9 @@ en:
|
||||
cta_switch: "Display a button"
|
||||
cta_label: "Button label"
|
||||
cta_url: "Button link"
|
||||
reservation_contexts:
|
||||
name: "Name"
|
||||
applicable_on: "Applicable on"
|
||||
machine: Machine
|
||||
training: Training
|
||||
space: Space
|
||||
|
@ -1588,6 +1588,7 @@ fr:
|
||||
create_plans_to_start: "Pour commencer, créez de nouvelles formules d'abonnement."
|
||||
click_here: "Cliquez ici pour créer votre première formule."
|
||||
average_cart: "Panier moyen :"
|
||||
reservation_context: Nature de la réservation
|
||||
#statistics graphs
|
||||
stats_graphs:
|
||||
statistics: "Statistiques"
|
||||
@ -1842,6 +1843,15 @@ fr:
|
||||
enable_family_account: "Activer l'option Compte Famille"
|
||||
child_validation_required: "l'option de validation des comptes enfants"
|
||||
child_validation_required_label: "Activer l'option de validation des comptes enfants"
|
||||
reservation_context_feature_title: Natures des réservations
|
||||
reservation_context_feature_info: "Si vous activez cette fonctionnalité, le membre devra obligatoirement saisir la nature de sa réservation au moment de réserver."
|
||||
reservation_context_feature: Activer la saisie obligatoire de la nature des réservations
|
||||
reservation_context_options: Natures des réservations possibles
|
||||
add_a_reservation_context: Ajouter une nouvelle nature
|
||||
confirmation_required: Confirmation requise
|
||||
do_you_really_want_to_delete_this_reservation_context: "Êtes-vous sûr de vouloir supprimer cette nature de réservation ?"
|
||||
unable_to_delete_reservation_context_already_related_to_reservations: "Impossible de supprimer cette nature de réservation car elle est actuellement associée à des réservations."
|
||||
unable_to_delete_reservation_context_an_error_occured: "Impossible de supprimer : une erreur est survenue."
|
||||
overlapping_options:
|
||||
training_reservations: "Formations"
|
||||
machine_reservations: "Machines"
|
||||
@ -2516,3 +2526,9 @@ fr:
|
||||
cta_switch: "Afficher un bouton"
|
||||
cta_label: "Libellé du bouton"
|
||||
cta_url: "Lien du bouton"
|
||||
reservation_contexts:
|
||||
name: "Nom"
|
||||
applicable_on: "Applicable à"
|
||||
machine: Machine
|
||||
training: Formation
|
||||
space: Espace
|
||||
|
@ -375,6 +375,9 @@ en:
|
||||
user_validation_required_alert: "Your administrator must validate your account. Then, you'll then be able to access all the booking features."
|
||||
child_validation_required_alert: "Your administrator must validate your child account. Then, you'll then be able to book the event."
|
||||
child_birthday_must_be_under_18_years_ago_alert: "Your child must be under 18. Then, you'll then be able to book the event."
|
||||
user_validation_required_alert: "Warning!<br>Your administrator must validate your account. Then, you'll then be able to access all the booking features."
|
||||
select_the_reservation_context: "Select the context of the reservation"
|
||||
please_select_a_reservation_context: "Please select the context of the reservation first"
|
||||
#feature-tour modal
|
||||
tour:
|
||||
previous: "Previous"
|
||||
|
@ -375,6 +375,8 @@ fr:
|
||||
user_validation_required_alert: "Attention !<br>Votre administrateur doit valider votre compte. Vous pourrez alors accéder à l'ensemble des fonctionnalités de réservation."
|
||||
child_validation_required_alert: "Attention !<br>Votre administrateur doit valider votre compte enfant. Vous pourrez alors réserver l'événement."
|
||||
child_birthday_must_be_under_18_years_ago_alert: "Attention !<br>La date de naissance de l'enfant doit être inférieure à 18 ans. Vous pourrez alors réserver l'événement."
|
||||
select_the_reservation_context: Sélectionnez la nature de la réservation
|
||||
please_select_a_reservation_context: "Veuillez tout d'abord sélectionner la nature de la réservation"
|
||||
#feature-tour modal
|
||||
tour:
|
||||
previous: "Précédent"
|
||||
|
@ -534,6 +534,7 @@ en:
|
||||
male: "Man"
|
||||
female: "Woman"
|
||||
deleted_user: "Deleted user"
|
||||
reservation_context: "Reservation context"
|
||||
#initial price's category for events, created to replace the old "reduced amount" property
|
||||
price_category:
|
||||
reduced_fare: "Reduced fare"
|
||||
@ -731,6 +732,7 @@ en:
|
||||
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
|
||||
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
|
||||
family_account: "Family account"
|
||||
reservation_context_feature: "Force member to select the nature of his reservation when reserving"
|
||||
#statuses of projects
|
||||
statuses:
|
||||
new: "New"
|
||||
|
@ -534,6 +534,7 @@ fr:
|
||||
male: "Homme"
|
||||
female: "Femme"
|
||||
deleted_user: "Utilisateur supprimé"
|
||||
reservation_context: "Nature de la réservation"
|
||||
#initial price's category for events, created to replace the old "reduced amount" property
|
||||
price_category:
|
||||
reduced_fare: "Tarif réduit"
|
||||
@ -730,6 +731,7 @@ fr:
|
||||
projects_list_date_filters_presence: "Filtre de présence de dates sur la liste des projets"
|
||||
project_categories_filter_placeholder: "Dans la galerie de projets, renommer le filtre \"Toutes les catégories\""
|
||||
project_categories_wording: "Dans la fiche projet, renommer l'intitulé de l'encart Catégories"
|
||||
reservation_context_feature: "Obligation pour le membre de saisir la nature de sa réservation au moment de réserver"
|
||||
#statuses of projects
|
||||
statuses:
|
||||
new: "Nouveau"
|
||||
|
@ -225,6 +225,9 @@ Rails.application.routes.draw do
|
||||
end
|
||||
# export accounting data to csv or equivalent
|
||||
post 'accounting/export' => 'accounting_exports#export'
|
||||
resources :reservation_contexts do
|
||||
get :applicable_on_values, on: :collection
|
||||
end
|
||||
|
||||
# i18n
|
||||
# regex allows using dots in URL for 'state'
|
||||
|
10
db/migrate/20230718133636_create_reservation_contexts.rb
Normal file
10
db/migrate/20230718133636_create_reservation_contexts.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class CreateReservationContexts < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
create_table :reservation_contexts do |t|
|
||||
t.string :name
|
||||
t.string :applicable_on, array: true, default: []
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class AddReservationContextIdToReservation < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_reference :reservations, :reservation_context, index: true, foreign_key: true
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class AddReservationContextIdToCartItemReservations < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_reference :cart_item_reservations, :reservation_context, foreign_key: true
|
||||
end
|
||||
end
|
@ -735,3 +735,5 @@ Setting.set('project_categories_filter_placeholder', 'Toutes les catégories') u
|
||||
Setting.set('project_categories_wording', 'Catégories') unless Setting.find_by(name: 'project_categories_wording').try(:value)
|
||||
Setting.set('family_account', false) unless Setting.find_by(name: 'family_account').try(:value)
|
||||
Setting.set('child_validation_required', false) unless Setting.find_by(name: 'child_validation_required').try(:value)
|
||||
|
||||
Setting.set('reservation_context_feature', false) unless Setting.find_by(name: 'reservation_context_feature')
|
||||
|
@ -839,7 +839,8 @@ CREATE TABLE public.cart_item_reservations (
|
||||
operator_profile_id bigint,
|
||||
type character varying,
|
||||
created_at timestamp without time zone NOT NULL,
|
||||
updated_at timestamp without time zone NOT NULL
|
||||
updated_at timestamp without time zone NOT NULL,
|
||||
reservation_context_id bigint
|
||||
);
|
||||
|
||||
|
||||
@ -3126,6 +3127,38 @@ CREATE SEQUENCE public.projects_themes_id_seq
|
||||
ALTER SEQUENCE public.projects_themes_id_seq OWNED BY public.projects_themes.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE TABLE public.reservation_contexts (
|
||||
id bigint NOT NULL,
|
||||
name character varying,
|
||||
applicable_on character varying[] DEFAULT '{}'::character varying[],
|
||||
created_at timestamp(6) without time zone NOT NULL,
|
||||
updated_at timestamp(6) without time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.reservation_contexts_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.reservation_contexts_id_seq OWNED BY public.reservation_contexts.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations; Type: TABLE; Schema: public; Owner: -
|
||||
--
|
||||
@ -3138,7 +3171,8 @@ CREATE TABLE public.reservations (
|
||||
reservable_type character varying,
|
||||
reservable_id integer,
|
||||
nb_reserve_places integer,
|
||||
statistic_profile_id integer
|
||||
statistic_profile_id integer,
|
||||
reservation_context_id bigint
|
||||
);
|
||||
|
||||
|
||||
@ -5007,6 +5041,13 @@ ALTER TABLE ONLY public.projects_spaces ALTER COLUMN id SET DEFAULT nextval('pub
|
||||
ALTER TABLE ONLY public.projects_themes ALTER COLUMN id SET DEFAULT nextval('public.projects_themes_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservation_contexts ALTER COLUMN id SET DEFAULT nextval('public.reservation_contexts_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
@ -5954,6 +5995,14 @@ ALTER TABLE ONLY public.projects_themes
|
||||
ADD CONSTRAINT projects_themes_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservation_contexts reservation_contexts_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservation_contexts
|
||||
ADD CONSTRAINT reservation_contexts_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations reservations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -6503,6 +6552,13 @@ CREATE INDEX index_cart_item_reservations_on_plan_id ON public.cart_item_reserva
|
||||
CREATE INDEX index_cart_item_reservations_on_reservable ON public.cart_item_reservations USING btree (reservable_type, reservable_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_cart_item_reservations_on_reservation_context_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_cart_item_reservations_on_reservation_context_id ON public.cart_item_reservations USING btree (reservation_context_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_cart_item_slots_on_cart_item; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -7259,6 +7315,13 @@ CREATE INDEX index_projects_themes_on_theme_id ON public.projects_themes USING b
|
||||
CREATE INDEX index_reservations_on_reservable_type_and_reservable_id ON public.reservations USING btree (reservable_type, reservable_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_reservations_on_reservation_context_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_reservations_on_reservation_context_id ON public.reservations USING btree (reservation_context_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_reservations_on_statistic_profile_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@ -8500,6 +8563,14 @@ ALTER TABLE ONLY public.accounting_periods
|
||||
ADD CONSTRAINT fk_rails_cc9abff81f FOREIGN KEY (closed_by) REFERENCES public.users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: reservations fk_rails_cfaaf202e7; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.reservations
|
||||
ADD CONSTRAINT fk_rails_cfaaf202e7 FOREIGN KEY (reservation_context_id) REFERENCES public.reservation_contexts(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: wallet_transactions fk_rails_d07bc24ce3; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -8548,6 +8619,14 @@ ALTER TABLE ONLY public.payment_schedule_items
|
||||
ADD CONSTRAINT fk_rails_d6030dd0e7 FOREIGN KEY (payment_schedule_id) REFERENCES public.payment_schedules(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: cart_item_reservations fk_rails_daa5b1b8b9; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.cart_item_reservations
|
||||
ADD CONSTRAINT fk_rails_daa5b1b8b9 FOREIGN KEY (reservation_context_id) REFERENCES public.reservation_contexts(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: product_stock_movements fk_rails_dc802d5f48; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@ -9076,5 +9155,6 @@ INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20230626122844'),
|
||||
('20230626122947'),
|
||||
('20230710072403');
|
||||
|
||||
|
||||
('20230718133636'),
|
||||
('20230718134350'),
|
||||
('20230720085857');
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fab-manager",
|
||||
"version": "6.0.9",
|
||||
"version": "6.0.10",
|
||||
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
|
||||
"keywords": [
|
||||
"fablab",
|
||||
|
14
test/fixtures/reservation_contexts.yml
vendored
Normal file
14
test/fixtures/reservation_contexts.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
reservation_context_1:
|
||||
id: 1
|
||||
name: Pédagogique
|
||||
applicable_on: [machine, training, space]
|
||||
created_at: 2023-07-20 15:39:08.259759000 Z
|
||||
updated_at: 2023-07-20 15:39:08.259759000 Z
|
||||
|
||||
reservation_context_2:
|
||||
id: 2
|
||||
name: Recherche
|
||||
applicable_on: [machine, training, space]
|
||||
created_at: 2023-07-20 15:39:08.265840000 Z
|
||||
updated_at: 2023-07-20 15:39:08.265840000 Z
|
6
test/fixtures/settings.yml
vendored
6
test/fixtures/settings.yml
vendored
@ -622,3 +622,9 @@ setting_105:
|
||||
name: child_validation_required
|
||||
created_at: 2023-03-31 14:38:40.000421500 Z
|
||||
updated_at: 2023-03-31 14:38:40.000421500 Z
|
||||
|
||||
setting_106:
|
||||
id: 106
|
||||
name: reservation_context_feature
|
||||
created_at: 2023-04-05 09:16:08.000511500 Z
|
||||
updated_at: 2023-04-05 09:16:08.000511500 Z
|
||||
|
8
test/frontend/__fixtures__/reservation_contexts.ts
Normal file
8
test/frontend/__fixtures__/reservation_contexts.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ReservationContext } from 'models/reservation';
|
||||
|
||||
const reservationContexts: Array<ReservationContext> = [
|
||||
{ id: 1, name: 'Research' },
|
||||
{ id: 2, name: 'Pedagogical' }
|
||||
];
|
||||
|
||||
export default reservationContexts;
|
@ -855,6 +855,12 @@ export const settings: Array<Setting> = [
|
||||
value: 'false',
|
||||
last_update: '2023-03-31T14:39:12+0100',
|
||||
localized: 'Family account'
|
||||
},
|
||||
{
|
||||
name: 'reservation_context_feature',
|
||||
value: 'false',
|
||||
last_update: '2022-12-23T14:39:12+0100',
|
||||
localized: 'Reservation context feature'
|
||||
}
|
||||
];
|
||||
|
||||
|
88
test/integration/reservation_contexts_test.rb
Normal file
88
test/integration/reservation_contexts_test.rb
Normal file
@ -0,0 +1,88 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ReservationContextsTest < ActionDispatch::IntegrationTest
|
||||
def setup
|
||||
@admin = User.find_by(username: 'admin')
|
||||
login_as(@admin, scope: :user)
|
||||
end
|
||||
|
||||
test 'create a reservation_context' do
|
||||
applicable_on = ["machine", "space", "training"]
|
||||
post '/api/reservation_contexts',
|
||||
params: {
|
||||
name: 'Enseignant',
|
||||
applicable_on: applicable_on
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 201, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the correct reservation_context was created
|
||||
res = json_response(response.body)
|
||||
reservation_context = ReservationContext.where(id: res[:id]).first
|
||||
assert_not_nil reservation_context, 'reservation_context was not created in database'
|
||||
|
||||
assert_equal 'Enseignant', res[:name]
|
||||
assert_equal applicable_on, res[:applicable_on]
|
||||
end
|
||||
|
||||
test 'update a reservation_context' do
|
||||
applicable_on = ["machine"]
|
||||
patch '/api/reservation_contexts/1',
|
||||
params: {
|
||||
name: 'Nouveau nom',
|
||||
applicable_on: applicable_on
|
||||
}.to_json,
|
||||
headers: default_headers
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the reservation_context was updated
|
||||
res = json_response(response.body)
|
||||
assert_equal 1, res[:id]
|
||||
assert_equal 'Nouveau nom', res[:name]
|
||||
assert_equal applicable_on, res[:applicable_on]
|
||||
end
|
||||
|
||||
test 'list all reservation_contexts' do
|
||||
logout @admin
|
||||
get '/api/reservation_contexts'
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the list items are ok
|
||||
reservation_contexts = json_response(response.body)
|
||||
assert_equal ReservationContext.count, reservation_contexts.count
|
||||
assert_equal reservation_contexts(:reservation_context_1).name, reservation_contexts[0][:name]
|
||||
end
|
||||
|
||||
test "list all applicable_on possible values" do
|
||||
get '/api/reservation_contexts/applicable_on_values'
|
||||
|
||||
# Check response format & reservation_context
|
||||
assert_equal 200, response.status, response.body
|
||||
assert_match Mime[:json].to_s, response.content_type
|
||||
|
||||
# Check the list items are ok
|
||||
applicable_on_values = json_response(response.body)
|
||||
assert_equal %w[machine space training], applicable_on_values
|
||||
end
|
||||
|
||||
test 'delete a reservation_context' do
|
||||
reservation_context = ReservationContext.create!(name: 'Gone too soon')
|
||||
delete "/api/reservation_contexts/#{reservation_context.id}"
|
||||
assert_response :success
|
||||
assert_empty response.body
|
||||
assert_raise ActiveRecord::RecordNotFound do
|
||||
reservation_context.reload
|
||||
end
|
||||
end
|
||||
end
|
44
test/meta/i18n_test.rb
Normal file
44
test/meta/i18n_test.rb
Normal file
@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class I18nTest < ActiveSupport::TestCase
|
||||
# Do not use I18n.default_locale, reference locale can be different
|
||||
REFERENCE_LOCALE = :fr
|
||||
LOCALES_TO_CHECK = [:en]
|
||||
|
||||
SKIP_FILES = %w[
|
||||
devise
|
||||
rails
|
||||
].freeze
|
||||
|
||||
FILE_IDS = Rails.root.glob("config/locales/*.#{REFERENCE_LOCALE}.yml")
|
||||
.map { File.basename(_1) }
|
||||
.map { _1.split(".")[0..-3].join(".") } # view.admin.fr.yml -> view.admin
|
||||
.reject { SKIP_FILES.include?(_1) }
|
||||
|
||||
LOCALES_TO_CHECK.each do |locale|
|
||||
FILE_IDS.each do |file_id|
|
||||
test "#{file_id}.#{locale}.yml have same keys as #{file_id}.#{REFERENCE_LOCALE}.yml" do
|
||||
reference_keys = read_keys(REFERENCE_LOCALE, file_id)
|
||||
locale_keys = read_keys(locale, file_id)
|
||||
|
||||
reference_keys.each_with_index do |reference_key, index|
|
||||
next if index.zero?
|
||||
|
||||
unless reference_key[0] == "#"
|
||||
assert_equal reference_key, locale_keys[index], "invalid key at line #{index + 1}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def read_keys(locale, file_id)
|
||||
file = Rails.root.join("config/locales/#{file_id}.#{locale}.yml")
|
||||
|
||||
File.read(file)
|
||||
.split("\n")
|
||||
.map { _1.split(":", 2).first.to_s.strip }
|
||||
end
|
||||
end
|
60
test/models/reservation_context_test.rb
Normal file
60
test/models/reservation_context_test.rb
Normal file
@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ReservationContextTest < ActiveSupport::TestCase
|
||||
test 'fixtures are valid' do
|
||||
ReservationContext.find_each do |reservation_context|
|
||||
assert reservation_context.valid?
|
||||
end
|
||||
end
|
||||
|
||||
test "applicable_on validation" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
|
||||
reservation_context.applicable_on << "wrong"
|
||||
|
||||
assert reservation_context.invalid?
|
||||
assert_equal reservation_context.errors.details, { applicable_on: [{ error: :invalid }] }
|
||||
end
|
||||
|
||||
test "name validation" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
|
||||
reservation_context.name = nil
|
||||
|
||||
assert reservation_context.invalid?
|
||||
assert_equal reservation_context.errors.details, { name: [{ error: :blank }] }
|
||||
end
|
||||
|
||||
test "#safe_destroy" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
reservation = reservations(:reservation_1).tap { |r| r.update!(reservation_context: reservation_context) }
|
||||
|
||||
assert_not reservation_context.safe_destroy
|
||||
|
||||
reservation.update!(reservation_context_id: nil)
|
||||
|
||||
assert reservation_context.safe_destroy
|
||||
assert reservation_context.destroyed?
|
||||
end
|
||||
|
||||
test "scope applicable_on" do
|
||||
assert_equal reservation_contexts(:reservation_context_1, :reservation_context_2), ReservationContext.applicable_on("space")
|
||||
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
reservation_context.applicable_on.delete("space")
|
||||
reservation_context.save!
|
||||
|
||||
assert_equal [reservation_contexts(:reservation_context_2)], ReservationContext.applicable_on("space")
|
||||
end
|
||||
|
||||
test "foreign key on reservations" do
|
||||
reservation_context = reservation_contexts(:reservation_context_1)
|
||||
reservation = reservations(:reservation_1).tap { |r| r.update!(reservation_context: reservation_context) }
|
||||
|
||||
assert_raise ActiveRecord::InvalidForeignKey do
|
||||
reservation_context.destroy!
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user