1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-26 20:54:21 +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 # 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 ## v5.4.15 2022 August 1

View File

@ -36,6 +36,7 @@ group :development do
gem 'web-console', '>= 3.3.0' gem 'web-console', '>= 3.3.0'
# Preview mail in the browser # Preview mail in the browser
gem 'listen', '~> 3.0.5' gem 'listen', '~> 3.0.5'
gem 'overcommit'
gem 'rb-readline' gem 'rb-readline'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'railroady' gem 'railroady'
@ -50,9 +51,9 @@ group :test do
gem 'faker' gem 'faker'
gem 'minitest-reporters' gem 'minitest-reporters'
gem 'pdf-reader' gem 'pdf-reader'
gem 'rubyXL'
gem 'vcr', '6.0.0' gem 'vcr', '6.0.0'
gem 'webmock' gem 'webmock'
gem 'rubyXL'
end end
group :production, :staging do group :production, :staging do
@ -66,8 +67,6 @@ gem 'pg_search'
# authentication # authentication
gem 'devise', '>= 4.6.0' gem 'devise', '>= 4.6.0'
gem 'omniauth', '~> 1.9.0' gem 'omniauth', '~> 1.9.0'
gem 'omniauth-oauth2' gem 'omniauth-oauth2'
gem 'omniauth_openid_connect' gem 'omniauth_openid_connect'
@ -145,4 +144,4 @@ gem 'tzinfo-data'
# compilation of dynamic stylesheets (home page & theme) # compilation of dynamic stylesheets (home page & theme)
gem 'sassc', '= 2.1.0' 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) caxlsx_rails (0.6.2)
actionpack (>= 3.1) actionpack (>= 3.1)
caxlsx (>= 3.0) caxlsx (>= 3.0)
childprocess (4.1.0)
chroma (0.2.0) chroma (0.2.0)
cldr-plurals-runtime-rb (1.0.1) cldr-plurals-runtime-rb (1.0.1)
coercible (1.0.0) coercible (1.0.0)
@ -176,6 +177,7 @@ GEM
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
iniparse (1.5.0)
jbuilder (2.10.0) jbuilder (2.10.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
jbuilder_cache_multi (0.1.0) jbuilder_cache_multi (0.1.0)
@ -272,6 +274,10 @@ GEM
openlab_ruby (0.0.7) openlab_ruby (0.0.7)
httparty (~> 0.20) httparty (~> 0.20)
orm_adapter (0.5.0) orm_adapter (0.5.0)
overcommit (0.59.1)
childprocess (>= 0.6.3, < 5)
iniparse (~> 1.4)
rexml (~> 3.2)
parallel (1.19.1) parallel (1.19.1)
parser (3.1.2.0) parser (3.1.2.0)
ast (~> 2.4.1) ast (~> 2.4.1)
@ -532,6 +538,7 @@ DEPENDENCIES
omniauth-rails_csrf_protection (~> 0.1) omniauth-rails_csrf_protection (~> 0.1)
omniauth_openid_connect omniauth_openid_connect
openlab_ruby openlab_ruby
overcommit
pdf-reader pdf-reader
pg pg
pg_search pg_search

View File

@ -52,7 +52,13 @@ class API::TrainingsController < API::ApiController
authorize Training authorize Training
@training = Training.find(params[:id]) @training = Training.find(params[:id])
@availabilities = @training.availabilities @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) .where('slots_reservations.canceled_at': nil)
.order('availabilities.start_at DESC') .order('availabilities.start_at DESC')
end end

View File

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

View File

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

View File

@ -9,15 +9,16 @@ export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<A
tooltip?: ReactNode, tooltip?: ReactNode,
className?: string, className?: string,
disabled?: boolean|((id: string) => boolean), disabled?: boolean|((id: string) => boolean),
onLabelClick?: (event: React.MouseEvent<HTMLLabelElement, MouseEvent>) => void, onLabelClick?: (event: React.MouseEvent<HTMLParagraphElement, MouseEvent>) => void,
inLine?: boolean, inLine?: boolean,
containerType?: 'label' | 'div'
} }
/** /**
* This abstract component should not be used directly. * 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. * 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 [isDirty, setIsDirty] = useState<boolean>(false);
const [fieldError, setFieldError] = useState<{ message: string }>(error); const [fieldError, setFieldError] = useState<{ message: string }>(error);
const [isDisabled, setIsDisabled] = useState<boolean>(false); 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. * This function is called when the label is clicked.
* It is used to focus the input. * 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') { if (typeof onLabelClick === 'function') {
onLabelClick(event); onLabelClick(event);
} }
} }
return ( return React.createElement(containerType, { className: `form-item ${classNames}` }, (
<label className={`form-item ${classNames}`} onClick={handleLabelClick}> <>
{(label && !inLine) && <div className='form-item-header'> {(label && !inLine) && <div className='form-item-header'>
<p>{label}</p> <p onClick={handleLabelClick}>{label}</p>
{tooltip && <div className="item-tooltip"> {tooltip && <div className="item-tooltip">
<span className="trigger"><i className="fa fa-question-circle" /></span> <span className="trigger"><i className="fa fa-question-circle" /></span>
<div className="content">{tooltip}</div> <div className="content">{tooltip}</div>
@ -79,6 +80,8 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
</div> </div>
{(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> } {(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> }
{(isDirty && warning) && <div className="form-item-warning">{warning.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) * 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. * 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(); event.preventDefault();
textEditorRef.current.focus(); textEditorRef.current.focus();
} }
return ( return (
<AbstractFormItem id={id} label={label} tooltip={tooltip} <AbstractFormItem id={id} label={label} tooltip={tooltip}
containerType={'div'}
className={`form-rich-text ${className || ''}`} className={`form-rich-text ${className || ''}`}
error={error} warning={warning} rules={rules} error={error} warning={warning} rules={rules}
disabled={disabled} formState={formState} onLabelClick={focusTextEditor}> 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 { FormSelect } from '../form/form-select';
import MemberAPI from '../../api/member'; import MemberAPI from '../../api/member';
import SettingAPI from '../../api/setting'; import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import UserLib from '../../lib/user'; import UserLib from '../../lib/user';
declare const Application: IApplication; declare const Application: IApplication;
@ -46,7 +45,7 @@ export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onErr
useEffect(() => { useEffect(() => {
GroupAPI.index({ disabled: false, admins: user?.role === 'admin' }).then(setGroups).catch(onError); GroupAPI.index({ disabled: false, admins: user?.role === 'admin' }).then(setGroups).catch(onError);
MemberAPI.current().then(setOperator).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'); setAllowedUserChangeGoup(setting.value === 'true');
}).catch(onError); }).catch(onError);
}, []); }, []);

View File

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

View File

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

View File

@ -13,7 +13,6 @@ import PriceAPI from '../../api/price';
import WalletAPI from '../../api/wallet'; import WalletAPI from '../../api/wallet';
import { Invoice } from '../../models/invoice'; import { Invoice } from '../../models/invoice';
import SettingAPI from '../../api/setting'; import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import { GoogleTagManager } from '../../models/gtm'; import { GoogleTagManager } from '../../models/gtm';
import { ComputePriceResult } from '../../models/price'; import { ComputePriceResult } from '../../models/price';
import { Wallet } from '../../models/wallet'; import { Wallet } from '../../models/wallet';
@ -91,7 +90,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
useEffect(() => { useEffect(() => {
mounted.current = true; mounted.current = true;
CustomAssetAPI.get(CustomAssetName.CgvFile).then(asset => setCgv(asset)); 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 // we capitalize the first letter of the name
if (setting.value) { if (setting.value) {
setGateway(setting.value.replace(/^\w/, (c) => c.toUpperCase())); 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 { ShoppingCart } from '../../models/payment';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { PaymentSchedule } from '../../models/payment-schedule'; import { PaymentSchedule } from '../../models/payment-schedule';
import { Setting, SettingName } from '../../models/setting'; import { Setting } from '../../models/setting';
import { Invoice } from '../../models/invoice'; import { Invoice } from '../../models/invoice';
import SettingAPI from '../../api/setting'; import SettingAPI from '../../api/setting';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -35,7 +35,7 @@ const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal
const [gateway, setGateway] = useState<Setting>(null); const [gateway, setGateway] = useState<Setting>(null);
useEffect(() => { useEffect(() => {
SettingAPI.get(SettingName.PaymentGateway) SettingAPI.get('payment_gateway')
.then(setting => setGateway(setting)) .then(setting => setGateway(setting))
.catch(error => onError(error)); .catch(error => onError(error));
}, []); }, []);

View File

@ -5,7 +5,6 @@ import { GatewayFormProps } from '../abstract-payment-modal';
import LocalPaymentAPI from '../../../api/local-payment'; import LocalPaymentAPI from '../../../api/local-payment';
import FormatLib from '../../../lib/format'; import FormatLib from '../../../lib/format';
import SettingAPI from '../../../api/setting'; import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
import { CardPaymentModal } from '../card-payment-modal'; import { CardPaymentModal } from '../card-payment-modal';
import { PaymentSchedule } from '../../../models/payment-schedule'; import { PaymentSchedule } from '../../../models/payment-schedule';
import { HtmlTranslate } from '../../base/html-translate'; import { HtmlTranslate } from '../../base/html-translate';
@ -75,7 +74,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
if (paymentSchedule && method === 'card') { if (paymentSchedule && method === 'card') {
// check that the online payment is active // check that the online payment is active
try { try {
const online = await SettingAPI.get(SettingName.OnlinePaymentModule); const online = await SettingAPI.get('online_payment_module');
if (online.value !== 'true') { if (online.value !== 'true') {
return onError(t('app.admin.local_payment_form.online_payment_disabled')); 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 KRGlue from '@lyracom/embedded-form-glue';
import { GatewayFormProps } from '../abstract-payment-modal'; import { GatewayFormProps } from '../abstract-payment-modal';
import SettingAPI from '../../../api/setting'; import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
import PayzenAPI from '../../../api/payzen'; import PayzenAPI from '../../../api/payzen';
import { import {
CreateTokenResponse, CreateTokenResponse,
@ -27,10 +26,10 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader'); const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
useEffect(() => { useEffect(() => {
SettingAPI.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey]).then(settings => { SettingAPI.query(['payzen_endpoint', 'payzen_public_key']).then(settings => {
createToken().then(formToken => { createToken().then(formToken => {
// Load the remote library // 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 }) => .then(({ KR }) =>
KR.setFormConfig({ KR.setFormConfig({
formToken: formToken.formToken formToken: formToken.formToken

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import { HtmlTranslate } from '../../base/html-translate'; import { HtmlTranslate } from '../../base/html-translate';
import { FabInput } from '../../base/fab-input'; import { FabInput } from '../../base/fab-input';
import { Loader } from '../../base/loader'; import { Loader } from '../../base/loader';
import { SettingName } from '../../../models/setting';
import StripeAPI from '../../../api/external/stripe'; import StripeAPI from '../../../api/external/stripe';
import SettingAPI from '../../../api/setting'; import SettingAPI from '../../../api/setting';
@ -42,9 +41,9 @@ const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidK
useEffect(() => { useEffect(() => {
mounted.current = true; mounted.current = true;
SettingAPI.query([SettingName.StripePublicKey, SettingName.StripeSecretKey]).then(stripeKeys => { SettingAPI.query(['stripe_public_key', 'stripe_secret_key']).then(stripeKeys => {
setPublicKey(stripeKeys.get(SettingName.StripePublicKey)); setPublicKey(stripeKeys.get('stripe_public_key'));
setSecretKey(stripeKeys.get(SettingName.StripeSecretKey)); setSecretKey(stripeKeys.get('stripe_secret_key'));
}).catch(error => console.error(error)); }).catch(error => console.error(error));
// when the component unmounts, mark it as unmounted // 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 { UserPack } from '../../models/user-pack';
import UserPackAPI from '../../api/user-pack'; import UserPackAPI from '../../api/user-pack';
import SettingAPI from '../../api/setting'; import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ProposePacksModal } from './propose-packs-modal'; 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); const [isPackOnlyForSubscription, setIsPackOnlyForSubscription] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
SettingAPI.get(SettingName.RenewPackThreshold) SettingAPI.get('renew_pack_threshold')
.then(data => setThreshold(parseFloat(data.value))) .then(data => setThreshold(parseFloat(data.value)))
.catch(error => onError(error)); .catch(error => onError(error));
SettingAPI.get(SettingName.PackOnlyForSubscription) SettingAPI.get('pack_only_for_subscription')
.then(data => setIsPackOnlyForSubscription(data.value === 'true')) .then(data => setIsPackOnlyForSubscription(data.value === 'true'))
.catch(error => onError(error)); .catch(error => onError(error));
}, []); }, []);

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ export const PasswordInput = <TFieldValues extends FieldValues>({ register, curr
rules={{ rules={{
required: true, required: true,
validate: (value: string) => { validate: (value: string) => {
if (value.length < 8) { if (value.length < 12) {
return t('app.shared.password_input.password_too_short') as string; return t('app.shared.password_input.password_too_short') as string;
} }
return true; 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); setValue('invoicing_profile_attributes.user_profile_custom_fields_attributes', userProfileCustomFields);
}).catch(error => onError(error)); }).catch(error => onError(error));
SettingAPI.query([SettingName.PhoneRequired, SettingName.AddressRequired]) SettingAPI.query(['phone_required', 'address_required'])
.then(settings => setRequiredFieldsSettings(settings)) .then(settings => setRequiredFieldsSettings(settings))
.catch(error => onError(error)); .catch(error => onError(error));
}, []); }, []);
@ -219,7 +219,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
value: phoneRegex, value: phoneRegex,
message: t('app.shared.user_profile_form.phone_number_invalid') message: t('app.shared.user_profile_form.phone_number_invalid')
}, },
required: requiredFieldsSettings.get(SettingName.PhoneRequired) === 'true' required: requiredFieldsSettings.get('phone_required') === 'true'
}} }}
disabled={isDisabled} disabled={isDisabled}
formState={formState} formState={formState}
@ -232,7 +232,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
<FormInput id="invoicing_profile_attributes.address_attributes.address" <FormInput id="invoicing_profile_attributes.address_attributes.address"
register={register} register={register}
disabled={isDisabled} disabled={isDisabled}
rules={{ required: requiredFieldsSettings.get(SettingName.AddressRequired) === 'true' }} rules={{ required: requiredFieldsSettings.get('address_required') === 'true' }}
label={t('app.shared.user_profile_form.address')} /> label={t('app.shared.user_profile_form.address')} />
</div> </div>
</div> </div>

View File

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

View File

@ -588,10 +588,18 @@
.checkbox-group { .checkbox-group {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start;
input[type=checkbox] { input[type=checkbox] {
font-size: 16px; flex-shrink: 0;
width: 2em; 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 { .signup-form {
.names-row { .names-row {
input.form-control { & > div {
width: 89%; display: flex;
display: inline-block;
} }
} }

View File

@ -41,11 +41,11 @@
<button class="btn btn-default" ng-click="rowform.$show()"> <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> <i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
</button> </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-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> <span ng-show="group.disabled"><i class="fa fa-eye"></i> <span translate>{{ 'app.admin.members.group_form.enable' }}</span></span>
</button> </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> <i class="fa fa-trash-o"></i>
</button> </button>
</div> </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' }" translate-attr="{ placeholder: 'app.public.common.your_password' }"
ng-minlength="8"/> ng-minlength="8"/>
</div> </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"> <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> </span>
<div class="alert alert-warning m-t-sm m-b-none text-xs p-sm" ng-show='isCapsLockOn' role="alert"> <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> <i class="fa fa-warning"></i>
@ -63,7 +63,7 @@
<p class="text-center font-sbold" ng-show="publicRegistrations"> <p class="text-center font-sbold" ng-show="publicRegistrations">
<span translate translate-default="Not registered?">{{ 'app.public.common.not_registered_to_the_fablab' }}</span> <span translate translate-default="Not registered?">{{ 'app.public.common.not_registered_to_the_fablab' }}</span>
<br/> <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> </p>
</div> </div>

View File

@ -124,7 +124,7 @@ module SingleSignOnConcern
logger.debug "mapping sso field #{field} with value=#{value}" 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 # 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 # 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 end
# run the account transfer in an SQL transaction to ensure data integrity # 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.extract! @training, :id, :name, :description, :machine_ids, :nb_total_places, :public_page
json.availabilities @availabilities do |a| json.availabilities @availabilities do |a|
json.id a.id json.id a.id
json.start_at a.start_at.iso8601 json.start_at a.start_at.iso8601
json.end_at a.end_at.iso8601 json.end_at a.end_at.iso8601
json.reservation_users a.slots.map do |slot| json.reservation_users a.slots.map(&:slots_reservations).flatten.map do |sr|
json.id slot.reservations.first.statistic_profile.user_id json.id sr.reservation.statistic_profile.user_id
json.full_name slot.reservations.first.statistic_profile&.user&.profile&.full_name json.full_name sr.reservation.statistic_profile.user&.profile&.full_name
json.is_valid slot.reservations.first.statistic_profile.trainings.include?(@training) json.is_valid sr.reservation.statistic_profile.trainings&.include?(@training)
end end
end end

View File

@ -71,9 +71,9 @@ pt:
email_is_required: "E-mail é obrigatório." email_is_required: "E-mail é obrigatório."
your_password: "Sua senha" your_password: "Sua senha"
password_is_required: "Senha é obrigatório." password_is_required: "Senha é obrigatório."
password_is_too_short: "Password is too short (minimum 12 characters)" password_is_too_short: "Senha muito curta (mínimo 12 caracteres)"
password_is_too_weak: "Password is too weak:" password_is_too_weak: "A senha é muito fraca:"
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_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" type_your_password_again: "Digite sua senha novamente"
password_confirmation_is_required: "Confirmação de senha é obrigatório." 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." 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_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" 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" 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 #password modification modal
change_your_password: "Mudar sua senha" change_your_password: "Mudar sua senha"
your_new_password: "Sua nova senha" your_new_password: "Sua nova senha"
@ -119,7 +119,7 @@ pt:
#confirmation modal #confirmation modal
you_will_receive_confirmation_instructions_by_email: "Você receberá instruções de confirmação por e-mail." you_will_receive_confirmation_instructions_by_email: "Você receberá instruções de confirmação por e-mail."
#forgotten password modal #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 #Fab-manager's version
version: "Versão:" version: "Versão:"
upgrade_fabmanager: "Atualizar Fab-manager" 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_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}" 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}" 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?" rmagick_processing_error: "Falha ao manipular com rmagick, talvez não seja uma imagem?"
mime_types_processing_error: "Failed to process file with MIME::Types, maybe not valid content-type?" mime_types_processing_error: "Falha ao processar arquivo com MIME::Types, talvez tipo de conteúdo inválido?"
mini_magick_processing_error: "Failed to manipulate the file, maybe it is not an image?" mini_magick_processing_error: "Falha ao manipular o arquivo, talvez não seja uma imagem?"
wrong_size: "é o tamanho errado (deveria ser %{file_size})" wrong_size: "é o tamanho errado (deveria ser %{file_size})"
size_too_small: "é muito pequeno (deve ser pelo menos %{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})" 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, 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`. 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. 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. 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. 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. 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> <a name="SUMMERNOTE_LOCALE"></a>
SUMMERNOTE_LOCALE SUMMERNOTE_LOCALE
@ -211,7 +211,7 @@ Default is **en** (even if it's not listed).
Configure the javascript summernote editor for l10n. 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. 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> <a name="ANGULAR_LOCALE"></a>
ANGULAR_LOCALE 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._ _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> <a name="FULLCALENDAR_LOCALE"></a>
FULLCALENDAR_LOCALE FULLCALENDAR_LOCALE
Configure the fullCalendar JS agenda library. 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> <a name="INTL_LOCALE"></a>
INTL_LOCALE INTL_LOCALE
@ -256,7 +256,7 @@ Available values: `danish, dutch, english, finnish, french, german, hungarian, i
TIME_ZONE 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. 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> <a name="WEEK_STARTING_DAY"></a>
WEEK_STARTING_DAY 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 #### System requirements
##### Memory ##### 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. 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. 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. 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 #### 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. 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). They can be easily installed using the [`prepare-vps.sleede.com` script below](#prepare-the-server).
<a name="setup-the-domain-name"></a> <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 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> <a name="useful-commands"></a>
## Useful commands ## 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. 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> <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. 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. 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 ```bash
\curl -sSL upgrade.fab.mn | bash -s -- -e "VAR=value" -p "rails fablab:do:things" -c "rails fablab:setup:command" \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> <a name="update-manually"></a>
### Update manually ### Update manually
@ -190,7 +192,7 @@ Then, you'll need to perform the upgrade with the following command:
`cd /apps/fabmanager` `cd /apps/fabmanager`
2. Pull last docker images 2. Pull last docker images
`docker compose pull` `docker compose pull`
@ -208,9 +210,9 @@ Then, you'll need to perform the upgrade with the following command:
6. Run specific commands 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]**. 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. 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`. 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. 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> <a name="upgrade-to-the-last-version"></a>
### Upgrade to the last version ### Upgrade to the last version
It's the default behaviour as `docker compose pull` command will fetch the latest versions of the docker images. 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. 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. __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> <a name="upgrade-to-a-specific-version"></a>
@ -252,4 +254,4 @@ For example, here we want to use the v3.1.2:
```yaml ```yaml
image: sleede/fab-manager:release-v3.1.2 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", "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.", "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": [ "keywords": [
"fablab", "fablab",

View File

@ -198,7 +198,7 @@ prepare_files()
if [[ "$confirm" = "n" ]]; then exit 1; fi if [[ "$confirm" = "n" ]]; then exit 1; fi
elevate_cmd mkdir -p "$FABMANAGER_PATH" 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 # 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 \ 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") var_doc=$(get_md_anchor "$doc" "$variable")
current=$(grep "$variable=" "$FABMANAGER_PATH/config/env") current=$(grep "$variable=" "$FABMANAGER_PATH/config/env")
echo "$var_doc" | bat --file-name "$variable" --language md --color=always 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 read -rep " > " value </dev/tty
if [ "$value" != "" ]; then if [ "$value" != "" ]; then
esc_val=$(printf '%s\n' "$value" | sed -e 's/\//\\\//g') esc_val=$(printf '%s\n' "$value" | sed -e 's/\//\\\//g')
@ -412,8 +412,12 @@ read_password()
local password confirmation local password confirmation
>&2 echo "Please input a password for this administrator's account" >&2 echo "Please input a password for this administrator's account"
read -rsp " > " password </dev/tty read -rsp " > " password </dev/tty
if [ ${#password} -lt 8 ]; then if [ ${#password} -lt 12 ]; then
>&2 printf "\nError: password is too short (minimal length: 8 characters)\n" >&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') password=$(read_password 'no-confirm')
fi fi
if [ "$1" != 'no-confirm' ]; then 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