From 468a6e1d09d6e959b28e7ec7c18da70ba8779b87 Mon Sep 17 00:00:00 2001 From: niladic Date: Thu, 27 Feb 2020 15:56:56 +0100 Subject: [PATCH] =?UTF-8?q?Wrap=20un=20premier=20appel=20bloquant=20=C3=A0?= =?UTF-8?q?=20la=20BDD=20dans=20une=20Future.=20(#521)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add an ExecutionContext for blocking DB queries and wrap one DB query into a Future. * Change nr of threads and remove config prefix. --- app/controllers/ApplicationController.scala | 416 ++++++++++---------- app/controllers/Operators.scala | 36 +- app/controllers/UserController.scala | 87 ++-- app/services/ServicesDependencies.scala | 19 + app/services/UserGroupService.scala | 19 +- conf/application.conf | 11 + 6 files changed, 331 insertions(+), 257 deletions(-) create mode 100644 app/services/ServicesDependencies.scala diff --git a/app/controllers/ApplicationController.scala b/app/controllers/ApplicationController.scala index 9a22e3746..2ba2ec04d 100644 --- a/app/controllers/ApplicationController.scala +++ b/app/controllers/ApplicationController.scala @@ -107,57 +107,61 @@ case class ApplicationController @Inject() ( private def fetchGroupsWithInstructors( areaId: UUID, currentUser: User - ): (List[UserGroup], List[User], List[User]) = { - val groupsOfArea = userGroupService.byArea(areaId) - val usersInThoseGroups = userService.byGroupIds(groupsOfArea.map(_.id)) - val instructorsOfGroups = usersInThoseGroups.filter(_.instructor) - // Note: we don't care about users who are in several areas - val coworkers = usersInThoseGroups - .filter(user => - user.helper && user.groupIds.toSet.intersect(currentUser.groupIds.toSet).nonEmpty - ) - .filterNot(user => (user.id: UUID) == (currentUser.id: UUID)) - // This could be optimized by doing only one SQL query - val groupIdsWithInstructors = instructorsOfGroups.flatMap(_.groupIds).toSet - val groupsOfAreaWithInstructor = - groupsOfArea.filter(user => groupIdsWithInstructors.contains(user.id)) - (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) + ): Future[(List[UserGroup], List[User], List[User])] = { + val groupsOfAreaFuture = userGroupService.byArea(areaId) + groupsOfAreaFuture.map { groupsOfArea => + val usersInThoseGroups = userService.byGroupIds(groupsOfArea.map(_.id)) + val instructorsOfGroups = usersInThoseGroups.filter(_.instructor) + // Note: we don't care about users who are in several areas + val coworkers = usersInThoseGroups + .filter(user => + user.helper && user.groupIds.toSet.intersect(currentUser.groupIds.toSet).nonEmpty + ) + .filterNot(user => (user.id: UUID) == (currentUser.id: UUID)) + // This could be optimized by doing only one SQL query + val groupIdsWithInstructors = instructorsOfGroups.flatMap(_.groupIds).toSet + val groupsOfAreaWithInstructor = + groupsOfArea.filter(user => groupIdsWithInstructors.contains(user.id)) + (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) + } } - def create = loginAction { implicit request => + def create = loginAction.async { implicit request => eventService.log(ApplicationFormShowed, "Visualise le formulaire de création de demande") - val (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) = - fetchGroupsWithInstructors(request.currentArea.id, request.currentUser) - Ok( - views.html.createApplication(request.currentUser, request.currentArea)( - instructorsOfGroups, - groupsOfAreaWithInstructor, - coworkers, - applicationForm - ) - ) + fetchGroupsWithInstructors(request.currentArea.id, request.currentUser).map { + case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => + Ok( + views.html.createApplication(request.currentUser, request.currentArea)( + instructorsOfGroups, + groupsOfAreaWithInstructor, + coworkers, + applicationForm + ) + ) + } } - def createSimplified = loginAction { implicit request => + def createSimplified = loginAction.async { implicit request => eventService .log(ApplicationFormShowed, "Visualise le formulaire simplifié de création de demande") - val (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) = - fetchGroupsWithInstructors(request.currentArea.id, request.currentUser) - val groupsOfAreaWithInstructorWithOrganisationSet = groupsOfAreaWithInstructor.filter({ - userGroup => - userGroup.organisationSetOrDeducted.nonEmpty - }) - val categories = organisationService.categories - Ok( - views.html.simplifiedCreateApplication(request.currentUser, request.currentArea)( - instructorsOfGroups, - groupsOfAreaWithInstructorWithOrganisationSet, - coworkers, - categories, - None, - applicationForm - ) - ) + fetchGroupsWithInstructors(request.currentArea.id, request.currentUser).map { + case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => + val groupsOfAreaWithInstructorWithOrganisationSet = groupsOfAreaWithInstructor.filter({ + userGroup => + userGroup.organisationSetOrDeducted.nonEmpty + }) + val categories = organisationService.categories + Ok( + views.html.simplifiedCreateApplication(request.currentUser, request.currentArea)( + instructorsOfGroups, + groupsOfAreaWithInstructorWithOrganisationSet, + coworkers, + categories, + None, + applicationForm + ) + ) + } } def createPost = createPostBis(false) @@ -188,7 +192,7 @@ case class ApplicationController @Inject() ( s"${capitalizedUserName} ${contexts.mkString(",")}" } - private def createPostBis(simplified: Boolean) = loginAction { implicit request => + private def createPostBis(simplified: Boolean) = loginAction.async { implicit request => val form = applicationForm.bindFromRequest val applicationId = AttachmentHelper.retrieveOrGenerateApplicationId(form.data) val (pendingAttachments, newAttachments) = @@ -199,85 +203,86 @@ case class ApplicationController @Inject() ( filesPath ) form.fold( - formWithErrors => { + formWithErrors => // binding failure, you retrieve the form containing errors: - val (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) = - fetchGroupsWithInstructors(request.currentArea.id, request.currentUser) - eventService.log( - ApplicationCreationInvalid, - s"L'utilisateur essaie de créer une demande invalide ${formWithErrors.errors.map(_.message)}" - ) - - if (simplified) { - val categories = organisationService.categories - val groupsOfAreaWithInstructorWithOrganisationSet = - groupsOfAreaWithInstructor.filter(_.organisationSetOrDeducted.nonEmpty) - BadRequest( - views.html.simplifiedCreateApplication(request.currentUser, request.currentArea)( - instructorsOfGroups, - groupsOfAreaWithInstructorWithOrganisationSet, - coworkers, - categories, - formWithErrors("category").value, - formWithErrors, - pendingAttachments.keys ++ newAttachments.keys + fetchGroupsWithInstructors(request.currentArea.id, request.currentUser).map { + case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => + eventService.log( + ApplicationCreationInvalid, + s"L'utilisateur essaie de créer une demande invalide ${formWithErrors.errors.map(_.message)}" ) + + if (simplified) { + val categories = organisationService.categories + val groupsOfAreaWithInstructorWithOrganisationSet = + groupsOfAreaWithInstructor.filter(_.organisationSetOrDeducted.nonEmpty) + BadRequest( + views.html.simplifiedCreateApplication(request.currentUser, request.currentArea)( + instructorsOfGroups, + groupsOfAreaWithInstructorWithOrganisationSet, + coworkers, + categories, + formWithErrors("category").value, + formWithErrors, + pendingAttachments.keys ++ newAttachments.keys + ) + ) + } else { + BadRequest( + views.html.createApplication(request.currentUser, request.currentArea)( + instructorsOfGroups, + groupsOfAreaWithInstructor, + coworkers, + formWithErrors, + pendingAttachments.keys ++ newAttachments.keys + ) + ) + } + }, + applicationData => + Future { + // Note: we will deprecate .currentArea as a variable stored in the cookies + val currentAreaId: UUID = request.currentArea.id + val invitedUsers: Map[UUID, String] = applicationData.users.flatMap { id => + userService.byId(id).map(user => id -> contextualizedUserName(user, currentAreaId)) + }.toMap + + val application = Application( + applicationId, + DateTime.now(timeZone), + contextualizedUserName(request.currentUser, currentAreaId), + request.currentUser.id, + applicationData.subject, + applicationData.description, + applicationData.infos, + invitedUsers, + request.currentArea.id, + false, + hasSelectedSubject = + applicationData.selectedSubject.contains[String](applicationData.subject), + category = applicationData.category, + files = newAttachments ++ pendingAttachments ) - } else { - BadRequest( - views.html.createApplication(request.currentUser, request.currentArea)( - instructorsOfGroups, - groupsOfAreaWithInstructor, - coworkers, - formWithErrors, - pendingAttachments.keys ++ newAttachments.keys + if (applicationService.createApplication(application)) { + notificationsService.newApplication(application) + eventService.log( + ApplicationCreated, + s"La demande ${application.id} a été créée", + Some(application) ) - ) - } - }, - applicationData => { - // Note: we will deprecate .currentArea as a variable stored in the cookies - val currentAreaId: UUID = request.currentArea.id - val invitedUsers: Map[UUID, String] = applicationData.users.flatMap { id => - userService.byId(id).map(user => id -> contextualizedUserName(user, currentAreaId)) - }.toMap - - val application = Application( - applicationId, - DateTime.now(timeZone), - contextualizedUserName(request.currentUser, currentAreaId), - request.currentUser.id, - applicationData.subject, - applicationData.description, - applicationData.infos, - invitedUsers, - request.currentArea.id, - false, - hasSelectedSubject = - applicationData.selectedSubject.contains[String](applicationData.subject), - category = applicationData.category, - files = newAttachments ++ pendingAttachments - ) - if (applicationService.createApplication(application)) { - notificationsService.newApplication(application) - eventService.log( - ApplicationCreated, - s"La demande ${application.id} a été créée", - Some(application) - ) - Redirect(routes.ApplicationController.myApplications()) - .flashing("success" -> "Votre demande a bien été envoyée") - } else { - eventService.log( - ApplicationCreationError, - s"La demande ${application.id} n'a pas pu être créée", - Some(application) - ) - InternalServerError( - "Erreur Interne: Votre demande n'a pas pu être envoyée. Merci de réessayer ou de contacter l'administrateur" - ) + Redirect(routes.ApplicationController.myApplications()) + .flashing("success" -> "Votre demande a bien été envoyée") + } else { + eventService.log( + ApplicationCreationError, + s"La demande ${application.id} n'a pas pu être créée", + Some(application) + ) + InternalServerError( + "Erreur Interne: Votre demande n'a pas pu être envoyée. Merci de réessayer ou de contacter l'administrateur" + ) + } } - } ) } @@ -586,62 +591,68 @@ case class ApplicationController @Inject() ( )(AnswerData.apply)(AnswerData.unapply) ) - def usersThatCanBeInvitedOn[A]( + private def usersWhoCanBeInvitedOn[A]( application: Application - )(implicit request: RequestWithUserData[A]) = + )(implicit request: RequestWithUserData[A]): Future[List[User]] = (if (request.currentUser.instructor || request.currentUser.expert) { - val groupsOfArea = userGroupService.byArea(application.area) - userService.byGroupIds(groupsOfArea.map(_.id)).filter(_.instructor) + userGroupService.byArea(application.area).map { groupsOfArea => + userService.byGroupIds(groupsOfArea.map(_.id)).filter(_.instructor) + } } else if (request.currentUser.helper && application.creatorUserId == request.currentUser.id) { - userService.byGroupIds(request.currentUser.groupIds).filter(_.helper) + Future(userService.byGroupIds(request.currentUser.groupIds).filter(_.helper)) } else { - List[User]() - }).filterNot(user => - user.id == request.currentUser.id || application.invitedUsers.contains(user.id) + Future(List[User]()) + }).map( + _.filterNot(user => + user.id == request.currentUser.id || application.invitedUsers.contains(user.id) + ) ) - def show(id: UUID) = loginAction { implicit request => + def show(id: UUID) = loginAction.async { implicit request => applicationService.byId(id, request.currentUser.id, request.currentUser.admin) match { case None => eventService.log(ApplicationNotFound, s"La demande $id n'existe pas") - NotFound("Nous n'avons pas trouvé cette demande") + Future(NotFound("Nous n'avons pas trouvé cette demande")) case Some(application) => if (application.canBeShowedBy(request.currentUser)) { - val usersThatCanBeInvited = usersThatCanBeInvitedOn(application) - val groups = userGroupService - .byIds(usersThatCanBeInvited.flatMap(_.groupIds)) - .filter(_.areaIds.contains[UUID](application.area)) - val groupsWithUsersThatCanBeInvited = groups.map { group => - group -> usersThatCanBeInvited.filter(_.groupIds.contains[UUID](group.id)) - } - val renderedApplication = - if ((application - .haveUserInvitedOn(request.currentUser) || request.currentUser.id == application.creatorUserId) && request.currentUser.expert && request.currentUser.admin && !application.closed) { - // If user is expert, admin and invited to the application we desanonymate - applicationService.byId(id, request.currentUser.id, false).get - } else { - application + usersWhoCanBeInvitedOn(application).map { usersWhoCanBeInvited => + val groups = userGroupService + .byIds(usersWhoCanBeInvited.flatMap(_.groupIds)) + .filter(_.areaIds.contains[UUID](application.area)) + val groupsWithUsersThatCanBeInvited = groups.map { group => + group -> usersWhoCanBeInvited.filter(_.groupIds.contains[UUID](group.id)) } - val openedTab = request.flash.get("opened-tab").getOrElse("answer") - - eventService.log(ApplicationShowed, s"Demande $id consultée", Some(application)) - Ok( - views.html.showApplication(request.currentUser)( - groupsWithUsersThatCanBeInvited, - renderedApplication, - answerForm, - openedTab, - request.currentArea + val renderedApplication = + if ((application + .haveUserInvitedOn(request.currentUser) || request.currentUser.id == application.creatorUserId) && request.currentUser.expert && request.currentUser.admin && !application.closed) { + // If user is expert, admin and invited to the application we desanonymate + applicationService.byId(id, request.currentUser.id, false).get + } else { + application + } + val openedTab = request.flash.get("opened-tab").getOrElse("answer") + + eventService.log(ApplicationShowed, s"Demande $id consultée", Some(application)) + Ok( + views.html.showApplication(request.currentUser)( + groupsWithUsersThatCanBeInvited, + renderedApplication, + answerForm, + openedTab, + request.currentArea + ) ) - ) + } } else { eventService.log( ApplicationUnauthorized, s"L'accès à la demande $id n'est pas autorisé", Some(application) ) - Unauthorized( - s"Vous n'avez pas les droits suffisants pour voir cette demande. Vous pouvez contacter l'équipe A+ : ${Constants.supportEmail}" + Future( + Unauthorized( + s"Vous n'avez pas les droits suffisants pour voir cette demande. Vous pouvez contacter l'équipe A+ : ${Constants.supportEmail}" + ) ) } } @@ -704,7 +715,7 @@ case class ApplicationController @Inject() ( } } - def answer(applicationId: UUID) = loginAction { implicit request => + def answer(applicationId: UUID) = loginAction.async { implicit request => withApplication(applicationId) { application => val form = answerForm.bindFromRequest val answerId = AttachmentHelper.retrieveOrGenerateAnswerId(form.data) @@ -720,8 +731,10 @@ case class ApplicationController @Inject() ( val error = s"Erreur dans le formulaire de réponse (${formWithErrors.errors.map(_.message).mkString(", ")})." eventService.log(AnswerNotCreated, s"$error") - Redirect(routes.ApplicationController.show(applicationId).withFragment("answer-error")) - .flashing("answer-error" -> error, "opened-tab" -> "anwser") + Future( + Redirect(routes.ApplicationController.show(applicationId).withFragment("answer-error")) + .flashing("answer-error" -> error, "opened-tab" -> "anwser") + ) }, answerData => { val currentAreaId = application.area @@ -745,15 +758,17 @@ case class ApplicationController @Inject() ( Some(application) ) notificationsService.newAnswer(application, answer) - Redirect(s"${routes.ApplicationController.show(applicationId)}#answer-${answer.id}") - .flashing("success" -> "Votre réponse a bien été envoyée") + Future( + Redirect(s"${routes.ApplicationController.show(applicationId)}#answer-${answer.id}") + .flashing("success" -> "Votre réponse a bien été envoyée") + ) } else { eventService.log( AnswerNotCreated, s"La réponse ${answer.id} n'a pas été créée sur la demande $applicationId : problème BDD", Some(application) ) - InternalServerError("Votre réponse n'a pas pu être envoyée") + Future(InternalServerError("Votre réponse n'a pas pu être envoyée")) } } ) @@ -768,52 +783,57 @@ case class ApplicationController @Inject() ( )(InvitationData.apply)(InvitationData.unapply) ) - def invite(applicationId: UUID) = loginAction { implicit request => + def invite(applicationId: UUID) = loginAction.async { implicit request => withApplication(applicationId) { application => inviteForm.bindFromRequest.fold( formWithErrors => { val error = s"Erreur dans le formulaire d'invitation (${formWithErrors.errors.map(_.message).mkString(", ")})." eventService.log(InviteNotCreated, error) - Redirect(routes.ApplicationController.show(applicationId).withFragment("answer-error")) - .flashing("answer-error" -> error, "opened-tab" -> "invite") + Future( + Redirect(routes.ApplicationController.show(applicationId).withFragment("answer-error")) + .flashing("answer-error" -> error, "opened-tab" -> "invite") + ) }, inviteData => { val currentAreaId = application.area - val usersThatCanBeInvited = usersThatCanBeInvitedOn(application) - val invitedUsers: Map[UUID, String] = usersThatCanBeInvited - .filter(user => inviteData.invitedUsers.contains[UUID](user.id)) - .map(user => (user.id, contextualizedUserName(user, currentAreaId))) - .toMap + usersWhoCanBeInvitedOn(application).map { + usersWhoCanBeInvited => + val invitedUsers: Map[UUID, String] = usersWhoCanBeInvited + .filter(user => inviteData.invitedUsers.contains[UUID](user.id)) + .map(user => (user.id, contextualizedUserName(user, currentAreaId))) + .toMap + + val answer = Answer( + UUID.randomUUID(), + applicationId, + DateTime.now(timeZone), + inviteData.message, + request.currentUser.id, + contextualizedUserName(request.currentUser, currentAreaId), + invitedUsers, + not(inviteData.privateToHelpers), + false, + Some(Map.empty) + ) - val answer = Answer( - UUID.randomUUID(), - applicationId, - DateTime.now(timeZone), - inviteData.message, - request.currentUser.id, - contextualizedUserName(request.currentUser, currentAreaId), - invitedUsers, - not(inviteData.privateToHelpers), - false, - Some(Map.empty) - ) - if (applicationService.add(applicationId, answer) == 1) { - notificationsService.newAnswer(application, answer) - eventService.log( - AgentsAdded, - s"L'ajout d'utilisateur ${answer.id} a été créé sur la demande $applicationId", - Some(application) - ) - Redirect(routes.ApplicationController.myApplications()) - .flashing("success" -> "Les utilisateurs ont été invités sur la demande") - } else { - eventService.log( - AgentsNotAdded, - s"L'ajout d'utilisateur ${answer.id} n'a pas été créé sur la demande $applicationId : problème BDD", - Some(application) - ) - InternalServerError("Les utilisateurs n'ont pas pu être invités") + if (applicationService.add(applicationId, answer) == 1) { + notificationsService.newAnswer(application, answer) + eventService.log( + AgentsAdded, + s"L'ajout d'utilisateur ${answer.id} a été créé sur la demande $applicationId", + Some(application) + ) + Redirect(routes.ApplicationController.myApplications()) + .flashing("success" -> "Les utilisateurs ont été invités sur la demande") + } else { + eventService.log( + AgentsNotAdded, + s"L'ajout d'utilisateur ${answer.id} n'a pas été créé sur la demande $applicationId : problème BDD", + Some(application) + ) + InternalServerError("Les utilisateurs n'ont pas pu être invités") + } } } ) diff --git a/app/controllers/Operators.scala b/app/controllers/Operators.scala index 7afe983ba..d22b09dbe 100644 --- a/app/controllers/Operators.scala +++ b/app/controllers/Operators.scala @@ -9,6 +9,7 @@ import models.EventType._ import models.{Application, EventType, User, UserGroup} import play.api.mvc.Results.{NotFound, Unauthorized} import play.api.mvc.{AnyContent, Result, Results} +import scala.concurrent.{ExecutionContext, Future} import services.{ApplicationService, EventService, UserGroupService, UserService} object Operators { @@ -33,7 +34,7 @@ object Operators { }) def asAdminOfGroupZone(group: UserGroup)(event: () => (EventType, String))( - payload: () => play.api.mvc.Result + payload: () => Result )(implicit request: RequestWithUserData[AnyContent]): Result = if (not(request.currentUser.admin)) { val (eventType, description) = event() @@ -72,7 +73,7 @@ object Operators { }) def asAdmin(event: () => (EventType, String))( - payload: () => play.api.mvc.Result + payload: () => Result )(implicit request: RequestWithUserData[AnyContent]): Result = if (not(request.currentUser.admin)) { val (eventType, description) = event() @@ -83,30 +84,30 @@ object Operators { } def asAdminWhoSeesUsersOfArea(areaId: UUID)(event: () => (EventType, String))( - payload: () => play.api.mvc.Result - )(implicit request: RequestWithUserData[AnyContent]): Result = + payload: () => Future[Result] + )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = if (not(request.currentUser.admin) || not(request.currentUser.canSeeUsersInArea(areaId))) { val (eventType, description) = event() eventService.log(eventType, description = description) - Unauthorized("Vous n'avez pas le droit de faire ça") + Future(Unauthorized("Vous n'avez pas le droit de faire ça")) } else { payload() } def asUserWhoSeesUsersOfArea(areaId: UUID)(event: () => (EventType, String))( - payload: () => play.api.mvc.Result - )(implicit request: RequestWithUserData[AnyContent]): Result = + payload: () => Future[Result] + )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = if (not(request.currentUser.canSeeUsersInArea(areaId))) { val (eventType, description) = event() eventService.log(eventType, description = description) - Unauthorized("Vous n'avez pas le droit de faire ça") + Future(Unauthorized("Vous n'avez pas le droit de faire ça")) } else { payload() } def asAdminOfUserZone(user: User)(event: () => (EventType, String))( - payload: () => play.api.mvc.Result - )(implicit request: RequestWithUserData[AnyContent]): play.api.mvc.Result = + payload: () => Result + )(implicit request: RequestWithUserData[AnyContent]): Result = if (not(request.currentUser.admin)) { val (eventType, description) = event() eventService.log(eventType, description = description) @@ -130,7 +131,9 @@ object Operators { def withApplication( applicationId: UUID - )(payload: Application => Result)(implicit request: RequestWithUserData[AnyContent]): Result = + )( + payload: Application => Future[Result] + )(implicit request: RequestWithUserData[AnyContent], ec: ExecutionContext): Future[Result] = applicationService .byId( applicationId, @@ -140,21 +143,24 @@ object Operators { .fold({ eventService.log( ApplicationNotFound, - description = "Tentative d'accès à une application inexistant." + description = "Tentative d'accès à une application inexistante." ) - NotFound("Application inexistante.") + Future(NotFound("Application inexistante.")) })({ application: Application => if (not(application.canBeShowedBy(request.currentUser))) { eventService.log( ApplicationUnauthorized, description = "Tentative d'accès à une application non autorisé." ) - Unauthorized( - s"Vous n'avez pas les droits suffisants pour voir cette demande. Vous pouvez contacter l'équipe A+ : ${Constants.supportEmail}" + Future( + Unauthorized( + s"Vous n'avez pas les droits suffisants pour voir cette demande. Vous pouvez contacter l'équipe A+ : ${Constants.supportEmail}" + ) ) } else { payload(application) } }) + } } diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 22de74e29..baa40c5f5 100644 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -1,6 +1,7 @@ package controllers import java.util.{Locale, UUID} +import scala.concurrent.{ExecutionContext, Future} import actions.{LoginAction, RequestWithUserData} import helper.BooleanHelper.not @@ -56,7 +57,7 @@ case class UserController @Inject() ( notificationsService: NotificationService, configuration: Configuration, eventService: EventService -)(implicit val webJarsUtil: WebJarsUtil) +)(implicit ec: ExecutionContext, webJarsUtil: WebJarsUtil) extends InjectedController with play.api.i18n.I18nSupport with UserOperators @@ -66,29 +67,30 @@ case class UserController @Inject() ( TemporaryRedirect(controllers.routes.UserController.all(Area.allArea.id).url) } - def all(areaId: UUID): Action[AnyContent] = loginAction { + def all(areaId: UUID): Action[AnyContent] = loginAction.async { implicit request: RequestWithUserData[AnyContent] => asUserWhoSeesUsersOfArea(areaId) { () => AllUserUnauthorized -> "Accès non autorisé à l'admin des utilisateurs" } { () => val selectedArea = Area.fromId(areaId).get - val users = ( + val usersFuture: Future[List[User]] = ( request.currentUser.admin, request.currentUser.groupAdmin, selectedArea.id == Area.allArea.id ) match { case (true, _, false) => { - val groupsOfArea = groupService.byArea(areaId) - userService.byGroupIds(groupsOfArea.map(_.id)) + groupService.byArea(areaId).map { groupsOfArea => + userService.byGroupIds(groupsOfArea.map(_.id)) + } } case (true, _, true) => { val groupsOfArea = groupService.byAreas(request.currentUser.areas) - userService.byGroupIds(groupsOfArea.map(_.id)) + Future(userService.byGroupIds(groupsOfArea.map(_.id))) } - case (false, true, _) => userService.byGroupIds(request.currentUser.groupIds) + case (false, true, _) => Future(userService.byGroupIds(request.currentUser.groupIds)) case _ => eventService.log(AllUserIncorrectSetup, "Erreur d'accès aux utilisateurs") - List() + Future(Nil) } val applications = applicationService.allByArea(selectedArea.id, anonymous = true) val groups: List[UserGroup] = ( @@ -104,40 +106,43 @@ case class UserController @Inject() ( List() } eventService.log(UsersShowed, "Visualise la vue des utilisateurs") - val result = request.getQueryString("vue").getOrElse("nouvelle") match { - case "nouvelle" if request.currentUser.admin => - views.html.allUsersNew(request.currentUser)( - groups, - users, - applications, - selectedArea, - configuration.underlying.getString("geoplus.host") - ) - case _ => - views.html.allUsersByGroup(request.currentUser)( - groups, - users, - applications, - selectedArea, - configuration.underlying.getString("geoplus.host") - ) + usersFuture.map { users => + val result = request.getQueryString("vue").getOrElse("nouvelle") match { + case "nouvelle" if request.currentUser.admin => + views.html.allUsersNew(request.currentUser)( + groups, + users, + applications, + selectedArea, + configuration.underlying.getString("geoplus.host") + ) + case _ => + views.html.allUsersByGroup(request.currentUser)( + groups, + users, + applications, + selectedArea, + configuration.underlying.getString("geoplus.host") + ) + } + Ok(result) } - Ok(result) } } - def allCSV(areaId: java.util.UUID): Action[AnyContent] = loginAction { + def allCSV(areaId: java.util.UUID): Action[AnyContent] = loginAction.async { implicit request: RequestWithUserData[AnyContent] => asAdminWhoSeesUsersOfArea(areaId) { () => AllUserCSVUnauthorized -> "Accès non autorisé à l'export utilisateur" } { () => val area = Area.fromId(areaId).get - val users = if (areaId == Area.allArea.id) { + val usersFuture: Future[List[User]] = if (areaId == Area.allArea.id) { val groupsOfArea = groupService.byAreas(request.currentUser.areas) - userService.byGroupIds(groupsOfArea.map(_.id)) + Future(userService.byGroupIds(groupsOfArea.map(_.id))) } else { - val groupsOfArea = groupService.byArea(areaId) - userService.byGroupIds(groupsOfArea.map(_.id)) + groupService.byArea(areaId).map { groupsOfArea => + userService.byGroupIds(groupsOfArea.map(_.id)) + } } val groups = groupService.allGroupByAreas(request.currentUser.areas) eventService.log(AllUserCsvShowed, "Visualise le CSV de tous les zones de l'utilisateur") @@ -179,15 +184,19 @@ case class UserController @Inject() ( "CGU", "Newsletter" ).mkString(";") - val csvContent = (List(headers) ++ users.map(userToCSV)).mkString("\n") - val date = DateTime.now(Time.dateTimeZone).toString("dd-MMM-YYY-HH'h'mm", new Locale("fr")) - Ok(csvContent) - .withHeaders( - "Content-Disposition" -> s"""attachment; filename="aplus-$date-users-${area.name - .replace(" ", "-")}.csv"""" - ) - .as("text/csv") + usersFuture.map { users => + val csvContent = (List(headers) ++ users.map(userToCSV)).mkString("\n") + val date = + DateTime.now(Time.dateTimeZone).toString("dd-MMM-YYY-HH'h'mm", new Locale("fr")) + + Ok(csvContent) + .withHeaders( + "Content-Disposition" -> s"""attachment; filename="aplus-$date-users-${area.name + .replace(" ", "-")}.csv"""" + ) + .as("text/csv") + } } } diff --git a/app/services/ServicesDependencies.scala b/app/services/ServicesDependencies.scala new file mode 100644 index 000000000..8ad8250b3 --- /dev/null +++ b/app/services/ServicesDependencies.scala @@ -0,0 +1,19 @@ +package services + +import akka.actor.ActorSystem +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} + +class ServicesDependencies @Inject() ( + actorSystem: ActorSystem +) { + + /** Gives access to an ExecutionContext for the DB queries, that are + * supposed to be blocking. + * + * https://www.playframework.com/documentation/2.8.x/ThreadPools#Many-specific-thread-pools + */ + implicit val databaseExecutionContext: ExecutionContext = + actorSystem.dispatchers.lookup("contexts.blocking-db-queries") + +} diff --git a/app/services/UserGroupService.scala b/app/services/UserGroupService.scala index dabc8bf77..547dd047a 100644 --- a/app/services/UserGroupService.scala +++ b/app/services/UserGroupService.scala @@ -2,6 +2,7 @@ package services import java.sql.ResultSet import java.util.UUID +import scala.concurrent.Future import anorm._ import javax.inject.Inject @@ -12,7 +13,13 @@ import helper.{Time, UUIDHelper} import org.postgresql.util.PSQLException @javax.inject.Singleton -class UserGroupService @Inject() (configuration: play.api.Configuration, db: Database) { +class UserGroupService @Inject() ( + configuration: play.api.Configuration, + db: Database, + dependencies: ServicesDependencies +) { + + import dependencies.databaseExecutionContext private val simpleUserGroup: RowParser[UserGroup] = Macro .parser[UserGroup]( @@ -114,10 +121,12 @@ class UserGroupService @Inject() (configuration: play.api.Configuration, db: Dat cardinality == 0 } - def byArea(areaId: UUID): List[UserGroup] = db.withConnection { implicit connection => - SQL("""SELECT * FROM "user_group" WHERE area_ids @> ARRAY[{areaId}]::uuid[]""") - .on('areaId -> areaId) - .as(simpleUserGroup.*) + def byArea(areaId: UUID): Future[List[UserGroup]] = Future { + db.withConnection { implicit connection => + SQL("""SELECT * FROM "user_group" WHERE area_ids @> ARRAY[{areaId}]::uuid[]""") + .on('areaId -> areaId) + .as(simpleUserGroup.*) + } } def byAreas(areaIds: List[UUID]): List[UserGroup] = db.withConnection { implicit connection => diff --git a/conf/application.conf b/conf/application.conf index 315567243..6670dd4c5 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -53,6 +53,17 @@ db.default.driver = ${?DATABASE_DRIVER} db.default.url = ${?DATABASE_URL} db.default.logSql=${?DEBUG} +### DB Execution Context +# https://www.playframework.com/documentation/2.8.x/ThreadPools#Many-specific-thread-pools +# This pool is for the blocking queries done by Anorm +contexts.blocking-db-queries { + executor = "thread-pool-executor" + throughput = 1 + thread-pool-executor { + fixed-pool-size = 10 + } +} + ### Mail play.mailer { host = ${?MAIL_HOST}