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

Merge branch 'dev' for release 5.4.16

This commit is contained in:
Sylvain 2022-08-24 17:10:10 +02:00
commit e98776d51f
44 changed files with 472 additions and 856 deletions

18
.overcommit.yml Normal file
View File

@ -0,0 +1,18 @@
PreCommit:
RuboCop:
enabled: true
on_warn: fail # Treat all warnings as failures
TrailingWhitespace:
enabled: true
CommitMsg:
CapitalizedSubject:
enabled: false
MessageFormat:
enabled: true
pattern: ^(\([a-z]+\) [\w ]+(\n\n.+)?)|(Version (\d+\.?)+)|(Merge branch .*)
expected_pattern_message: (type) title\n\ndescription
sample_message: (bug) no validation on date\n\nThe birthdate was not validated...

View File

@ -1,6 +1,20 @@
# Changelog Fab-manager
## next release
## v5.4.16 2022 August 24
- Updated portuguese translations
- Added automatic RuboCop validation on pre-commit
- Use union type instead of enum for SettingName
- Clarified documentation about default values for environment variables
- Updated documentation about the minimum RAM required (#385)
- Fix a bug: wrong variable reference in `SingleSignOnConcern:Merge_form_sso`
- Fix a bug: wrong focus behavior on text editor
- Fix a bug: trainings monitoring is not available
- Fix a bug: invalid password length verification in profile edtion form
- Fix a bug: invalid password verification in setup script
- Fix a bug: during setup, unable to chown the installation folder, if the current user does not have a self-named group
- Fix a bug: during setup, the current value in config/env is not shown
- Fix a bug: disabling/removing a group has side effects on other groups
## v5.4.15 2022 August 1

View File

@ -36,6 +36,7 @@ group :development do
gem 'web-console', '>= 3.3.0'
# Preview mail in the browser
gem 'listen', '~> 3.0.5'
gem 'overcommit'
gem 'rb-readline'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'railroady'
@ -50,9 +51,9 @@ group :test do
gem 'faker'
gem 'minitest-reporters'
gem 'pdf-reader'
gem 'rubyXL'
gem 'vcr', '6.0.0'
gem 'webmock'
gem 'rubyXL'
end
group :production, :staging do
@ -66,8 +67,6 @@ gem 'pg_search'
# authentication
gem 'devise', '>= 4.6.0'
gem 'omniauth', '~> 1.9.0'
gem 'omniauth-oauth2'
gem 'omniauth_openid_connect'
@ -145,4 +144,4 @@ gem 'tzinfo-data'
# compilation of dynamic stylesheets (home page & theme)
gem 'sassc', '= 2.1.0'
gem 'redis-session-store'
gem 'redis-session-store'

View File

@ -93,6 +93,7 @@ GEM
caxlsx_rails (0.6.2)
actionpack (>= 3.1)
caxlsx (>= 3.0)
childprocess (4.1.0)
chroma (0.2.0)
cldr-plurals-runtime-rb (1.0.1)
coercible (1.0.0)
@ -176,6 +177,7 @@ GEM
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
iniparse (1.5.0)
jbuilder (2.10.0)
activesupport (>= 5.0.0)
jbuilder_cache_multi (0.1.0)
@ -272,6 +274,10 @@ GEM
openlab_ruby (0.0.7)
httparty (~> 0.20)
orm_adapter (0.5.0)
overcommit (0.59.1)
childprocess (>= 0.6.3, < 5)
iniparse (~> 1.4)
rexml (~> 3.2)
parallel (1.19.1)
parser (3.1.2.0)
ast (~> 2.4.1)
@ -532,6 +538,7 @@ DEPENDENCIES
omniauth-rails_csrf_protection (~> 0.1)
omniauth_openid_connect
openlab_ruby
overcommit
pdf-reader
pg
pg_search

View File

@ -52,7 +52,13 @@ class API::TrainingsController < API::ApiController
authorize Training
@training = Training.find(params[:id])
@availabilities = @training.availabilities
.includes(slots: { slots_reservations: { reservations: { statistic_profile: [:trainings, user: [:profile]] } } })
.includes(slots: {
slots_reservations: {
reservation: {
statistic_profile: [:trainings, { user: [:profile] }]
}
}
})
.where('slots_reservations.canceled_at': nil)
.order('availabilities.start_at DESC')
end

View File

@ -55,7 +55,7 @@ export default class SettingAPI {
itemData.localized = item.localized;
}
map.set(item.name as SettingName, itemData);
map.set(item.name, itemData);
});
return map;
}

View File

@ -20,17 +20,17 @@ const ReservationsDashboard: React.FC<ReservationsDashboardProps> = ({ onError,
const [modules, setModules] = useState<Map<SettingName, string>>();
useEffect(() => {
SettingAPI.query([SettingName.SpacesModule, SettingName.MachinesModule])
SettingAPI.query(['spaces_module', 'machines_module'])
.then(res => setModules(res))
.catch(error => onError(error));
}, []);
return (
<div className="reservations-dashboard">
{modules?.get(SettingName.MachinesModule) !== 'false' && <CreditsPanel userId={userId} onError={onError} reservableType="Machine" />}
{modules?.get(SettingName.SpacesModule) !== 'false' && <CreditsPanel userId={userId} onError={onError} reservableType="Space" />}
{modules?.get(SettingName.MachinesModule) !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Machine" />}
{modules?.get(SettingName.SpacesModule) !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Space" />}
{modules?.get('machines_module') !== 'false' && <CreditsPanel userId={userId} onError={onError} reservableType="Machine" />}
{modules?.get('spaces_module') !== 'false' && <CreditsPanel userId={userId} onError={onError} reservableType="Space" />}
{modules?.get('machines_module') !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Machine" />}
{modules?.get('spaces_module') !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Space" />}
</div>
);
};

View File

@ -9,15 +9,16 @@ export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<A
tooltip?: ReactNode,
className?: string,
disabled?: boolean|((id: string) => boolean),
onLabelClick?: (event: React.MouseEvent<HTMLLabelElement, MouseEvent>) => void,
onLabelClick?: (event: React.MouseEvent<HTMLParagraphElement, MouseEvent>) => void,
inLine?: boolean,
containerType?: 'label' | 'div'
}
/**
* This abstract component should not be used directly.
* Other forms components that are intended to be used with react-hook-form must extend this component.
*/
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, children }: AbstractFormItemProps<TFieldValues>) => {
export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label, tooltip, className, disabled, error, warning, rules, formState, onLabelClick, inLine, containerType, children }: AbstractFormItemProps<TFieldValues>) => {
const [isDirty, setIsDirty] = useState<boolean>(false);
const [fieldError, setFieldError] = useState<{ message: string }>(error);
const [isDisabled, setIsDisabled] = useState<boolean>(false);
@ -52,16 +53,16 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
* This function is called when the label is clicked.
* It is used to focus the input.
*/
function handleLabelClick (event: React.MouseEvent<HTMLLabelElement, MouseEvent>) {
function handleLabelClick (event: React.MouseEvent<HTMLParagraphElement, MouseEvent>) {
if (typeof onLabelClick === 'function') {
onLabelClick(event);
}
}
return (
<label className={`form-item ${classNames}`} onClick={handleLabelClick}>
return React.createElement(containerType, { className: `form-item ${classNames}` }, (
<>
{(label && !inLine) && <div className='form-item-header'>
<p>{label}</p>
<p onClick={handleLabelClick}>{label}</p>
{tooltip && <div className="item-tooltip">
<span className="trigger"><i className="fa fa-question-circle" /></span>
<div className="content">{tooltip}</div>
@ -79,6 +80,8 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
</div>
{(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> }
{(isDirty && warning) && <div className="form-item-warning">{warning.message}</div> }
</label>
);
</>
));
};
AbstractFormItem.defaultProps = { containerType: 'label' };

View File

@ -35,13 +35,14 @@ export const FormRichText = <TFieldValues extends FieldValues, TContext extends
* We do not want the default behavior (focus the first child, which is the Bold button)
* but we want to focus the text edition area.
*/
function focusTextEditor (event: React.MouseEvent<HTMLLabelElement, MouseEvent>) {
function focusTextEditor (event: React.MouseEvent<HTMLParagraphElement, MouseEvent>) {
event.preventDefault();
textEditorRef.current.focus();
}
return (
<AbstractFormItem id={id} label={label} tooltip={tooltip}
containerType={'div'}
className={`form-rich-text ${className || ''}`}
error={error} warning={warning} rules={rules}
disabled={disabled} formState={formState} onLabelClick={focusTextEditor}>

View File

@ -11,7 +11,6 @@ import { useForm } from 'react-hook-form';
import { FormSelect } from '../form/form-select';
import MemberAPI from '../../api/member';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import UserLib from '../../lib/user';
declare const Application: IApplication;
@ -46,7 +45,7 @@ export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onErr
useEffect(() => {
GroupAPI.index({ disabled: false, admins: user?.role === 'admin' }).then(setGroups).catch(onError);
MemberAPI.current().then(setOperator).catch(onError);
SettingAPI.get(SettingName.UserChangeGroup).then((setting) => {
SettingAPI.get('user_change_group').then((setting) => {
setAllowedUserChangeGoup(setting.value === 'true');
}).catch(onError);
}, []);

View File

@ -10,7 +10,6 @@ import { Machine } from '../../models/machine';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
declare const Application: IApplication;
@ -46,7 +45,7 @@ const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, o
// check the trainings after we retrieved the machine data
useEffect(() => checkTraining(), [machine]);
useEffect(() => {
SettingAPI.get(SettingName.PackOnlyForSubscription)
SettingAPI.get('pack_only_for_subscription')
.then(data => setIsPackOnlyForSubscription(data.value === 'true'))
.catch(error => onError(error));
}, []);

View File

@ -12,7 +12,7 @@ import FormatLib from '../../lib/format';
import { PaymentScheduleItemActions, TypeOnce } from './payment-schedule-item-actions';
import { StripeElements } from '../payment/stripe/stripe-elements';
import SettingAPI from '../../api/setting';
import { Setting, SettingName } from '../../models/setting';
import { Setting } from '../../models/setting';
interface PaymentSchedulesTableProps {
paymentSchedules: Array<PaymentSchedule>,
@ -40,7 +40,7 @@ const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSc
const [gateway, setGateway] = useState<Setting>(null);
useEffect(() => {
SettingAPI.get(SettingName.PaymentGateway)
SettingAPI.get('payment_gateway')
.then(setting => setGateway(setting))
.catch(error => onError(error));
}, []);

View File

@ -13,7 +13,6 @@ import PriceAPI from '../../api/price';
import WalletAPI from '../../api/wallet';
import { Invoice } from '../../models/invoice';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import { GoogleTagManager } from '../../models/gtm';
import { ComputePriceResult } from '../../models/price';
import { Wallet } from '../../models/wallet';
@ -91,7 +90,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
useEffect(() => {
mounted.current = true;
CustomAssetAPI.get(CustomAssetName.CgvFile).then(asset => setCgv(asset));
SettingAPI.get(SettingName.PaymentGateway).then((setting) => {
SettingAPI.get('payment_gateway').then((setting) => {
// we capitalize the first letter of the name
if (setting.value) {
setGateway(setting.value.replace(/^\w/, (c) => c.toUpperCase()));

View File

@ -7,7 +7,7 @@ import { IApplication } from '../../models/application';
import { ShoppingCart } from '../../models/payment';
import { User } from '../../models/user';
import { PaymentSchedule } from '../../models/payment-schedule';
import { Setting, SettingName } from '../../models/setting';
import { Setting } from '../../models/setting';
import { Invoice } from '../../models/invoice';
import SettingAPI from '../../api/setting';
import { useTranslation } from 'react-i18next';
@ -35,7 +35,7 @@ const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal
const [gateway, setGateway] = useState<Setting>(null);
useEffect(() => {
SettingAPI.get(SettingName.PaymentGateway)
SettingAPI.get('payment_gateway')
.then(setting => setGateway(setting))
.catch(error => onError(error));
}, []);

View File

@ -5,7 +5,6 @@ import { GatewayFormProps } from '../abstract-payment-modal';
import LocalPaymentAPI from '../../../api/local-payment';
import FormatLib from '../../../lib/format';
import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
import { CardPaymentModal } from '../card-payment-modal';
import { PaymentSchedule } from '../../../models/payment-schedule';
import { HtmlTranslate } from '../../base/html-translate';
@ -75,7 +74,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
if (paymentSchedule && method === 'card') {
// check that the online payment is active
try {
const online = await SettingAPI.get(SettingName.OnlinePaymentModule);
const online = await SettingAPI.get('online_payment_module');
if (online.value !== 'true') {
return onError(t('app.admin.local_payment_form.online_payment_disabled'));
}

View File

@ -2,7 +2,6 @@ import React, { FormEvent, FunctionComponent, useEffect, useRef, useState } from
import KRGlue from '@lyracom/embedded-form-glue';
import { GatewayFormProps } from '../abstract-payment-modal';
import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
import PayzenAPI from '../../../api/payzen';
import {
CreateTokenResponse,
@ -27,10 +26,10 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
useEffect(() => {
SettingAPI.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey]).then(settings => {
SettingAPI.query(['payzen_endpoint', 'payzen_public_key']).then(settings => {
createToken().then(formToken => {
// Load the remote library
KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey))
KRGlue.loadLibrary(settings.get('payzen_endpoint'), settings.get('payzen_public_key'))
.then(({ KR }) =>
KR.setFormConfig({
formToken: formToken.formToken

View File

@ -17,9 +17,9 @@ interface PayzenKeysFormProps {
}
// all settings related to PayZen that are requested by this form
const payZenSettings: Array<SettingName> = [SettingName.PayZenUsername, SettingName.PayZenPassword, SettingName.PayZenEndpoint, SettingName.PayZenHmacKey, SettingName.PayZenPublicKey];
// settings related the to PayZen REST API (server side)
const restApiSettings: Array<SettingName> = [SettingName.PayZenUsername, SettingName.PayZenPassword, SettingName.PayZenEndpoint, SettingName.PayZenHmacKey];
const payzenSettings: Array<SettingName> = ['payzen_username', 'payzen_password', 'payzen_endpoint', 'payzen_hmac', 'payzen_public_key'];
// settings related to the PayZen REST API (server side)
const restApiSettings: Array<SettingName> = ['payzen_username', 'payzen_password', 'payzen_endpoint', 'payzen_hmac'];
// Prevent multiples call to the payzen keys validation endpoint.
// this cannot be handled by a React state because of their asynchronous nature
@ -32,7 +32,7 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
const { t } = useTranslation('admin');
// values of the PayZen settings
const [settings, updateSettings] = useImmer<Map<SettingName, string>>(new Map(payZenSettings.map(name => [name, ''])));
const [settings, updateSettings] = useImmer<Map<SettingName, string>>(new Map(payzenSettings.map(name => [name, ''])));
// Icon of the fieldset for the PayZen's keys concerning the REST API. Used to display if the key is valid.
const [restApiAddOn, setRestApiAddOn] = useState<ReactNode>(null);
// Style class for the add-on icon, for the REST API
@ -46,7 +46,7 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
* When the component loads for the first time, initialize the keys with the values fetched from the API (if any)
*/
useEffect(() => {
SettingAPI.query(payZenSettings).then(payZenKeys => {
SettingAPI.query(payzenSettings).then(payZenKeys => {
updateSettings(new Map(payZenKeys));
}).catch(error => console.error(error));
}, []);
@ -78,7 +78,7 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
setPublicKeyAddOnClassName('key-invalid');
return;
}
updateSettings(draft => draft.set(SettingName.PayZenPublicKey, key));
updateSettings(draft => draft.set('payzen_public_key', key));
setPublicKeyAddOn(<i className="fa fa-check" />);
setPublicKeyAddOnClassName('key-valid');
};
@ -94,9 +94,9 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
if (valid && !pendingKeysValidation) {
pendingKeysValidation = true;
PayzenAPI.chargeSDKTest(
settings.get(SettingName.PayZenEndpoint),
settings.get(SettingName.PayZenUsername),
settings.get(SettingName.PayZenPassword)
settings.get('payzen_endpoint'),
settings.get('payzen_username'),
settings.get('payzen_password')
).then(result => {
pendingKeysValidation = false;
@ -123,7 +123,7 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
/**
* Assign the inputted key to the given settings
*/
const setApiKey = (setting: SettingName.PayZenUsername | SettingName.PayZenPassword | SettingName.PayZenEndpoint | SettingName.PayZenHmacKey) => {
const setApiKey = (setting: typeof restApiSettings[number]) => {
return (key: string) => {
updateSettings(draft => draft.set(setting, key));
};
@ -148,7 +148,7 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
<label htmlFor="payzen_public_key">{ t('app.admin.invoices.payzen_keys_form.payzen_public_key') } *</label>
<FabInput id="payzen_public_key"
icon={<i className="fas fa-info" />}
defaultValue={settings.get(SettingName.PayZenPublicKey)}
defaultValue={settings.get('payzen_public_key')}
onChange={testPublicKey}
addOn={publicKeyAddOn}
addOnClassName={publicKeyAddOnClassName}
@ -166,8 +166,8 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
<FabInput id="payzen_username"
type="number"
icon={<i className="fas fa-user-alt" />}
defaultValue={settings.get(SettingName.PayZenUsername)}
onChange={setApiKey(SettingName.PayZenUsername)}
defaultValue={settings.get('payzen_username')}
onChange={setApiKey('payzen_username')}
debounce={200}
required />
</div>
@ -175,8 +175,8 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
<label htmlFor="payzen_password">{ t('app.admin.invoices.payzen_keys_form.payzen_password') } *</label>
<FabInput id="payzen_password"
icon={<i className="fas fa-key" />}
defaultValue={settings.get(SettingName.PayZenPassword)}
onChange={setApiKey(SettingName.PayZenPassword)}
defaultValue={settings.get('payzen_password')}
onChange={setApiKey('payzen_password')}
debounce={200}
required />
</div>
@ -185,8 +185,8 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
<FabInput id="payzen_endpoint"
type="url"
icon={<i className="fas fa-link" />}
defaultValue={settings.get(SettingName.PayZenEndpoint)}
onChange={setApiKey(SettingName.PayZenEndpoint)}
defaultValue={settings.get('payzen_endpoint')}
onChange={setApiKey('payzen_endpoint')}
debounce={200}
required />
</div>
@ -194,8 +194,8 @@ const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidK
<label htmlFor="payzen_hmac">{ t('app.admin.invoices.payzen_keys_form.payzen_hmac') } *</label>
<FabInput id="payzen_hmac"
icon={<i className="fas fa-subscript" />}
defaultValue={settings.get(SettingName.PayZenHmacKey)}
onChange={setApiKey(SettingName.PayZenHmacKey)}
defaultValue={settings.get('payzen_hmac')}
onChange={setApiKey('payzen_hmac')}
debounce={200}
required />
</div>

View File

@ -21,21 +21,21 @@ interface PayzenSettingsProps {
const PAYZEN_HIDDEN = 'HiDdEnHIddEnHIdDEnHiDdEnHIddEnHIdDEn';
// settings related to PayZen that can be shown publicly
const payZenPublicSettings: Array<SettingName> = [SettingName.PayZenPublicKey, SettingName.PayZenEndpoint, SettingName.PayZenUsername];
const payZenPublicSettings: Array<SettingName> = ['payzen_public_key', 'payzen_endpoint', 'payzen_username'];
// settings related to PayZen that must be kept on server-side
const payZenPrivateSettings: Array<SettingName> = [SettingName.PayZenPassword, SettingName.PayZenHmacKey];
const payZenPrivateSettings: Array<SettingName> = ['payzen_password', 'payzen_hmac'];
// other settings related to PayZen
const payZenOtherSettings: Array<SettingName> = [SettingName.PayZenCurrency];
const payZenOtherSettings: Array<SettingName> = ['payzen_currency'];
// all PayZen settings
const payZenSettings: Array<SettingName> = payZenPublicSettings.concat(payZenPrivateSettings).concat(payZenOtherSettings);
// icons for the inputs of each setting
const icons:Map<SettingName, string> = new Map([
[SettingName.PayZenHmacKey, 'subscript'],
[SettingName.PayZenPassword, 'key'],
[SettingName.PayZenUsername, 'user'],
[SettingName.PayZenEndpoint, 'link'],
[SettingName.PayZenPublicKey, 'info']
['payzen_hmac', 'subscript'],
['payzen_password', 'key'],
['payzen_username', 'user'],
['payzen_endpoint', 'link'],
['payzen_public_key', 'info']
]);
/**
@ -55,11 +55,11 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
*/
useEffect(() => {
SettingAPI.query(payZenPublicSettings.concat(payZenOtherSettings)).then(payZenKeys => {
SettingAPI.isPresent(SettingName.PayZenPassword).then(pzPassword => {
SettingAPI.isPresent(SettingName.PayZenHmacKey).then(pzHmac => {
SettingAPI.isPresent('payzen_password').then(pzPassword => {
SettingAPI.isPresent('payzen_hmac').then(pzHmac => {
const map = new Map(payZenKeys);
map.set(SettingName.PayZenPassword, pzPassword ? PAYZEN_HIDDEN : '');
map.set(SettingName.PayZenHmacKey, pzHmac ? PAYZEN_HIDDEN : '');
map.set('payzen_password', pzPassword ? PAYZEN_HIDDEN : '');
map.set('payzen_hmac', pzHmac ? PAYZEN_HIDDEN : '');
updateSettings(map);
}).catch(error => { console.error(error); });
@ -81,7 +81,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
const handleCurrencyUpdate = (value: string, validity?: ValidityState): void => {
if (!validity || validity.valid) {
setError('');
updateSettings(draft => draft.set(SettingName.PayZenCurrency, value));
updateSettings(draft => draft.set('payzen_currency', value));
} else {
setError(t('app.admin.invoices.payment.payzen_settings.currency_error'));
}
@ -92,9 +92,9 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
* This will update the setting on the server.
*/
const saveCurrency = (): void => {
SettingAPI.update(SettingName.PayZenCurrency, settings.get(SettingName.PayZenCurrency)).then(result => {
SettingAPI.update('payzen_currency', settings.get('payzen_currency')).then(result => {
setError('');
updateSettings(draft => draft.set(SettingName.PayZenCurrency, result.value));
updateSettings(draft => draft.set('payzen_currency', result.value));
onCurrencyUpdateSuccess(result.value);
}, reason => {
setError(t('app.admin.invoices.payment.payzen_settings.error_while_saving') + reason);
@ -130,7 +130,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
<div className="payzen-currency-form">
<div className="currency-wrapper">
<label htmlFor="payzen_currency">{t('app.admin.invoices.payment.payzen_settings.payzen_currency')}</label>
<FabInput defaultValue={settings.get(SettingName.PayZenCurrency)}
<FabInput defaultValue={settings.get('payzen_currency')}
id="payzen_currency"
icon={<i className="fas fa-money-bill" />}
onChange={handleCurrencyUpdate}

View File

@ -38,7 +38,7 @@ export const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isO
// request the configured gateway to the API
useEffect(() => {
SettingAPI.get(SettingName.PaymentGateway).then(gateway => {
SettingAPI.get('payment_gateway').then(gateway => {
setSelectedGateway(gateway.value ? gateway.value : '');
});
}, []);
@ -73,8 +73,8 @@ export const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isO
const handleValidStripeKeys = (publicKey: string, secretKey: string): void => {
setGatewayConfig((prev) => {
const newMap = new Map(prev);
newMap.set(SettingName.StripeSecretKey, secretKey);
newMap.set(SettingName.StripePublicKey, publicKey);
newMap.set('stripe_secret_key', secretKey);
newMap.set('stripe_public_key', publicKey);
return newMap;
});
setPreventConfirmGateway(false);
@ -100,7 +100,7 @@ export const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isO
*/
const updateSettings = (): void => {
const settings = new Map<SettingName, string>(gatewayConfig);
settings.set(SettingName.PaymentGateway, selectedGateway);
settings.set('payment_gateway', selectedGateway);
SettingAPI.bulkUpdate(settings, true).then(result => {
const errorResults = Array.from(result.values()).filter(item => !item.status);

View File

@ -1,7 +1,6 @@
import React, { memo, useEffect, useState } from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe, Stripe } from '@stripe/stripe-js';
import { SettingName } from '../../../models/setting';
import SettingAPI from '../../../api/setting';
/**
@ -14,7 +13,7 @@ export const StripeElements: React.FC = memo(({ children }) => {
* When this component is mounted, we initialize the <Elements> tag with the Stripe's public key
*/
useEffect(() => {
SettingAPI.get(SettingName.StripePublicKey).then(key => {
SettingAPI.get('stripe_public_key').then(key => {
if (key?.value) {
const promise = loadStripe(key.value);
setStripe(promise);

View File

@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import { HtmlTranslate } from '../../base/html-translate';
import { FabInput } from '../../base/fab-input';
import { Loader } from '../../base/loader';
import { SettingName } from '../../../models/setting';
import StripeAPI from '../../../api/external/stripe';
import SettingAPI from '../../../api/setting';
@ -42,9 +41,9 @@ const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidK
useEffect(() => {
mounted.current = true;
SettingAPI.query([SettingName.StripePublicKey, SettingName.StripeSecretKey]).then(stripeKeys => {
setPublicKey(stripeKeys.get(SettingName.StripePublicKey));
setSecretKey(stripeKeys.get(SettingName.StripeSecretKey));
SettingAPI.query(['stripe_public_key', 'stripe_secret_key']).then(stripeKeys => {
setPublicKey(stripeKeys.get('stripe_public_key'));
setSecretKey(stripeKeys.get('stripe_secret_key'));
}).catch(error => console.error(error));
// when the component unmounts, mark it as unmounted

View File

@ -5,7 +5,6 @@ import { User } from '../../models/user';
import { UserPack } from '../../models/user-pack';
import UserPackAPI from '../../api/user-pack';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import { FabButton } from '../base/fab-button';
import { useTranslation } from 'react-i18next';
import { ProposePacksModal } from './propose-packs-modal';
@ -44,10 +43,10 @@ const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, o
const [isPackOnlyForSubscription, setIsPackOnlyForSubscription] = useState<boolean>(true);
useEffect(() => {
SettingAPI.get(SettingName.RenewPackThreshold)
SettingAPI.get('renew_pack_threshold')
.then(data => setThreshold(parseFloat(data.value)))
.catch(error => onError(error));
SettingAPI.get(SettingName.PackOnlyForSubscription)
SettingAPI.get('pack_only_for_subscription')
.then(data => setIsPackOnlyForSubscription(data.value === 'true'))
.catch(error => onError(error));
}, []);

View File

@ -6,7 +6,7 @@ import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import { SettingName, titleSettings } from '../../models/setting';
import UserLib from '../../lib/user';
declare const Application: IApplication;
@ -27,7 +27,7 @@ export const CompletionHeaderInfo: React.FC<CompletionHeaderInfoProps> = ({ user
const userLib = new UserLib(user);
useEffect(() => {
SettingAPI.query([SettingName.NameGenre, SettingName.FablabName]).then(setSettings).catch(onError);
SettingAPI.query(titleSettings).then(setSettings).catch(onError);
}, []);
return (
@ -39,8 +39,8 @@ export const CompletionHeaderInfo: React.FC<CompletionHeaderInfoProps> = ({ user
<p className="intro">
<span>
{t('app.logged.profile_completion.completion_header_info.sso_intro', {
GENDER: settings?.get(SettingName.NameGenre),
NAME: settings?.get(SettingName.FablabName)
GENDER: settings?.get('name_genre'),
NAME: settings?.get('fablab_name')
})}
</span>
<span className="provider-name">

View File

@ -36,7 +36,7 @@ export const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ on
const updateSetting = (name: SettingName, value: string) => {
SettingAPI.update(name, value)
.then(() => {
if (name === SettingName.UserValidationRequired) {
if (name === 'user_validation_required') {
onSuccess(t('app.admin.settings.account.user_validation_setting.customization_of_SETTING_successfully_saved', {
SETTING: t(`app.admin.settings.account.${name}`) // eslint-disable-line fabmanager/scoped-translation
}));
@ -45,7 +45,7 @@ export const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ on
if (err.status === 304) return;
if (err.status === 423) {
if (name === SettingName.UserValidationRequired) {
if (name === 'user_validation_required') {
onError(t('app.admin.settings.account.user_validation_setting.error_SETTING_locked', {
SETTING: t(`app.admin.settings.account.${name}`) // eslint-disable-line fabmanager/scoped-translation
}));
@ -62,19 +62,19 @@ export const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ on
* Callback triggered when the 'save' button is clicked.
*/
const handleSave = () => {
updateSetting(SettingName.UserValidationRequired, userValidationRequired);
updateSetting('user_validation_required', userValidationRequired);
if (userValidationRequiredList !== null) {
if (userValidationRequired === 'true') {
updateSetting(SettingName.UserValidationRequiredList, userValidationRequiredList);
updateSetting('user_validation_required_list', userValidationRequiredList);
} else {
updateSetting(SettingName.UserValidationRequiredList, null);
updateSetting('user_validation_required_list', null);
}
}
};
return (
<div className="user-validation-setting">
<BooleanSetting name={SettingName.UserValidationRequired}
<BooleanSetting name={'user_validation_required'}
label={t('app.admin.settings.account.user_validation_setting.user_validation_required_option_label')}
hideSave={true}
onChange={setUserValidationRequired}
@ -90,7 +90,7 @@ export const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ on
<FabAlert level="warning">
{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_other_info')}
</FabAlert>
<CheckListSetting name={SettingName.UserValidationRequiredList}
<CheckListSetting name={'user_validation_required_list'}
label=""
availableOptions={userValidationRequiredOptions}
defaultValue={userValidationRequiredListDefault.join(',')}

View File

@ -25,7 +25,7 @@ interface FabSocialsProps {
*/
export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, onSuccess }) => {
const { t } = useTranslation('shared');
// regular expression to validate the the input fields
// regular expression to validate the input fields
const urlRegex = /^(https?:\/\/)([\da-z.-]+)\.([-a-z\d.]{2,30})([/\w .-]*)*\/?$/;
const { handleSubmit, register, setValue, formState } = useForm();

View File

@ -23,7 +23,7 @@ export const PasswordInput = <TFieldValues extends FieldValues>({ register, curr
rules={{
required: true,
validate: (value: string) => {
if (value.length < 8) {
if (value.length < 12) {
return t('app.shared.password_input.password_too_short') as string;
}
return true;

View File

@ -99,7 +99,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
});
setValue('invoicing_profile_attributes.user_profile_custom_fields_attributes', userProfileCustomFields);
}).catch(error => onError(error));
SettingAPI.query([SettingName.PhoneRequired, SettingName.AddressRequired])
SettingAPI.query(['phone_required', 'address_required'])
.then(settings => setRequiredFieldsSettings(settings))
.catch(error => onError(error));
}, []);
@ -219,7 +219,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
value: phoneRegex,
message: t('app.shared.user_profile_form.phone_number_invalid')
},
required: requiredFieldsSettings.get(SettingName.PhoneRequired) === 'true'
required: requiredFieldsSettings.get('phone_required') === 'true'
}}
disabled={isDisabled}
formState={formState}
@ -232,7 +232,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
<FormInput id="invoicing_profile_attributes.address_attributes.address"
register={register}
disabled={isDisabled}
rules={{ required: requiredFieldsSettings.get(SettingName.AddressRequired) === 'true' }}
rules={{ required: requiredFieldsSettings.get('address_required') === 'true' }}
label={t('app.shared.user_profile_form.address')} />
</div>
</div>

View File

@ -70,20 +70,23 @@ Application.Controllers.controller('GroupsController', ['$scope', 'groupsPromise
/**
* Deletes the group at the specified index
* @param index {number} group index in the $scope.groups array
* @param groupId {number} group id to delete
*/
$scope.removeGroup = index =>
Group.delete({ id: $scope.groups[index].id }, function (resp) {
$scope.removeGroup = (groupId) => {
Group.delete({ id: groupId }, function (resp) {
growl.success(_t('app.admin.members.group_form.group_successfully_deleted'));
return $scope.groups.splice(index, 1);
const index = $scope.groups.findIndex(e => e.id === groupId);
$scope.groups.splice(index, 1);
}
, () => growl.error(_t('app.admin.members.group_form.unable_to_delete_group_because_some_users_and_or_groups_are_still_linked_to_it')));
};
/**
* Enable/disable the group at the specified index
* @param index {number} group index in the $scope.groups array
* @param groupId {number} id of the group to enable/disable
*/
return $scope.toggleDisableGroup = function (index) {
return $scope.toggleDisableGroup = function (groupId) {
const index = $scope.groups.findIndex(e => e.id === groupId);
const group = $scope.groups[index];
if (!group.disabled && (group.users > 0)) {
return growl.error(_t('app.admin.members.group_form.unable_to_disable_group_with_users', { USERS: group.users }));

View File

@ -1,144 +1,242 @@
import { HistoryValue } from './history-value';
import { TDateISO } from '../typings/date-iso';
export enum SettingName {
AboutTitle = 'about_title',
AboutBody = 'about_body',
AboutContacts = 'about_contacts',
PrivacyDraft = 'privacy_draft',
PrivacyBody = 'privacy_body',
PrivacyDpo = 'privacy_dpo',
TwitterName = 'twitter_name',
HomeBlogpost = 'home_blogpost',
MachineExplicationsAlert = 'machine_explications_alert',
TrainingExplicationsAlert = 'training_explications_alert',
TrainingInformationMessage = 'training_information_message',
SubscriptionExplicationsAlert = 'subscription_explications_alert',
InvoiceLogo = 'invoice_logo',
InvoiceReference = 'invoice_reference',
InvoiceCodeActive = 'invoice_code-active',
InvoiceCodeValue = 'invoice_code-value',
InvoiceOrderNb = 'invoice_order-nb',
InvoiceVATActive = 'invoice_VAT-active',
InvoiceVATRate = 'invoice_VAT-rate',
InvoiceVATRateMachine = 'invoice_VAT-rate_Machine',
InvoiceVATRateTraining = 'invoice_VAT-rate_Training',
InvoiceVATRateSpace = 'invoice_VAT-rate_Space',
InvoiceVATRateEvent = 'invoice_VAT-rate_Event',
InvoiceVATRateSubscription = 'invoice_VAT-rate_Subscription',
InvoiceText = 'invoice_text',
InvoiceLegals = 'invoice_legals',
BookingWindowStart = 'booking_window_start',
BookingWindowEnd = 'booking_window_end',
BookingMoveEnable = 'booking_move_enable',
BookingMoveDelay = 'booking_move_delay',
BookingCancelEnable = 'booking_cancel_enable',
BookingCancelDelay = 'booking_cancel_delay',
MainColor = 'main_color',
SecondaryColor = 'secondary_color',
FablabName = 'fablab_name',
NameGenre = 'name_genre',
ReminderEnable = 'reminder_enable',
ReminderDelay = 'reminder_delay',
EventExplicationsAlert = 'event_explications_alert',
SpaceExplicationsAlert = 'space_explications_alert',
VisibilityYearly = 'visibility_yearly',
VisibilityOthers = 'visibility_others',
DisplayNameEnable = 'display_name_enable',
MachinesSortBy = 'machines_sort_by',
AccountingJournalCode = 'accounting_journal_code',
AccountingCardClientCode = 'accounting_card_client_code',
AccountingCardClientLabel = 'accounting_card_client_label',
AccountingWalletClientCode = 'accounting_wallet_client_code',
AccountingWalletClientLabel = 'accounting_wallet_client_label',
AccountingOtherClientCode = 'accounting_other_client_code',
AccountingOtherClientLabel = 'accounting_other_client_label',
AccountingWalletCode = 'accounting_wallet_code',
AccountingWalletLabel = 'accounting_wallet_label',
AccountingVATCode = 'accounting_VAT_code',
AccountingVATLabel = 'accounting_VAT_label',
AccountingSubscriptionCode = 'accounting_subscription_code',
AccountingSubscriptionLabel = 'accounting_subscription_label',
AccountingMachineCode = 'accounting_Machine_code',
AccountingMachineLabel = 'accounting_Machine_label',
AccountingTrainingCode = 'accounting_Training_code',
AccountingTrainingLabel = 'accounting_Training_label',
AccountingEventCode = 'accounting_Event_code',
AccountingEventLabel = 'accounting_Event_label',
AccountingSpaceCode = 'accounting_Space_code',
AccountingSpaceLabel = 'accounting_Space_label',
HubLastVersion = 'hub_last_version',
HubPublicKey = 'hub_public_key',
FabAnalytics = 'fab_analytics',
LinkName = 'link_name',
HomeContent = 'home_content',
HomeCss = 'home_css',
Origin = 'origin',
Uuid = 'uuid',
PhoneRequired = 'phone_required',
TrackingId = 'tracking_id',
BookOverlappingSlots = 'book_overlapping_slots',
SlotDuration = 'slot_duration',
EventsInCalendar = 'events_in_calendar',
SpacesModule = 'spaces_module',
PlansModule = 'plans_module',
InvoicingModule = 'invoicing_module',
FacebookAppId = 'facebook_app_id',
TwitterAnalytics = 'twitter_analytics',
RecaptchaSiteKey = 'recaptcha_site_key',
RecaptchaSecretKey = 'recaptcha_secret_key',
FeatureTourDisplay = 'feature_tour_display',
EmailFrom = 'email_from',
DisqusShortname = 'disqus_shortname',
AllowedCadExtensions = 'allowed_cad_extensions',
AllowedCadMimeTypes = 'allowed_cad_mime_types',
OpenlabAppId = 'openlab_app_id',
OpenlabAppSecret = 'openlab_app_secret',
OpenlabDefault = 'openlab_default',
OnlinePaymentModule = 'online_payment_module',
StripePublicKey = 'stripe_public_key',
StripeSecretKey = 'stripe_secret_key',
StripeCurrency = 'stripe_currency',
InvoicePrefix = 'invoice_prefix',
ConfirmationRequired = 'confirmation_required',
WalletModule = 'wallet_module',
StatisticsModule = 'statistics_module',
UpcomingEventsShown = 'upcoming_events_shown',
PaymentSchedulePrefix = 'payment_schedule_prefix',
TrainingsModule = 'trainings_module',
AddressRequired = 'address_required',
PaymentGateway = 'payment_gateway',
PayZenUsername = 'payzen_username',
PayZenPassword = 'payzen_password',
PayZenEndpoint = 'payzen_endpoint',
PayZenPublicKey = 'payzen_public_key',
PayZenHmacKey = 'payzen_hmac',
PayZenCurrency = 'payzen_currency',
PublicAgendaModule = 'public_agenda_module',
RenewPackThreshold = 'renew_pack_threshold',
PackOnlyForSubscription = 'pack_only_for_subscription',
OverlappingCategories = 'overlapping_categories',
ExtendedPricesInSameDay = 'extended_prices_in_same_day',
PublicRegistrations = 'public_registrations',
SocialsFacebook = 'facebook',
SocialsTwitter = 'twitter',
SocialsViadeo = 'viadeo',
SocialsLinkedin = 'linkedin',
SocialsInstagram = 'instagram',
SocialsYoutube = 'youtube',
SocialsVimeo = 'vimeo',
SocialsDailymotion = 'dailymotion',
SocialsGithub = 'github',
SocialsEchosciences = 'echosciences',
SocialsPinterest = 'pinterest',
SocialsLastfm = 'lastfm',
SocialsFlickr = 'flickr',
MachinesModule = 'machines_module',
UserChangeGroup = 'user_change_group',
UserValidationRequired = 'user_validation_required',
UserValidationRequiredList = 'user_validation_required_list',
ShowUsernameInAdminList = 'show_username_in_admin_list'
}
export const homePageSettings = [
'twitter_name',
'home_blogpost',
'home_content',
'home_css',
'upcoming_events_shown'
];
export const privacyPolicySettings = [
'privacy_draft',
'privacy_body',
'privacy_dpo'
];
export const aboutPageSettings = [
'about_title',
'about_body',
'about_contacts',
'link_name'
];
export const socialNetworksSettings = [
'facebook',
'twitter',
'viadeo',
'linkedin',
'instagram',
'youtube',
'vimeo',
'dailymotion',
'github',
'echosciences',
'pinterest',
'lastfm',
'flickr'
];
export const messagesSettings = [
'machine_explications_alert',
'training_explications_alert',
'training_information_message',
'subscription_explications_alert',
'event_explications_alert',
'space_explications_alert'
];
export const invoicesSettings = [
'invoice_logo',
'invoice_reference',
'invoice_code-active',
'invoice_code-value',
'invoice_order-nb',
'invoice_VAT-active',
'invoice_VAT-rate',
'invoice_VAT-rate_Machine',
'invoice_VAT-rate_Training',
'invoice_VAT-rate_Space',
'invoice_VAT-rate_Event',
'invoice_VAT-rate_Subscription',
'invoice_text',
'invoice_legals',
'invoice_prefix',
'payment_schedule_prefix'
];
export const bookingSettings = [
'booking_window_start',
'booking_window_end',
'booking_move_enable',
'booking_move_delay',
'booking_cancel_enable',
'booking_cancel_delay',
'reminder_enable',
'reminder_delay',
'visibility_yearly',
'visibility_others',
'display_name_enable',
'book_overlapping_slots',
'slot_duration',
'overlapping_categories'
];
export const themeSettings = [
'main_color',
'secondary_color'
];
export const titleSettings = [
'fablab_name',
'name_genre'
];
export const accountingSettings = [
'accounting_journal_code',
'accounting_card_client_code',
'accounting_card_client_label',
'accounting_wallet_client_code',
'accounting_wallet_client_label',
'accounting_other_client_code',
'accounting_other_client_label',
'accounting_wallet_code',
'accounting_wallet_label',
'accounting_VAT_code',
'accounting_VAT_label',
'accounting_subscription_code',
'accounting_subscription_label',
'accounting_Machine_code',
'accounting_Machine_label',
'accounting_Training_code',
'accounting_Training_label',
'accounting_Event_code',
'accounting_Event_label',
'accounting_Space_code',
'accounting_Space_label'
];
export const modulesSettings = [
'spaces_module',
'plans_module',
'wallet_module',
'statistics_module',
'trainings_module',
'machines_module',
'online_payment_module',
'public_agenda_module',
'invoicing_module'
];
export const stripeSettings = [
'stripe_public_key',
'stripe_secret_key',
'stripe_currency'
];
export const payzenSettings = [
'payzen_username',
'payzen_password',
'payzen_endpoint',
'payzen_public_key',
'payzen_hmac',
'payzen_currency'
];
export const openLabSettings = [
'openlab_app_id',
'openlab_app_secret',
'openlab_default'
];
export const accountSettings = [
'phone_required',
'confirmation_required',
'address_required',
'user_change_group',
'user_validation_required',
'user_validation_required_list'
];
export const analyticsSettings = [
'tracking_id',
'facebook_app_id',
'twitter_analytics'
];
export const fabHubSettings = [
'hub_last_version',
'hub_public_key',
'fab_analytics',
'origin',
'uuid'
];
export const projectsSettings = [
'allowed_cad_extensions',
'allowed_cad_mime_types',
'disqus_shortname'
];
export const prepaidPacksSettings = [
'renew_pack_threshold',
'pack_only_for_subscription'
];
export const registrationSettings = [
'public_registrations',
'recaptcha_site_key',
'recaptcha_secret_key'
];
export const adminSettings = [
'feature_tour_display',
'show_username_in_admin_list'
];
export const pricingSettings = [
'extended_prices_in_same_day'
];
export const poymentSettings = [
'payment_gateway'
];
export const displaySettings = [
'machines_sort_by',
'events_in_calendar',
'email_from'
];
export const allSettings = [
...homePageSettings,
...privacyPolicySettings,
...aboutPageSettings,
...socialNetworksSettings,
...messagesSettings,
...invoicesSettings,
...bookingSettings,
...themeSettings,
...titleSettings,
...accountingSettings,
...modulesSettings,
...stripeSettings,
...payzenSettings,
...openLabSettings,
...accountSettings,
...analyticsSettings,
...fabHubSettings,
...projectsSettings,
...prepaidPacksSettings,
...registrationSettings,
...adminSettings,
...pricingSettings,
...poymentSettings,
...displaySettings
] as const;
export type SettingName = typeof allSettings[number];
export type SettingValue = string|boolean|number;
@ -153,7 +251,7 @@ export interface Setting {
export interface SettingError {
error: string,
id: number,
name: string
name: SettingName
}
export interface SettingBulkResult {

View File

@ -588,10 +588,18 @@
.checkbox-group {
display: flex;
justify-content: flex-start;
align-items: flex-start;
input[type=checkbox] {
font-size: 16px;
flex-shrink: 0;
width: 2em;
height: 2rem;
margin-right: 0.5rem;
font-size: 16px;
}
label {
margin: 0;
font-weight: 500;
}
}

View File

@ -1,8 +1,7 @@
.signup-form {
.names-row {
input.form-control {
width: 89%;
display: inline-block;
& > div {
display: flex;
}
}

View File

@ -41,11 +41,11 @@
<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-default" ng-click="toggleDisableGroup($index)">
<button class="btn btn-default" ng-click="toggleDisableGroup(group.id)">
<span ng-hide="group.disabled"><i class="fa fa-eye-slash"></i> <span translate>{{ 'app.admin.members.group_form.disable' }}</span></span>
<span ng-show="group.disabled"><i class="fa fa-eye"></i> <span translate>{{ 'app.admin.members.group_form.enable' }}</span></span>
</button>
<button class="btn btn-danger" ng-click="removeGroup($index)">
<button class="btn btn-danger" ng-click="removeGroup(group.id)">
<i class="fa fa-trash-o"></i>
</button>
</div>

View File

@ -1,566 +0,0 @@
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
<input name="_method" type="hidden" ng-value="method">
<input name="user[profile_attributes][id]" type="hidden" ng-value="user.profile_attributes.id">
<input name="user[invoicing_profile_attributes][id]" type="hidden" ng-value="user.invoicing_profile.id">
<input name="user[statistic_profile_attributes][id]" type="hidden" ng-value="user.statistic_profile.id">
<div class="row m-t">
<div class="col-sm-3 col-sm-offset-1">
<div class="form-group m-t-lg">
<div class="fileinput text-center" data-provides="fileinput" ng-class="fileinputClass(user.profile_attributes.user_avatar_attributes.attachment_url)">
<div class="fileinput-new thumbnail rounded thumb-128-wrapper" style="width: 140px; height: 140px;">
<img src="../../images/no_avatar.png" class="img-circle">
</div>
<div class="fileinput-preview fileinput-exists thumbnail rounded thumb-128-wrapper" data-trigger="fileinput" style="width: 140px; height: 140px; line-height: 140px;">
<img ng-src="{{ user.profile_attributes.user_avatar_attributes.attachment_url }}" />
</div>
<div class="m-t-sm">
<input type="hidden" name="user[profile_attributes][user_avatar_attributes][id]" ng-value="user.profile_attributes.user_avatar_attributes.id">
<input type="hidden" name="user[profile_attributes][user_avatar_attributes][_destroy]" ng-value="true" ng-if="user.profile_attributes.user_avatar._destory">
<span class="btn btn-default btn-file"
ng-click="user.profile_attributes.user_avatar_attributes._destory = false"
ng-hide="preventField['profile.avatar'] && user.profile_attributes.user_avatar_attributes.attachment_url && !userForm['user[profile_attributes][user_avatar_attributes]'].$dirty">
<span class="fileinput-new" translate>{{ 'app.shared.user.add_an_avatar' }}</span>
<span class="fileinput-exists" translate>{{ 'app.shared.buttons.change' }}</span>
<input type="file" name="user[profile_attributes][user_avatar_attributes][attachment]" accept="image/jpeg,image/gif,image/png">
</span>
<button class="btn btn-danger fileinput-exists"
data-dismiss="fileinput"
ng-click="user.profile_attributes.user_avatar_attributes._destory = true"
ng-hide="preventField['profile.avatar'] && user.profile_attributes.user_avatar.attachment_url && !userForm['user[profile_attributes][user_avatar_attributes]'].$dirty">
<i class="fa fa-trash-o"></i>
</button>
</div>
</div>
</div>
</div>
<div class="col-sm-offset-1 col-sm-6">
<div class="form-group" ng-class="{'has-error': userForm['user[statistic_profile_attributes][gender]'].$dirty && userForm['user[statistic_profile_attributes][gender]'].$invalid}">
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="user[statistic_profile_attributes][gender]"
ng-model="user.statistic_profile_attributes.gender"
value="true"
ng-disabled="preventField['profile.gender'] && user.statistic_profile_attributes.gender && !userForm['user[statistic_profile_attributes][gender]'].$dirty"
required/>
<i class="fa fa-male m-l-sm"></i> {{ 'app.shared.user.man' | translate }}
</label>
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="user[statistic_profile_attributes][gender]"
ng-model="user.statistic_profile_attributes.gender"
value="false"
ng-disabled="preventField['profile.gender'] && user.statistic_profile_attributes.gender && !userForm['user[statistic_profile_attributes][gender]'].$dirty"/>
<i class="fa fa-female m-l-sm"></i> {{ 'app.shared.user.woman' | translate }}
</label>
<span class="exponent m-l-xs help-cursor" title="{{ 'app.shared.user.used_for_statistics' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="userForm['user[statistic_profile_attributes][gender]'].$dirty && userForm['user[statistic_profile_attributes][gender]'].$error.required" translate>{{ 'app.shared.user.gender_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[username]'].$dirty && userForm['user[username]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</span>
<input type="text"
name="user[username]"
ng-model="user.username"
class="form-control"
id="user_username"
placeholder="{{ 'app.shared.user.pseudonym' | translate }}"
ng-disabled="preventField['user.username'] && user.username && !userForm['user[username]'].$dirty"
required/>
</div>
<span class="help-block" ng-show="userForm['user[username]'].$dirty && userForm['user[username]'].$error.required" translate>{{ 'app.shared.user.pseudonym_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][last_name]'].$dirty && userForm['user[profile_attributes][last_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][last_name]"
ng-model="user.profile_attributes.last_name"
class="form-control"
id="user_last_name"
placeholder="{{ 'app.shared.user.surname' | translate }}"
ng-disabled="preventField['profile.last_name'] && user.profile_attributes.last_name && !userForm['user[profile_attributes][last_name]'].$dirty"
required/>
</div>
<span class="help-block" ng-show="userForm['user[profile_attributes][last_name]'].$dirty && userForm['user[profile_attributes][last_name]'].$error.required" translate>{{ 'app.shared.user.surname_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][first_name]'].$dirty && userForm['user[profile_attributes][first_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][first_name]"
ng-model="user.profile_attributes.first_name"
class="form-control"
id="user_first_name"
placeholder="{{ 'app.shared.user.first_name' | translate }}"
ng-disabled="preventField['profile.first_name'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][first_name]'].$dirty"
required/>
</div>
<span class="help-block" ng-show="userForm['user[profile_attributes][first_name]'].$dirty && userForm['user[profile_attributes][first_name]'].$error.required" translate>{{ 'app.shared.user.first_name_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[email]'].$dirty && userForm['user[email]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-envelope"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="email"
name="user[email]"
ng-model="user.email"
class="form-control"
id="user_email"
placeholder="{{ 'app.shared.user.email_address' | translate }}"
ng-disabled="preventField['user.email'] && user.email && !userForm['user[email]'].$dirty"
required/>
</div>
<span class="help-block" ng-show="userForm['user[email]'].$dirty && userForm['user[email]'].$error.required" translate>{{ 'app.shared.user.email_address_is_required' }}</span>
</div>
<div class="form-group" ng-hide="preventPassword">
<button class="btn btn-warning btn-block"
ng-click="password.change = !password.change; $event.stopPropagation(); $event.preventDefault()"
translate>{{ 'app.shared.user.change_password' }}</button>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[password]'].$dirty && userForm['user[password]'].$invalid}" ng-if="password.change">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-key"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="password"
name="user[password]"
ng-model="user.password"
class="form-control"
id="user_password"
placeholder="{{ 'app.shared.user.new_password' | translate }}"
ng-minlength="12"
required/>
</div>
<span class="help-block" ng-show="userForm['user[password]'].$dirty && userForm['user[password]'].$error.required" translate>{{ 'app.shared.user.password_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[password]'].$dirty && userForm['user[password]'].$error.minlength" translate>{{ 'app.shared.user.password_is_too_short' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$invalid}" ng-if="password.change">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-key"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="password"
name="user[password_confirmation]"
ng-model="user.password_confirmation"
class="form-control"
id="user_password_confirmation"
placeholder="{{ 'app.shared.user.confirmation_of_new_password' | translate }}"
ng-minlength="12"
required
match="user.password"/>
</div>
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$error.required" translate>{{ 'app.shared.user.confirmation_of_password_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$error.minlength" translate>{{ 'app.shared.user.confirmation_of_password_is_too_short' }}</span>
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$error.match" translate>{{ 'app.shared.user.confirmation_mismatch_with_password' }}</span>
</div>
<div class="form-group" ng-if="user.invoicing_profile_attributes.organization" ng-class="{'has-error': userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-building-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[invoicing_profile_attributes][organization_attributes][id]"
ng-value="user.invoicing_profile_attributes.organization.id" />
<input type="text"
name="user[invoicing_profile_attributes][organization_attributes][name]"
ng-model="user.invoicing_profile_attributes.organization.name"
class="form-control"
placeholder="{{ 'app.shared.user.organization_name' | translate }}"
ng-required="user.invoicing_profile.organization"
ng-disabled="preventField['profile.organization_name'] && user.invoicing_profile.organization.name && !userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty">
</div>
<span class="help-block" ng-show="userForm['user[invoicing_][organization_attributes][name]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$error.required" translate>{{ 'app.shared.user.organization_name_is_required' }}</span>
</div>
<div class="form-group" ng-if="user.invoicing_profile_attributes.organization" ng-class="{'has-error': userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-map-marker"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[invoicing_profile_attributes][organization_attributes][address_attributes][id]"
ng-value="user.invoicing_profile_attributes.organization.address.id" />
<input type="text"
name="user[invoicing_profile_attributes][organization_attributes][address_attributes][address]"
ng-model="user.invoicing_profile_attributes.organization.address.address"
class="form-control"
placeholder="{{ 'app.shared.user.organization_address' | translate }}"
ng-required="user.invoicing_profile.organization"
ng-disabled="preventField['profile.organization_address'] && user.invoicing_profile_attributes.organization.address.address && !userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty">
</div>
<span class="help-block" ng-show="userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$error.required" translate>{{ 'app.shared.user.organization_address_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[statistic_profile_attributes][birthday]'].$dirty && userForm['user[statistic_profile_attributes][birthday]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_statistics' | translate }}"><i class="fa fa-calendar-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
id="user_birthday"
class="form-control"
ng-model="user.statistic_profile_attributes.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
placeholder="{{ 'app.shared.user.date_of_birth' | translate }}"
ng-click="openDatePicker($event)"
ng-disabled="preventField['profile.birthday'] && user.statistic_profile_attributes.birthday && !userForm['user[statistic_profile_attributes][birthday]'].$dirty"
required/>
<input type="hidden"
name="user[statistic_profile_attributes][birthday]"
value="{{user.statistic_profile_attributes.birthday | toIsoDate}}" />
</div>
<span class="help-block" ng-show="userForm['user[statistic_profile_attributes][birthday]'].$dirty && userForm['user[statistic_profile_attributes][birthday]'].$error.required" translate>{{ 'app.shared.user.date_of_birth_is_required' }}</span>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_invoicing' | translate }}"><i class="fa fa-map-marker"></i> <span class="exponent" ng-show="addressRequired"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[invoicing_profile_attributes][address_attributes][id]"
ng-value="user.invoicing_profile_attributes.address.id" />
<input type="text"
name="user[invoicing_profile_attributes][address_attributes][address]"
ng-model="user.invoicing_profile_attributes.address.address"
class="form-control"
id="user_address"
ng-disabled="preventField['profile.address'] && user.invoicing_profile_attributes.address.address && !userForm['user[invoicing_profile_attributes][address_attributes][address]'].$dirty"
placeholder="{{ 'app.shared.user.address' | translate }}"
ng-required="addressRequired"/>
</div>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][phone]'].$dirty && userForm['user[profile_attributes][phone]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_reservation' | translate }}"><i class="fa fa-phone"></i> <span class="exponent" ng-show="phoneRequired"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][phone]"
ng-model="user.profile_attributes.phone"
class="form-control"
id="user_phone"
placeholder="{{ 'app.shared.user.phone_number' | translate }}"
ng-disabled="preventField['profile.phone'] && user.profile_attributes.phone && !userForm['user[profile_attributes][phone]'].$dirty"
ng-required="phoneRequired"/>
</div>
<span class="help-block" ng-show="userForm['user[profile_attributes][phone]'].$dirty && userForm['user[profile_attributes][phone]'].$error.required" translate>{{ 'app.shared.user.phone_number_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][website]'].$dirty && userForm['user[profile_attributes][website]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-globe"></i> </span>
<input type="url"
name="user[profile_attributes][website]"
ng-model="user.profile_attributes.website"
class="form-control"
id="user_website"
ng-pattern="/^https?:\/\//"
placeholder="{{ 'app.shared.user.website' | translate }} (http://...)"
ng-disabled="preventField['profile.website'] && user.profile_attributes.website && !userForm['user[profile_attributes][website]'].$dirty"/>
</div>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][job]'].$dirty && userForm['user[profile_attributes][job]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-briefcase"></i> </span>
<input type="text"
name="user[profile_attributes][job]"
ng-model="user.profile_attributes.job"
class="form-control"
id="user_job"
placeholder="{{ 'app.shared.user.job' | translate }}"
ng-disabled="preventField['profile.job'] && user.profile_attributes.job && !userForm['user[profile_attributes][job]'].$dirty"/>
</div>
</div>
<div class="form-group">
<label for="user_interest" class="help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}" translate>{{ 'app.shared.user.interests' }}</label>
<textarea name="user[profile_attributes][interest]"
ng-model="user.profile_attributes.interest"
rows="5"
class="form-control"
id="user_interest"
placeholder=""
ng-disabled="preventField['profile.interest'] && user.profile_attributes.interest && !userForm['user[profile_attributes][interest]'].$dirty"></textarea>
</div>
<div class="form-group">
<label for="user_software_mastered" class="help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}" translate>{{ 'app.shared.user.CAD_softwares_mastered' }}</label>
<textarea name="user[profile_attributes][software_mastered]"
ng-model="user.profile_attributes.software_mastered"
rows="5"
class="form-control"
id="user_software_mastered"
placeholder=""
ng-disabled="preventField['profile.software_mastered'] && user.profile_attributes.software_mastered && !userForm['user[profile_attributes][software_mastered]'].$dirty"></textarea>
</div>
<!-- allow contact-->
<div class="form-group">
<label for="allowContact" class="help-cursor" title="{{ 'app.shared.user.public_profile' | translate }}" translate>{{ 'app.shared.user.i_authorize_Fablab_users_registered_on_the_site_to_contact_me' }}</label>
<input bs-switch
ng-model="user.is_allow_contact"
id="allowContact"
type="checkbox"
class="form-control"
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
switch-animate="true"/>
<input type="hidden" name="user[is_allow_contact]" value="{{user.is_allow_contact}}"/>
</div>
<!-- allow receive newsletter -->
<div class="form-group">
<label for="allowNewsletter" translate>{{ 'app.shared.user.i_accept_to_receive_information_from_the_fablab' }}</label>
<input bs-switch
ng-model="user.is_allow_newsletter"
id="allowNewsletter"
type="checkbox"
class="form-control"
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
switch-animate="true" />
<input type="hidden" name="user[is_allow_newsletter]" value="{{user.is_allow_newsletter}}"/>
</div>
<div id="social" ng-init="social={}">
<div class="form-group" ng-show="social.facebook || user.profile_attributes.facebook" ng-class="{'has-error': userForm['user[profile_attributes][facebook]'].$dirty && userForm['user[profile_attributes][facebook]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-facebook"></i></span>
<input type="text"
name="user[profile_attributes][facebook]"
ng-model="user.profile_attributes.facebook"
class="form-control"
id="user_facebook"
ng-pattern="/^https?:\/\/.*?facebook/i"
placeholder="https://www.facebook.com/..."
ng-disabled="preventField['profile.facebook'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][facebook]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.twitter || user.profile_attributes.twitter" ng-class="{'has-error': userForm['user[profile_attributes][twitter]'].$dirty && userForm['user[profile_attributes][twitter]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-twitter"></i></span>
<input type="text"
name="user[profile_attributes][twitter]"
ng-model="user.profile_attributes.twitter"
class="form-control"
id="user_twitter"
ng-pattern="/^https?:\/\/.*?twitter/"
placeholder="https://twitter.com/..."
ng-disabled="preventField['profile.twitter'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][twitter]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.google_plus || user.profile_attributes.google_plus" ng-class="{'has-error': userForm['user[profile_attributes][google_plus]'].$dirty && userForm['user[profile_attributes][google_plus]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-google-plus"></i></span>
<input type="text"
name="user[profile_attributes][google_plus]"
ng-model="user.profile_attributes.google_plus"
class="form-control"
id="user_google_plus"
ng-pattern="/^https?:\/\/.*?google/"
placeholder="https://plus.google.com/+..."
ng-disabled="preventField['profile.google_plus'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][google_plus]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.viadeo || user.profile_attributes.viadeo" ng-class="{'has-error': userForm['user[profile_attributes][viadeo]'].$dirty && userForm['user[profile_attributes][viadeo]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-viadeo"></i></span>
<input type="text"
name="user[profile_attributes][viadeo]"
ng-model="user.profile_attributes.viadeo"
class="form-control"
id="user_viadeo"
ng-pattern="/^https?:\/\/.*?viadeo/"
placeholder="http://www.viadeo.com/fr/profile/..."
ng-disabled="preventField['profile.viadeo'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][viadeo]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.linkedin || user.profile_attributes.linkedin" ng-class="{'has-error': userForm['user[profile_attributes][linkedin]'].$dirty && userForm['user[profile_attributes][linkedin]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-linkedin"></i></span>
<input type="text"
name="user[profile_attributes][linkedin]"
ng-model="user.profile_attributes.linkedin"
class="form-control"
id="user_linkedin"
ng-pattern="/^https?:\/\/.*?linkedin/"
placeholder="https://www.linkedin.com/in/..."
ng-disabled="preventField['profile.linkedin'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][linkedin]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.instagram || user.profile_attributes.instragram" ng-class="{'has-error': userForm['user[profile_attributes][instagram]'].$dirty && userForm['user[profile_attributes][instagram]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-instagram"></i></span>
<input type="text"
name="user[profile_attributes][instagram]"
ng-model="user.profile_attributes.instagram"
class="form-control"
id="user_instagram"
ng-pattern="/^https?:\/\/.*?instagram/"
placeholder="https://www.instagram.com/..."
ng-disabled="preventField['profile.instagram'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][instagram]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.youtube || user.profile_attributes.youtube" ng-class="{'has-error': userForm['user[profile_attributes][youtube]'].$dirty && userForm['user[profile_attributes][youtube]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-youtube"></i></span>
<input type="text"
name="user[profile_attributes][youtube]"
ng-model="user.profile_attributes.youtube"
class="form-control"
id="user_youtube"
ng-pattern="/^https?:\/\/.*?youtube/"
placeholder="https://www.youtube.com/..."
ng-disabled="preventField['profile.youtube'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][youtube]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.vimeo || user.profile_attributes.vimeo" ng-class="{'has-error': userForm['user[profile_attributes][vimeo]'].$dirty && userForm['user[profile_attributes][vimeo]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-vimeo"></i></span>
<input type="text"
name="user[profile_attributes][vimeo]"
ng-model="user.profile_attributes.vimeo"
class="form-control"
id="user_vimeo"
ng-pattern="/^https?:\/\/.*?vimeo/"
placeholder="https://vimeo.com/..."
ng-disabled="preventField['profile.vimeo'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][vimeo]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.dailymotion || user.profile_attributes.dailymotion" ng-class="{'has-error': userForm['user[profile_attributes][dailymotion]'].$dirty && userForm['user[profile_attributes][dailymotion]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><img src="../../images/social/dailymotion.png" alt="d" class="fa-img"/></span>
<input type="text"
name="user[profile_attributes][dailymotion]"
ng-model="user.profile_attributes.dailymotion"
class="form-control"
id="user_dailymotion"
ng-pattern="/^https?:\/\/.*?dailymotion/"
placeholder="http://www.dailymotion.com/..."
ng-disabled="preventField['profile.dailymotion'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][dailymotion]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.github || user.profile_attributes.github" ng-class="{'has-error': userForm['user[profile_attributes][github]'].$dirty && userForm['user[profile_attributes][github]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-github"></i></span>
<input type="text"
name="user[profile_attributes][github]"
ng-model="user.profile_attributes.github"
class="form-control"
id="user_github"
ng-pattern="/^https?:\/\/.*?github/"
placeholder="https://github.com/..."
ng-disabled="preventField['profile.github'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][github]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.echosciences || user.profile_attributes.echosciences" ng-class="{'has-error': userForm['user[profile_attributes][echosciences]'].$dirty && userForm['user[profile_attributes][echosciences]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><img src="../../images/social/echosciences.png" alt="d" class="fa-img"/></span>
<input type="text"
name="user[profile_attributes][echosciences]"
ng-model="user.profile_attributes.echosciences"
class="form-control"
id="user_echosciences"
ng-pattern="/^https?:\/\/.*?echosciences/"
placeholder="http://www.echosciences-local.fr/membres/..."
ng-disabled="preventField['profile.echosciences'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][echosciences]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.pinterest || user.profile_attributes.pinterest" ng-class="{'has-error': userForm['user[profile_attributes][pinterest]'].$dirty && userForm['user[profile_attributes][pinterest]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-pinterest"></i></span>
<input type="text"
name="user[profile_attributes][pinterest]"
ng-model="user.profile_attributes.pinterest"
class="form-control"
id="user_pinterest"
ng-pattern="/^https?:\/\/.*?pinterest/"
placeholder="https://fr.pinterest.com/..."
ng-disabled="preventField['profile.pinterest'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][pinterest]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.lastfm || user.profile_attributes.lastfm" ng-class="{'has-error': userForm['user[profile_attributes][lastfm]'].$dirty && userForm['user[profile_attributes][lastfm]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-lastfm"></i></span>
<input type="text"
name="user[profile_attributes][lastfm]"
ng-model="user.profile_attributes.lastfm"
class="form-control"
id="user_lastfm"
ng-pattern="/^https?:\/\/.*?last.fm/"
placeholder="http://www.last.fm/fr/user/..."
ng-disabled="preventField['profile.lastfm'] && user.profile_attributes.first_name && !userForm['user[profile_attributes][lastfm]'].$dirty"
/>
</div>
</div>
<div class="form-group" ng-show="social.flickr || user.profile_attributes.flickr" ng-class="{'has-error': userForm['user[profile_attributes][flickr]'].$dirty && userForm['user[profile_attributes][flickr]'].$invalid}">
<div class="input-group">
<span class="input-group-addon help-cursor" title="{{ 'app.shared.user.used_for_profile' | translate }}"><i class="fa fa-flickr"></i></span>
<input type="text"
name="user[profile_attributes][flickr]"
ng-model="user.profile_attributes.flickr"
class="form-control"
id="user_flickr"
ng-pattern="/^https?:\/\/.*?flickr/"
placeholder="https://www.flickr.com/photos/..."
ng-disabled="preventField['profile.flickr'] && user.profile_attributes.flickr && !userForm['user[profile_attributes][flickr]'].$dirty"
/>
</div>
</div>
<div class="social-icons m-b">
<div ng-click="social.facebook = !social.facebook" ng-hide="social.facebook || user.profile_attributes.facebook"><i class="fa fa-facebook fa-2x"></i></div>
<div ng-click="social.twitter = !social.twitter" ng-hide="social.twitter || user.profile_attributes.twitter"><i class="fa fa-twitter fa-2x"></i></div>
<div ng-click="social.google_plus = !social.google_plus" ng-hide="social.google_plus || user.profile_attributes.google_plus"><i class="fa fa-google-plus fa-2x"></i></div>
<div ng-click="social.viadeo = !social.viadeo" ng-hide="social.viadeo || user.profile_attributes.viadeo"><i class="fa fa-viadeo fa-2x"></i></div>
<div ng-click="social.linkedin = !social.linkedin" ng-hide="social.linkedin || user.profile_attributes.linkedin"><i class="fa fa-linkedin fa-2x"></i></div>
<div ng-click="social.instagram = !social.instagram" ng-hide="social.instagram || user.profile_attributes.instagram"><i class="fa fa-instagram fa-2x"></i></div>
<div ng-click="social.youtube = !social.youtube" ng-hide="social.youtube || user.profile_attributes.youtube"><i class="fa fa-youtube fa-2x"></i></div>
<div ng-click="social.vimeo = !social.vimeo" ng-hide="social.vimeo || user.profile_attributes.vimeo"><i class="fa fa-vimeo fa-2x"></i></div>
<div ng-click="social.dailymotion = !social.dailymotion" ng-hide="social.dailymotion || user.profile_attributes.dailymotion"><img src="../../images/social/dailymotion.png" alt="d" class="fa-img contrast-250 fa-2x"/></div>
<div ng-click="social.github = !social.github" ng-hide="social.github || user.profile_attributes.github"><i class="fa fa-github fa-2x"></i></div>
<div ng-click="social.echosciences = !social.echosciences" ng-hide="social.echosciences || user.profile_attributes.echosciences"><img src="../../images/social/echosciences.png" alt="E" class="fa-img contrast-250 fa-2x"/></div>
<div ng-click="social.pinterest = !social.pinterest" ng-hide="social.pinterest || user.profile_attributes.pinterest"><i class="fa fa-pinterest fa-2x"></i></div>
<div ng-click="social.lastfm = !social.lastfm" ng-hide="social.lastfm || user.profile_attributes.lastfm"><i class="fa fa-lastfm fa-2x"></i></div>
<div ng-click="social.flickr = !social.flickr" ng-hide="social.flickr || user.profile_attributes.flickr"><i class="fa fa-flickr fa-2x"></i></div>
</div>
</div>
</div>
</div>

View File

@ -42,9 +42,9 @@
translate-attr="{ placeholder: 'app.public.common.your_password' }"
ng-minlength="8"/>
</div>
<a ng-click="openResetPassword($event)" class="text-xs" translate translate-default="Forgotten password">{{ 'app.public.common.password_forgotten' }}</a>
<a ng-click="openResetPassword($event)" class="text-xs pointer" translate translate-default="Forgotten password">{{ 'app.public.common.password_forgotten' }}</a>
<span ng-if="confirmationRequired">
<br><a ng-click="openConfirmationNewModal($event)" class="text-xs" translate translate-default="Confirm account">{{ 'app.public.common.confirm_my_account' }}</a>
<br><a ng-click="openConfirmationNewModal($event)" class="text-xs pointer" translate translate-default="Confirm account">{{ 'app.public.common.confirm_my_account' }}</a>
</span>
<div class="alert alert-warning m-t-sm m-b-none text-xs p-sm" ng-show='isCapsLockOn' role="alert">
<i class="fa fa-warning"></i>
@ -63,7 +63,7 @@
<p class="text-center font-sbold" ng-show="publicRegistrations">
<span translate translate-default="Not registered?">{{ 'app.public.common.not_registered_to_the_fablab' }}</span>
<br/>
<a ng-click="openSignup($event)" class="text-u-l" translate translate-default="Create an account">{{ 'app.public.common.create_an_account' }}</a></br>
<a ng-click="openSignup($event)" class="text-u-l pointer" translate translate-default="Create an account">{{ 'app.public.common.create_an_account' }}</a></br>
</p>
</div>

View File

@ -124,7 +124,7 @@ module SingleSignOnConcern
logger.debug "mapping sso field #{field} with value=#{value}"
# we do not merge the email field if its end with the special value '-duplicate' as this means
# that the user is currently merging with the account that have the same email than the sso
set_data_from_sso_mapping(field, value) unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && user.admin?)
set_data_from_sso_mapping(field, value) unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && sso_user.admin?)
end
# run the account transfer in an SQL transaction to ensure data integrity

View File

@ -1,11 +1,13 @@
# frozen_string_literal: true
json.extract! @training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page
json.availabilities @availabilities do |a|
json.id a.id
json.start_at a.start_at.iso8601
json.end_at a.end_at.iso8601
json.reservation_users a.slots.map do |slot|
json.id slot.reservations.first.statistic_profile.user_id
json.full_name slot.reservations.first.statistic_profile&.user&.profile&.full_name
json.is_valid slot.reservations.first.statistic_profile.trainings.include?(@training)
json.reservation_users a.slots.map(&:slots_reservations).flatten.map do |sr|
json.id sr.reservation.statistic_profile.user_id
json.full_name sr.reservation.statistic_profile.user&.profile&.full_name
json.is_valid sr.reservation.statistic_profile.trainings&.include?(@training)
end
end

View File

@ -71,9 +71,9 @@ pt:
email_is_required: "E-mail é obrigatório."
your_password: "Sua senha"
password_is_required: "Senha é obrigatório."
password_is_too_short: "Password is too short (minimum 12 characters)"
password_is_too_weak: "Password is too weak:"
password_is_too_weak_explanations: "minimum 12 characters, at least one uppercase letter, one lowercase letter, one number and one special character"
password_is_too_short: "Senha muito curta (mínimo 12 caracteres)"
password_is_too_weak: "A senha é muito fraca:"
password_is_too_weak_explanations: "mínimo de 12 caracteres, pelo menos uma letra maiúscula, uma letra minúscula, um número e um caractere especial"
type_your_password_again: "Digite sua senha novamente"
password_confirmation_is_required: "Confirmação de senha é obrigatório."
password_does_not_match_with_confirmation: "A senha não é igual ao da confirmação."
@ -103,7 +103,7 @@ pt:
used_for_reservation: "Estes dados serão utilizados em caso de alteração em uma das suas reservas"
used_for_profile: "Estes dados serão exibidos apenas no seu perfil"
public_profile: "Você terá um perfil público e outros usuários poderão associá-lo em seus projetos"
you_will_receive_confirmation_instructions_by_email_detailed: "If your e-mail address is valid, you will receive an email with instructions about how to confirm your account in a few minutes."
you_will_receive_confirmation_instructions_by_email_detailed: "Se seu endereço de e-mail for válido, você receberá em breve um e-mail com instruções sobre como confirmar sua conta."
#password modification modal
change_your_password: "Mudar sua senha"
your_new_password: "Sua nova senha"
@ -119,7 +119,7 @@ pt:
#confirmation modal
you_will_receive_confirmation_instructions_by_email: "Você receberá instruções de confirmação por e-mail."
#forgotten password modal
you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password: "If your e-mail address is valid, you will receive in a moment an e-mail with instructions to reset your password."
you_will_receive_in_a_moment_an_email_with_instructions_to_reset_your_password: "Se o seu endereço de e-mail for válido, você receberá em breve um e-mail com instruções para redefinir sua senha."
#Fab-manager's version
version: "Versão:"
upgrade_fabmanager: "Atualizar Fab-manager"

View File

@ -19,9 +19,9 @@ pt:
extension_whitelist_error: "Você não tem permissão para fazer o upload de arquivos com esta extensão %{extension}, tipos permitidos: %{allowed_types}"
extension_blacklist_error: "Você não tem permissão para carregar arquivos %{extension}, tipos proibidos: %{prohibited_types}"
content_type_whitelist_error: "Você não tem permissão para enviar arquivos %{content_type}, tipos permitidos: %{allowed_types}"
rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
mime_types_processing_error: "Failed to process file with MIME::Types, maybe not valid content-type?"
mini_magick_processing_error: "Failed to manipulate the file, maybe it is not an image?"
rmagick_processing_error: "Falha ao manipular com rmagick, talvez não seja uma imagem?"
mime_types_processing_error: "Falha ao processar arquivo com MIME::Types, talvez tipo de conteúdo inválido?"
mini_magick_processing_error: "Falha ao manipular o arquivo, talvez não seja uma imagem?"
wrong_size: "é o tamanho errado (deveria ser %{file_size})"
size_too_small: "é muito pequeno (deve ser pelo menos %{file_size})"
size_too_big: "é muito grande (deve ser no máximo %{file_size})"

View File

@ -192,7 +192,7 @@ _Eg.: configuring **es-ES** will set the currency symbol to **€** but **es-MX*
Available values: `en, en-AU-CA, en-GB, en-IE, en-IN, en-NZ, en-US, en-ZA, fr, fa-CA, fr-CH, fr-CM, fr-FR, es, es-419, es-AR, es-CL, es-CO, es-CR, es-DO,
es-EC, es-ES, es-MX, es-MX, es-PA, es-PE, es-US, es-VE, no, pt, pt-BR, zu`.
Default is **en**.
When not defined, it defaults to **en**.
If your locale is not present in that list or any locale doesn't have your exact expectations, please open a pull request to share your modifications with the community and obtain a rebuilt docker image.
You can find templates of these files at https://github.com/svenfuchs/rails-i18n/tree/rails-5-x/rails/locale.
@ -203,7 +203,7 @@ You can find templates of these files at https://github.com/svenfuchs/rails-i18n
Configure the moment.js library for l10n.
See [github.com/moment/momentlocale/*.js](https://github.com/moment/moment/tree/2.22.2/locale) for a list of available locales.
Default is **en** (even if it's not listed).
When not defined, it defaults to **en** (even if it's not listed).
<a name="SUMMERNOTE_LOCALE"></a>
SUMMERNOTE_LOCALE
@ -211,7 +211,7 @@ Default is **en** (even if it's not listed).
Configure the javascript summernote editor for l10n.
See [github.com/summernote/summernote/lang/summernote-*.js](https://github.com/summernote/summernote/tree/v0.8.18/lang) for a list of available locales.
Default is **en-US** (even if it's not listed).
When not defined, it defaults to **en-US** (even if it's not listed).
<a name="ANGULAR_LOCALE"></a>
ANGULAR_LOCALE
@ -222,14 +222,14 @@ Please, be aware that **the configured locale will imply the CURRENCY displayed
_Eg.: configuring **fr-fr** will set the currency symbol to **€** but **fr-ca** will set **$** as currency symbol, so setting the `ANGULAR_LOCALE` to simple **fr** (without country indication) will probably not do what you expect._
See [code.angularjs.org/i18n/angular-locale_*.js](https://code.angularjs.org/1.8.2/i18n/) for a list of available locales. Default is **en**.
See [code.angularjs.org/i18n/angular-locale_*.js](https://code.angularjs.org/1.8.2/i18n/) for a list of available locales. When not defined, it defaults to **en**.
<a name="FULLCALENDAR_LOCALE"></a>
FULLCALENDAR_LOCALE
Configure the fullCalendar JS agenda library.
See [github.com/fullcalendar/fullcalendar/locale/*.js](https://github.com/fullcalendar/fullcalendar/tree/v3.10.2/locale) for a list of available locales. Default is **en**.
See [github.com/fullcalendar/fullcalendar/locale/*.js](https://github.com/fullcalendar/fullcalendar/tree/v3.10.2/locale) for a list of available locales. When not defined, it defaults to **en**.
<a name="INTL_LOCALE"></a>
INTL_LOCALE
@ -256,7 +256,7 @@ Available values: `danish, dutch, english, finnish, french, german, hungarian, i
TIME_ZONE
In Rails: set Time.zone default to the specified zone and make Active Record auto-convert to this zone. Run `rails time:zones:all` for a list of available time zone names.
Default is **UTC**.
When not defined, it defaults to **UTC**.
<a name="WEEK_STARTING_DAY"></a>
WEEK_STARTING_DAY

View File

@ -35,7 +35,9 @@ Choose one, depending on your budget, on the server's location, on the uptime gu
#### System requirements
##### Memory
If you do not plan to use the statistics module, you will need at least 2 GB of addressable memory (RAM + swap) to install and use Fab-manager.
To install or upgrade Fab-manager you need at least 4 GB of RAM + 2 GB of swap to be able to compile the assets.
Once installed, if you do not plan to use the statistics module, you will need at least 2 GB of addressable memory (RAM + swap) to use Fab-manager.
We recommend 4 GB of RAM to take full advantage of Fab-manager and be able to use the statistics module.
If you have a large community (~ 200 active membres), we recommend 4 GB of RAM, even without the statistics module.
@ -47,10 +49,10 @@ Supported operating systems are Ubuntu LTS 16.04+ and Debian 8+ with an x86 64-b
This might work on other linux systems, and CPU architectures but this is untested for now, and we do not recommend for production purposes.
#### Software requirements
`curl` and `bash` are needed to retrieve and run the automated deployment scripts.
`curl` and `bash` are needed to retrieve and run the automated deployment scripts.
Then the various scripts will check for their own dependencies.
Moreover, the main software dependencies to run fab-manager are [Docker](https://docs.docker.com/engine/installation/linux/docker-ce/debian/) v20.10 or above and [Docker Compose](https://docs.docker.com/compose/install/)
Moreover, the main software dependencies to run fab-manager are [Docker](https://docs.docker.com/engine/installation/linux/docker-ce/debian/) v20.10 or above and [Docker Compose](https://docs.docker.com/compose/install/)
They can be easily installed using the [`prepare-vps.sleede.com` script below](#prepare-the-server).
<a name="setup-the-domain-name"></a>
@ -109,7 +111,7 @@ Then, you can remove the `elasticsearch` service from the [docker-compose.yml](.
docker compose down && docker compose up -d
```
Disabling ElasticSearch will save up to 800 Mb of memory.
Disabling ElasticSearch will save up to 800 Mb of memory.
<a name="useful-commands"></a>
## Useful commands
@ -143,7 +145,7 @@ When a new version is available, follow this procedure to update Fab-manager in
You can subscribe to [this atom feed](https://github.com/sleede/fab-manager/releases.atom) to get notified when a new release comes out.
<a name="scripted-update"></a>
### Scripted update
### Scripted update
Starting with Fab-manager v4.5.0, you can upgrade Fab-manager in one single easy command, specified in the GitHub releases notes.
To upgrade multiple versions at once, read the GitHub release notes of all versions between your current version, and the target version.
@ -164,7 +166,7 @@ Then, you'll need to perform the upgrade with the following command:
```bash
\curl -sSL upgrade.fab.mn | bash -s -- -e "VAR=value" -p "rails fablab:do:things" -c "rails fablab:setup:command"
```
> ⚠ Do not confuse commands prefixed with `-p` and with `-c` because they are not intended to run at the same moment of the upgrade process.
> ⚠ Do not confuse commands prefixed with `-p` and with `-c` because they are not intended to run at the same moment of the upgrade process.
<a name="update-manually"></a>
### Update manually
@ -190,7 +192,7 @@ Then, you'll need to perform the upgrade with the following command:
`cd /apps/fabmanager`
2. Pull last docker images
2. Pull last docker images
`docker compose pull`
@ -208,9 +210,9 @@ Then, you'll need to perform the upgrade with the following command:
6. Run specific commands
**Do not forget** to check if there are commands to run for your upgrade. Those commands
**Do not forget** to check if there are commands to run for your upgrade. Those commands
are always specified in the [CHANGELOG](https://github.com/sleede/fab-manager/blob/master/CHANGELOG.md) and prefixed by **[TODO DEPLOY]**.
Those commands execute specific tasks and have to be run manually.
You must prefix the commands starting by `rails...` or `rake...` with: `docker compose run --rm fabmanager bundle exec`.
In any other cases, the other commands (like those invoking curl `\curl -sSL... | bash`) must not be prefixed.
@ -228,8 +230,8 @@ You can check that all containers are running with `docker compose ps`.
<a name="upgrade-to-the-last-version"></a>
### Upgrade to the last version
It's the default behaviour as `docker compose pull` command will fetch the latest versions of the docker images.
Be sure to run all the specific commands listed in the [CHANGELOG](https://github.com/sleede/fab-manager/blob/master/CHANGELOG.md) between your actual, and the new version in sequential order.
It's the default behaviour as `docker compose pull` command will fetch the latest versions of the docker images.
Be sure to run all the specific commands listed in the [CHANGELOG](https://github.com/sleede/fab-manager/blob/master/CHANGELOG.md) between your actual, and the new version in sequential order.
__Example:__ to update from v2.4.0 to v2.4.3, you will run the specific commands for the v2.4.1, v2.4.2 and v2.4.3.
<a name="upgrade-to-a-specific-version"></a>
@ -252,4 +254,4 @@ For example, here we want to use the v3.1.2:
```yaml
image: sleede/fab-manager:release-v3.1.2
```
Then run the normal upgrade procedure.
Then run the normal upgrade procedure.

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "5.4.15",
"version": "5.4.16",
"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",

View File

@ -198,7 +198,7 @@ prepare_files()
if [[ "$confirm" = "n" ]]; then exit 1; fi
elevate_cmd mkdir -p "$FABMANAGER_PATH"
elevate_cmd chown -R "$(whoami):$(whoami)" "$FABMANAGER_PATH"
elevate_cmd chown -R "$(whoami)" "$FABMANAGER_PATH"
# create folders before starting the containers, otherwise root will own them
local folders=(accounting config elasticsearch/config exports imports invoices log payment_schedules plugins postgresql \
@ -384,7 +384,7 @@ configure_env_file()
var_doc=$(get_md_anchor "$doc" "$variable")
current=$(grep "$variable=" "$FABMANAGER_PATH/config/env")
echo "$var_doc" | bat --file-name "$variable" --language md --color=always
printf "- \e[1mCurrent value: %s\e[21m\n- New value? (leave empty to keep the current value)\n" "$current"
printf -- "- \e[1mCurrent value: %s\e[21m\n- New value? (leave empty to keep the current value)\n" "$current"
read -rep " > " value </dev/tty
if [ "$value" != "" ]; then
esc_val=$(printf '%s\n' "$value" | sed -e 's/\//\\\//g')
@ -412,8 +412,12 @@ read_password()
local password confirmation
>&2 echo "Please input a password for this administrator's account"
read -rsp " > " password </dev/tty
if [ ${#password} -lt 8 ]; then
>&2 printf "\nError: password is too short (minimal length: 8 characters)\n"
if [ ${#password} -lt 12 ]; then
>&2 printf "\nError: password is too short (minimal length: 12 characters)\n"
password=$(read_password 'no-confirm')
fi
if [[ ! $password =~ [0-9] || ! $password =~ [a-z] || ! $password =~ [A-Z] || ! $password =~ [[:punct:]] ]]; then
>&2 printf "\nError: password is too weak (should contain uppercases, lowercases, digits and special characters)\n"
password=$(read_password 'no-confirm')
fi
if [ "$1" != 'no-confirm' ]; then

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'test_helper'
module Trainings; end
class Trainings::AvailabilitiesTest < ActionDispatch::IntegrationTest
def setup
@admin = User.find_by(username: 'admin')
login_as(@admin, scope: :user)
end
test 'get trainings availabilities list' do
training = Training.find(1)
get "/api/trainings/#{training.id}/availabilities"
# Check response format & status
assert_equal 200, response.status, response.body
assert_equal Mime[:json], response.content_type
# Check the correct training was returned
result = json_response(response.body)
assert_equal training.id, result[:id], 'training id does not match'
assert_not_empty result[:availabilities], 'no training availabilities were returned'
end
end