From 47a4b574561e6db90d529b5b6b2d391ce4c1006e Mon Sep 17 00:00:00 2001 From: niladic Date: Tue, 11 May 2021 18:49:53 +0200 Subject: [PATCH] =?UTF-8?q?Permet=20la=20d=C3=A9sactivation=20=C3=A0=20la?= =?UTF-8?q?=20place=20du=20retrait=20d=E2=80=99un=20groupe=20(#1030)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/constants/Constants.scala | 6 + app/controllers/Operators.scala | 44 +++-- app/controllers/SignupController.scala | 5 +- app/controllers/UserController.scala | 212 +++++++++++++++++-------- app/models/Authorization.scala | 24 ++- app/models/EventType.scala | 4 +- app/models/User.scala | 2 - app/services/UserService.scala | 62 ++++++-- app/views/editMyGroups.scala.html | 49 +----- app/views/editMyGroupsPage.scala | 126 ++++++++++++++- conf/routes | 3 + public/stylesheets/main.css | 2 + typescript/src/dialog.ts | 23 +-- typescript/src/editGroup.ts | 4 +- typescript/src/editMyGroups.ts | 37 ++--- typescript/src/editUser.ts | 4 +- 16 files changed, 414 insertions(+), 193 deletions(-) diff --git a/app/constants/Constants.scala b/app/constants/Constants.scala index 07abb1f22..8156eabed 100644 --- a/app/constants/Constants.scala +++ b/app/constants/Constants.scala @@ -2,4 +2,10 @@ package constants object Constants { val supportEmail = "support@aplus.beta.gouv.fr" + + val error500FlashMessage = + "Une erreur interne est survenue. " + + "Celle-ci étant possiblement temporaire, " + + "nous vous invitons à réessayer plus tard." + } diff --git a/app/controllers/Operators.scala b/app/controllers/Operators.scala index 38c166d6e..fd0bbb771 100644 --- a/app/controllers/Operators.scala +++ b/app/controllers/Operators.scala @@ -3,6 +3,7 @@ package controllers import java.util.UUID import actions.RequestWithUserData +import cats.syntax.all._ import constants.Constants import helper.BooleanHelper.not import models.EventType._ @@ -60,33 +61,42 @@ object Operators { import Results._ - def withUser(userId: UUID, includeDisabled: Boolean = false)( + def withUser( + userId: UUID, + includeDisabled: Boolean = false, + errorMessage: String = "Tentative d'accès à un utilisateur inexistant", + errorResult: Option[Result] = none + )( payload: User => Future[Result] - )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = + )(implicit request: RequestWithUserData[_]): Future[Result] = userService .byId(userId, includeDisabled) .fold({ - eventService - .log(UserNotFound, description = "Tentative d'accès à un utilisateur inexistant.") - Future(NotFound("Utilisateur inexistant.")) + eventService.log(UserNotFound, description = errorMessage) + Future.successful( + errorResult.getOrElse(NotFound("Utilisateur inexistant")) + ) })({ user: User => payload(user) }) def asUserWithAuthorization(authorizationCheck: Authorization.Check)( - event: () => (EventType, String) + errorEvent: () => (EventType, String), + errorResult: Option[Result] = none )( payload: () => Future[Result] - )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = - if (not(authorizationCheck(request.rights))) { - val (eventType, description) = event() - eventService.log(eventType, description = description) - Future(Unauthorized("Vous n'avez pas le droit de faire ça")) - } else { + )(implicit request: RequestWithUserData[_]): Future[Result] = + if (authorizationCheck(request.rights)) { payload() + } else { + val (eventType, description) = errorEvent() + eventService.log(eventType, description = description) + Future.successful( + errorResult.getOrElse(Unauthorized("Vous n'avez pas le droit de faire ça")) + ) } def asAdmin(event: () => (EventType, String))( payload: () => Future[Result] - )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = + )(implicit request: RequestWithUserData[_], ec: ExecutionContext): Future[Result] = if (not(request.currentUser.admin)) { val (eventType, description) = event() eventService.log(eventType, description = description) @@ -97,7 +107,7 @@ object Operators { def asAdminWhoSeesUsersOfArea(areaId: UUID)(event: () => (EventType, String))( payload: () => Future[Result] - )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = + )(implicit request: RequestWithUserData[_], ec: ExecutionContext): Future[Result] = if (not(request.currentUser.admin) || not(request.currentUser.canSeeUsersInArea(areaId))) { val (eventType, description) = event() eventService.log(eventType, description = description) @@ -108,7 +118,7 @@ object Operators { def asUserWhoSeesUsersOfArea(areaId: UUID)(event: () => (EventType, String))( payload: () => Future[Result] - )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = + )(implicit request: RequestWithUserData[_], ec: ExecutionContext): Future[Result] = // TODO: use only Authorization if ( not( @@ -125,7 +135,7 @@ object Operators { def asAdminOfUserZone(user: User)(event: () => (EventType, String))( payload: () => Future[Result] - )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = + )(implicit request: RequestWithUserData[_], ec: ExecutionContext): Future[Result] = if (not(request.currentUser.admin)) { val (eventType, description) = event() eventService.log(eventType, description = description) @@ -176,7 +186,7 @@ object Operators { applicationId: UUID )( payload: Application => Future[Result] - )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = + )(implicit request: RequestWithUserData[_], ec: ExecutionContext): Future[Result] = applicationService .byId( applicationId, diff --git a/app/controllers/SignupController.scala b/app/controllers/SignupController.scala index 24eee5abe..a4f949886 100644 --- a/app/controllers/SignupController.scala +++ b/app/controllers/SignupController.scala @@ -290,12 +290,9 @@ case class SignupController @Inject() ( _.fold( e => { eventService.logErrorNoUser(e) - val message = "Une erreur interne est survenue. " + - "Celle-ci étant possiblement temporaire, " + - "nous vous invitons à réessayer plus tard." Future.successful( Redirect(routes.LoginController.login) - .flashing("error" -> message) + .flashing("error" -> Constants.error500FlashMessage) .withSession(request.session - Keys.Session.signupId) ) }, diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 9fac7f963..6560561a5 100644 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -5,6 +5,7 @@ import java.util.UUID import actions.{LoginAction, RequestWithUserData} import cats.syntax.all._ +import constants.Constants import controllers.Operators.{GroupOperators, UserOperators} import helper.BooleanHelper.not import helper.StringHelper.{capitalizeName, commonStringInputNormalization} @@ -90,21 +91,28 @@ case class UserController @Inject() ( private def getGroupsUsersAndApplicationsBy(user: User) = for { groups <- groupService.byIdsFuture(user.groupIds) - users <- userService.byGroupIdsFuture(groups.map(_.id)) + users <- userService.byGroupIdsFuture(groups.map(_.id), includeDisabled = true) applications <- applicationService.allForUserIds(users.map(_.id)) } yield (groups, users, applications) - private def updateMyGroupsNotAllowed(groupId: UUID)(implicit request: RequestWithUserData[_]) = { - eventService.log( - EditMyGroupUpdatedError, - s"L'utilisateur n'est pas autorisé à éditer le groupe $groupId" - ) - val message = "Vous n’avez pas le droit de modifier ce groupe" - Redirect(routes.UserController.showEditMyGroups).flashing("error" -> message) - } + private def myGroupsAction( + groupId: UUID + )(inner: RequestWithUserData[_] => Future[Result]): Action[AnyContent] = + loginAction.async { implicit request => + asUserWithAuthorization(Authorization.canAddOrRemoveOtherUser(groupId))( + () => + ( + EventType.EditMyGroupUnauthorized, + s"L'utilisateur n'est pas autorisé à éditer le groupe $groupId" + ), { + val message = "Vous n’avez pas le droit de modifier ce groupe" + Redirect(routes.UserController.showEditMyGroups).flashing("error" -> message).some + } + )(() => inner(request)) + } def addToGroup(groupId: UUID) = - loginAction.async { implicit request => + myGroupsAction(groupId) { implicit request => val user = request.currentUser AddUserToGroupFormData.form .bindFromRequest() @@ -117,71 +125,139 @@ case class UserController @Inject() ( } }, data => - if (user.belongsTo(groupId)) { - userService - .byEmailFuture(data.email) - .zip(userService.byGroupIdsFuture(List(groupId), includeDisabled = true)) - .flatMap { - case (None, _) => - eventService.log( - EditMyGroupBadUserInput, - s"Tentative d'ajout de l'utilisateur inexistant ${data.email} au groupe $groupId" - ) - successful( - Redirect(routes.UserController.showEditMyGroups) - .flashing("error" -> "L’utilisateur n’existe pas dans Administration+") - ) - case (Some(userToAdd), usersInGroup) - if usersInGroup.map(_.id).contains[UUID](userToAdd.id) => - eventService.log( - EditMyGroupBadUserInput, - s"Tentative d'ajout de l'utilisateur ${userToAdd.id} déjà présent au groupe $groupId", - involvesUser = userToAdd.id.some - ) - successful( + userService + .byEmailFuture(data.email) + .zip(userService.byGroupIdsFuture(List(groupId), includeDisabled = true)) + .flatMap { + case (None, _) => + eventService.log( + EditMyGroupBadUserInput, + s"Tentative d'ajout de l'utilisateur inexistant ${data.email} au groupe $groupId" + ) + successful( + Redirect(routes.UserController.showEditMyGroups) + .flashing("error" -> "L’utilisateur n’existe pas dans Administration+") + ) + case (Some(userToAdd), usersInGroup) + if usersInGroup.map(_.id).contains[UUID](userToAdd.id) => + eventService.log( + EditMyGroupBadUserInput, + s"Tentative d'ajout de l'utilisateur ${userToAdd.id} déjà présent au groupe $groupId", + involvesUser = userToAdd.id.some + ) + successful( + Redirect(routes.UserController.showEditMyGroups) + .flashing("error" -> "L’utilisateur est déjà présent dans le groupe") + ) + case (Some(userToAdd), _) => + userService + .addToGroup(userToAdd.id, groupId) + .map { _ => + eventService.log( + EditMyGroupUpdated, + s"Utilisateur ${userToAdd.id} ajouté au groupe $groupId", + involvesUser = userToAdd.id.some + ) Redirect(routes.UserController.showEditMyGroups) - .flashing("error" -> "L’utilisateur est déjà présent dans le groupe") - ) - case (Some(userToAdd), _) => - userService - .addToGroup(userToAdd.id, groupId) - .map { _ => - eventService.log( - EditMyGroupUpdated, - s"Utilisateur ${userToAdd.id} ajouté au groupe $groupId", - involvesUser = userToAdd.id.some - ) - Redirect(routes.UserController.showEditMyGroups) - .flashing("success" -> "L’utilisateur a été ajouté au groupe") - } - } - } else successful(updateMyGroupsNotAllowed(groupId)) + .flashing("success" -> "L’utilisateur a été ajouté au groupe") + } + } ) } - def removeFromGroup(userId: UUID, groupId: UUID) = + def enableUser(userId: UUID) = loginAction.async { implicit request => - if (request.currentUser.belongsTo(groupId)) - userService - .removeFromGroup(userId, groupId) - .map { _ => - eventService.log( - EditMyGroupUpdated, - s"Utilisateur $userId retiré du groupe $groupId", - involvesUser = userId.some - ) - Redirect(routes.UserController.showEditMyGroups) - .flashing("success" -> "L’utilisateur a bien été retiré du groupe.") - } - .recover { e => - eventService.log( - EditMyGroupUpdatedError, - s"Erreur lors de la tentative d'ajout de l'utilisateur $userId au groupe $groupId : ${e.getMessage}" + withUser( + userId, + includeDisabled = true, + errorMessage = s"L'utilisateur $userId n'existe pas et ne peut pas être réactivé", + errorResult = Redirect(routes.UserController.showEditMyGroups) + .flashing( + "error" -> ("L’utilisateur n’existe pas dans Administration+. " + + "S’il s’agit d’une erreur, vous pouvez contacter le support.") + ) + .some + ) { otherUser => + asUserWithAuthorization(Authorization.canEnableOtherUser(otherUser))(() => + ( + EventType.EditUserUnauthorized, + s"L'utilisateur n'est pas autorisé à réactiver l'utilisateur $userId" + ) + ) { () => + userService + .enable(userId) + .map( + _.fold( + error => { + eventService.logError(error) + Redirect(routes.UserController.showEditMyGroups) + .flashing("error" -> Constants.error500FlashMessage) + }, + _ => { + eventService.log( + EventType.UserEdited, + s"Utilisateur $userId réactivé", + involvesUser = userId.some + ) + Redirect(routes.UserController.showEditMyGroups) + .flashing("success" -> "L’utilisateur a bien été réactivé.") + } + ) ) - Redirect(routes.UserController.showEditMyGroups) - .flashing("error" -> "Une erreur technique est survenue") + } + } + } + + def removeFromGroup(userId: UUID, groupId: UUID) = + myGroupsAction(groupId) { implicit request => + withUser( + userId, + includeDisabled = true, + errorMessage = s"L'utilisateur $userId n'existe pas et ne peut pas être désactivé", + errorResult = Redirect(routes.UserController.showEditMyGroups) + .flashing( + "error" -> ("L’utilisateur n’existe pas dans Administration+. " + + "S’il s’agit d’une erreur, vous pouvez contacter le support.") + ) + .some + ) { otherUser => + val result: Future[Either[Error, Result]] = + if (otherUser.groupIds.toSet.size <= 1) { + userService + .disable(userId) + .map(_.map { _ => + eventService.log( + EventType.UserEdited, + s"Utilisateur $userId désactivé", + involvesUser = userId.some + ) + Redirect(routes.UserController.showEditMyGroups) + .flashing("success" -> "L’utilisateur a bien été désactivé.") + }) + } else { + userService + .removeFromGroup(userId, groupId) + .map(_.map { _ => + eventService.log( + EditMyGroupUpdated, + s"Utilisateur $userId retiré du groupe $groupId", + involvesUser = userId.some + ) + Redirect(routes.UserController.showEditMyGroups) + .flashing("success" -> "L’utilisateur a bien été retiré du groupe.") + }) } - else successful(updateMyGroupsNotAllowed(groupId)) + result.map( + _.fold( + error => { + eventService.logError(error) + Redirect(routes.UserController.showEditMyGroups) + .flashing("error" -> Constants.error500FlashMessage) + }, + identity + ) + ) + } } def showEditMyGroups = diff --git a/app/models/Authorization.scala b/app/models/Authorization.scala index 213a3638e..147bd8f0d 100644 --- a/app/models/Authorization.scala +++ b/app/models/Authorization.scala @@ -12,11 +12,12 @@ object Authorization { import UserRight._ UserRights( Set[Option[UserRight]]( - Some(HasUserId(user.id)), - if (user.helper && not(user.disabled)) Some(Helper) else None, + HasUserId(user.id).some, + IsInGroups(user.groupIds.toSet).some, + if (user.helper && not(user.disabled)) Helper.some else none, if (user.expert && not(user.disabled)) - Some(ExpertOfAreas(user.areas.toSet)) - else None, + ExpertOfAreas(user.areas.toSet).some + else none, if (user.admin && not(user.disabled)) Some(AdminOfAreas(user.areas.toSet)) else None, @@ -37,6 +38,7 @@ object Authorization { object UserRight { case class HasUserId(id: UUID) extends UserRight + case class IsInGroups(groups: Set[UUID]) extends UserRight case object Helper extends UserRight case class ExpertOfAreas(expertOfAreas: Set[UUID]) extends UserRight case class InstructorOfGroups(groupsManaged: Set[UUID]) extends UserRight @@ -65,6 +67,12 @@ object Authorization { def allMustBeAuthorized(checks: Check*): Check = rights => checks.forall(_(rights)) + def isInGroup(groupId: UUID): Check = + _.rights.exists { + case UserRight.IsInGroups(groups) if groups.contains(groupId) => true + case _ => false + } + def isAdmin: Check = _.rights.exists { case UserRight.AdminOfAreas(_) => true @@ -147,7 +155,13 @@ object Authorization { ) def canEditOtherUser(editedUser: User): Check = - rights => isAdminOfOneOfAreas(editedUser.areas.toSet)(rights) + isAdminOfOneOfAreas(editedUser.areas.toSet) + + def canAddOrRemoveOtherUser(otherUserGroupId: UUID): Check = + atLeastOneIsAuthorized(isAdmin, isInGroup(otherUserGroupId)) + + def canEnableOtherUser(otherUser: User): Check = + atLeastOneIsAuthorized(isAdmin, atLeastOneIsAuthorized(otherUser.groupIds.map(isInGroup): _*)) def canEditGroups: Check = atLeastOneIsAuthorized(isAdmin, isManager) diff --git a/app/models/EventType.scala b/app/models/EventType.scala index 708d95769..4a90f5eaf 100644 --- a/app/models/EventType.scala +++ b/app/models/EventType.scala @@ -74,6 +74,7 @@ object EventType { object EditUserError extends Error object EditUserGroupError extends Error object EditUserShowed extends Info + object EditUserUnauthorized extends Warn object EventsShowed extends Info object EventsUnauthorized extends Warn object FileNotFound extends Error @@ -125,7 +126,7 @@ object EventType { object UserGroupEdited extends Info object UserGroupNotFound extends Error object UserIsUsed extends Error - object UserNotFound extends Error + object UserNotFound extends Warn object UserShowed extends Info object UsersCreated extends Info object UsersImported extends Info @@ -151,6 +152,7 @@ object EventType { object EditMyGroupUpdated extends Info object EditMyGroupBadUserInput extends Warn object EditMyGroupUpdatedError extends Error + object EditMyGroupUnauthorized extends Error // Signups object SignupFormShowed extends Info diff --git a/app/models/User.scala b/app/models/User.scala index 84cde14b8..b30c310e8 100644 --- a/app/models/User.scala +++ b/app/models/User.scala @@ -69,8 +69,6 @@ case class User( phoneNumber = phoneNumber ) - def belongsTo(groupId: UUID) = groupIds.contains[UUID](groupId) - lazy val firstNameLog: String = firstName.map(withQuotes).getOrElse("") lazy val lastNameLog: String = lastName.map(withQuotes).getOrElse("") lazy val nameLog: String = withQuotes(name) diff --git a/app/services/UserService.scala b/app/services/UserService.scala index cf56d478d..3cc966b4e 100644 --- a/app/services/UserService.scala +++ b/app/services/UserService.scala @@ -1,17 +1,17 @@ package services -import java.util.UUID - import anorm._ import cats.syntax.all._ import helper.StringHelper.StringOps import helper.{Hash, Time, UUIDHelper} +import java.sql.Connection +import java.util.UUID import javax.inject.Inject -import models.{Error, User} +import models.{Error, EventType, User} import org.postgresql.util.PSQLException import play.api.db.Database - import scala.concurrent.Future +import scala.util.Try @javax.inject.Singleton class UserService @Inject() ( @@ -308,17 +308,55 @@ class UserService @Inject() ( } } - def removeFromGroup(userId: UUID, groupId: UUID) = - Future { - db.withConnection { implicit connection => - SQL""" - UPDATE "user" SET + def removeFromGroup(userId: UUID, groupId: UUID): Future[Either[Error, Unit]] = + executeUserUpdate( + s"Impossible d'ajouter l'utilisateur $userId au groupe $groupId" + ) { implicit connection => + SQL""" + UPDATE "user" SET group_ids = array_remove(group_ids, $groupId::uuid) - WHERE id = $userId::uuid - """.executeUpdate() - } + WHERE id = $userId::uuid + """.executeUpdate() } + def enable(userId: UUID): Future[Either[Error, Unit]] = + executeUserUpdate( + s"Impossible de réactiver l'utilisateur $userId" + ) { implicit connection => + SQL""" + UPDATE "user" SET + disabled = false + WHERE id = $userId::uuid + """.executeUpdate() + } + + def disable(userId: UUID): Future[Either[Error, Unit]] = + executeUserUpdate( + s"Impossible de désactiver l'utilisateur $userId" + ) { implicit connection => + SQL""" + UPDATE "user" SET + disabled = true + WHERE id = $userId::uuid + """.executeUpdate() + } + + private def executeUserUpdate( + errorMessage: String + )(inner: Connection => _): Future[Either[Error, Unit]] = + Future( + Try(db.withConnection(inner)).toEither + .map(_ => ()) + .left + .map(error => + Error.SqlException( + EventType.EditUserError, + errorMessage, + error + ) + ) + ) + private def instructorFlag(user: User): Boolean = user.instructor && groupsWhichCannotHaveInstructors.intersect(user.groupIds.toSet).isEmpty diff --git a/app/views/editMyGroups.scala.html b/app/views/editMyGroups.scala.html index d1b98b0d8..9c0eff4bd 100644 --- a/app/views/editMyGroups.scala.html +++ b/app/views/editMyGroups.scala.html @@ -13,53 +13,8 @@
Liste des membres du groupe
- @for(user <- users) { - - - - - - - - + @for(user <- users.sortBy(user => (user.disabled, user.name))) { + @toHtml(views.editMyGroupsPage.userLine(user, groupId, applications, currentUser, currentUserRights)) }
- @if(Authorization.canSeeEditUserPage(currentUserRights)) { - @user.name - } else { - @user.name - } - -
- @user.qualite
@user.email
-
- @if(user.groupAdmin) { - Responsable - } - @if(user.admin) { - Admin - } - - @if(user.instructor) { - Instructeur - } - @if(user.helper) { - Aidant - } - -
- chat_bubble - @applications.count(_.creatorUserId === user.id) demandes -
- question_answer - @applications.count(_.invitedUsers.contains(user.id)) - sollicitations -
-
diff --git a/app/views/editMyGroupsPage.scala b/app/views/editMyGroupsPage.scala index 759516359..0d37e7b80 100644 --- a/app/views/editMyGroupsPage.scala +++ b/app/views/editMyGroupsPage.scala @@ -1,14 +1,138 @@ package views +import cats.syntax.all._ import controllers.routes.UserController import helpers.forms.CSRFInput import java.util.UUID -import models.User +import models.{Application, Authorization, User} import play.api.mvc.RequestHeader import scalatags.Text.all._ object editMyGroupsPage { + def userLine( + user: User, + groupId: UUID, + applications: List[Application], + currentUser: User, + currentUserRights: Authorization.UserRights + )(implicit request: RequestHeader): Tag = + tr( + cls := "no-hover td--clear-border", + td( + cls := "mdl-data-table__cell--non-numeric" + + (if (user.disabled) " text--strikethrough mdl-color-text--grey-600" else ""), + ( + if (Authorization.canSeeEditUserPage(currentUserRights)) + a(href := UserController.editUser(user.id).url, user.name) + else + span( + cls := (if (user.disabled) "mdl-color-text--grey-600" else "application__name"), + user.name + ) + ), + br, + span( + cls := (if (user.disabled) "mdl-color-text--grey-600" else "application__subject"), + user.qualite + ) + ), + td( + cls := "mdl-data-table__cell--non-numeric" + + (if (user.disabled) " text--strikethrough mdl-color-text--grey-600" else ""), + user.email + ), + td( + cls := "mdl-data-table__cell--non-numeric mdl-data-table__cell--content-size", + userRoleTags(user) + ), + td( + cls := "mdl-data-table__cell--non-numeric mdl-data-table__cell--content-size", + div( + cls := "vertical-align--middle", + i(cls := "material-icons icon--light", "chat_bubble"), + span( + cls := "application__anwsers", + s"${applications.count(_.creatorUserId === user.id)} demandes" + ), + br, + i(cls := "material-icons icon--light", "question_answer"), + span( + cls := "application__anwsers", + s"${applications.count(_.invitedUsers.contains(user.id))} sollicitations" + ) + ) + ), + td( + cls := "remove-link-panel", + lineActionButton(user, groupId, currentUser, currentUserRights) + ) + ) + + private def userRoleTags(user: User): Modifier = + if (user.disabled) + span(cls := "tag mdl-color--grey-400 mdl-color-text--black", "Désactivé") + else + frag( + user.groupAdmin.some + .filter(identity) + .map(_ => span(cls := "tag tag--responsable", "Responsable")), + " ", + user.admin.some.filter(identity).map(_ => span(cls := "tag tag--admin", "Admin")), + " ", + user.instructor.some + .filter(identity) + .map(_ => span(cls := "tag tag--instructor", "Instructeur")), + " ", + user.helper.some.filter(identity).map(_ => span(cls := "tag tag--aidant", "Aidant")), + ) + + private def lineActionButton( + user: User, + groupId: UUID, + currentUser: User, + currentUserRights: Authorization.UserRights + )(implicit request: RequestHeader): Modifier = + if (user.id =!= currentUser.id) { + if (user.disabled && Authorization.canEnableOtherUser(user)(currentUserRights)) + form( + action := UserController.enableUser(user.id).path, + method := UserController.enableUser(user.id).method, + CSRFInput, + button( + `type` := "submit", + cls := "remove-link", + "Réactiver le compte" + ) + ) + else if ( + user.groupIds.toSet.size === 1 && + Authorization.canAddOrRemoveOtherUser(groupId)(currentUserRights) + ) + form( + action := UserController.removeFromGroup(user.id, groupId).path, + method := UserController.removeFromGroup(user.id, groupId).method, + CSRFInput, + button( + `type` := "submit", + cls := "remove-link", + "Désactiver le compte" + ) + ) + else if ( + user.groupIds.toSet.size > 1 && + Authorization.canAddOrRemoveOtherUser(groupId)(currentUserRights) + ) + button( + cls := "remove-link remove-user-from-group-button", + data("user-id") := user.id.toString, + data("group-id") := groupId.toString, + "Retirer du groupe" + ) + else + () + } else () + private def dialogId(groupId: UUID, userId: UUID): String = s"remove-user-from-group-dialog-$groupId-$userId" diff --git a/conf/routes b/conf/routes index b6dbc73dc..4d332ba7e 100644 --- a/conf/routes +++ b/conf/routes @@ -58,13 +58,16 @@ POST /me GET /utilisateurs controllers.UserController.home GET /utilisateurs/:userId controllers.UserController.editUser(userId: java.util.UUID) POST /utilisateurs/:userId controllers.UserController.editUserPost(userId: java.util.UUID) +POST /utilisateurs/:userId/reactivation controllers.UserController.enableUser(userId: java.util.UUID) GET /user/delete/unused/:userId controllers.UserController.deleteUnusedUserById(userId: java.util.UUID) + # Importation GET /importation/utilisateurs-depuis-csv controllers.CSVImportController.importUsersFromCSV POST /importation/revue-import controllers.CSVImportController.importUsersReview POST /importation/import controllers.CSVImportController.importUsersAfterReview + # Groups POST /groups controllers.GroupController.addGroup GET /groups controllers.UserController.showEditMyGroups diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css index c62ec4850..2cb110ac6 100644 --- a/public/stylesheets/main.css +++ b/public/stylesheets/main.css @@ -400,10 +400,12 @@ html, body { .tag--instructor { background-color: #bee1ff; + color: black; } .tag--aidant { background-color: greenyellow; + color: black; } .tag--disabled { diff --git a/typescript/src/dialog.ts b/typescript/src/dialog.ts index 6c1c59cd9..b52b9dd4a 100644 --- a/typescript/src/dialog.ts +++ b/typescript/src/dialog.ts @@ -4,23 +4,28 @@ import "dialog-polyfill/dist/dialog-polyfill.css"; export function addDialogButtonsClickListeners( - dialogId: string, - showModalButtonId: string, - closeModalButtonId: string, + dialog: HTMLDialogElement | null, + showButton: HTMLElement | null, + closeButton: HTMLElement | null, ) { - const showButton = document.getElementById(showModalButtonId); - const dialog = document.getElementById(dialogId); - const closeButton = document.getElementById(closeModalButtonId); - if (showButton != null && dialog != null && closeButton != null) { - if (!dialog.showModal) { dialogPolyfill.registerDialog(dialog); } showButton.addEventListener("click", () => dialog.showModal()); closeButton.addEventListener("click", () => dialog.close()); - } +} + +export function addDialogButtonsClickListenersByIds( + dialogId: string, + showModalButtonId: string, + closeModalButtonId: string, +) { + const showButton = document.getElementById(showModalButtonId); + const dialog = document.getElementById(dialogId); + const closeButton = document.getElementById(closeModalButtonId); + addDialogButtonsClickListeners(dialog, showButton, closeButton); } diff --git a/typescript/src/editGroup.ts b/typescript/src/editGroup.ts index ac0ab6f47..d7d24079a 100644 --- a/typescript/src/editGroup.ts +++ b/typescript/src/editGroup.ts @@ -1,4 +1,4 @@ -import { addDialogButtonsClickListeners } from "./dialog"; +import { addDialogButtonsClickListenersByIds } from "./dialog"; const dialogDeleteGroupId = "dialog-delete-group"; const dialogDeleteGroupButtonShowId = "dialog-delete-group-show"; @@ -13,7 +13,7 @@ setupDeleteGroupModal(); function setupDeleteGroupModal() { - addDialogButtonsClickListeners( + addDialogButtonsClickListenersByIds( dialogDeleteGroupId, dialogDeleteGroupButtonShowId, dialogDeleteGroupButtonCancelId diff --git a/typescript/src/editMyGroups.ts b/typescript/src/editMyGroups.ts index 7ae42f066..2b626f8c7 100644 --- a/typescript/src/editMyGroups.ts +++ b/typescript/src/editMyGroups.ts @@ -1,5 +1,4 @@ -import dialogPolyfill from "dialog-polyfill"; -import "dialog-polyfill/dist/dialog-polyfill.css"; +import { addDialogButtonsClickListeners } from "./dialog"; const removeUserFromGroupButtonClass = "remove-user-from-group-button"; const removeUserFromGroupDialogId = (groupId: string, userId: string) => @@ -15,27 +14,19 @@ setupRemoveUserFromGroupModal(); function setupRemoveUserFromGroupModal() { - Array.from(document.querySelectorAll("." + removeUserFromGroupButtonClass)).forEach(button => { - const userId = button.dataset["userId"]; - const groupId = button.dataset["groupId"]; - if (userId != null && groupId != null) { - const dialogId = removeUserFromGroupDialogId(groupId, userId); - const dialog = document.getElementById(dialogId); - - if (dialog != null) { - if (dialog && !dialog.showModal) { - dialogPolyfill.registerDialog(dialog); - } - button.addEventListener("click", () => { - dialog.showModal(); - }) - const closeButton = dialog.querySelector("." + closeModalClass); - if (closeButton != null) { - closeButton.addEventListener("click", () => dialog.close()); - } + Array.from(document.querySelectorAll("." + removeUserFromGroupButtonClass)) + .forEach(button => { + const userId = button.dataset["userId"]; + const groupId = button.dataset["groupId"]; + if (userId != null && groupId != null) { + const dialogId = removeUserFromGroupDialogId(groupId, userId); + const dialog = document.getElementById(dialogId); + addDialogButtonsClickListeners( + dialog, + button, + dialog.querySelector("." + closeModalClass) + ); } - - } - }) + }) } diff --git a/typescript/src/editUser.ts b/typescript/src/editUser.ts index 2fd4ed5d2..49d9b55bc 100644 --- a/typescript/src/editUser.ts +++ b/typescript/src/editUser.ts @@ -1,4 +1,4 @@ -import { addDialogButtonsClickListeners } from "./dialog"; +import { addDialogButtonsClickListenersByIds } from "./dialog"; const deleteUserButtonId = "delete-user-button"; const deleteUserModalId = "dialog-delete-user"; @@ -13,7 +13,7 @@ setupDeleteUserModal(); function setupDeleteUserModal() { - addDialogButtonsClickListeners( + addDialogButtonsClickListenersByIds( deleteUserModalId, deleteUserButtonId, deleteUserModalQuitButtonId