1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

Ability to configure data sources for preventing booking on overlapping slots

This commit is contained in:
Sylvain 2021-10-22 14:50:02 +02:00
parent b1245a5248
commit 840c536c75
14 changed files with 94 additions and 34 deletions

View File

@ -1,6 +1,7 @@
# Changelog Fab-manager
- Refactored subscription new/renew/free extend interfaces and API
- Refactored subscription new/renew/free extend interfaces and API
- Ability to configure data sources for preventing booking on overlapping slots
- Updated production documentation
- Updated SSO documentation
- Improved stripe subscription process with better error handling
@ -23,6 +24,7 @@
- Fix a security issue: updated nokogiri to 1.12.5 to fix [CVE-2021-41098](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41098)
- Fix a security issue: updated puma to 4.3.9 to fix [CVE-2021-41136](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-41136)
- Fix a security issue: updated sidekiq to 6.2.1 to fix [CVE-2021-30151](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-30151)
- [TODO DEPLOY] `rails db:seed`
## v5.1.10 2021 October 04

View File

@ -5,6 +5,7 @@ import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
import SettingAPI from '../../api/setting';
import { Loader } from '../base/loader';
import { FabButton } from '../base/fab-button';
declare const Application: IApplication;
@ -12,23 +13,32 @@ interface CheckListSettingProps {
name: SettingName,
label: string,
className?: string,
allSettings: Record<SettingName, string>,
availableOptions: Array<string>,
// availableOptions must be like this [['option1', 'label 1'], ['option2', 'label 2']]
availableOptions: Array<Array<string>>,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, className, allSettings, availableOptions, onSuccess, onError }) => {
/**
* This component allows to configure multiples values for a setting, like a check list.
* The result is stored as a string, composed of the checked values, e.g. 'option1,option2'
*/
const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, className, availableOptions, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [value, setValue] = useState<string>(null);
// on component load, we retrieve the current value of the list from the API
useEffect(() => {
if (!allSettings) return;
setValue(allSettings[name]);
}, [allSettings]);
SettingAPI.get(name)
.then(res => setValue(res.value))
.catch(err => onError(err));
}, []);
/**
* Callback triggered when a checkbox is ticked or unticked.
* This function construct the resulting string, by adding or deleting the provided option identifier.
*/
const toggleCheckbox = (option: string) => {
return (event: BaseSyntheticEvent) => {
if (event.target.checked) {
@ -42,34 +52,41 @@ const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label, classN
};
};
/**
* Callback triggered when the 'save' button is clicked.
* Save the built string to the Setting API
*/
const handleSave = () => {
SettingAPI.update(name, value)
.then(() => onSuccess(t('app.admin.check_list_setting.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.${name}`) })))
.catch(err => onError(err));
};
/**
* Verify if the provided option is currently ticked (i.e. included in the value string)
*/
const isChecked = (option) => {
return value?.includes(option);
};
return (
<div className={`check-list-setting ${className || ''}`}>
<span className="check-list-title">{label}</span>
{availableOptions.map(option => <div key={option}>
<input id={`setting-${name}-${option}`} type="checkbox" checked={isChecked(option)} onChange={toggleCheckbox(option)} />
<label htmlFor={`setting-${name}-${option}`}>{option}</label>
<h4 className="check-list-title">{label}</h4>
{availableOptions.map(option => <div key={option[0]}>
<input id={`setting-${name}-${option[0]}`} type="checkbox" checked={isChecked(option[0])} onChange={toggleCheckbox(option[0])} />
<label htmlFor={`setting-${name}-${option[0]}`}>{option[1]}</label>
</div>)}
<button className="save" onClick={handleSave}>{t('app.admin.buttons.save')}</button>
<FabButton className="save" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton>
</div>
);
};
const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ allSettings, availableOptions, onSuccess, onError, label, className, name }) => {
const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name }) => {
return (
<Loader>
<CheckListSetting allSettings={allSettings} availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} />
<CheckListSetting availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} />
</Loader>
);
};
Application.Components.component('checkListSetting', react2angular(CheckListSettingWrapper, ['allSettings', 'className', 'name', 'label', 'availableOptions', 'onSuccess', 'onError']));
Application.Components.component('checkListSetting', react2angular(CheckListSettingWrapper, ['className', 'name', 'label', 'availableOptions', 'onSuccess', 'onError']));

View File

@ -328,6 +328,16 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
growl.error(message);
};
/**
* Options for allow/prevent book overlapping slots: which kind of slots are used in the overlapping computation
*/
$scope.availableOverlappingOptions = [
['training_reservations', _t('app.admin.settings.overlapping_options.training_reservations')],
['machine_reservations', _t('app.admin.settings.overlapping_options.machine_reservations')],
['space_reservations', _t('app.admin.settings.overlapping_options.space_reservations')],
['events_reservations', _t('app.admin.settings.overlapping_options.events_reservations')]
];
/**
* Setup the feature-tour for the admin/settings page.
* This is intended as a contextual help (when pressing F1)

View File

@ -456,12 +456,7 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
* @param callback {function}
*/
const validateSameTimeReservations = function (slot, callback) {
let sameTimeReservations = [
'training_reservations',
'machine_reservations',
'space_reservations',
'events_reservations'
].map(function (k) {
$scope.settings.overlapping_categories.split(',').map(function (k) {
return _.filter($scope.user[k], function (r) {
return slot.start.isSame(r.start_at) ||
(slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) ||

View File

@ -111,6 +111,7 @@ export enum SettingName {
PublicAgendaModule = 'public_agenda_module',
RenewPackThreshold = 'renew_pack_threshold',
PackOnlyForSubscription = 'pack_only_for_subscription',
OverlappingCategories = 'overlapping_categories'
}
export type SettingValue = string|boolean|number;

View File

@ -365,7 +365,7 @@ 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']"
"'online_payment_module', 'payment_gateway', 'overlapping_categories']"
}).$promise;
}]
}
@ -451,7 +451,7 @@ angular.module('application.router', ['ui.router'])
return Setting.query({
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']"
"'space_explications_alert', 'online_payment_module', 'payment_gateway', 'overlapping_categories']"
}).$promise;
}]
}
@ -505,7 +505,7 @@ 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']"
"'payment_gateway', 'overlapping_categories']"
}).$promise;
}]
}
@ -534,7 +534,7 @@ angular.module('application.router', ['ui.router'])
resolve: {
subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway', 'overlapping_categories']" }).$promise; }]
}
})
@ -1080,7 +1080,7 @@ angular.module('application.router', ['ui.router'])
"'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " +
"'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " +
"'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown', 'public_agenda_module'," +
"'renew_pack_threshold', 'pack_only_for_subscription']"
"'renew_pack_threshold', 'pack_only_for_subscription', 'overlapping_categories']"
}).$promise;
}],
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],

View File

@ -63,6 +63,7 @@
@import "modules/pricing/pack-form";
@import "modules/pricing/delete-pack";
@import "modules/pricing/edit-pack";
@import "modules/settings/check-list-setting";
@import "modules/prepaid-packs/propose-packs-modal";
@import "modules/prepaid-packs/packs-summary";
@import "modules/subscriptions/free-extend-modal";

View File

@ -0,0 +1,14 @@
.check-list-setting {
.check-list-title {
font-weight: 600;
}
label {
margin-left: 1em;
}
.save {
background-color: #999;
border-color: #999;
}
}

View File

@ -91,12 +91,14 @@
label="app.admin.settings.allow_booking"
classes="m-l">
</boolean-setting>
<div class="alert alert-warning" ng-show="allSettings.book_overlapping_slots !== 'true'" translate>
{{ 'app.admin.settings.overlapping_categories_info' }}
</div>
</div>
<div class="col-md-6">
<check-list-setting name="overlapping_categories"
<div class="col-md-6" ng-show="allSettings.book_overlapping_slots !== 'true'">
<check-list-setting name="'overlapping_categories'"
label="'app.admin.settings.overlapping_categories' | translate"
all-setting="allSettings"
available-options="['training_reservations','machine_reservations','space_reservations','events_reservations']"
available-options="availableOverlappingOptions"
on-success="onSuccess"
on-error="onError">
</check-list-setting>

View File

@ -122,8 +122,11 @@ class Setting < ApplicationRecord
renew_pack_threshold
pack_only_for_subscription
overlapping_categories] }
# WARNING: when adding a new key, you may also want to add it in app/policies/setting_policy.rb#public_whitelist
# and in config/locales/en.yml#settings
# WARNING: when adding a new key, you may also want to add it in:
# - config/locales/en.yml#settings
# - app/frontend/src/javascript/models/setting.ts#SettingName
# - db/seeds.rb (to set the default value)
# - app/policies/setting_policy.rb#public_whitelist (if the setting can be read by anyone)
def value
last_value = history_values.order(HistoryValue.arel_table['created_at'].desc).limit(1).first

View File

@ -40,7 +40,7 @@ class SettingPolicy < ApplicationPolicy
recaptcha_site_key feature_tour_display disqus_shortname allowed_cad_extensions openlab_app_id openlab_default
online_payment_module stripe_public_key confirmation_required wallet_module trainings_module address_required
payment_gateway payzen_endpoint payzen_public_key public_agenda_module renew_pack_threshold statistics_module
pack_only_for_subscription]
pack_only_for_subscription overlapping_categories]
end
##

View File

@ -1181,6 +1181,8 @@ en:
an_error_occurred_saving_the_setting: "An error occurred while saving the setting. Please try again later."
book_overlapping_slots_info: "Allow / prevent the reservation of overlapping slots"
allow_booking: "Allow booking"
overlapping_categories: "Overlapping categories"
overlapping_categories_info: "Preventing booking on overlapping slots will be done by comparing the date and time of the following categories of reservations."
default_slot_duration: "Default duration for slots"
duration_minutes: "Duration (in minutes)"
default_slot_duration_info: "Machine and space availabilities are divided in multiple slots of this duration. This value can be overridden per availability."
@ -1237,6 +1239,11 @@ en:
pack_only_for_subscription_info_html: "If this option is activated, the purchase and use of a prepaid pack is only possible for the user with a valid subscription."
pack_only_for_subscription: "Subscription valid for purchase and use of a prepaid pack"
pack_only_for_subscription_info: "Make subscription mandatory for prepaid packs"
overlapping_options:
training_reservations: "Trainings"
machine_reservations: "Machines"
space_reservations: "Spaces"
events_reservations: "Events"
general:
general: "General"
title: "Title"
@ -1397,6 +1404,9 @@ en:
card_collection_info: "By validating, you'll be prompted for the member's card number. This card will be automatically charged at the deadlines."
check_collection_info: "By validating, you confirm that you have {DEADLINES} checks, allowing you to collect all the monthly payments."
online_payment_disabled: "Online payment is not available. You cannot collect this payment schedule by online card."
check_list_setting:
save: 'Save'
customization_of_SETTING_successfully_saved: "Customization of the {SETTING} successfully saved."
#feature tour
tour:
conclusion:

View File

@ -534,3 +534,4 @@ en:
public_agenda_module: "Public agenda module"
renew_pack_threshold: "Threshold for packs renewal"
pack_only_for_subscription: "Restrict packs for subscribers"
overlapping_categories: "Categories for overlapping booking prevention"

View File

@ -901,6 +901,10 @@ Setting.set('renew_pack_threshold', 0.2) unless Setting.find_by(name: 'renew_pac
Setting.set('pack_only_for_subscription', true) unless Setting.find_by(name: 'pack_only_for_subscription').try(:value)
unless Setting.find_by(name: 'overlapping_categories').try(:value)
Setting.set('overlapping_categories', 'training_reservations,machine_reservations,space_reservations,events_reservations')
end
if StatisticCustomAggregation.count.zero?
# available reservations hours for machines
machine_hours = StatisticType.find_by(key: 'hour', statistic_index_id: 2)