From 2cc1044608fd063226bc71af98d9191da81198eb Mon Sep 17 00:00:00 2001 From: niladic Date: Wed, 15 Jan 2025 08:31:06 +0100 Subject: [PATCH] =?UTF-8?q?Ajoute=20la=20notion=20de=20r=C3=A9seau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/ApiController.scala | 1 + app/controllers/ApplicationController.scala | 255 +++++++++++--------- app/controllers/CSVImportController.scala | 2 + app/models/Application.scala | 2 + app/models/UserGroup.scala | 1 + app/models/dataModels.scala | 3 + app/models/forms.scala | 1 + app/serializers/ApiModel.scala | 8 +- app/services/ApplicationService.scala | 7 +- app/services/NotificationService.scala | 2 +- app/services/UserGroupService.scala | 66 +++-- app/views/addGroup.scala | 15 ++ app/views/application.scala | 25 +- app/views/createApplication.scala.html | 6 + app/views/editGroup.scala.html | 7 + app/views/editMyGroups.scala | 23 +- app/views/showApplication.scala.html | 12 +- conf/evolutions/default/80.sql | 10 + typescript/src/applicationsAdmin.ts | 6 + typescript/src/users.ts | 11 + 20 files changed, 305 insertions(+), 158 deletions(-) create mode 100644 conf/evolutions/default/80.sql diff --git a/app/controllers/ApiController.scala b/app/controllers/ApiController.scala index 9cfb102da..9ba0299dd 100644 --- a/app/controllers/ApiController.scala +++ b/app/controllers/ApiController.scala @@ -140,6 +140,7 @@ case class ApiController @Inject() ( .toList, organisationId = Organisation.franceServicesId.some, email = newLine.email.map(StringHelper.commonStringInputNormalization), + isInFranceServicesNetwork = true, publicNote = none, internalSupportComment = newLine.internalSupportComment .map(StringHelper.commonStringInputNormalization), diff --git a/app/controllers/ApplicationController.scala b/app/controllers/ApplicationController.scala index b279cf0c0..e73676441 100644 --- a/app/controllers/ApplicationController.scala +++ b/app/controllers/ApplicationController.scala @@ -141,12 +141,17 @@ case class ApplicationController @Inject() ( ) } + /** Groups with instructors available to the current user */ + private def fetchGroupsWithInstructors( areaId: UUID, currentUser: User, - rights: Authorization.UserRights + rights: Authorization.UserRights, + currentUserGroups: List[UserGroup] ): Future[(List[UserGroup], List[User], List[User])] = { - val groupsOfAreaFuture = userGroupService.byArea(areaId) + val hasFranceServicesAccess = currentUserGroups.exists(_.isInFranceServicesNetwork) + val groupsOfAreaFuture = + userGroupService.byArea(areaId, excludeFranceServicesNetwork = !hasFranceServicesAccess) groupsOfAreaFuture.map { groupsOfArea => val visibleGroups = filterVisibleGroups(areaId, currentUser, rights)(groupsOfArea) val usersInThoseGroups = userService.byGroupIds(visibleGroups.map(_.id)) @@ -199,21 +204,25 @@ case class ApplicationController @Inject() ( userGroupService .byIdsFuture(request.currentUser.groupIds) .flatMap(userGroups => - fetchGroupsWithInstructors(currentArea.id, request.currentUser, request.rights).map { - case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => - val categories = organisationService.categories - Ok( - views.html.createApplication(request.currentUser, request.rights, currentArea)( - userGroups, - instructorsOfGroups, - groupsOfAreaWithInstructor, - coworkers, - readSharedAccountUserSignature(request.session), - canCreatePhoneMandat = currentArea === Area.calvados, - categories, - ApplicationFormData.form(request.currentUser) - ) + fetchGroupsWithInstructors( + currentArea.id, + request.currentUser, + request.rights, + userGroups + ).map { case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => + val categories = organisationService.categories + Ok( + views.html.createApplication(request.currentUser, request.rights, currentArea)( + userGroups, + instructorsOfGroups, + groupsOfAreaWithInstructor, + coworkers, + readSharedAccountUserSignature(request.session), + canCreatePhoneMandat = currentArea === Area.calvados, + categories, + ApplicationFormData.form(request.currentUser) ) + ) } ) ) @@ -275,25 +284,29 @@ case class ApplicationController @Inject() ( userGroupService .byIdsFuture(request.currentUser.groupIds) .flatMap(userGroups => - fetchGroupsWithInstructors(currentArea.id, request.currentUser, request.rights).map { - case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => - val message = - "Erreur lors de l'envoi de fichiers. Cette erreur est possiblement temporaire." - BadRequest( - views.html - .createApplication(request.currentUser, request.rights, currentArea)( - userGroups, - instructorsOfGroups, - groupsOfAreaWithInstructor, - coworkers, - None, - canCreatePhoneMandat = currentArea === Area.calvados, - organisationService.categories, - form, - Nil, - ) - ) - .flashing("application-error" -> message) + fetchGroupsWithInstructors( + currentArea.id, + request.currentUser, + request.rights, + userGroups + ).map { case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => + val message = + "Erreur lors de l'envoi de fichiers. Cette erreur est possiblement temporaire." + BadRequest( + views.html + .createApplication(request.currentUser, request.rights, currentArea)( + userGroups, + instructorsOfGroups, + groupsOfAreaWithInstructor, + coworkers, + None, + canCreatePhoneMandat = currentArea === Area.calvados, + organisationService.categories, + form, + Nil, + ) + ) + .flashing("application-error" -> message) } ) } { files => @@ -303,7 +316,12 @@ case class ApplicationController @Inject() ( userGroupService .byIdsFuture(request.currentUser.groupIds) .flatMap(userGroups => - fetchGroupsWithInstructors(currentArea.id, request.currentUser, request.rights) + fetchGroupsWithInstructors( + currentArea.id, + request.currentUser, + request.rights, + userGroups + ) .map { case (groupsOfAreaWithInstructor, instructorsOfGroups, coworkers) => eventService.log( EventType.ApplicationCreationInvalid, @@ -358,6 +376,8 @@ case class ApplicationController @Inject() ( if infoName.trim.nonEmpty && infoValue.trim.nonEmpty => infoName.trim -> infoValue.trim } + + val isInFranceServicesNetwork = creatorGroup.exists(_.isInFranceServicesNetwork) val application = Application( applicationId, Time.nowParis(), @@ -380,7 +400,8 @@ case class ApplicationController @Inject() ( category = applicationData.category, mandatType = Application.MandatType.Paper.some, mandatDate = Some(applicationData.mandatDate), - invitedGroupIdsAtCreation = applicationData.groups + invitedGroupIdsAtCreation = applicationData.groups, + isInFranceServicesNetwork = isInFranceServicesNetwork, ) if (applicationService.createApplication(application)) { notificationsService.newApplication(application) @@ -1120,33 +1141,41 @@ case class ApplicationController @Inject() ( private def usersWhoCanBeInvitedOn(application: Application, currentAreaId: UUID)(implicit request: RequestWithUserData[_] - ): Future[List[User]] = - (if (request.currentUser.expert || request.currentUser.admin) { - val creator = userService.byId(application.creatorUserId, includeDisabled = true) - val creatorGroups: Set[UUID] = creator.toList.flatMap(_.groupIds).toSet - userGroupService.byArea(currentAreaId).map { groupsOfArea => - userService - .byGroupIds(groupsOfArea.map(_.id)) - .filter(user => user.instructor || user.groupIds.toSet.intersect(creatorGroups).nonEmpty) - } - } else { - // 1. coworkers - val coworkers = Future(userService.byGroupIds(request.currentUser.groupIds)) - // 2. coworkers of instructors that are already on the application - // these will mostly be the ones that have been added as users after - // the application has been sent. - val instructorsCoworkers = { - val invitedUsers: List[User] = - userService.byIds(application.invitedUsers.keys.toList, includeDisabled = true) - val groupsOfInvitedUsers: Set[UUID] = invitedUsers.flatMap(_.groupIds).toSet - userGroupService.byArea(application.area).map { groupsOfArea => - val invitedGroups: Set[UUID] = - groupsOfInvitedUsers.intersect(groupsOfArea.map(_.id).toSet) - userService.byGroupIds(invitedGroups.toList).filter(_.instructor) - } - } - coworkers.combine(instructorsCoworkers) - }) + ): Future[List[User]] = { + val excludeFranceServicesNetwork = !application.isInFranceServicesNetwork + if (request.currentUser.expert || request.currentUser.admin) { + val creator = userService.byId(application.creatorUserId, includeDisabled = true) + val creatorGroups: Set[UUID] = creator.toList.flatMap(_.groupIds).toSet + userGroupService + .byArea(currentAreaId, excludeFranceServicesNetwork = excludeFranceServicesNetwork) + .map { groupsOfArea => + userService + .byGroupIds(groupsOfArea.map(_.id)) + .filter(user => + user.instructor || user.groupIds.toSet.intersect(creatorGroups).nonEmpty + ) + } + } else { + // 1. coworkers + val coworkers = Future(userService.byGroupIds(request.currentUser.groupIds)) + // 2. coworkers of instructors that are already on the application + // these will mostly be the ones that have been added as users after + // the application has been sent. + val instructorsCoworkers = { + val invitedUsers: List[User] = + userService.byIds(application.invitedUsers.keys.toList, includeDisabled = true) + val groupsOfInvitedUsers: Set[UUID] = invitedUsers.flatMap(_.groupIds).toSet + userGroupService + .byArea(application.area, excludeFranceServicesNetwork = excludeFranceServicesNetwork) + .map { groupsOfArea => + val invitedGroups: Set[UUID] = + groupsOfInvitedUsers.intersect(groupsOfArea.map(_.id).toSet) + userService.byGroupIds(invitedGroups.toList).filter(_.instructor) + } + } + coworkers.combine(instructorsCoworkers) + } + } .map( _.filterNot(user => user.id === request.currentUser.id || application.invitedUsers.contains(user.id) @@ -1162,19 +1191,22 @@ case class ApplicationController @Inject() ( userService.byIds(application.invitedUsers.keys.toList, includeDisabled = true) // Groups already present on the Application val groupsOfInvitedUsers: Set[UUID] = invitedUsers.flatMap(_.groupIds).toSet - userGroupService.byArea(forAreaId).map { groupsOfArea => - val groupsThatAreNotInvited = - groupsOfArea.filterNot(group => groupsOfInvitedUsers.contains(group.id)) - val groupIdsWithInstructors: Set[UUID] = - userService - .byGroupIds(groupsThatAreNotInvited.map(_.id)) - .filter(_.instructor) - .flatMap(_.groupIds) - .toSet - val groupsThatAreNotInvitedWithInstructor = - groupsThatAreNotInvited.filter(user => groupIdsWithInstructors.contains(user.id)) - groupsThatAreNotInvitedWithInstructor.sortBy(_.name) - } + val excludeFranceServicesNetwork = !application.isInFranceServicesNetwork + userGroupService + .byArea(forAreaId, excludeFranceServicesNetwork = excludeFranceServicesNetwork) + .map { groupsOfArea => + val groupsThatAreNotInvited = + groupsOfArea.filterNot(group => groupsOfInvitedUsers.contains(group.id)) + val groupIdsWithInstructors: Set[UUID] = + userService + .byGroupIds(groupsThatAreNotInvited.map(_.id)) + .filter(_.instructor) + .flatMap(_.groupIds) + .toSet + val groupsThatAreNotInvitedWithInstructor = + groupsThatAreNotInvited.filter(user => groupIdsWithInstructors.contains(user.id)) + groupsThatAreNotInvitedWithInstructor.sortBy(_.name) + } } def applicationInvitableGroups(applicationId: UUID, areaId: UUID): Action[AnyContent] = @@ -1206,41 +1238,44 @@ case class ApplicationController @Inject() ( application.answers.map(_.creatorUserID) usersWhoCanBeInvitedOn(application, selectedAreaId).flatMap { usersWhoCanBeInvited => groupsWhichCanBeInvited(selectedAreaId, application).flatMap { invitableGroups => - val filesF = EitherT(fileService.byApplicationId(application.id)) - val organisationsF = EitherT(userService.usersOrganisations(applicationUsers)) - (for { - files <- filesF - organisations <- organisationsF - } yield (files, organisations)).value - .map( - _.fold( - error => { - eventService.logError(error) - InternalServerError(Constants.genericError500Message) - }, - { case (files, organisations) => - val groups = userGroupService - .byIds(usersWhoCanBeInvited.flatMap(_.groupIds)) - .filter(_.areaIds.contains[UUID](selectedAreaId)) - val groupsWithUsersThatCanBeInvited = groups.map { group => - group -> usersWhoCanBeInvited.filter(_.groupIds.contains[UUID](group.id)) + userGroupService.byIdsFuture(request.currentUser.groupIds).flatMap { userGroups => + val filesF = EitherT(fileService.byApplicationId(application.id)) + val organisationsF = EitherT(userService.usersOrganisations(applicationUsers)) + (for { + files <- filesF + organisations <- organisationsF + } yield (files, organisations)).value + .map( + _.fold( + error => { + eventService.logError(error) + InternalServerError(Constants.genericError500Message) + }, + { case (files, organisations) => + val groups = userGroupService + .byIds(usersWhoCanBeInvited.flatMap(_.groupIds)) + .filter(_.areaIds.contains[UUID](selectedAreaId)) + val groupsWithUsersThatCanBeInvited = groups.map { group => + group -> usersWhoCanBeInvited.filter(_.groupIds.contains[UUID](group.id)) + } + toResult( + views.html.showApplication(request.currentUser, request.rights)( + userGroups, + groupsWithUsersThatCanBeInvited, + invitableGroups, + application, + form, + openedTab, + selectedArea, + readSharedAccountUserSignature(request.session), + files, + organisations + ) + ).withHeaders(CACHE_CONTROL -> "no-store") } - toResult( - views.html.showApplication(request.currentUser, request.rights)( - groupsWithUsersThatCanBeInvited, - invitableGroups, - application, - form, - openedTab, - selectedArea, - readSharedAccountUserSignature(request.session), - files, - organisations - ) - ).withHeaders(CACHE_CONTROL -> "no-store") - } + ) ) - ) + } } } } diff --git a/app/controllers/CSVImportController.scala b/app/controllers/CSVImportController.scala index 1dd0e420c..11fdc6ee1 100644 --- a/app/controllers/CSVImportController.scala +++ b/app/controllers/CSVImportController.scala @@ -135,6 +135,7 @@ case class CSVImportController @Inject() ( areaIds = group.group.areaIds, organisationId = group.group.organisationId, email = group.group.email, + isInFranceServicesNetwork = true, publicNote = None, internalSupportComment = None ), @@ -196,6 +197,7 @@ case class CSVImportController @Inject() ( _.exists(Organisation.isValidId) ), "email" -> optional(email), + "isInFranceServicesNetwork" -> ignored(true), "publicNote" -> ignored(Option.empty[String]), "internalSupportComment" -> ignored(Option.empty[String]) )(UserGroup.apply)(toTupleOpt) diff --git a/app/models/Application.scala b/app/models/Application.scala index 114041c63..9d05d2cb1 100644 --- a/app/models/Application.scala +++ b/app/models/Application.scala @@ -39,6 +39,7 @@ case class Application( mandatType: Option[Application.MandatType], mandatDate: Option[String], invitedGroupIdsAtCreation: List[UUID], + isInFranceServicesNetwork: Boolean, personalDataWiped: Boolean = false, ) extends AgeModel { @@ -253,6 +254,7 @@ case class Application( mandatType = wiped.mandatType, mandatDate = anonMandatDate, invitedGroupIdsAtCreation = wiped.invitedGroupIdsAtCreation, + isInFranceServicesNetwork = wiped.isInFranceServicesNetwork, personalDataWiped = wiped.personalDataWiped, ) } diff --git a/app/models/UserGroup.scala b/app/models/UserGroup.scala index 0a6ad5f37..decc218eb 100644 --- a/app/models/UserGroup.scala +++ b/app/models/UserGroup.scala @@ -20,6 +20,7 @@ case class UserGroup( areaIds: List[UUID], organisationId: Option[Organisation.Id] = None, email: Option[String] = None, + isInFranceServicesNetwork: Boolean, // This is a note displayed to users trying to select this group publicNote: Option[String], // This is a comment only visible by the admins diff --git a/app/models/dataModels.scala b/app/models/dataModels.scala index e57badaa0..572746184 100644 --- a/app/models/dataModels.scala +++ b/app/models/dataModels.scala @@ -300,6 +300,7 @@ object dataModels { application.mandatType.map(dataModels.Application.MandatType.dataModelSerialization), mandatDate = application.mandatDate, invitedGroupIds = application.invitedGroupIdsAtCreation, + isInFranceServicesNetwork = application.isInFranceServicesNetwork, personalDataWiped = application.personalDataWiped ) @@ -329,6 +330,7 @@ object dataModels { mandatType: Option[String], mandatDate: Option[String], invitedGroupIds: List[UUID], + isInFranceServicesNetwork: Boolean, personalDataWiped: Boolean, ) { @@ -360,6 +362,7 @@ object dataModels { mandatType = mandatType.flatMap(Application.MandatType.dataModelDeserialization), mandatDate = mandatDate, invitedGroupIdsAtCreation = invitedGroupIds, + isInFranceServicesNetwork = isInFranceServicesNetwork, personalDataWiped = personalDataWiped, ) } diff --git a/app/models/forms.scala b/app/models/forms.scala index 7549f2ad7..043117b6a 100644 --- a/app/models/forms.scala +++ b/app/models/forms.scala @@ -163,6 +163,7 @@ package forms { .verifying("Vous devez sélectionner au moins 1 territoire", _.nonEmpty), "organisation" -> optional(of[Organisation.Id]), "email" -> optional(email), + "isInFranceServicesNetwork" -> default(boolean, false), "publicNote" -> normalizedOptionalText, "internalSupportComment" -> normalizedOptionalText )(UserGroup.apply)(toTupleOpt) diff --git a/app/serializers/ApiModel.scala b/app/serializers/ApiModel.scala index 97d033274..45478a80e 100644 --- a/app/serializers/ApiModel.scala +++ b/app/serializers/ApiModel.scala @@ -262,6 +262,7 @@ object ApiModel { areas = group.areaIds.flatMap(Area.fromId).map(_.toString), organisation = group.organisation.map(_.shortName), email = group.email, + isInFranceServicesNetwork = group.isInFranceServicesNetwork, publicNote = group.publicNote ) @@ -275,6 +276,7 @@ object ApiModel { areas: List[String], organisation: Option[String], email: Option[String], + isInFranceServicesNetwork: Boolean, publicNote: Option[String], ) @@ -331,6 +333,7 @@ object ApiModel { closedDay: Option[String], status: String, currentUserCanSeeAnonymousApplication: Boolean, + network: String, groups: ApplicationMetadata.Groups, stats: ApplicationMetadata.Stats, ) @@ -394,7 +397,7 @@ object ApiModel { ApplicationMetadata( id = application.id, creationDateFormatted = Time.formatForAdmins(application.creationDate.toInstant), - creationDay = Time.formatPatternFr(application.creationDate, "YYY-MM-dd"), + creationDay = Time.formatPatternFr(application.creationDate, "yyyy-MM-dd"), creatorUserName = application.creatorUserName, creatorUserId = application.creatorUserId, areaName = areaName, @@ -405,11 +408,12 @@ object ApiModel { closedDateFormatted = application.closedDate.map(date => Time.formatForAdmins(date.toInstant)), closedDay = application.closedDate.map(date => - Time.formatPatternFr(application.creationDate, "YYY-MM-dd") + Time.formatPatternFr(application.creationDate, "yyyy-MM-dd") ), status = application.status.show, currentUserCanSeeAnonymousApplication = Authorization.canSeeApplication(application)(rights), + network = if (application.isInFranceServicesNetwork) "FS" else "Général", groups = ApplicationMetadata.Groups( creatorUserGroupsNames = creatorUserGroupsNames, creatorGroupName = creatorGroupName, diff --git a/app/services/ApplicationService.scala b/app/services/ApplicationService.scala index c0c8d94e8..be23364d1 100644 --- a/app/services/ApplicationService.scala +++ b/app/services/ApplicationService.scala @@ -49,6 +49,7 @@ class ApplicationService @Inject() ( "mandat_type", "mandat_date", "invited_group_ids", + "is_in_france_services_network", "personal_data_wiped", ) @@ -358,7 +359,8 @@ class ApplicationService @Inject() ( category, mandat_type, mandat_date, - invited_group_ids + invited_group_ids, + is_in_france_services_network ) VALUES ( ${row.id}::uuid, ${row.creationDate}, @@ -375,7 +377,8 @@ class ApplicationService @Inject() ( ${row.category}, ${row.mandatType}, ${row.mandatDate}, - array[${row.invitedGroupIds}]::uuid[] + array[${row.invitedGroupIds}]::uuid[], + ${row.isInFranceServicesNetwork} ) """.executeUpdate() === 1 } diff --git a/app/services/NotificationService.scala b/app/services/NotificationService.scala index 7f64c0f4e..b29ec468c 100644 --- a/app/services/NotificationService.scala +++ b/app/services/NotificationService.scala @@ -118,7 +118,7 @@ class NotificationService @Inject() ( // Send emails to groups allGroups .collect { - case group @ UserGroup(id, _, _, _, _, _, _, Some(email), _, _) + case group @ UserGroup(id, _, _, _, _, _, _, Some(email), _, _, _) if !usersEmails.contains(email) => if (alreadyPresentGroupIds.contains(id)) generateNotificationBALEmail(application, answer.some, users)(group) diff --git a/app/services/UserGroupService.scala b/app/services/UserGroupService.scala index fb6be84bb..03a990d4d 100644 --- a/app/services/UserGroupService.scala +++ b/app/services/UserGroupService.scala @@ -31,6 +31,7 @@ class UserGroupService @Inject() ( "area_ids", "organisation", "email", + "is_in_france_services_network", "public_note", "internal_support_comment" ) @@ -46,17 +47,30 @@ class UserGroupService @Inject() ( groups.foldRight(true) { (group, success) => success && SQL""" - INSERT INTO user_group(id, name, description, insee_code, creation_date, create_by_user_id, area_ids, organisation, email) VALUES ( - ${group.id}::uuid, - ${group.name}, - ${group.description}, - array[${group.inseeCode}]::character varying(5)[], - ${group.creationDate}, - ${UUIDHelper.namedFrom("deprecated")}::uuid, - array[${group.areaIds}]::uuid[], - ${group.organisationId.map(_.id)}, - ${group.email}) - """.executeUpdate() === 1 + INSERT INTO user_group( + id, + name, + description, + insee_code, + creation_date, + create_by_user_id, + area_ids, + organisation, + email, + is_in_france_services_network + ) VALUES ( + ${group.id}::uuid, + ${group.name}, + ${group.description}, + array[${group.inseeCode}]::character varying(5)[], + ${group.creationDate}, + ${UUIDHelper.namedFrom("deprecated")}::uuid, + array[${group.areaIds}]::uuid[], + ${group.organisationId.map(_.id)}, + ${group.email}, + ${group.isInFranceServicesNetwork} + ) + """.executeUpdate() === 1 } } if (result) @@ -89,7 +103,8 @@ class UserGroupService @Inject() ( create_by_user_id, area_ids, organisation, - email + email, + is_in_france_services_network ) VALUES ( ${group.id}::uuid, ${group.name}, @@ -99,8 +114,9 @@ class UserGroupService @Inject() ( ${UUIDHelper.namedFrom("deprecated")}::uuid, array[${group.areaIds}]::uuid[], ${group.organisationId.map(_.id)}, - ${group.email}) - """.executeUpdate() + ${group.email}, + ${group.isInFranceServicesNetwork} + )""".executeUpdate() ().asRight } ).toEither.left.map { @@ -133,6 +149,7 @@ class UserGroupService @Inject() ( organisation = ${group.organisationId.map(_.id)}, area_ids = array[${group.areaIds}]::uuid[], email = ${group.email}, + is_in_france_services_network = ${group.isInFranceServicesNetwork}, public_note = ${group.publicNote}, internal_support_comment = ${group.internalSupportComment} WHERE id = ${group.id}::uuid @@ -195,14 +212,23 @@ class UserGroupService @Inject() ( cardinality === 0 } - def byArea(areaId: UUID): Future[List[UserGroup]] = + def byArea(areaId: UUID, excludeFranceServicesNetwork: Boolean = false): Future[List[UserGroup]] = Future { db.withConnection { implicit connection => - SQL(s"""SELECT $fieldsInSelect - FROM "user_group" - WHERE area_ids @> ARRAY[{areaId}]::uuid[]""") - .on("areaId" -> areaId) - .as(simpleUserGroup.*) + if (excludeFranceServicesNetwork) { + SQL(s"""SELECT $fieldsInSelect + FROM "user_group" + WHERE area_ids @> ARRAY[{areaId}]::uuid[] + AND is_in_france_services_network = false""") + .on("areaId" -> areaId) + .as(simpleUserGroup.*) + } else { + SQL(s"""SELECT $fieldsInSelect + FROM "user_group" + WHERE area_ids @> ARRAY[{areaId}]::uuid[]""") + .on("areaId" -> areaId) + .as(simpleUserGroup.*) + } } } diff --git a/app/views/addGroup.scala b/app/views/addGroup.scala index b2a63eda3..846c67d2d 100644 --- a/app/views/addGroup.scala +++ b/app/views/addGroup.scala @@ -99,6 +99,21 @@ object addGroup { ) ) ), + div( + cls := "single--width-max-content single--margin-top-16px single--margin-bottom-16px", + label( + cls := "mdl-checkbox mdl-js-checkbox", + input( + `type` := "checkbox", + id := "isInFranceServicesNetwork", + name := "isInFranceServicesNetwork", + value := "true", + checked := "checked", + cls := "mdl-checkbox__input" + ), + span(cls := "mdl-checkbox__label", "Ce groupe est restreint au réseau France Services"), + ) + ), button( cls := "mdl-button mdl-js-button mdl-button--raised", `type` := "submit", diff --git a/app/views/application.scala b/app/views/application.scala index ea453aa69..d40f0d01c 100644 --- a/app/views/application.scala +++ b/app/views/application.scala @@ -533,6 +533,7 @@ object application { def inviteForm( currentUser: User, currentUserRights: Authorization.UserRights, + userGroups: List[UserGroup], groupsWithUsersThatCanBeInvited: List[(UserGroup, List[User])], groupsThatCanBeInvited: List[UserGroup], application: Application, @@ -548,16 +549,20 @@ object application { readonly := true, value := selectedArea.id.toString ), - div( - "Territoire concerné : ", - views.helpers - .changeAreaSelect( - selectedArea, - Area.all, - ApplicationController.show(application.id), - "onglet" -> "invitation" - ) - ), + if (currentUser.admin || userGroups.exists(group => group.isInFranceServicesNetwork)) { + div( + "Territoire concerné : ", + views.helpers + .changeAreaSelect( + selectedArea, + Area.all, + ApplicationController.show(application.id), + "onglet" -> "invitation" + ) + ) + } else { + () + }, views.helpers.forms.CSRFInput, groupsWithUsersThatCanBeInvited.nonEmpty.some .filter(identity) diff --git a/app/views/createApplication.scala.html b/app/views/createApplication.scala.html index f162743ef..4ee2a1fdd 100644 --- a/app/views/createApplication.scala.html +++ b/app/views/createApplication.scala.html @@ -32,6 +32,7 @@

Nouvelle demande

* sont obligatoires + @if(userGroups.exists(group => group.isInFranceServicesNetwork)) { @if(mainInfos.config.featureCanSendApplicationsAnywhere) {
Territoire concerné
@@ -44,6 +45,7 @@
Territoire concerné
@toHtml(views.helpers.changeAreaSelect(currentArea, currentUser.areas.flatMap(Area.fromId), routes.ApplicationController.create))
} + } @helper.form(action = routes.ApplicationController.createPost, "enctype" -> "multipart/form-data", "id" -> "create-application-form", "class" -> "aplus-protected-form") { @helper.CSRF.formField @@ -51,6 +53,10 @@
Territoire concerné
+ @if(organisationGroups.isEmpty) { + Il n’existe pas de groupes répondant à des demandes dans ce territoire. + } + @toHtml(views.helpers.applications.creatorGroup(applicationForm, userGroups))
diff --git a/app/views/editGroup.scala.html b/app/views/editGroup.scala.html index 9e3a0d7fb..4b2e07440 100644 --- a/app/views/editGroup.scala.html +++ b/app/views/editGroup.scala.html @@ -90,6 +90,13 @@ +
+ +
+ @if(Authorization.canEditSupportMessages(currentUserRights)) {
diff --git a/app/views/editMyGroups.scala b/app/views/editMyGroups.scala index 94cf209c8..cbed0b695 100644 --- a/app/views/editMyGroups.scala +++ b/app/views/editMyGroups.scala @@ -96,13 +96,22 @@ object editMyGroups { id := s"group-${group.id}", div( div( - cls := "header", - if (Authorization.canEditGroup(group)(currentUserRights)) { - a(href := GroupController.editGroup(group.id).url, group.name) - } else { - group.name - }, - span(cls := "text--font-size-medium single--margin-left-8px", group.description) + cls := "header single--display-flex", + div(cls := "single--flex-grow-1")( + if (Authorization.canEditGroup(group)(currentUserRights)) { + a(href := GroupController.editGroup(group.id).url, group.name) + } else { + group.name + }, + span(cls := "text--font-size-medium single--margin-left-8px", group.description) + ), + div(cls := "single--margin-right-24px text--font-size-medium")( + if (group.isInFranceServicesNetwork) { + "Réseau France Services" + } else { + "" + } + ) ) ), table( diff --git a/app/views/showApplication.scala.html b/app/views/showApplication.scala.html index 54e50b7f8..385c89ade 100644 --- a/app/views/showApplication.scala.html +++ b/app/views/showApplication.scala.html @@ -6,7 +6,7 @@ @import models.Authorization @import serializers.Keys -@(currentUser: User, currentUserRights: Authorization.UserRights)(groupsWithUsersThatCanBeInvited: List[(UserGroup,List[User])], groupsThatCanBeInvited: List[UserGroup], application: Application, answerToAgentsForm: Form[_], openedTab: String, selectedArea: Area, userSignature: Option[String], attachments: List[FileMetadata], usersOrganisations: Map[java.util.UUID, List[Organisation.Id]])(implicit webJarsUtil: org.webjars.play.WebJarsUtil, flash: Flash, messagesProvider: MessagesProvider, request: RequestHeader, mainInfos: MainInfos) +@(currentUser: User, currentUserRights: Authorization.UserRights)(userGroups: List[UserGroup], groupsWithUsersThatCanBeInvited: List[(UserGroup,List[User])], groupsThatCanBeInvited: List[UserGroup], application: Application, answerToAgentsForm: Form[_], openedTab: String, selectedArea: Area, userSignature: Option[String], attachments: List[FileMetadata], usersOrganisations: Map[java.util.UUID, List[Organisation.Id]])(implicit webJarsUtil: org.webjars.play.WebJarsUtil, flash: Flash, messagesProvider: MessagesProvider, request: RequestHeader, mainInfos: MainInfos) @main(currentUser, currentUserRights, modals = toHtml(views.application.closeApplicationModal(application.id)))(s"Demande de ${application.creatorUserName} - ${Area.fromId(application.area).get.name}") { @@ -192,8 +192,8 @@

@application.subject

}
chat_bubble @application.answers.length messages | - Créée il y a @application.ageString (@Time.formatPatternFr(application.creationDate, "dd MMM YYYY - HH:mm")) | - @for(area <- Area.fromId(application.area)){ @area.name} + Créée il y a @application.ageString (@Time.formatPatternFr(application.creationDate, "dd MMM yyyy - HH:mm")) | + @for(area <- Area.fromId(application.area)){ @area.name} @if(application.isInFranceServicesNetwork) { | Réseau France Services }
@@ -202,7 +202,7 @@

@application.subject

} @for(answer <- application.answers) { @for((key, value) <- answer.userInfos.getOrElse(Map())) { - @key: @value (ajouté le @Time.formatPatternFr(answer.creationDate, "E dd MMM YYYY"))
+ @key: @value (ajouté le @Time.formatPatternFr(answer.creationDate, "E dd MMM yyyy"))
} }
@@ -270,7 +270,7 @@

@application.subject

remove_red_eye
- Consultée par l’utilisateur (@Time.formatPatternFr(date.atZone(currentUser.timeZone), "dd MMM YYYY - HH:mm")) + Consultée par l’utilisateur (@Time.formatPatternFr(date.atZone(currentUser.timeZone), "dd MMM yyyy - HH:mm"))
} } @@ -442,7 +442,7 @@
Votre réponse :
- @toHtml(views.application.inviteForm(currentUser, currentUserRights, groupsWithUsersThatCanBeInvited, groupsThatCanBeInvited, application, selectedArea)) + @toHtml(views.application.inviteForm(currentUser, currentUserRights, userGroups, groupsWithUsersThatCanBeInvited, groupsThatCanBeInvited, application, selectedArea))
diff --git a/conf/evolutions/default/80.sql b/conf/evolutions/default/80.sql new file mode 100644 index 000000000..70454c8e8 --- /dev/null +++ b/conf/evolutions/default/80.sql @@ -0,0 +1,10 @@ +-- !Ups + +ALTER TABLE user_group ADD is_in_france_services_network BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE application ADD is_in_france_services_network BOOLEAN NOT NULL DEFAULT true; + + +-- !Downs + +ALTER TABLE application DROP is_in_france_services_network; +ALTER TABLE user_group DROP is_in_france_services_network; diff --git a/typescript/src/applicationsAdmin.ts b/typescript/src/applicationsAdmin.ts index 2e71de390..0c9cbd99a 100644 --- a/typescript/src/applicationsAdmin.ts +++ b/typescript/src/applicationsAdmin.ts @@ -183,6 +183,12 @@ if (window.document.getElementById(applicationsTableId)) { headerFilter: "input", maxWidth: 300, }, + { + title: "Réseau", + field: "network", + headerFilter: "input", + maxWidth: 90, + }, { title: "Invités", field: "stats.numberOfInvitedUsers", diff --git a/typescript/src/users.ts b/typescript/src/users.ts index fc19ef28a..f0278238a 100644 --- a/typescript/src/users.ts +++ b/typescript/src/users.ts @@ -54,6 +54,7 @@ interface UserGroupInfos { areas: Array; organisation: string | null, email: string | null; + isInFranceServicesNetwork: boolean; publicNote: string | null; } @@ -356,6 +357,16 @@ if (window.document.getElementById(usersTableId)) { headerFilter: "input", width: 200, }, + { + title: "FS", + field: "isInFranceServicesNetwork", + formatter: "tickCross", + headerFilter: "tickCross", + headerFilterParams: { tristate: true }, + headerVertical: verticalHeader, + bottomCalc: "count", + width: 40, + }, { title: "Description", field: "description",