From 37a6f0cca0fcc225be87240d650365764db33c1d Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Wed, 15 May 2024 18:05:05 +0100 Subject: [PATCH 1/3] Store editionsClientCards for editions clipboard data --- app/controllers/UserDataController.scala | 38 ++++++++----------- app/controllers/V2App.scala | 6 +-- app/model/ClipboardCard.scala | 27 +++++++++++++ app/model/UserData.scala | 7 ++-- .../editions/EditionsClientCollection.scala | 2 + 5 files changed, 51 insertions(+), 29 deletions(-) create mode 100644 app/model/ClipboardCard.scala diff --git a/app/controllers/UserDataController.scala b/app/controllers/UserDataController.scala index 52e23b24719..8ccb126c9be 100644 --- a/app/controllers/UserDataController.scala +++ b/app/controllers/UserDataController.scala @@ -1,20 +1,17 @@ package controllers -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB -import com.gu.facia.client.models.{Trail, TrailMetaData} +import com.gu.facia.client.models.Trail +import model.editions.{CardType, EditionsClientCard} import org.scanamo._ import org.scanamo.syntax._ import model.{FeatureSwitch, FeatureSwitches, UserData} import org.scanamo.generic.auto.genericDerivedFormat import org.scanamo.query.UniqueKey -import play.api.libs.json.{JsArray, JsValue, Json} -import model.{UserData} - -import play.api.libs.json.JsValue +import play.api.libs.json.{JsError, JsResult, JsSuccess, JsValue, Json, JsonValidationError, Reads} import services.FrontsApi import software.amazon.awssdk.services.dynamodb.DynamoDbClient -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.ExecutionContext import org.scanamo.generic.semiauto._ import scala.util.{Failure, Success, Try} @@ -28,35 +25,30 @@ class UserDataController(frontsApi: FrontsApi, dynamoClient: DynamoDbClient, val x => (Json.stringify(x)) ) implicit val trail: DynamoFormat[Trail] = deriveDynamoFormat[Trail] + implicit val cardType: DynamoFormat[CardType] = deriveDynamoFormat[CardType] + implicit val editionsCard: DynamoFormat[EditionsClientCard] = deriveDynamoFormat[EditionsClientCard] implicit val userData: DynamoFormat[UserData] = deriveDynamoFormat[UserData] private lazy val userDataTable = Table[UserData](config.faciatool.userDataTable) - private def updateClipboardContentByFieldName(articles: Option[JsValue], userEmail: String, fieldName: String) = { - val clipboardArticles: Option[List[Trail]] = articles.flatMap(jsValue => - jsValue.asOpt[List[Trail]]) - - clipboardArticles match { - case Some(articles) => { - - Scanamo(dynamoClient).exec(userDataTable.update(UniqueKey("email" === userEmail), set(fieldName, articles))) + private def updateClipboardContentByFieldName[T](maybeJson: Option[JsValue], userEmail: String, fieldName: String)(implicit dynamoFormat: DynamoFormat[T], jsonFormat: Reads[T]) = { + val a = maybeJson.map(_.validate[T]) + maybeJson.map(_.validate[T]) match { + case Some(JsSuccess(model, _)) => + Scanamo(dynamoClient).exec(userDataTable.update(UniqueKey("email" === userEmail), set(fieldName, model))) Ok - } + case Some(JsError(errors)) => + BadRequest(errors.toString()) case None => BadRequest } - } def putClipboardContent() = APIAuthAction { request => - - updateClipboardContentByFieldName(request.body.asJson, request.user.email, "clipboardArticles") - + updateClipboardContentByFieldName[List[Trail]](request.body.asJson, request.user.email, "clipboardArticles") } def putEditionsClipboardContent() = APIAuthAction { request => - - updateClipboardContentByFieldName(request.body.asJson, request.user.email, "editionsClipboardArticles") - + updateClipboardContentByFieldName[List[EditionsClientCard]](request.body.asJson, request.user.email, "editionsClipboardArticles") } def putFrontIds() = APIAuthAction { request => diff --git a/app/controllers/V2App.scala b/app/controllers/V2App.scala index 37215b91afb..ec7a4e1042e 100644 --- a/app/controllers/V2App.scala +++ b/app/controllers/V2App.scala @@ -3,7 +3,7 @@ package controllers import com.amazonaws.services.dynamodbv2.AmazonDynamoDB import org.scanamo._ import org.scanamo.syntax._ -import model.{FeatureSwitch, UserData, UserDataForDefaults} +import model.{ClipboardCard, FeatureSwitch, UserData, UserDataForDefaults} import scala.concurrent.ExecutionContext import com.gu.facia.client.models.{Metadata, TargetedTerritory} @@ -48,9 +48,9 @@ class V2App(isDev: Boolean, val acl: Acl, dynamoClient: DynamoDbClient, val deps userDataTable.get("email" === userEmail)).flatMap(_.toOption) val clipboardArticles = if (editingEdition) - maybeUserData.map(_.editionsClipboardArticles.getOrElse(List())) + maybeUserData.map(_.editionsClipboardArticles.getOrElse(List()).map(ClipboardCard.apply)) else - maybeUserData.map(_.clipboardArticles.getOrElse(List())) + maybeUserData.map(_.clipboardArticles.getOrElse(List()).map(ClipboardCard.apply)) val userDataForDefaults = UserDataForDefaults.fromUserData( maybeUserData.getOrElse(UserData(userEmail)), diff --git a/app/model/ClipboardCard.scala b/app/model/ClipboardCard.scala new file mode 100644 index 00000000000..aaee11703e4 --- /dev/null +++ b/app/model/ClipboardCard.scala @@ -0,0 +1,27 @@ +package model + +import com.gu.facia.client.models.Trail +import model.editions.EditionsClientCard +import play.api.libs.json.{Format, JsPath, JsValue, Json, Reads, Writes} +import play.api.libs.functional.syntax._ + +object ClipboardCard { + def apply(trail: Trail): ClipboardCard = ClipboardCard(Left(trail)) + def apply(editionsCard: EditionsClientCard): ClipboardCard = ClipboardCard(Right(editionsCard)) + + val reads: Reads[ClipboardCard] = + JsPath.read[EditionsClientCard].map(ClipboardCard.apply) or + JsPath.read[Trail].map(ClipboardCard.apply) + + val writes = new Writes[ClipboardCard] { + override def writes(o: ClipboardCard): JsValue = + o.card.fold( + trail => Json.toJson(trail), + editionsCard => Json.toJson(editionsCard) + ) + } + + implicit val format = Format(reads, writes) +} + +case class ClipboardCard(card: Either[Trail, EditionsClientCard]) diff --git a/app/model/UserData.scala b/app/model/UserData.scala index 580c94b5ad9..2a1e38f780b 100644 --- a/app/model/UserData.scala +++ b/app/model/UserData.scala @@ -1,6 +1,7 @@ package model import com.gu.facia.client.models.Trail +import model.editions.EditionsClientCard import org.scanamo.{DynamoFormat, TypeCoercionError} import play.api.libs.json.{JsValue, Json, OFormat} @@ -31,7 +32,7 @@ object UserData { case class UserData( email: String, clipboardArticles: Option[List[Trail]] = None, - editionsClipboardArticles: Option[List[Trail]] = None, + editionsClipboardArticles: Option[List[EditionsClientCard]] = None, frontIds: Option[List[String]] = None, frontIdsByPriority: Option[Map[String, List[String]]] = None, favouriteFrontIdsByPriority: Option[Map[String, List[String]]] = None, @@ -41,7 +42,7 @@ case class UserData( object UserDataForDefaults { implicit val jsonFormat: OFormat[UserDataForDefaults] = Json.format[UserDataForDefaults] - def fromUserData(userData: UserData, clipboardArticles: Option[List[Trail]]): UserDataForDefaults = { + def fromUserData(userData: UserData, clipboardArticles: Option[List[ClipboardCard]]): UserDataForDefaults = { val featureSwitches = userData.featureSwitches.fold(FeatureSwitches.all) { userFeatureSwitches => val userFeatureSwitchKeys = userFeatureSwitches.map(_.key) val unsetFeatureSwitches = FeatureSwitches.all.filter(featureSwitch => !userFeatureSwitchKeys.contains(featureSwitch.key)) @@ -58,7 +59,7 @@ object UserDataForDefaults { } case class UserDataForDefaults( - clipboardArticles: Option[List[Trail]], + clipboardArticles: Option[List[ClipboardCard]], frontIds: Option[List[String]], frontIdsByPriority: Option[Map[String, List[String]]], favouriteFrontIdsByPriority: Option[Map[String, List[String]]], diff --git a/app/model/editions/EditionsClientCollection.scala b/app/model/editions/EditionsClientCollection.scala index 25df85b1e32..485baa7fcb4 100644 --- a/app/model/editions/EditionsClientCollection.scala +++ b/app/model/editions/EditionsClientCollection.scala @@ -9,6 +9,8 @@ import services.editions.prefills.CapiQueryTimeWindow case class EditionsClientCard(id: String, cardType: Option[CardType], frontPublicationDate: Long, meta: Option[ClientCardMetadata]) object EditionsClientCard { + implicit val format: OFormat[EditionsClientCard] = Json.format[EditionsClientCard] + def fromCard(card: EditionsCard): EditionsClientCard = { val id = card.cardType match { case CardType.Article => "internal-code/page/" + card.id From 6eb392c490ab62aaffc0c3609e0792162212dbe0 Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Thu, 30 May 2024 08:52:40 +0100 Subject: [PATCH 2/3] Update app/controllers/UserDataController.scala --- app/controllers/UserDataController.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/UserDataController.scala b/app/controllers/UserDataController.scala index 8ccb126c9be..d5eb70eb454 100644 --- a/app/controllers/UserDataController.scala +++ b/app/controllers/UserDataController.scala @@ -31,7 +31,6 @@ class UserDataController(frontsApi: FrontsApi, dynamoClient: DynamoDbClient, val private lazy val userDataTable = Table[UserData](config.faciatool.userDataTable) private def updateClipboardContentByFieldName[T](maybeJson: Option[JsValue], userEmail: String, fieldName: String)(implicit dynamoFormat: DynamoFormat[T], jsonFormat: Reads[T]) = { - val a = maybeJson.map(_.validate[T]) maybeJson.map(_.validate[T]) match { case Some(JsSuccess(model, _)) => Scanamo(dynamoClient).exec(userDataTable.update(UniqueKey("email" === userEmail), set(fieldName, model))) From 1524285d4c59e6d002acfb64c1e0d8abe597f5b1 Mon Sep 17 00:00:00 2001 From: Jonathon Herbert Date: Mon, 3 Jun 2024 09:40:45 +0100 Subject: [PATCH 3/3] Add comment for Trail vs EditionsCard --- app/model/editions/EditionsCard.scala | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/model/editions/EditionsCard.scala b/app/model/editions/EditionsCard.scala index cf5ead0ee32..e9aa8cae6f4 100644 --- a/app/model/editions/EditionsCard.scala +++ b/app/model/editions/EditionsCard.scala @@ -61,6 +61,21 @@ object CardType extends PlayEnum[CardType] { override def values = findValues } +/** + * A Card for Editions-based platforms. Analogous to the `Trail` type in + * facia-scala-client. + * + * I suspect it's distinct from `Trail` because the Editions cards have + * slightly different properties: + * - `frontPublicationDate` does not make sense in this context and is + * replaced with `addedOn` + * - `publishedBy` is not required ... and `Trail` is in a library upstream + * that is not used by the Editions product. + * + * Ideally, this and Trail would be perhaps be represented by a sealed trait + * and a discriminator field (arguably cardType) – the client does not + * distinguish between these two types. + */ case class EditionsCard(id: String, cardType: CardType, addedOn: Long, metadata: Option[CardMetadata]) extends Logging { def toPublishedCard: PublishedArticle = {