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

Merge branch 'role' into dev

This commit is contained in:
Sylvain 2020-05-05 11:36:43 +02:00
commit 53aff97328
22 changed files with 230 additions and 7 deletions

View File

@ -4,6 +4,7 @@
- The invoices list displays the operator in case of offline payment
- Interface to manage partners
- Ability to define, per availability, a custom duration for the reservation slots
- Ability to promote a user to a higher role (member > manager > admin)
- Corrected the documentation about BOOK_SLOT_AT_SAME_TIME
- Auto-adjusts text colors based on the selected theme colors
- Fix a bug: unable to change group if the previous was deactivated

View File

@ -691,6 +691,54 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// current active authentication provider
$scope.activeProvider = activeProviderPromise;
/**
* Open a modal dialog asking for confirmation to change the role of the given user
* @param userId {number} id of the user to "promote"
* @returns {*}
*/
$scope.changeUserRole = function() {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "admin/members/change_role_modal.html" %>',
size: 'lg',
resolve: {
user() { return $scope.user; }
},
controller: ['$scope', '$uibModalInstance', 'Member', 'user', '_t', function ($scope, $uibModalInstance, Member, user, _t) {
$scope.user = user;
$scope.role = user.role;
$scope.roles = [
{ key: 'admin', label: _t('app.admin.members_edit.admin') },
{ key: 'manager', label: _t('app.admin.members_edit.manager'), notAnOption: (user.role === 'admin') },
{ key: 'member', label: _t('app.admin.members_edit.member'), notAnOption: (user.role === 'admin' || user.role === 'manager') },
];
$scope.ok = function () {
Member.updateRole(
{ id: $scope.user.id },
{ role: $scope.role },
function (_res) {
growl.success(_t('app.admin.members_edit.role_changed', { OLD: _t(`app.admin.members_edit.${user.role}`), NEW: _t(`app.admin.members_edit.${$scope.role}`) }));
return $uibModalInstance.close(_res);
},
function (error) {
growl.error(_t('app.admin.members_edit.error_while_changing_role'));
console.error(error);
}
);
};
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
// once the form was validated successfully ...
return modalInstance.result.then(function (user) {
// remove the user for the old list add to the new
});
}
/**
* Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not)
* @param subscription {Object} User's subscription object

View File

@ -44,6 +44,10 @@ Application.Services.factory('Member', ['$resource', '$q', function ($resource,
return response.data;
}
}
},
updateRole: {
method: 'PATCH',
url: '/api/members/:id/update_role'
}
}
);

View File

@ -0,0 +1,4 @@
.promote-member img {
width: 16px;
height: 21px;
}

View File

@ -0,0 +1,12 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate>{{ 'app.admin.members_edit.change_role' }}</h1>
</div>
<div class="modal-body">
<p class="alert alert-warning" translate>{{ 'app.admin.members_edit.warning_role_change' }}</p>
<select ng-model="role" class="form-control" ng-options="role.key as role.label disable when role.notAnOption for role in roles"></select>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -17,8 +17,8 @@
<div class="col-md-3">
<section class="heading-actions wrapper">
<div class="btn btn-lg btn-block btn-default m-t-xs" ng-click="cancel()" translate>
{{ 'app.shared.buttons.cancel' }}
<div class="btn btn-lg btn-block btn-default promote-member m-t-xs" ng-click="changeUserRole()" 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>
</section>

View File

@ -41,9 +41,9 @@
<th style="width:15%"><a href="" ng-click="setOrderMember('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%" class="hidden-xs"><a href="" ng-click="setOrderMember('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:10%" class="hidden-xs hidden-sm hidden-md"><a href="" ng-click="setOrderMember('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:20%" class="hidden-xs hidden-sm"><a href="" ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%" class="hidden-xs hidden-sm"><a href="" ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:15%" class="hidden-xs hidden-sm hidden-md"><a href="" ng-click="setOrderMember('plan')">{{ 'app.admin.members.subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
<th style="width:10%" class="buttons-col"></th>
<th style="width:15%" class="buttons-col"></th>
</tr>
</thead>
<tbody>

View File

@ -3,7 +3,7 @@
# API Controller for resources of type User with role 'member'
class API::MembersController < API::ApiController
before_action :authenticate_user!, except: [:last_subscribed]
before_action :set_member, only: %i[update destroy merge complete_tour]
before_action :set_member, only: %i[update destroy merge complete_tour update_role]
respond_to :json
def index
@ -202,6 +202,35 @@ class API::MembersController < API::ApiController
end
end
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]
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
end
private
def set_member

View File

@ -51,6 +51,8 @@ class NotificationType
notify_privacy_policy_changed
notify_admin_import_complete
notify_admin_refund_created
notify_admins_role_update
notify_user_role_update
]
# deprecated:
# - notify_member_subscribed_plan_is_changed

View File

@ -160,6 +160,8 @@ class User < ApplicationRecord
'admin'
elsif manager?
'manager'
elsif member?
'member'
else
'other'
end

View File

@ -39,7 +39,7 @@ class UserPolicy < ApplicationPolicy
end
end
%w[create mapping].each do |action|
%w[create mapping update_role].each do |action|
define_method "#{action}?" do
user.admin?
end

View File

@ -1,3 +1,3 @@
# frozen_string_literal: true
json.extract! i_cal, :id, :name, :url, :color, :primary_text_color, :text_hidden
json.extract! i_cal, :id, :name, :url, :color, :text_color, :text_hidden

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.user_NAME_changed_ROLE_html',
NAME: notification.attached_object&.profile&.full_name || t('api.notifications.deleted_user'),
ROLE: t("roles.#{notification.attached_object&.role}"))
json.url notification_url(notification, format: :json)

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.your_role_is_ROLE', ROLE: t("roles.#{notification.attached_object&.role}"))
json.url notification_url(notification, format: :json)

View File

@ -0,0 +1,8 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p><%= t('.body.user_role_changed_html', NAME: @attached_object&.profile&.full_name || t('api.notifications.deleted_user')) %></p>
<p>
<small><%= t('.body.previous_role') %> <strong><%= t("roles.#{@notification.get_meta_data(:ex_role)}") %></strong></small>
<br/><%= t('.body.new_role') %> <strong><%= t("roles.#{@attached_object.role}") %></strong>
</p>

View File

@ -0,0 +1,7 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<%
name = Setting.find_by(name: 'fablab_name').value
gender = Setting.find_by(name: 'name_genre').value
%>
<p><%= t('.body.role_changed', NAME: name, GENDER: gender, ROLE: t("roles.#{@attached_object.role}")) %></p>

View File

@ -723,6 +723,13 @@ en:
error_details: "Error's details:"
#edit a member
members_edit:
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>"
admin: "Administrator"
manager: "Manager"
member: "Member"
role_changed: "Role successfully changed from {OLD} to {NEW}."
error_while_changing_role: "An error occurred while changing the role. Please try again later."
subscription: "Subscription"
duration: "Duration:"
expires_at: "Expires at:"

View File

@ -723,6 +723,13 @@ fr:
error_details: "Détails de l'erreur :"
#edit a member
members_edit:
change_role: "Changer de rôle"
warning_role_change: "<p><strong>Attention :</strong> changer le rôle d'un utilisateur n'est pas une opération anodine. Il n'est actuellement pas possible de destituer un utilisateur vers un rôle de moindre privilège.</p><ul><li><strong>Les membres</strong> ne peuvent que prendre des réservations pour eux-même, en payant par carte bancaire ou par porte-monnaie.</li><li><strong>Les gestionnaires</strong> peuvent prendre des réservations pour eux-même, en payant par carte bancaire ou par porte-monnaie, ainsi que pour les autres membres et gestionnaires, en encaissant les paiements à la caisse.</li><li><strong>Les administrateurs</strong> ne peuvent que prendre des réservations pour les membres et gestionnaires, en encaissant les paiements à la caisse. De plus, ils peuvent modifier l'ensemble des paramètres de l'application.</li></ul>"
admin: "Administrateur"
manager: "Gestionnaire"
member: "Membre"
role_changed: "Le rôle de {OLD} à été changé en {NEW}."
error_while_changing_role: "Une erreur est survenue lors du changement de rôle. Merci de réessayer plus tard."
subscription: "Abonnement"
duration: "Durée :"
expires_at: "Expire le :"

View File

@ -214,6 +214,10 @@ en:
event: "Event"
reservations: "Reservations"
available_seats: "Available seats"
roles:
member: "Member"
manager: "Manager"
admin: "Administrator"
api:
#internal app notifications
notifications:
@ -329,6 +333,10 @@ en:
click_to_show: "Click here to consult"
notify_admin_refund_created:
refund_created: "A refund of %{AMOUNT} has been created for user %{USER}"
notify_user_role_update:
your_role_is_ROLE: "Your role has been changed to %{ROLE}."
notify_admins_role_update:
user_NAME_changed_ROLE_html: "User <strong><em>%{NAME}</strong></em> is now %{ROLE}."
#statistics tools for admins
statistics:
subscriptions: "Subscriptions"

View File

@ -273,5 +273,15 @@ en:
body:
refund_created: "A refund of %{AMOUNT} has been generated on invoice %{INVOICE} of user %{USER}"
download: "Click here to download this refund invoice"
notify_admins_role_update:
subject: "The role of a user has changed"
body:
user_role_changed_html: "The role of the user <em><strong>%{NAME}</strong></em> has changed."
previous_role: "Previous role:"
new_role: "New role:"
notify_user_role_update:
subject: "Your role has changed"
body:
role_changed_html: "Your role at {GENDER, select, male{the} female{the} neutral{} other{the}} {NAME} has changed. You are now <strong>{ROLE}</strong>.<br/>With great power comes great responsibility, use your new privileges fairly and respectfully."
shared:
hello: "Hello %{user_name}"

View File

@ -55,6 +55,7 @@ Rails.application.routes.draw do
get 'search/:query', action: 'search', on: :collection
get 'mapping', action: 'mapping', on: :collection
patch ':id/complete_tour', action: 'complete_tour', on: :collection
patch ':id/update_role', action: 'update_role', on: :collection
end
resources :reservations, only: %i[show create index update]
resources :notifications, only: %i[index show update] do

61
public/rank-icon.svg Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
height="74.5"
width="56"
sodipodi:docname="rank-icon.svg"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 56 74.499999"
enable-background="new 0 0 100 100"
xml:space="preserve"
id="svg102"><sodipodi:namedview
inkscape:current-layer="svg102"
inkscape:window-maximized="0"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:cy="60.2"
inkscape:cx="27.8"
inkscape:zoom="6.744"
lock-margins="false"
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
showgrid="false"
id="namedview11"
inkscape:window-height="1007"
inkscape:window-width="1374"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff"
inkscape:document-rotation="0" /><metadata
id="metadata108"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs106" /><g
style="stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none"
transform="translate(-22.2,-2.3)"
id="g92"><path
style="stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 50.2,75.8 c -0.2,0 -0.3,0 -0.5,-0.1 L 23.7,60.8 C 23.4,60.6 23.2,60.3 23.2,59.9 V 45.1 c 0,-0.4 0.2,-0.7 0.5,-0.9 0.3,-0.2 0.7,-0.2 1,0 L 50.2,58.8 75.7,44.2 c 0.3,-0.2 0.7,-0.2 1,0 0.3,0.2 0.5,0.5 0.5,0.9 V 60 c 0,0.4 -0.2,0.7 -0.5,0.9 l -26,14.9 c -0.1,0 -0.3,0 -0.5,0 z m -25,-16.4 25,14.3 25,-14.3 V 46.9 l -24.5,14 c -0.3,0.2 -0.7,0.2 -1,0 l -24.5,-14 z"
id="path90" /></g><g
style="stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="translate(-22.2,-2.3)"
id="g96"><path
style="stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 62.5,45.1 c -0.3,0 -0.7,-0.1 -1,-0.2 L 50.3,39 39,44.8 C 38.1,45.3 37,45 36.4,44.3 36,43.8 35.8,43.2 35.9,42.6 L 38,30.1 29,21.2 c -0.6,-0.6 -0.8,-1.4 -0.5,-2.1 0.3,-0.7 0.9,-1.3 1.7,-1.4 L 42.7,15.9 48.4,4.5 c 0.4,-0.7 1.1,-1.2 1.9,-1.2 0.8,0 1.5,0.4 1.9,1.2 l 5.6,11.3 12.6,1.9 c 0.8,0.1 1.4,0.7 1.7,1.4 0.2,0.8 0,1.6 -0.5,2.1 l -9.1,8.8 2.1,12.5 c 0.1,0.6 -0.1,1.2 -0.5,1.7 -0.4,0.6 -1,0.9 -1.6,0.9 z M 50.2,36.9 c 0.3,0 0.7,0.1 1,0.2 L 62.4,43 c 0,0 0.1,0 0.1,0 L 60.4,30.5 c -0.1,-0.7 0.1,-1.4 0.6,-1.9 l 9.1,-8.8 c 0,0 0,0 0,-0.1 0,-0.1 -0.1,-0.1 -0.1,-0.1 L 57.5,17.8 C 56.8,17.7 56.2,17.3 55.9,16.7 L 50.3,5.3 c 0,0 -0.2,0 -0.2,0 l -5.6,11.3 c -0.3,0.6 -0.9,1 -1.6,1.1 l -12.5,1.8 c 0,0 -0.1,0 -0.1,0.1 0,0.1 0,0.1 0,0.1 l 9.1,8.8 c 0.5,0.5 0.7,1.2 0.6,1.9 L 37.9,42.9 38,43 49.2,37.1 c 0.4,-0.1 0.7,-0.2 1,-0.2 z"
id="path94" /></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB