mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
Merge branch 'dev' for release 5.4.21
This commit is contained in:
commit
1533adeb2d
@ -1,5 +1,11 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## v5.4.21 2022 October 05
|
||||
|
||||
- Ability to dismiss a user to a lower privileged role
|
||||
- Fix a bug: unable to generate statistics
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:regenerate_statistics[2022,08]`
|
||||
|
||||
## v5.4.20 2022 September 27
|
||||
|
||||
- Fix a bug: unable to show the daily view of the public agenda, if it contains trainings or events
|
||||
|
@ -18,15 +18,7 @@ class API::MembersController < API::ApiController
|
||||
end
|
||||
|
||||
def last_subscribed
|
||||
@query = User.active.with_role(:member)
|
||||
.includes(:statistic_profile, profile: [:user_avatar])
|
||||
.where('is_allow_contact = true AND confirmed_at IS NOT NULL')
|
||||
.order('created_at desc')
|
||||
.limit(params[:last])
|
||||
|
||||
# remove unmerged profiles from list
|
||||
@members = @query.to_a
|
||||
@members.delete_if(&:need_completion?)
|
||||
@query, @members = Members::MembersService.last_registered(params[:last])
|
||||
|
||||
@requested_attributes = ['profile']
|
||||
render :index
|
||||
@ -74,9 +66,7 @@ class API::MembersController < API::ApiController
|
||||
def export_subscriptions
|
||||
authorize :export
|
||||
|
||||
export = Export.where(category: 'users', export_type: 'subscriptions')
|
||||
.where('created_at > ?', Subscription.maximum('updated_at'))
|
||||
.last
|
||||
export = ExportService.last_export('users/subscription')
|
||||
if export.nil? || !FileTest.exist?(export.file)
|
||||
@export = Export.new(category: 'users', export_type: 'subscriptions', user: current_user)
|
||||
if @export.save
|
||||
@ -85,7 +75,7 @@ class API::MembersController < API::ApiController
|
||||
render json: @export.errors, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
send_file File.join(Rails.root, export.file),
|
||||
send_file Rails.root.join(export.file),
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
disposition: 'attachment'
|
||||
end
|
||||
@ -95,9 +85,7 @@ class API::MembersController < API::ApiController
|
||||
def export_reservations
|
||||
authorize :export
|
||||
|
||||
export = Export.where(category: 'users', export_type: 'reservations')
|
||||
.where('created_at > ?', Reservation.maximum('updated_at'))
|
||||
.last
|
||||
export = ExportService.last_export('users/reservations')
|
||||
if export.nil? || !FileTest.exist?(export.file)
|
||||
@export = Export.new(category: 'users', export_type: 'reservations', user: current_user)
|
||||
if @export.save
|
||||
@ -106,7 +94,7 @@ class API::MembersController < API::ApiController
|
||||
render json: @export.errors, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
send_file File.join(Rails.root, export.file),
|
||||
send_file Rails.root.join(export.file),
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
disposition: 'attachment'
|
||||
end
|
||||
@ -115,17 +103,7 @@ class API::MembersController < API::ApiController
|
||||
def export_members
|
||||
authorize :export
|
||||
|
||||
last_update = [
|
||||
User.members.maximum('updated_at'),
|
||||
Profile.where(user_id: User.members).maximum('updated_at'),
|
||||
InvoicingProfile.where(user_id: User.members).maximum('updated_at'),
|
||||
StatisticProfile.where(user_id: User.members).maximum('updated_at'),
|
||||
Subscription.maximum('updated_at') || DateTime.current
|
||||
].max
|
||||
|
||||
export = Export.where(category: 'users', export_type: 'members')
|
||||
.where('created_at > ?', last_update)
|
||||
.last
|
||||
export = ExportService.last_export('users/members')
|
||||
if export.nil? || !FileTest.exist?(export.file)
|
||||
@export = Export.new(category: 'users', export_type: 'members', user: current_user)
|
||||
if @export.save
|
||||
@ -134,7 +112,7 @@ class API::MembersController < API::ApiController
|
||||
render json: @export.errors, status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
send_file File.join(Rails.root, export.file),
|
||||
send_file Rails.root.join(export.file),
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
disposition: 'attachment'
|
||||
end
|
||||
@ -158,8 +136,8 @@ class API::MembersController < API::ApiController
|
||||
else
|
||||
render json: @member.errors, status: :unprocessable_entity
|
||||
end
|
||||
rescue DuplicateIndexError => error
|
||||
render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: error.message) },
|
||||
rescue DuplicateIndexError => e
|
||||
render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: e.message) },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
else
|
||||
@ -176,7 +154,6 @@ class API::MembersController < API::ApiController
|
||||
query = Members::ListService.list(query_params)
|
||||
@max_members = query.except(:offset, :limit, :order).count
|
||||
@members = query.to_a
|
||||
|
||||
end
|
||||
|
||||
def search
|
||||
@ -196,7 +173,7 @@ class API::MembersController < API::ApiController
|
||||
render json: { tours: [params[:tour]] }
|
||||
else
|
||||
tours = "#{@member.profile.tours} #{params[:tour]}"
|
||||
@member.profile.update_attributes(tours: tours.strip)
|
||||
@member.profile.update(tours: tours.strip)
|
||||
|
||||
render json: { tours: @member.profile.tours.split }
|
||||
end
|
||||
@ -205,31 +182,8 @@ class API::MembersController < API::ApiController
|
||||
def update_role
|
||||
authorize @member
|
||||
|
||||
# we do not allow dismissing a user to a lower role
|
||||
if params[:role] == 'member'
|
||||
render 403 and return if @member.role == 'admin' || @member.role == 'manager'
|
||||
elsif params[:role] == 'manager'
|
||||
render 403 and return if @member.role == 'admin'
|
||||
end
|
||||
|
||||
# do nothing if the role does not change
|
||||
render json: @member and return if params[:role] == @member.role
|
||||
|
||||
ex_role = @member.role.to_sym
|
||||
@member.remove_role ex_role
|
||||
@member.add_role params[:role]
|
||||
|
||||
# if the new role is 'admin', then change the group to the admins group
|
||||
@member.update_attributes(group_id: Group.find_by(slug: 'admins').id) if params[:role] == 'admin'
|
||||
|
||||
NotificationCenter.call type: 'notify_user_role_update',
|
||||
receiver: @member,
|
||||
attached_object: @member
|
||||
|
||||
NotificationCenter.call type: 'notify_admins_role_update',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: @member,
|
||||
meta_data: { ex_role: ex_role }
|
||||
service = Members::MembersService.new(@member)
|
||||
service.update_role(params[:role], params[:group_id])
|
||||
|
||||
render json: @member
|
||||
end
|
||||
@ -265,12 +219,14 @@ class API::MembersController < API::ApiController
|
||||
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
|
||||
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||
user_avatar_attributes: %i[id attachment destroy]],
|
||||
{ user_avatar_attributes: %i[id attachment destroy] }],
|
||||
invoicing_profile_attributes: [
|
||||
:id, :organization,
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, address_attributes: %i[id address]],
|
||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
||||
{
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, { address_attributes: %i[id address] }],
|
||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
||||
}
|
||||
],
|
||||
statistic_profile_attributes: %i[id gender birthday])
|
||||
|
||||
@ -280,14 +236,16 @@ class API::MembersController < API::ApiController
|
||||
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
|
||||
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||
user_avatar_attributes: %i[id attachment destroy]],
|
||||
{ user_avatar_attributes: %i[id attachment destroy] }],
|
||||
invoicing_profile_attributes: [
|
||||
:id, :organization,
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, address_attributes: %i[id address]],
|
||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
||||
{
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, { address_attributes: %i[id address] }],
|
||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
||||
}
|
||||
],
|
||||
statistic_profile_attributes: [:id, :gender, :birthday, training_ids: []])
|
||||
statistic_profile_attributes: [:id, :gender, :birthday, { training_ids: [] }])
|
||||
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { serialize } from 'object-to-formdata';
|
||||
import { User, UserIndexFilter } from '../models/user';
|
||||
import { User, UserIndexFilter, UserRole } from '../models/user';
|
||||
|
||||
export default class MemberAPI {
|
||||
static async list (filters: UserIndexFilter): Promise<Array<User>> {
|
||||
@ -35,6 +35,11 @@ export default class MemberAPI {
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async updateRole (user: User, role: UserRole, groupId?: number): Promise<User> {
|
||||
const res: AxiosResponse<User> = await apiClient.patch(`/api/members/${user.id}/update_role`, { role, group_id: groupId });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async current (): Promise<User> {
|
||||
const res: AxiosResponse<User> = await apiClient.get('/api/members/current');
|
||||
return res?.data;
|
||||
|
@ -0,0 +1,129 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HtmlTranslate } from '../base/html-translate';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MemberAPI from '../../api/member';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { Group } from '../../models/group';
|
||||
import GroupAPI from '../../api/group';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface ChangeRoleModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
user: User,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
interface RoleFormData {
|
||||
role: UserRole,
|
||||
groupId?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Option format, expected by react-select
|
||||
* @see https://github.com/JedWatson/react-select
|
||||
*/
|
||||
type selectRoleOption = { value: UserRole, label: string, isDisabled: boolean };
|
||||
type selectGroupOption = { value: number, label: string };
|
||||
|
||||
/**
|
||||
* This modal dialog allows to change the current role of the given user
|
||||
*/
|
||||
export const ChangeRoleModal: React.FC<ChangeRoleModalProps> = ({ isOpen, toggleModal, user, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
const { control, handleSubmit } = useForm<RoleFormData>({ defaultValues: { groupId: user.group_id } });
|
||||
|
||||
const [groups, setGroups] = useState<Array<Group>>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<UserRole>(user.role);
|
||||
|
||||
useEffect(() => {
|
||||
GroupAPI.index({ disabled: false, admins: false }).then(setGroups).catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle the form submission: update the role on the API
|
||||
*/
|
||||
const onSubmit = (data: RoleFormData) => {
|
||||
MemberAPI.updateRole(user, data.role, data.groupId).then(res => {
|
||||
onSuccess(
|
||||
t(
|
||||
'app.admin.change_role_modal.role_changed',
|
||||
{ OLD: t(`app.admin.change_role_modal.${user.role}`), NEW: t(`app.admin.change_role_modal.${res.role}`) }
|
||||
)
|
||||
);
|
||||
toggleModal();
|
||||
}).catch(err => onError(t('app.admin.change_role_modal.error_while_changing_role') + err));
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user changes the selected role in the dropdown selection list
|
||||
*/
|
||||
const onRoleSelect = (data: UserRole) => {
|
||||
setSelectedRole(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the various available roles for the select input
|
||||
*/
|
||||
const buildRolesOptions = (): Array<selectRoleOption> => {
|
||||
return [
|
||||
{ value: 'admin' as UserRole, label: t('app.admin.change_role_modal.admin'), isDisabled: user.role === 'admin' },
|
||||
{ value: 'manager' as UserRole, label: t('app.admin.change_role_modal.manager'), isDisabled: user.role === 'manager' },
|
||||
{ value: 'member' as UserRole, label: t('app.admin.change_role_modal.member'), isDisabled: user.role === 'member' }
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the various available groups for the select input
|
||||
*/
|
||||
const buildGroupsOptions = (): Array<selectGroupOption> => {
|
||||
return groups.map(group => {
|
||||
return { value: group.id, label: group.name };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.change_role_modal.change_role')}
|
||||
width={ModalSize.medium}
|
||||
onConfirmSendFormId="user-role-form"
|
||||
confirmButton={t('app.admin.change_role_modal.confirm')}
|
||||
closeButton>
|
||||
<HtmlTranslate trKey={'app.admin.change_role_modal.warning_role_change'} />
|
||||
<form onSubmit={handleSubmit(onSubmit)} id="user-role-form">
|
||||
<FormSelect options={buildRolesOptions()}
|
||||
control={control}
|
||||
id="role"
|
||||
label={t('app.admin.change_role_modal.new_role')}
|
||||
rules={{ required: true }}
|
||||
onChange={onRoleSelect} />
|
||||
{selectedRole !== 'admin' &&
|
||||
<FormSelect options={buildGroupsOptions()}
|
||||
control={control}
|
||||
id="groupId"
|
||||
label={t('app.admin.change_role_modal.new_group')}
|
||||
tooltip={t('app.admin.change_role_modal.new_group_help')}
|
||||
rules={{ required: true }} />}
|
||||
</form>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangeRoleModalWrapper: React.FC<ChangeRoleModalProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<ChangeRoleModal {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('changeRoleModal', react2angular(ChangeRoleModalWrapper, ['isOpen', 'toggleModal', 'user', 'onError', 'onSuccess']));
|
@ -724,6 +724,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
// modal dialog to take a new subscription
|
||||
$scope.isOpenSubscribeModal = false;
|
||||
|
||||
// modal dialog to change the user's role
|
||||
$scope.isOpenChangeRoleModal = false;
|
||||
|
||||
/**
|
||||
* Open a modal dialog asking for confirmation to change the role of the given user
|
||||
* @returns {*}
|
||||
@ -800,6 +803,17 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
$scope.$apply();
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens/closes the modal dialog to change the user's role
|
||||
*/
|
||||
$scope.toggleChangeRoleModal = () => {
|
||||
setTimeout(() => {
|
||||
$scope.isOpenChangeRoleModal = !$scope.isOpenChangeRoleModal;
|
||||
$scope.$apply();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered if the subscription was successfully extended
|
||||
*/
|
||||
|
@ -37,6 +37,7 @@
|
||||
@import "modules/form/abstract-form-item";
|
||||
@import "modules/form/form-input";
|
||||
@import "modules/form/form-rich-text";
|
||||
@import "modules/form/form-select";
|
||||
@import "modules/form/form-switch";
|
||||
@import "modules/group/change-group";
|
||||
@import "modules/machines/machine-card";
|
||||
|
@ -0,0 +1,9 @@
|
||||
.form-select {
|
||||
.rs__menu .rs__menu-list {
|
||||
.rs__option {
|
||||
&--is-disabled {
|
||||
color: var(--gray-hard-lightest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -21,9 +21,10 @@
|
||||
|
||||
<div class="col-md-3">
|
||||
<section class="heading-actions wrapper">
|
||||
<div class="btn btn-lg btn-block btn-default promote-member m-t-xs" ng-click="changeUserRole()" ng-show="isAuthorized('admin')">
|
||||
<div class="btn btn-lg btn-block btn-default promote-member m-t-xs" ng-click="toggleChangeRoleModal()" ng-show="isAuthorized('admin')">
|
||||
<img src="/rank-icon.svg" alt="role icon" /><span class="m-l" translate>{{ 'app.admin.members_edit.change_role' }}</span>
|
||||
</div>
|
||||
<change-role-modal is-open="isOpenChangeRoleModal" toggle-modal="toggleChangeRoleModal" user="user" on-success="onSuccess" onError="onError" />
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
48
app/services/export_service.rb
Normal file
48
app/services/export_service.rb
Normal file
@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Provides helper methods for Exports resources and properties
|
||||
class ExportService
|
||||
class << self
|
||||
# Check if the last export of the provided type is still accurate or if it must be regenerated
|
||||
def last_export(type)
|
||||
case type
|
||||
when 'users/members'
|
||||
last_export_members
|
||||
when 'users/reservations'
|
||||
last_export_reservations
|
||||
when 'users/subscription'
|
||||
last_export_subscriptions
|
||||
else
|
||||
raise TypeError "unknown export type: #{type}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def last_export_subscriptions
|
||||
Export.where(category: 'users', export_type: 'subscriptions')
|
||||
.where('created_at > ?', Subscription.maximum('updated_at'))
|
||||
.last
|
||||
end
|
||||
|
||||
def last_export_reservations
|
||||
Export.where(category: 'users', export_type: 'reservations')
|
||||
.where('created_at > ?', Reservation.maximum('updated_at'))
|
||||
.last
|
||||
end
|
||||
|
||||
def last_export_members
|
||||
last_update = [
|
||||
User.members.maximum('updated_at'),
|
||||
Profile.where(user_id: User.members).maximum('updated_at'),
|
||||
InvoicingProfile.where(user_id: User.members).maximum('updated_at'),
|
||||
StatisticProfile.where(user_id: User.members).maximum('updated_at'),
|
||||
Subscription.maximum('updated_at') || DateTime.current
|
||||
].max
|
||||
|
||||
Export.where(category: 'users', export_type: 'members')
|
||||
.where('created_at > ?', last_update)
|
||||
.last
|
||||
end
|
||||
end
|
||||
end
|
@ -107,6 +107,44 @@ class Members::MembersService
|
||||
params
|
||||
end
|
||||
|
||||
def self.last_registered(limit)
|
||||
query = User.active.with_role(:member)
|
||||
.includes(:statistic_profile, profile: [:user_avatar])
|
||||
.where('is_allow_contact = true AND confirmed_at IS NOT NULL')
|
||||
.order('created_at desc')
|
||||
.limit(limit)
|
||||
|
||||
# remove unmerged profiles from list
|
||||
members = query.to_a
|
||||
members.delete_if(&:need_completion?)
|
||||
|
||||
[query, members]
|
||||
end
|
||||
|
||||
def update_role(new_role, new_group_id = Group.first.id)
|
||||
# do nothing if the role does not change
|
||||
return if new_role == @member.role
|
||||
|
||||
# update role
|
||||
ex_role = @member.role.to_sym
|
||||
@member.remove_role ex_role
|
||||
@member.add_role new_role
|
||||
|
||||
# if the new role is 'admin', then change the group to the admins group, otherwise to change to the provided group
|
||||
group_id = new_role == 'admin' ? Group.find_by(slug: 'admins').id : new_group_id
|
||||
@member.update(group_id: group_id)
|
||||
|
||||
# notify
|
||||
NotificationCenter.call type: 'notify_user_role_update',
|
||||
receiver: @member,
|
||||
attached_object: @member
|
||||
|
||||
NotificationCenter.call type: 'notify_admins_role_update',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: @member,
|
||||
meta_data: { ex_role: ex_role }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_user_profile_complete(previous_state)
|
||||
|
@ -12,5 +12,15 @@ class Statistics::BuilderService
|
||||
Statistics::Builders::MembersBuilderService.build(options)
|
||||
Statistics::Builders::ProjectsBuilderService.build(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_options
|
||||
yesterday = 1.day.ago
|
||||
{
|
||||
start_date: yesterday.beginning_of_day,
|
||||
end_date: yesterday.end_of_day
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -974,15 +974,20 @@ en:
|
||||
to_complete: "To complete"
|
||||
refuse_documents: "Refusing the documents"
|
||||
refuse_documents_info: "After verification, you may notify the member that the evidence submitted is not acceptable. You can specify the reasons for your refusal and indicate the actions to be taken. The member will be notified by e-mail."
|
||||
#edit a member
|
||||
members_edit:
|
||||
change_role_modal:
|
||||
change_role: "Change role"
|
||||
warning_role_change: "<p><strong>Warning:</strong> changing the role of a user is not a harmless operation. Is not currently possible to dismiss a user to a lower privileged role.</p><ul><li><strong>Members</strong> can only book reservations for themselves, paying by card or wallet.</li><li><strong>Managers</strong> can book reservations for themselves, paying by card or wallet, and for other members and managers, by collecting payments at the checkout.</li><li><strong>Administrators</strong> can only book reservations for members and managers, by collecting payments at the checkout. Moreover, they can change every settings of the application.</li></ul>"
|
||||
warning_role_change: "<p><strong>Warning:</strong> changing the role of a user is not a harmless operation.</p><ul><li><strong>Members</strong> can only book reservations for themselves, paying by card or wallet.</li><li><strong>Managers</strong> can book reservations for themselves, paying by card or wallet, and for other members and managers, by collecting payments at the checkout.</li><li><strong>Administrators</strong> can only book reservations for members and managers, by collecting payments at the checkout. Moreover, they can change every settings of the application.</li></ul>"
|
||||
new_role: "New role"
|
||||
admin: "Administrator"
|
||||
manager: "Manager"
|
||||
member: "Member"
|
||||
new_group: "New group"
|
||||
new_group_help: "Members and managers must be placed in a group."
|
||||
confirm: "Change role"
|
||||
role_changed: "Role successfully changed from {OLD} to {NEW}."
|
||||
error_while_changing_role: "An error occurred while changing the role. Please try again later."
|
||||
#edit a member
|
||||
members_edit:
|
||||
subscription: "Subscription"
|
||||
duration: "Duration:"
|
||||
expires_at: "Expires at:"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fab-manager",
|
||||
"version": "5.4.20",
|
||||
"version": "5.4.21",
|
||||
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
|
||||
"keywords": [
|
||||
"fablab",
|
||||
|
@ -9,6 +9,10 @@ class StatisticServiceTest < ActionDispatch::IntegrationTest
|
||||
login_as(@admin, scope: :user)
|
||||
end
|
||||
|
||||
test 'build default stats' do
|
||||
::Statistics::BuilderService.generate_statistic
|
||||
end
|
||||
|
||||
test 'build stats' do
|
||||
# Create a reservation to generate an invoice
|
||||
machine = Machine.find(1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user