From 06ec049de6ad7d07488a1cf181bf8b57f5c13557 Mon Sep 17 00:00:00 2001 From: mdulac Date: Fri, 30 Oct 2020 16:33:43 +0100 Subject: [PATCH 1/2] Application / Mandat model Refactoring --- app/controllers/ApplicationController.scala | 13 ++- app/models/Answer.scala | 35 +++++++ app/models/Application.scala | 94 ++++++++++++++--- app/serializers/DataModel.scala | 37 ++++--- app/serializers/JsonFormats.scala | 35 +++---- app/services/ApplicationService.scala | 109 +++++--------------- app/views/createApplication.scala.html | 14 +-- app/views/showApplication.scala.html | 12 +-- app/views/stats/StatsData.scala | 12 +-- test/browser/AnswerSpec.scala | 6 +- test/browser/ApplicationAccessSpec.scala | 5 +- test/models/ApplicationSpec.scala | 10 +- 12 files changed, 210 insertions(+), 172 deletions(-) diff --git a/app/controllers/ApplicationController.scala b/app/controllers/ApplicationController.scala index 17b41e2b5..3d49ec678 100644 --- a/app/controllers/ApplicationController.scala +++ b/app/controllers/ApplicationController.scala @@ -5,6 +5,7 @@ import java.time.{LocalDate, ZonedDateTime} import java.util.UUID import actions._ +import cats.implicits.catsSyntaxOptionId import cats.syntax.all._ import constants.Constants import forms.FormsPlusMap @@ -228,9 +229,9 @@ case class ApplicationController @Inject() ( val capitalizedUserName = user.name.split(' ').map(_.capitalize).mkString(" ") if (contexts.isEmpty) - s"${capitalizedUserName} ( ${user.qualite} )" + s"$capitalizedUserName ( ${user.qualite} )" else - s"${capitalizedUserName} ${contexts.mkString(",")}" + s"$capitalizedUserName ${contexts.mkString(",")}" } def createPost: Action[AnyContent] = @@ -314,9 +315,11 @@ case class ApplicationController @Inject() ( applicationData.selectedSubject.contains[String](applicationData.subject), category = applicationData.category, files = newAttachments ++ pendingAttachments, - mandatType = DataModel.Application.MandatType - .dataModelDeserialization(applicationData.mandatType), - mandatDate = Some(applicationData.mandatDate) + mandat = ( + DataModel.Application.MandatType + .dataModelDeserialization(applicationData.mandatType), + applicationData.mandatDate.some + ).mapN(Application.Mandat.apply) ) if (applicationService.createApplication(application)) { notificationsService.newApplication(application) diff --git a/app/models/Answer.scala b/app/models/Answer.scala index eb9ed9589..82ff92b0e 100644 --- a/app/models/Answer.scala +++ b/app/models/Answer.scala @@ -3,6 +3,15 @@ package models import java.time.ZonedDateTime import java.util.UUID +import anorm.Column.nonNull +import anorm.{MetaDataItem, TypeDoesNotMatch} +import cats.implicits.catsSyntaxEitherId +import helper.Time +import play.api.libs.json.{Json, Reads, Writes} +import serializers.Anorm.className + +import serializers.JsonFormats._ + case class Answer( id: UUID, applicationId: UUID, @@ -24,3 +33,29 @@ case class Answer( } } + +object Answer { + + implicit val Reads: Reads[Answer] = Json + .reads[Answer] + .map(answer => + answer.copy(creationDate = answer.creationDate.withZoneSameInstant(Time.timeZoneParis)) + ) + + implicit val Writes: Writes[Answer] = Json.writes[Answer] + + implicit val answerListParser: anorm.Column[List[Answer]] = + nonNull { (value, meta) => + val MetaDataItem(qualified, _, _) = meta + value match { + case json: org.postgresql.util.PGobject => + Json.parse(json.getValue).as[List[Answer]].asRight[Nothing] + case json: String => Json.parse(json).as[List[Answer]].asRight[Nothing] + case _ => + TypeDoesNotMatch( + s"Cannot convert $value: ${className(value)} to List[Answer] for column $qualified" + ).asLeft[List[Answer]] + } + } + +} diff --git a/app/models/Application.scala b/app/models/Application.scala index 07013dd9e..5d01d2e8f 100644 --- a/app/models/Application.scala +++ b/app/models/Application.scala @@ -4,10 +4,18 @@ import java.time.ZonedDateTime import java.time.temporal.ChronoUnit.MINUTES import java.util.UUID +import anorm.SqlParser.{bool, get, int, str} +import anorm.{~, RowParser} import cats.Eq import cats.syntax.all._ import helper.BooleanHelper.not +import helper.Time +import models.Application.Mandat +import models.Application.Mandat.MandatType import models.Authorization.{isExpert, isHelper, isInstructor, UserRights} +import serializers.Anorm.{fieldsMapLongParser, fieldsMapStringParser, fieldsMapUUIDParser} +import serializers.DataModel +import serializers.JsonFormats._ case class Application( id: UUID, @@ -21,18 +29,17 @@ case class Application( invitedUsers: Map[UUID, String], area: UUID, irrelevant: Boolean, - answers: List[Answer] = List(), + answers: List[Answer] = List.empty[Answer], internalId: Int = -1, closed: Boolean = false, - seenByUserIds: List[UUID] = List(), + seenByUserIds: List[UUID] = List.empty[UUID], usefulness: Option[String] = None, - closedDate: Option[ZonedDateTime] = None, + closedDate: Option[ZonedDateTime] = Option.empty[ZonedDateTime], expertInvited: Boolean = false, hasSelectedSubject: Boolean = false, - category: Option[String] = None, - files: Map[String, Long] = Map(), - mandatType: Option[Application.MandatType], - mandatDate: Option[String] + category: Option[String] = Option.empty[String], + files: Map[String, Long] = Map.empty[String, Long], + mandat: Option[Mandat] ) extends AgeModel { lazy val filesAvailabilityLeftInDays: Option[Int] = if (ageInDays > 8) { @@ -186,14 +193,75 @@ case class Application( object Application { - sealed trait MandatType + implicit val Parser: RowParser[Application] = + (get[UUID]("id") ~ + get[ZonedDateTime]("creation_date").map(_.withZoneSameInstant(Time.timeZoneParis)) ~ + str(columnName = "creator_user_name") ~ + get[UUID]("creator_user_id") ~ + str(columnName = "subject") ~ + str(columnName = "description") ~ + get[Map[String, String]]("user_infos") ~ + get[Map[UUID, String]]("invited_users") ~ + get[UUID]("area") ~ + bool(columnName = "irrelevant") ~ + get[List[Answer]]("answers") ~ + int("internal_id") ~ + bool("closed") ~ + get[List[UUID]]("seen_by_user_ids") ~ + str("usefulness").? ~ + get[ZonedDateTime]("closed_date").? ~ + bool("expert_invited") ~ + bool("has_selected_subject") ~ + str("category").? ~ + get[Map[String, Long]]("files") ~ + Application.Mandat.Parser) + .map { + case id ~ creation ~ creatorName ~ creatorId ~ subject ~ description ~ userInfos ~ invitedUsers + ~ area ~ irrelevant ~ answers ~ internalId ~ closed ~ seen ~ usefulness ~ closedDate ~ experts ~ hasSelectedSubject ~ category ~ files ~ mandat => + Application( + id, + creation, + creatorName, + creatorId, + subject, + description, + userInfos, + invitedUsers, + area, + irrelevant, + answers, + internalId, + closed, + seen, + usefulness, + closedDate, + experts, + hasSelectedSubject, + category, + files, + mandat + ) + } + + final case class Mandat(_type: MandatType, date: String) + + object Mandat { + + implicit val Parser: RowParser[Option[Mandat]] = + (str("mandat_type").?.map( + _.flatMap(DataModel.Application.MandatType.dataModelDeserialization) + ) ~ str("mandat_date").?).map { case _type ~ date => (_type, date).mapN(Mandat.apply) } - object MandatType { - case object Sms extends MandatType - case object Phone extends MandatType - case object Paper extends MandatType + sealed trait MandatType - implicit val Eq: Eq[MandatType] = (x: MandatType, y: MandatType) => x == y + object MandatType { + case object Sms extends MandatType + case object Phone extends MandatType + case object Paper extends MandatType + + implicit val Eq: Eq[MandatType] = (x: MandatType, y: MandatType) => x == y + + } } diff --git a/app/serializers/DataModel.scala b/app/serializers/DataModel.scala index 5d07575f6..eba67aaee 100644 --- a/app/serializers/DataModel.scala +++ b/app/serializers/DataModel.scala @@ -1,6 +1,7 @@ package serializers -import models.Application.MandatType +import cats.implicits.catsSyntaxOptionId +import models.Application.Mandat.MandatType import play.api.libs.json._ object DataModel { @@ -8,7 +9,7 @@ object DataModel { object Application { object MandatType { - import models.Application.MandatType._ + import models.Application.Mandat.MandatType._ def dataModelSerialization(entity: MandatType): String = entity match { @@ -19,9 +20,9 @@ object DataModel { def dataModelDeserialization(raw: String): Option[MandatType] = raw match { - case "sms" => Some(Sms) - case "phone" => Some(Phone) - case "paper" => Some(Paper) + case "sms" => Sms.some + case "phone" => Phone.some + case "paper" => Paper.some case _ => None } @@ -53,20 +54,18 @@ object DataModel { } implicit val smsApiWrites: Writes[Sms] = - Writes( - _ match { - case sms: Sms.Outgoing => - smsOutgoingFormat.writes(sms) match { - case obj: JsObject => obj + ("tag" -> JsString("outgoing")) - case other => other - } - case sms: Sms.Incoming => - smsIncomingFormat.writes(sms) match { - case obj: JsObject => obj + ("tag" -> JsString("incoming")) - case other => other - } - } - ) + Writes { + case sms: Sms.Outgoing => + smsOutgoingFormat.writes(sms) match { + case obj: JsObject => obj + ("tag" -> JsString("outgoing")) + case other => other + } + case sms: Sms.Incoming => + smsIncomingFormat.writes(sms) match { + case obj: JsObject => obj + ("tag" -> JsString("incoming")) + case other => other + } + } } diff --git a/app/serializers/JsonFormats.scala b/app/serializers/JsonFormats.scala index f39c2642c..bdc8749c6 100644 --- a/app/serializers/JsonFormats.scala +++ b/app/serializers/JsonFormats.scala @@ -3,43 +3,38 @@ package serializers import constants.Constants import helper.{StringHelper, UUIDHelper} import java.util.UUID + import models.mandat.{Mandat, SmsMandatInitiation} import play.api.libs.json.Json.JsValueWrapper import play.api.libs.json._ import play.api.libs.functional.syntax._ +import play.api.libs.json.JsonConfiguration.Aux import play.api.mvc.Results.InternalServerError object JsonFormats { - implicit val jsonConfiguration = JsonConfiguration(naming = JsonNaming.SnakeCase) - - implicit val mapUUIDReads = new Reads[Map[UUID, String]] { - - def reads(jv: JsValue): JsResult[Map[UUID, String]] = - JsSuccess(jv.as[Map[String, String]].map { case (k, v) => - UUIDHelper.fromString(k).get -> v.asInstanceOf[String] - }) - - } - implicit val mapUUIDWrites = new Writes[Map[UUID, String]] { + implicit val jsonConfiguration: Aux[Json.MacroOptions] = + JsonConfiguration(naming = JsonNaming.SnakeCase) - def writes(map: Map[UUID, String]): JsValue = - Json.obj(map.map { case (s, o) => - val ret: (String, JsValueWrapper) = s.toString -> JsString(o) - ret - }.toSeq: _*) + implicit val mapUUIDReads: Reads[Map[UUID, String]] = (jv: JsValue) => + JsSuccess(jv.as[Map[String, String]].map { case (k, v) => + UUIDHelper.fromString(k).get -> v.asInstanceOf[String] + }) - } + implicit val mapUUIDWrites: Writes[Map[UUID, String]] = (map: Map[UUID, String]) => + Json.obj(map.map { case (s, o) => + val ret: (String, JsValueWrapper) = s.toString -> JsString(o) + ret + }.toSeq: _*) - implicit val mapUUIDFormat = Format(mapUUIDReads, mapUUIDWrites) + implicit val mapUUIDFormat: Format[Map[UUID, String]] = Format(mapUUIDReads, mapUUIDWrites) // // Mandat // import serializers.DataModel.SmsFormats._ - implicit val mandatIdReads: Reads[Mandat.Id] = - implicitly[Reads[UUID]].map(Mandat.Id.apply) + implicit val mandatIdReads: Reads[Mandat.Id] = implicitly[Reads[UUID]].map(Mandat.Id.apply) implicit val mandatIdWrites: Writes[Mandat.Id] = implicitly[Writes[UUID]].contramap((id: Mandat.Id) => id.underlying) diff --git a/app/services/ApplicationService.scala b/app/services/ApplicationService.scala index 15499b379..93965e248 100644 --- a/app/services/ApplicationService.scala +++ b/app/services/ApplicationService.scala @@ -3,10 +3,8 @@ package services import java.time.ZonedDateTime import java.util.UUID -import anorm.Column.nonNull import anorm._ import cats.syntax.all._ -import helper.Time import javax.inject.Inject import models.Authorization.UserRights import models.{Answer, Application, Authorization, Error, EventType} @@ -22,100 +20,38 @@ class ApplicationService @Inject() ( dependencies: ServicesDependencies ) { import dependencies.databaseExecutionContext - import serializers.Anorm._ import serializers.JsonFormats._ - // Note: - // anorm.Column[String] => anorm.Column[Option[Application.MandatType]] does not work - // throws exception "AnormException: 'mandat_type' not found, available columns: ..." - implicit val mandatTypeAnormParser: anorm.Column[Option[Application.MandatType]] = - implicitly[anorm.Column[Option[String]]] - .map(_.flatMap(DataModel.Application.MandatType.dataModelDeserialization)) - - private implicit val answerReads = Json.reads[Answer] - private implicit val answerWrite = Json.writes[Answer] - - implicit val answerListParser: anorm.Column[List[Answer]] = - nonNull { (value, meta) => - val MetaDataItem(qualified, _, _) = meta - value match { - case json: org.postgresql.util.PGobject => - Right(Json.parse(json.getValue).as[List[Answer]]) - case json: String => - Right(Json.parse(json).as[List[Answer]]) - case _ => - Left( - TypeDoesNotMatch( - s"Cannot convert $value: ${className(value)} to List[Answer] for column $qualified" - ) - ) - } - } - - private val simpleApplication: RowParser[Application] = Macro - .parser[Application]( - "id", - "creation_date", - "creator_user_name", - "creator_user_id", - "subject", - "description", - "user_infos", - "invited_users", - "area", - "irrelevant", - "answers", // Data have been left bad migrated from answser_unsed - "internal_id", - "closed", - "seen_by_user_ids", - "usefulness", - "closed_date", - "expert_invited", - "has_selected_subject", - "category", - "files", - "mandat_type", - "mandat_date" - ) - .map(application => - application.copy( - creationDate = application.creationDate.withZoneSameInstant(Time.timeZoneParis), - answers = application.answers.map(answer => - answer.copy(creationDate = answer.creationDate.withZoneSameInstant(Time.timeZoneParis)) - ) - ) - ) - def byId(id: UUID, fromUserId: UUID, rights: UserRights): Future[Either[Error, Application]] = Future { db.withConnection { implicit connection => val result = SQL( "UPDATE application SET seen_by_user_ids = seen_by_user_ids || {seen_by_user_id}::uuid WHERE id = {id}::uuid RETURNING *" ).on("id" -> id, "seen_by_user_id" -> fromUserId) - .as(simpleApplication.singleOpt) + .as(Application.Parser.singleOpt) result match { case None => - Left( - Error.EntityNotFound( + Error + .EntityNotFound( EventType.ApplicationNotFound, s"Tentative d'accès à une application inexistante: $id" ) - ) + .asLeft[Application] case Some(application) => if (Authorization.canSeeApplication(application)(rights)) { if (Authorization.canSeePrivateDataOfApplication(application)(rights)) { - Right(application) + application.asRight[Error] } else { - Right(application.anonymousApplication) + application.anonymousApplication.asRight[Error] } } else { - Left( - Error.Authorization( + Error + .Authorization( EventType.ApplicationUnauthorized, s"Tentative d'accès à une application non autorisé: $id" ) - ) + .asLeft[Application] } } } @@ -125,7 +61,7 @@ class ApplicationService @Inject() ( db.withConnection { implicit connection => SQL( s"SELECT * FROM application WHERE closed = false AND age(creation_date) > '$day days' AND expert_invited = false" - ).as(simpleApplication.*) + ).as(Application.Parser.*) } def allOpenOrRecentForUserId( @@ -139,7 +75,7 @@ class ApplicationService @Inject() ( | (closed = FALSE OR DATE_PART('day', {referenceDate} - closed_date) < 30) |ORDER BY creation_date DESC""".stripMargin) .on("userId" -> userId, "referenceDate" -> referenceDate) - .as(simpleApplication.*) + .as(Application.Parser.*) if (anonymous) { result.map(_.anonymousApplication) } else { @@ -156,7 +92,7 @@ class ApplicationService @Inject() ( AND closed = false ORDER BY creation_date DESC""" ).on("userId" -> userId) - .as(simpleApplication.*) + .as(Application.Parser.*) result.map(_.anonymousApplication) } } @@ -166,7 +102,7 @@ class ApplicationService @Inject() ( val result = SQL( "SELECT * FROM application WHERE creator_user_id = {userId}::uuid OR invited_users ?? {userId} ORDER BY creation_date DESC" ).on("userId" -> userId) - .as(simpleApplication.*) + .as(Application.Parser.*) if (anonymous) { result.map(_.anonymousApplication) } else { @@ -178,7 +114,7 @@ class ApplicationService @Inject() ( Future { db.withConnection { implicit connection => SQL"SELECT * FROM application WHERE ARRAY[$userIds]::uuid[] @> ARRAY[creator_user_id]::uuid[] OR ARRAY(select jsonb_object_keys(invited_users))::uuid[] && ARRAY[$userIds]::uuid[] ORDER BY creation_date DESC" - .as(simpleApplication.*) + .as(Application.Parser.*) .map(_.anonymousApplication) } } @@ -188,7 +124,7 @@ class ApplicationService @Inject() ( val result = SQL("SELECT * FROM application WHERE area = {areaId}::uuid ORDER BY creation_date DESC") .on("areaId" -> areaId) - .as(simpleApplication.*) + .as(Application.Parser.*) if (anonymous) { result.map(_.anonymousApplication) } else { @@ -200,7 +136,7 @@ class ApplicationService @Inject() ( Future { db.withConnection { implicit connection => SQL"""SELECT * FROM application WHERE ARRAY[$areaIds]::uuid[] @> ARRAY[area]::uuid[] ORDER BY creation_date DESC""" - .as(simpleApplication.*) + .as(Application.Parser.*) .map(_.anonymousApplication) } } @@ -208,7 +144,7 @@ class ApplicationService @Inject() ( def all(): Future[List[Application]] = Future { db.withConnection { implicit connection => - SQL"""SELECT * FROM application""".as(simpleApplication.*).map(_.anonymousApplication) + SQL"""SELECT * FROM application""".as(Application.Parser.*).map(_.anonymousApplication) } } @@ -218,7 +154,12 @@ class ApplicationService @Inject() ( key.toString -> value }) val mandatType = - newApplication.mandatType.map(DataModel.Application.MandatType.dataModelSerialization) + newApplication.mandat + .map(_._type) + .map(DataModel.Application.MandatType.dataModelSerialization) + + val mandatDate = newApplication.mandat.map(_.date) + SQL""" INSERT INTO application ( id, @@ -243,13 +184,13 @@ class ApplicationService @Inject() ( ${newApplication.subject}, ${newApplication.description}, ${Json.toJson(newApplication.userInfos)}, - ${invitedUserJson}, + $invitedUserJson, ${newApplication.area}::uuid, ${newApplication.hasSelectedSubject}, ${newApplication.category}, ${Json.toJson(newApplication.files)}::jsonb, $mandatType, - ${newApplication.mandatDate} + $mandatDate ) """.executeUpdate() === 1 } diff --git a/app/views/createApplication.scala.html b/app/views/createApplication.scala.html index 3909ccf5c..ff49f73c3 100644 --- a/app/views/createApplication.scala.html +++ b/app/views/createApplication.scala.html @@ -15,7 +15,7 @@ }{
Mes demandes > Nouvelle Demande
- +
@if(applicationForm.hasErrors) {
@@ -77,7 +77,7 @@
@if(applicationForm("users").hasErrors) {

@applicationForm("users").errors.map(_.format).mkString(", ")

} - + @for(organisationGroup <- organisationGroups.sortBy(_.name)) { @defining(organisationGroup.organisationSetOrDeducted) { organisation: Option[Organisation] => @@ -258,12 +258,12 @@
@defining(( if (canCreatePhoneMandat) List( - Application.MandatType.Sms -> "par sms", - Application.MandatType.Phone -> "par téléphone (à l’oral)", - Application.MandatType.Paper -> "par mandat signé" + Application.Mandat.MandatType.Sms -> "par sms", + Application.Mandat.MandatType.Phone -> "par téléphone (à l’oral)", + Application.Mandat.MandatType.Paper -> "par mandat signé" ) else List( - Application.MandatType.Sms -> "par sms", - Application.MandatType.Paper -> "par mandat signé" + Application.Mandat.MandatType.Sms -> "par sms", + Application.Mandat.MandatType.Paper -> "par mandat signé" ) ).map(option => (DataModel.Application.MandatType.dataModelSerialization(option._1), option._2))) { options => @options.map { case (optionValue, optionLabel) => diff --git a/app/views/showApplication.scala.html b/app/views/showApplication.scala.html index 6e4335f85..1b4d60a5b 100644 --- a/app/views/showApplication.scala.html +++ b/app/views/showApplication.scala.html @@ -223,21 +223,21 @@

@application.subject

}

@application.description

- @for(mandatType <- application.mandatType) { + @for(mandat <- application.mandat) { J’atteste avoir recueilli l’autorisation de l’usager - @mandatType match { - case Application.MandatType.Sms => { + @mandat._type match { + case Application.Mandat.MandatType.Sms => { par sms } - case Application.MandatType.Phone => { + case Application.Mandat.MandatType.Phone => { par téléphone (à l’oral) } - case Application.MandatType.Paper => { + case Application.Mandat.MandatType.Paper => { par mandat signé } } - @for(mandatDate <- application.mandatDate) { (@mandatDate) } + (@mandat.date) } @for((filename, fileSize) <- application.files) { diff --git a/app/views/stats/StatsData.scala b/app/views/stats/StatsData.scala index 0520d12e0..ae4f74475 100644 --- a/app/views/stats/StatsData.scala +++ b/app/views/stats/StatsData.scala @@ -78,25 +78,25 @@ object StatsData { ) } - private def applicationsCountByMandat(mandatType: Application.MandatType): List[Int] = + private def applicationsCountByMandat(mandatType: Application.Mandat.MandatType): List[Int] = applicationsGroupedByMonth.map( - _._2.count(application => application.mandatType === mandatType.some) + _._2.count(application => application.mandat.map(_._type) === mandatType.some) ) lazy val applicationsCountByMandatPaper: List[Int] = applicationsGroupedByMonth.map( _._2 .count(application => - application.mandatType.isEmpty || - application.mandatType === Application.MandatType.Paper.some + application.mandat.map(_._type).isEmpty || + application.mandat.map(_._type) === Application.Mandat.MandatType.Paper.some ) ) lazy val applicationsCountByMandatSms: List[Int] = - applicationsCountByMandat(Application.MandatType.Sms) + applicationsCountByMandat(Application.Mandat.MandatType.Sms) lazy val applicationsCountByMandatPhone: List[Int] = - applicationsCountByMandat(Application.MandatType.Phone) + applicationsCountByMandat(Application.Mandat.MandatType.Phone) // Conditional Series diff --git a/test/browser/AnswerSpec.scala b/test/browser/AnswerSpec.scala index ce2fc7749..53e7713d2 100644 --- a/test/browser/AnswerSpec.scala +++ b/test/browser/AnswerSpec.scala @@ -77,7 +77,6 @@ class AnswerSpec extends Specification with Tables with BaseSpec { invitedUsers: List[User], applicationService: ApplicationService ): Application = { - // Create Application val application = Application( UUIDHelper.randomUUID, creationDate = Time.nowParis(), @@ -90,8 +89,9 @@ class AnswerSpec extends Specification with Tables with BaseSpec { area = group.areaIds.head, irrelevant = false, expertInvited = true, - mandatType = Some(Application.MandatType.Paper), - mandatDate = Some(java.time.ZonedDateTime.now().toString) + mandat = Application + .Mandat(Application.MandatType.Paper, java.time.ZonedDateTime.now().toString) + .some ) val result = applicationService.createApplication(application) result must beTrue diff --git a/test/browser/ApplicationAccessSpec.scala b/test/browser/ApplicationAccessSpec.scala index c95d9c023..53adafa0a 100644 --- a/test/browser/ApplicationAccessSpec.scala +++ b/test/browser/ApplicationAccessSpec.scala @@ -281,8 +281,9 @@ class ApplicationAccessSpec extends Specification with Tables with BaseSpec { ), area = area, irrelevant = false, - mandatType = Some(Application.MandatType.Paper), - mandatDate = Some(java.time.ZonedDateTime.now().toString) + mandat = Application + .Mandat(Application.MandatType.Paper, java.time.ZonedDateTime.now().toString) + .some ) applicationService.createApplication(application) diff --git a/test/models/ApplicationSpec.scala b/test/models/ApplicationSpec.scala index fd97420a5..a5ff98e9e 100644 --- a/test/models/ApplicationSpec.scala +++ b/test/models/ApplicationSpec.scala @@ -3,7 +3,6 @@ package models import java.time.ZonedDateTime import java.util.UUID -import models.Application.MandatType import org.junit.runner.RunWith import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner @@ -26,8 +25,7 @@ class ApplicationSpec extends Specification { invitedUsers = Map.empty[UUID, String], area = UUID.randomUUID(), irrelevant = false, - mandatType = Option.empty[MandatType], - mandatDate = Option.empty[String] + mandat = Option.empty[Application.Mandat] ) application.status must equalTo("Clôturée") @@ -77,8 +75,7 @@ class ApplicationSpec extends Specification { invitedUsers = Map.empty[UUID, String], area = UUID.randomUUID(), irrelevant = false, - mandatType = Option.empty[MandatType], - mandatDate = Option.empty[String] + mandat = Option.empty[Application.Mandat] ) application.status must equalTo("Répondu") @@ -127,8 +124,7 @@ class ApplicationSpec extends Specification { invitedUsers = Map.empty[UUID, String], area = UUID.randomUUID(), irrelevant = false, - mandatType = Option.empty[MandatType], - mandatDate = Option.empty[String] + mandat = Option.empty[Application.Mandat] ) application.status must equalTo("Nouvelle") From 5952b29f226c2bbfef9f5a38600a5315fc0274f0 Mon Sep 17 00:00:00 2001 From: mdulac Date: Mon, 2 Nov 2020 20:00:06 +0100 Subject: [PATCH 2/2] Fix after review --- app/controllers/ApplicationController.scala | 2 +- app/models/Application.scala | 62 +----------- app/models/sql/ApplicationRow.scala | 104 ++++++++++++++++++++ app/serializers/DataModel.scala | 44 +++++---- app/services/ApplicationService.scala | 70 +++++++------ app/services/UserService.scala | 6 -- app/views/createApplication.scala.html | 2 +- app/views/showApplication.scala.html | 2 +- app/views/stats/StatsData.scala | 6 +- test/browser/AnswerSpec.scala | 2 +- test/browser/ApplicationAccessSpec.scala | 2 +- 11 files changed, 179 insertions(+), 123 deletions(-) create mode 100644 app/models/sql/ApplicationRow.scala diff --git a/app/controllers/ApplicationController.scala b/app/controllers/ApplicationController.scala index 3d49ec678..8d55502cb 100644 --- a/app/controllers/ApplicationController.scala +++ b/app/controllers/ApplicationController.scala @@ -316,7 +316,7 @@ case class ApplicationController @Inject() ( category = applicationData.category, files = newAttachments ++ pendingAttachments, mandat = ( - DataModel.Application.MandatType + DataModel.Application.Mandat.MandatType .dataModelDeserialization(applicationData.mandatType), applicationData.mandatDate.some ).mapN(Application.Mandat.apply) diff --git a/app/models/Application.scala b/app/models/Application.scala index 5d01d2e8f..f4e28faa3 100644 --- a/app/models/Application.scala +++ b/app/models/Application.scala @@ -4,17 +4,12 @@ import java.time.ZonedDateTime import java.time.temporal.ChronoUnit.MINUTES import java.util.UUID -import anorm.SqlParser.{bool, get, int, str} -import anorm.{~, RowParser} import cats.Eq import cats.syntax.all._ import helper.BooleanHelper.not -import helper.Time import models.Application.Mandat import models.Application.Mandat.MandatType import models.Authorization.{isExpert, isHelper, isInstructor, UserRights} -import serializers.Anorm.{fieldsMapLongParser, fieldsMapStringParser, fieldsMapUUIDParser} -import serializers.DataModel import serializers.JsonFormats._ case class Application( @@ -193,65 +188,10 @@ case class Application( object Application { - implicit val Parser: RowParser[Application] = - (get[UUID]("id") ~ - get[ZonedDateTime]("creation_date").map(_.withZoneSameInstant(Time.timeZoneParis)) ~ - str(columnName = "creator_user_name") ~ - get[UUID]("creator_user_id") ~ - str(columnName = "subject") ~ - str(columnName = "description") ~ - get[Map[String, String]]("user_infos") ~ - get[Map[UUID, String]]("invited_users") ~ - get[UUID]("area") ~ - bool(columnName = "irrelevant") ~ - get[List[Answer]]("answers") ~ - int("internal_id") ~ - bool("closed") ~ - get[List[UUID]]("seen_by_user_ids") ~ - str("usefulness").? ~ - get[ZonedDateTime]("closed_date").? ~ - bool("expert_invited") ~ - bool("has_selected_subject") ~ - str("category").? ~ - get[Map[String, Long]]("files") ~ - Application.Mandat.Parser) - .map { - case id ~ creation ~ creatorName ~ creatorId ~ subject ~ description ~ userInfos ~ invitedUsers - ~ area ~ irrelevant ~ answers ~ internalId ~ closed ~ seen ~ usefulness ~ closedDate ~ experts ~ hasSelectedSubject ~ category ~ files ~ mandat => - Application( - id, - creation, - creatorName, - creatorId, - subject, - description, - userInfos, - invitedUsers, - area, - irrelevant, - answers, - internalId, - closed, - seen, - usefulness, - closedDate, - experts, - hasSelectedSubject, - category, - files, - mandat - ) - } - - final case class Mandat(_type: MandatType, date: String) + final case class Mandat(type_ : MandatType, date: String) object Mandat { - implicit val Parser: RowParser[Option[Mandat]] = - (str("mandat_type").?.map( - _.flatMap(DataModel.Application.MandatType.dataModelDeserialization) - ) ~ str("mandat_date").?).map { case _type ~ date => (_type, date).mapN(Mandat.apply) } - sealed trait MandatType object MandatType { diff --git a/app/models/sql/ApplicationRow.scala b/app/models/sql/ApplicationRow.scala new file mode 100644 index 000000000..d0910a8ae --- /dev/null +++ b/app/models/sql/ApplicationRow.scala @@ -0,0 +1,104 @@ +package models.sql + +import java.time.ZonedDateTime +import java.util.UUID + +import anorm.{Macro, RowParser} +import cats.implicits.catsSyntaxTuple2Semigroupal +import helper.Time +import models.Application.Mandat +import models.{Answer, Application} +import serializers.DataModel + +final case class ApplicationRow( + id: UUID, + creationDate: ZonedDateTime, + creatorUserName: String, + creatorUserId: UUID, + subject: String, + description: String, + userInfos: Map[String, String], + invitedUsers: Map[UUID, String], + area: UUID, + irrelevant: Boolean, + answers: List[Answer] = List.empty[Answer], + internalId: Int = -1, + closed: Boolean = false, + seenByUserIds: List[UUID] = List.empty[UUID], + usefulness: Option[String] = Option.empty[String], + closedDate: Option[ZonedDateTime] = Option.empty[ZonedDateTime], + expertInvited: Boolean = false, + hasSelectedSubject: Boolean = false, + category: Option[String] = Option.empty[String], + files: Map[String, Long] = Map.empty[String, Long], + mandatType: Option[Application.Mandat.MandatType], + mandatDate: Option[String] +) + +object ApplicationRow { + + import serializers.Anorm._ + import DataModel.Application.Mandat.MandatType.MandatTypeParser + + val ApplicationRowParser: RowParser[ApplicationRow] = Macro + .parser[ApplicationRow]( + "id", + "creation_date", + "creator_user_name", + "creator_user_id", + "subject", + "description", + "user_infos", + "invited_users", + "area", + "irrelevant", + "answers", + "internal_id", + "closed", + "seen_by_user_ids", + "usefulness", + "closed_date", + "expert_invited", + "has_selected_subject", + "category", + "files", + "mandat_type", + "mandat_date" + ) + .map(application => + application.copy( + creationDate = application.creationDate.withZoneSameInstant(Time.timeZoneParis), + answers = application.answers.map(answer => + answer.copy(creationDate = answer.creationDate.withZoneSameInstant(Time.timeZoneParis)) + ) + ) + ) + + def toApplication(row: ApplicationRow): Application = { + import row._ + Application( + id, + creationDate, + creatorUserName, + creatorUserId, + subject, + description = description, + userInfos = userInfos, + invitedUsers = invitedUsers, + area = area, + irrelevant = irrelevant, + answers = answers, + internalId = internalId, + closed = closed, + seenByUserIds = seenByUserIds, + usefulness = usefulness, + closedDate = closedDate, + expertInvited = expertInvited, + hasSelectedSubject = hasSelectedSubject, + category = category, + files = files, + mandat = (row.mandatType, row.mandatDate).mapN(Mandat.apply) + ) + } + +} diff --git a/app/serializers/DataModel.scala b/app/serializers/DataModel.scala index eba67aaee..78d317c60 100644 --- a/app/serializers/DataModel.scala +++ b/app/serializers/DataModel.scala @@ -1,5 +1,6 @@ package serializers +import anorm.Column import cats.implicits.catsSyntaxOptionId import models.Application.Mandat.MandatType import play.api.libs.json._ @@ -8,23 +9,32 @@ object DataModel { object Application { - object MandatType { - import models.Application.Mandat.MandatType._ - - def dataModelSerialization(entity: MandatType): String = - entity match { - case Sms => "sms" - case Phone => "phone" - case Paper => "paper" - } - - def dataModelDeserialization(raw: String): Option[MandatType] = - raw match { - case "sms" => Sms.some - case "phone" => Phone.some - case "paper" => Paper.some - case _ => None - } + object Mandat { + + object MandatType { + + import models.Application.Mandat.MandatType._ + + implicit val MandatTypeParser: Column[Option[MandatType]] = + implicitly[Column[Option[String]]] + .map(_.flatMap(dataModelDeserialization)) + + def dataModelSerialization(entity: MandatType): String = + entity match { + case Sms => "sms" + case Phone => "phone" + case Paper => "paper" + } + + def dataModelDeserialization(raw: String): Option[MandatType] = + raw match { + case "sms" => Sms.some + case "phone" => Phone.some + case "paper" => Paper.some + case _ => None + } + + } } diff --git a/app/services/ApplicationService.scala b/app/services/ApplicationService.scala index 93965e248..9c2b4d75b 100644 --- a/app/services/ApplicationService.scala +++ b/app/services/ApplicationService.scala @@ -7,6 +7,7 @@ import anorm._ import cats.syntax.all._ import javax.inject.Inject import models.Authorization.UserRights +import models.sql.ApplicationRow import models.{Answer, Application, Authorization, Error, EventType} import play.api.db.Database import play.api.libs.json.Json @@ -29,7 +30,8 @@ class ApplicationService @Inject() ( val result = SQL( "UPDATE application SET seen_by_user_ids = seen_by_user_ids || {seen_by_user_id}::uuid WHERE id = {id}::uuid RETURNING *" ).on("id" -> id, "seen_by_user_id" -> fromUserId) - .as(Application.Parser.singleOpt) + .as(ApplicationRow.ApplicationRowParser.singleOpt) + .map(ApplicationRow.toApplication) result match { case None => Error @@ -61,7 +63,7 @@ class ApplicationService @Inject() ( db.withConnection { implicit connection => SQL( s"SELECT * FROM application WHERE closed = false AND age(creation_date) > '$day days' AND expert_invited = false" - ).as(Application.Parser.*) + ).as(ApplicationRow.ApplicationRowParser.*).map(ApplicationRow.toApplication) } def allOpenOrRecentForUserId( @@ -75,12 +77,10 @@ class ApplicationService @Inject() ( | (closed = FALSE OR DATE_PART('day', {referenceDate} - closed_date) < 30) |ORDER BY creation_date DESC""".stripMargin) .on("userId" -> userId, "referenceDate" -> referenceDate) - .as(Application.Parser.*) - if (anonymous) { - result.map(_.anonymousApplication) - } else { - result - } + .as(ApplicationRow.ApplicationRowParser.*) + .map(ApplicationRow.toApplication) + if (anonymous) result.map(_.anonymousApplication) + else result } def allOpenAndCreatedByUserIdAnonymous(userId: UUID): Future[List[Application]] = @@ -92,7 +92,8 @@ class ApplicationService @Inject() ( AND closed = false ORDER BY creation_date DESC""" ).on("userId" -> userId) - .as(Application.Parser.*) + .as(ApplicationRow.ApplicationRowParser.*) + .map(ApplicationRow.toApplication) result.map(_.anonymousApplication) } } @@ -102,7 +103,8 @@ class ApplicationService @Inject() ( val result = SQL( "SELECT * FROM application WHERE creator_user_id = {userId}::uuid OR invited_users ?? {userId} ORDER BY creation_date DESC" ).on("userId" -> userId) - .as(Application.Parser.*) + .as(ApplicationRow.ApplicationRowParser.*) + .map(ApplicationRow.toApplication) if (anonymous) { result.map(_.anonymousApplication) } else { @@ -114,7 +116,8 @@ class ApplicationService @Inject() ( Future { db.withConnection { implicit connection => SQL"SELECT * FROM application WHERE ARRAY[$userIds]::uuid[] @> ARRAY[creator_user_id]::uuid[] OR ARRAY(select jsonb_object_keys(invited_users))::uuid[] && ARRAY[$userIds]::uuid[] ORDER BY creation_date DESC" - .as(Application.Parser.*) + .as(ApplicationRow.ApplicationRowParser.*) + .map(ApplicationRow.toApplication) .map(_.anonymousApplication) } } @@ -124,7 +127,8 @@ class ApplicationService @Inject() ( val result = SQL("SELECT * FROM application WHERE area = {areaId}::uuid ORDER BY creation_date DESC") .on("areaId" -> areaId) - .as(Application.Parser.*) + .as(ApplicationRow.ApplicationRowParser.*) + .map(ApplicationRow.toApplication) if (anonymous) { result.map(_.anonymousApplication) } else { @@ -136,7 +140,8 @@ class ApplicationService @Inject() ( Future { db.withConnection { implicit connection => SQL"""SELECT * FROM application WHERE ARRAY[$areaIds]::uuid[] @> ARRAY[area]::uuid[] ORDER BY creation_date DESC""" - .as(Application.Parser.*) + .as(ApplicationRow.ApplicationRowParser.*) + .map(ApplicationRow.toApplication) .map(_.anonymousApplication) } } @@ -144,21 +149,24 @@ class ApplicationService @Inject() ( def all(): Future[List[Application]] = Future { db.withConnection { implicit connection => - SQL"""SELECT * FROM application""".as(Application.Parser.*).map(_.anonymousApplication) + SQL"""SELECT * FROM application""" + .as(ApplicationRow.ApplicationRowParser.*) + .map(ApplicationRow.toApplication) + .map(_.anonymousApplication) } } - def createApplication(newApplication: Application) = + def createApplication(application: Application) = db.withConnection { implicit connection => - val invitedUserJson = Json.toJson(newApplication.invitedUsers.map { case (key, value) => + val invitedUserJson = Json.toJson(application.invitedUsers.map { case (key, value) => key.toString -> value }) val mandatType = - newApplication.mandat - .map(_._type) - .map(DataModel.Application.MandatType.dataModelSerialization) + application.mandat + .map(_.type_) + .map(DataModel.Application.Mandat.MandatType.dataModelSerialization) - val mandatDate = newApplication.mandat.map(_.date) + val mandatDate = application.mandat.map(_.date) SQL""" INSERT INTO application ( @@ -177,18 +185,18 @@ class ApplicationService @Inject() ( mandat_type, mandat_date ) VALUES ( - ${newApplication.id}::uuid, - ${newApplication.creationDate}, - ${newApplication.creatorUserName}, - ${newApplication.creatorUserId}::uuid, - ${newApplication.subject}, - ${newApplication.description}, - ${Json.toJson(newApplication.userInfos)}, + ${application.id}::uuid, + ${application.creationDate}, + ${application.creatorUserName}, + ${application.creatorUserId}::uuid, + ${application.subject}, + ${application.description}, + ${Json.toJson(application.userInfos)}, $invitedUserJson, - ${newApplication.area}::uuid, - ${newApplication.hasSelectedSubject}, - ${newApplication.category}, - ${Json.toJson(newApplication.files)}::jsonb, + ${application.area}::uuid, + ${application.hasSelectedSubject}, + ${application.category}, + ${Json.toJson(application.files)}::jsonb, $mandatType, $mandatDate ) diff --git a/app/services/UserService.scala b/app/services/UserService.scala index ffe933da3..3805df960 100644 --- a/app/services/UserService.scala +++ b/app/services/UserService.scala @@ -4,17 +4,11 @@ import java.util.UUID import anorm._ import cats.syntax.all._ -import cats.implicits.{catsKernelStdMonoidForString, catsSyntaxOption} import helper.{Hash, Time} import javax.inject.Inject import models.User -import javax.inject.Inject -import models.User -import models.formModels.ValidateSubscriptionForm import org.postgresql.util.PSQLException import play.api.db.Database -import play.api.db.Database -import views.html.helper.form import scala.concurrent.Future diff --git a/app/views/createApplication.scala.html b/app/views/createApplication.scala.html index ff49f73c3..da7a3bf52 100644 --- a/app/views/createApplication.scala.html +++ b/app/views/createApplication.scala.html @@ -265,7 +265,7 @@
Application.Mandat.MandatType.Sms -> "par sms", Application.Mandat.MandatType.Paper -> "par mandat signé" ) - ).map(option => (DataModel.Application.MandatType.dataModelSerialization(option._1), option._2))) { options => + ).map(option => (DataModel.Application.Mandat.MandatType.dataModelSerialization(option._1), option._2))) { options => @options.map { case (optionValue, optionLabel) =>