mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-21 15:54:22 +01:00
Merge branch 'dev' for release 5.4.21
This commit is contained in:
commit
1533adeb2d
@ -1,5 +1,11 @@
|
|||||||
# Changelog Fab-manager
|
# 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
|
## 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
|
- 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
|
end
|
||||||
|
|
||||||
def last_subscribed
|
def last_subscribed
|
||||||
@query = User.active.with_role(:member)
|
@query, @members = Members::MembersService.last_registered(params[:last])
|
||||||
.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?)
|
|
||||||
|
|
||||||
@requested_attributes = ['profile']
|
@requested_attributes = ['profile']
|
||||||
render :index
|
render :index
|
||||||
@ -74,9 +66,7 @@ class API::MembersController < API::ApiController
|
|||||||
def export_subscriptions
|
def export_subscriptions
|
||||||
authorize :export
|
authorize :export
|
||||||
|
|
||||||
export = Export.where(category: 'users', export_type: 'subscriptions')
|
export = ExportService.last_export('users/subscription')
|
||||||
.where('created_at > ?', Subscription.maximum('updated_at'))
|
|
||||||
.last
|
|
||||||
if export.nil? || !FileTest.exist?(export.file)
|
if export.nil? || !FileTest.exist?(export.file)
|
||||||
@export = Export.new(category: 'users', export_type: 'subscriptions', user: current_user)
|
@export = Export.new(category: 'users', export_type: 'subscriptions', user: current_user)
|
||||||
if @export.save
|
if @export.save
|
||||||
@ -85,7 +75,7 @@ class API::MembersController < API::ApiController
|
|||||||
render json: @export.errors, status: :unprocessable_entity
|
render json: @export.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
send_file File.join(Rails.root, export.file),
|
send_file Rails.root.join(export.file),
|
||||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
disposition: 'attachment'
|
disposition: 'attachment'
|
||||||
end
|
end
|
||||||
@ -95,9 +85,7 @@ class API::MembersController < API::ApiController
|
|||||||
def export_reservations
|
def export_reservations
|
||||||
authorize :export
|
authorize :export
|
||||||
|
|
||||||
export = Export.where(category: 'users', export_type: 'reservations')
|
export = ExportService.last_export('users/reservations')
|
||||||
.where('created_at > ?', Reservation.maximum('updated_at'))
|
|
||||||
.last
|
|
||||||
if export.nil? || !FileTest.exist?(export.file)
|
if export.nil? || !FileTest.exist?(export.file)
|
||||||
@export = Export.new(category: 'users', export_type: 'reservations', user: current_user)
|
@export = Export.new(category: 'users', export_type: 'reservations', user: current_user)
|
||||||
if @export.save
|
if @export.save
|
||||||
@ -106,7 +94,7 @@ class API::MembersController < API::ApiController
|
|||||||
render json: @export.errors, status: :unprocessable_entity
|
render json: @export.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
send_file File.join(Rails.root, export.file),
|
send_file Rails.root.join(export.file),
|
||||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
disposition: 'attachment'
|
disposition: 'attachment'
|
||||||
end
|
end
|
||||||
@ -115,17 +103,7 @@ class API::MembersController < API::ApiController
|
|||||||
def export_members
|
def export_members
|
||||||
authorize :export
|
authorize :export
|
||||||
|
|
||||||
last_update = [
|
export = ExportService.last_export('users/members')
|
||||||
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
|
|
||||||
if export.nil? || !FileTest.exist?(export.file)
|
if export.nil? || !FileTest.exist?(export.file)
|
||||||
@export = Export.new(category: 'users', export_type: 'members', user: current_user)
|
@export = Export.new(category: 'users', export_type: 'members', user: current_user)
|
||||||
if @export.save
|
if @export.save
|
||||||
@ -134,7 +112,7 @@ class API::MembersController < API::ApiController
|
|||||||
render json: @export.errors, status: :unprocessable_entity
|
render json: @export.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
send_file File.join(Rails.root, export.file),
|
send_file Rails.root.join(export.file),
|
||||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
disposition: 'attachment'
|
disposition: 'attachment'
|
||||||
end
|
end
|
||||||
@ -158,8 +136,8 @@ class API::MembersController < API::ApiController
|
|||||||
else
|
else
|
||||||
render json: @member.errors, status: :unprocessable_entity
|
render json: @member.errors, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
rescue DuplicateIndexError => error
|
rescue DuplicateIndexError => e
|
||||||
render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: error.message) },
|
render json: { error: t('members.please_input_the_authentication_code_sent_to_the_address', EMAIL: e.message) },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@ -176,7 +154,6 @@ class API::MembersController < API::ApiController
|
|||||||
query = Members::ListService.list(query_params)
|
query = Members::ListService.list(query_params)
|
||||||
@max_members = query.except(:offset, :limit, :order).count
|
@max_members = query.except(:offset, :limit, :order).count
|
||||||
@members = query.to_a
|
@members = query.to_a
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
@ -196,7 +173,7 @@ class API::MembersController < API::ApiController
|
|||||||
render json: { tours: [params[:tour]] }
|
render json: { tours: [params[:tour]] }
|
||||||
else
|
else
|
||||||
tours = "#{@member.profile.tours} #{params[:tour]}"
|
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 }
|
render json: { tours: @member.profile.tours.split }
|
||||||
end
|
end
|
||||||
@ -205,31 +182,8 @@ class API::MembersController < API::ApiController
|
|||||||
def update_role
|
def update_role
|
||||||
authorize @member
|
authorize @member
|
||||||
|
|
||||||
# we do not allow dismissing a user to a lower role
|
service = Members::MembersService.new(@member)
|
||||||
if params[:role] == 'member'
|
service.update_role(params[:role], params[:group_id])
|
||||||
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 }
|
|
||||||
|
|
||||||
render json: @member
|
render json: @member
|
||||||
end
|
end
|
||||||
@ -265,12 +219,14 @@ class API::MembersController < API::ApiController
|
|||||||
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
|
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
|
||||||
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
||||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||||
user_avatar_attributes: %i[id attachment destroy]],
|
{ user_avatar_attributes: %i[id attachment destroy] }],
|
||||||
invoicing_profile_attributes: [
|
invoicing_profile_attributes: [
|
||||||
:id, :organization,
|
:id, :organization,
|
||||||
address_attributes: %i[id address],
|
{
|
||||||
organization_attributes: [:id, :name, address_attributes: %i[id address]],
|
address_attributes: %i[id address],
|
||||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
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])
|
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,
|
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
|
||||||
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
|
||||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||||
user_avatar_attributes: %i[id attachment destroy]],
|
{ user_avatar_attributes: %i[id attachment destroy] }],
|
||||||
invoicing_profile_attributes: [
|
invoicing_profile_attributes: [
|
||||||
:id, :organization,
|
:id, :organization,
|
||||||
address_attributes: %i[id address],
|
{
|
||||||
organization_attributes: [:id, :name, address_attributes: %i[id address]],
|
address_attributes: %i[id address],
|
||||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
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
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import apiClient from './clients/api-client';
|
import apiClient from './clients/api-client';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { serialize } from 'object-to-formdata';
|
import { serialize } from 'object-to-formdata';
|
||||||
import { User, UserIndexFilter } from '../models/user';
|
import { User, UserIndexFilter, UserRole } from '../models/user';
|
||||||
|
|
||||||
export default class MemberAPI {
|
export default class MemberAPI {
|
||||||
static async list (filters: UserIndexFilter): Promise<Array<User>> {
|
static async list (filters: UserIndexFilter): Promise<Array<User>> {
|
||||||
@ -35,6 +35,11 @@ export default class MemberAPI {
|
|||||||
return res?.data;
|
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> {
|
static async current (): Promise<User> {
|
||||||
const res: AxiosResponse<User> = await apiClient.get('/api/members/current');
|
const res: AxiosResponse<User> = await apiClient.get('/api/members/current');
|
||||||
return res?.data;
|
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
|
// modal dialog to take a new subscription
|
||||||
$scope.isOpenSubscribeModal = false;
|
$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
|
* Open a modal dialog asking for confirmation to change the role of the given user
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
@ -800,6 +803,17 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
|||||||
$scope.$apply();
|
$scope.$apply();
|
||||||
}, 50);
|
}, 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
|
* Callback triggered if the subscription was successfully extended
|
||||||
*/
|
*/
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
@import "modules/form/abstract-form-item";
|
@import "modules/form/abstract-form-item";
|
||||||
@import "modules/form/form-input";
|
@import "modules/form/form-input";
|
||||||
@import "modules/form/form-rich-text";
|
@import "modules/form/form-rich-text";
|
||||||
|
@import "modules/form/form-select";
|
||||||
@import "modules/form/form-switch";
|
@import "modules/form/form-switch";
|
||||||
@import "modules/group/change-group";
|
@import "modules/group/change-group";
|
||||||
@import "modules/machines/machine-card";
|
@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">
|
<div class="col-md-3">
|
||||||
<section class="heading-actions wrapper">
|
<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>
|
<img src="/rank-icon.svg" alt="role icon" /><span class="m-l" translate>{{ 'app.admin.members_edit.change_role' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<change-role-modal is-open="isOpenChangeRoleModal" toggle-modal="toggleChangeRoleModal" user="user" on-success="onSuccess" onError="onError" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</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
|
params
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def notify_user_profile_complete(previous_state)
|
def notify_user_profile_complete(previous_state)
|
||||||
|
@ -12,5 +12,15 @@ class Statistics::BuilderService
|
|||||||
Statistics::Builders::MembersBuilderService.build(options)
|
Statistics::Builders::MembersBuilderService.build(options)
|
||||||
Statistics::Builders::ProjectsBuilderService.build(options)
|
Statistics::Builders::ProjectsBuilderService.build(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def default_options
|
||||||
|
yesterday = 1.day.ago
|
||||||
|
{
|
||||||
|
start_date: yesterday.beginning_of_day,
|
||||||
|
end_date: yesterday.end_of_day
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -974,15 +974,20 @@ en:
|
|||||||
to_complete: "To complete"
|
to_complete: "To complete"
|
||||||
refuse_documents: "Refusing the documents"
|
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."
|
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
|
change_role_modal:
|
||||||
members_edit:
|
|
||||||
change_role: "Change role"
|
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"
|
admin: "Administrator"
|
||||||
manager: "Manager"
|
manager: "Manager"
|
||||||
member: "Member"
|
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}."
|
role_changed: "Role successfully changed from {OLD} to {NEW}."
|
||||||
error_while_changing_role: "An error occurred while changing the role. Please try again later."
|
error_while_changing_role: "An error occurred while changing the role. Please try again later."
|
||||||
|
#edit a member
|
||||||
|
members_edit:
|
||||||
subscription: "Subscription"
|
subscription: "Subscription"
|
||||||
duration: "Duration:"
|
duration: "Duration:"
|
||||||
expires_at: "Expires at:"
|
expires_at: "Expires at:"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fab-manager",
|
"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.",
|
"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",
|
||||||
|
@ -9,6 +9,10 @@ class StatisticServiceTest < ActionDispatch::IntegrationTest
|
|||||||
login_as(@admin, scope: :user)
|
login_as(@admin, scope: :user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'build default stats' do
|
||||||
|
::Statistics::BuilderService.generate_statistic
|
||||||
|
end
|
||||||
|
|
||||||
test 'build stats' do
|
test 'build stats' do
|
||||||
# Create a reservation to generate an invoice
|
# Create a reservation to generate an invoice
|
||||||
machine = Machine.find(1)
|
machine = Machine.find(1)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user