app/controllers/EditionsController.scala (517 lines of code) (raw):

package controllers import java.time.{LocalDate, OffsetDateTime} import cats.syntax.either._ import com.gu.contentapi.json.CirceEncoders._ import io.circe.syntax._ import logging.Logging import logic.EditionsChecker import model.editions._ import model.editions.templates.CuratedPlatformDefinition import model.forms._ import net.logstash.logback.marker.Markers import play.api.libs.json.{JsObject, Json} import play.api.mvc.Result import services.Capi import services.editions.EditionsTemplating import services.editions.db.EditionsDB import services.editions.prefills.{ CapiPrefillTimeParams, MetadataForLogging, PrefillParamsAdapter } import services.editions.publishing.Publishing import services.editions.publishing.PublishedIssueFormatters._ import util.ContentUpgrade.rewriteBody import util.{SearchResponseUtil, UserUtil} import scala.jdk.CollectionConverters._ import scala.concurrent.ExecutionContext import scala.util.Try import model.editions.client.EditionsFrontendCollectionWrapper import play.api.libs.json.Format.GenericFormat import scalikejdbc.DB import java.util.UUID class EditionsController( db: EditionsDB, templating: EditionsTemplating, publishing: Publishing, capi: Capi, val deps: BaseFaciaControllerComponents )(implicit ec: ExecutionContext) extends BaseFaciaController(deps) with Logging { def createIssue(name: String) = EditEditionsAuthAction(parse.json[CreateIssue]) { req => val form = req.body val result = for { edition <- Either.fromOption[Result, Edition]( Edition.withNameOption(name), NotFound(s"Edition $name not found") ) templateResult <- templating.generateEditionTemplate( edition, form.issueDate ) issueId = db.insertIssue( edition, templateResult.issueSkeleton, req.user, OffsetDateTime.now() ) issue <- Either.fromOption( db.getIssue(issueId), NotFound("Issue created but could not retrieve it from the database") ) } yield issue result.fold(identity, issue => Created(Json.toJson(issue))) } def createIssueFromPreviousIssue(name: String) = EditEditionsAuthAction(parse.json[CreateIssue]) { req => val form = req.body val result = for { edition <- Edition .withNameOption(name) .toRight(NotFound(s"Edition $name not found")) issue <- db .insertIssueFromClosestPreviousIssue( edition = edition, issueDate = form.issueDate, user = req.user, now = OffsetDateTime.now() ) .left .map { case EditionsDB.NotFoundError(message) => NotFound(message) case e => InternalServerError(e.getMessage) } } yield issue result.fold(identity, issue => Created(Json.toJson(issue))) } def getIssue(id: String) = EditEditionsAuthAction { _ => db.getIssue(id) .map { issue => Ok(Json.toJson(issue)) } .getOrElse(NotFound(s"Issue $id not found")) } def deleteIssue(id: String) = EditEditionsAuthAction { req => { db.getIssue(id) .map { issue => { val markers = Markers.appendEntries( Map( "id" -> id, "issueDate" -> issue.issueDate.toString, "user" -> req.user.email ).asJava ) logger.info(s"Deleting issue ${id}")(markers) db.deleteIssue(id) Accepted } } .getOrElse(NotFound(s"Issue $id not found")) } } def getIssueSummary(id: String) = EditEditionsAuthAction { _ => db.getIssueSummary(id) .map { case Right(issue) => Ok(Json.toJson(issue).as[JsObject] - "fronts") case Left(error) => InternalServerError(error) } .getOrElse(NotFound(s"Issue $id not found")) } def getVersions(id: String) = AccessAPIAuthAction { _ => db.getIssue(id) .map(_ => Ok(Json.toJson(db.getIssueVersions(id)))) .getOrElse(NotFound(s"Issue $id not found")) } def getLastProofedVersion(id: String) = AccessAPIAuthAction { _ => db.getIssue(id) .map(_ => Ok(Json.toJson(db.getLastProofedIssueVersion(id)))) .getOrElse(NotFound(s"Issue $id not found")) } def republishEditionsAppEditionsList = EditEditionsAuthAction { _ => { try { // TODO: Make this a case class and serialise it properly val raw = Json .toJson(Map("action" -> "editionList")) .as[JsObject] + ("content", Json.toJson( EditionsAppTemplates.getAvailableEditionsAppTemplates )) publishing.putEditionsList(raw.toString()) Ok("Published. Please check processing has succeeded.") } catch { case e: Exception => InternalServerError(e.getMessage) } } } def getAvailableEditions = EditEditionsAuthAction { _ => { Ok(Json.toJson(getAvailableCuratedPlatformEditions)) } } def checkIssue(id: String) = EditEditionsAuthAction { _ => val maybeIssue = db.getIssue(id) maybeIssue match { case Some(issue) => Ok(Json.toJson(EditionsChecker.checkIssue(issue))) case _ => BadRequest("Unknown issue") } } def listIssues(edition: Edition) = EditEditionsAuthAction { req => val params = Try { val dateFrom = req.queryString.get("dateFrom").map(_.head).get val dateTo = req.queryString.get("dateTo").map(_.head).get (LocalDate.parse(dateFrom), LocalDate.parse(dateTo)) }.toEither.left.map { e => BadRequest(s"Error getting dates from query param: ${e.getMessage}") } val response = for { dates <- params (localDateFrom, localDateTo) = dates platform <- AllTemplates.templates .get(edition) .toRight(BadRequest("No platform for this edition")) issues <- db .listIssues(edition, localDateFrom, localDateTo) .left .map(errors => InternalServerError(s"Error listing issues: ${errors.mkString(", ")}") ) } yield { val result = Json.obj( "issues" -> Json.toJson(issues), "platform" -> platform.platform.entryName ) Ok(Json.toJson(result)) } response.merge } // Ideally the frontend can be changed so we don't have this bonkers modelling! def getCollections() = EditEditionsAuthAction(parse.json[List[GetCollectionsFilter]]) { req => val filters = req.body if (filters.isEmpty) { Ok(Json.toJson(List.empty[EditionsFrontendCollectionWrapper])) } else { Ok( Json.toJson( db.getCollections(filters) .map(EditionsFrontendCollectionWrapper.fromCollection) ) ) } } def getCollection(collectionId: String) = EditEditionsAuthAction { req => val collection = db.getCollections(List(GetCollectionsFilter(id = collectionId, None))) if (collection.isEmpty) { logger.warn(s"Collection not found ${collectionId}") NotFound(s"Collection $collectionId not found") } else { Ok( Json.toJson( EditionsFrontendCollectionWrapper.fromCollection(collection.head) ) ) } } def updateCollection(collectionId: String) = EditEditionsAuthAction( parse.json[EditionsFrontendCollectionWrapper] ) { req => val form = req.body val collectionToUpdate = EditionsFrontendCollectionWrapper.toCollection(form) val updatedCollection = db.updateCollection(collectionToUpdate) for { issueId <- db.getIssueIdFromCollectionId(updatedCollection.id) issue <- db.getIssue(issueId) } { logger.info("Updating preview") publishing.updatePreview(issue) } Ok( Json.toJson( EditionsFrontendCollectionWrapper.fromCollection(updatedCollection) ) ) } def renameCollection(collectionId: String) = EditEditionsAuthAction( parse.json[EditionsFrontendCollectionWrapper] ) { req => logger.info(s"Renaming collection ${collectionId}") val collection = db.getCollections(List(GetCollectionsFilter(id = collectionId, None))) if (collection.isEmpty) { logger.warn(s"Collection not found ${collectionId}") NotFound(s"Collection $collectionId not found") } else { val updatingCollection = collection.head.copy( displayName = req.body.collection.displayName, updatedBy = Some(UserUtil.getDisplayName(req.user)), updatedEmail = Some(req.user.email) ) val updatedCollection = db.updateCollectionName(updatingCollection) Ok( Json.toJson( EditionsFrontendCollectionWrapper.fromCollection(updatedCollection) ) ) } } def updateCollectionRegions(collectionId: String) = EditEditionsAuthAction( parse.json[EditionsFrontendCollectionWrapper] ) { req => val collection = db.getCollections(List(GetCollectionsFilter(id = collectionId, None))) if (collection.isEmpty) { logger.warn(s"Collection not found ${collectionId}") NotFound(s"Collection ${collectionId} not found") } else { val updatingCollection = collection.head.copy( targetedRegions = req.body.collection.targetedRegions, excludedRegions = req.body.collection.excludedRegions, updatedBy = Some(UserUtil.getDisplayName(req.user)), updatedEmail = Some(req.user.email) ) val updatedCollection = db.updateCollectionRegionsInDB(updatingCollection) Ok( Json.toJson( EditionsFrontendCollectionWrapper.fromCollection(updatedCollection) ) ) } } def moveCollection(frontId: String, collectionId: String) = EditEditionsAuthAction(parse.json[MoveCollection]) { req => val form = req.body val result = for { updatedFront <- db .moveCollection(frontId, collectionId, form.newIndex) .left .map { case EditionsDB.NotFoundError(message) => NotFound(message) case error => InternalServerError(error.getMessage) } issueId <- db.getIssueIdFromCollectionId(collectionId).toRight { InternalServerError("Issue ID not found for updated collection") } issue <- db.getIssue(issueId).toRight { InternalServerError("Issue not found for updated collection") } } yield { logger.info("Updating preview") publishing.updatePreview(issue) val clientCollections = toClientCollections(updatedFront) Ok(Json.toJson(clientCollections)) } result.merge } def getPreviewEdition(id: String) = EditEditionsAuthAction { _ => db.getIssue(id) .map { issue => Ok(Json.toJson(issue.toPreviewIssue)) } .getOrElse(NotFound(s"Issue $id not found")) } def proofIssue(id: String) = EditEditionsAuthAction { req => db.getIssue(id) .map { issue => publishing.proof(issue, req.user, OffsetDateTime.now()) NoContent } .getOrElse(NotFound(s"Issue $id not found")) } def publishIssue(id: String, version: EditionIssueVersionId) = EditEditionsAuthAction { req => val lastProofedIssueVersion = db.getLastProofedIssueVersion(id) db.getIssue(id) .map { issue => // Protect against stale requests, if our output platform supports proofing if ( issue.supportsProofing && !lastProofedIssueVersion.exists( _.equals(version) ) ) { BadRequest( s"Last proofed version of issue '${id}' is '${lastProofedIssueVersion .getOrElse("none")}', not '${version}'" ) } else { publishing.publish(issue, req.user, version) } NoContent } .getOrElse(NotFound(s"Issue $id not found")) } def getPrefillForCollection(id: String) = EditEditionsAuthAction { req => db.getCollectionPrefill(id) .map { prefillUpdate => logger.info(s"getPrefillForCollection id=$id, prefillUpdate") import prefillUpdate._ val capiDateQueryParam = EditionsAppTemplates.templates(edition).template.capiDateQueryParam val capiPrefillTimeParams = CapiPrefillTimeParams(capiQueryTimeWindow, capiDateQueryParam) // TODO // when we click (suggest articles) for collection we are not using ophan metrics and we are not sorting on them // we should converge that val getPrefillParams = PrefillParamsAdapter( issueDate, capiPrefillQuery, capiPrefillTimeParams, maybeOphanPath = None, maybeOphanQueryPrefillParams = None, edition, metadataForLogging = MetadataForLogging( issueDate, collectionId = Some(id), collectionName = None ) ) val responses = capi.getPrefillArticles( getPrefillParams, prefillUpdate.currentPageCodes ) /** TODO aggregating results from multiple SearchResponses into single * SearchResponse Is a quick temp solution to be able to handle * pagination on Suggest Articles request currently result item from * SearchResponse.results is a dynamic type which is enhanced by * rewriteBody function see rewriteBody function implementation * rewriteBody dynamically adjust Content type from SearchResponse into * type CapiArticle type used on Front-end making that type safe will * require more work and will be done later */ val responseWithAggregatedResults = SearchResponseUtil.aggregateResults(responses) val json = "{\"response\":" + responseWithAggregatedResults.asJson.noSpaces + "}" val decorated = rewriteBody(json) Ok(decorated).as("application/json") } .getOrElse(NotFound("Collection not found")) } def putFrontMetadata(id: String) = EditEditionsAuthAction(parse.json[EditionsFrontMetadata]) { req => Ok(Json.toJson(db.updateFrontMetadata(id, req.body))) } def putFrontHiddenState(id: String, state: Boolean) = EditEditionsAuthAction { req => db.updateFrontHiddenState(id, state).map { state => Ok(Json.toJson(state)) } getOrElse NotFound(s"Front $id not found") } def addCollectionToFront(id: String) = EditEditionsAuthAction { req => db.addCollectionToFront( frontId = id, user = req.user, now = OffsetDateTime.now(), name = req.queryString.get("name").flatMap(_.headOption) ) match { case Right((front, _)) => val collections = toClientCollections(front) Ok(Json.toJson(collections)) case Left(EditionsDB.NotFoundError(message)) => NotFound(message) case Left(error) => InternalServerError(error.getMessage) } } def removeCollectionFromFront(frontId: String, collectionId: String) = EditEditionsAuthAction { req => db.removeCollectionFromFront( frontId = frontId, collectionId, user = req.user, now = OffsetDateTime.now() ) match { case Right(front) => val clientCollections = toClientCollections(front) Ok(Json.toJson(clientCollections)) case Left(EditionsDB.NotFoundError(message)) => NotFound case Left(error) => InternalServerError(error.getMessage()) } } private def toClientCollections(front: EditionsFront) = front.collections.map(collection => EditionsFrontendCollectionWrapper.fromCollection(collection).collection ) private def getAvailableCuratedPlatformEditions : Map[String, List[CuratedPlatformDefinition]] = { val feastAppEditions = FeastAppTemplates.getAvailableTemplates EditionsAppTemplates.getAvailableEditionsAppTemplates ++ Map( "feastEditions" -> feastAppEditions ) } private def getFeastCollectionContent( containerData: EditionsCollection, cardId: String ) = { containerData.items.find(item => item.id == cardId && item.cardType == CardType.FeastCollection ) match { case None => Left(EditionsDB.NotFoundError("No Feast collection found with that ID")) case Some( EditionsFeastCollection( sourceCollectionId, sourceCollectionMTime, sourceCollectionMeta ) ) => sourceCollectionMeta match { case None => Left( EditionsDB.InvalidInput("This card is not properly configured") ) case Some(meta) => Right(meta) } case _ => Left(EditionsDB.InvalidInput("This card is not a Feast collection")) } } private def lookupCollection(collectionId: String) = db.getCollections(List(GetCollectionsFilter(collectionId, None))).headOption def feastCollectionToContainer( frontId: String, collectionId: String, collectionCardId: String ) = EditEditionsAuthAction { req => // collectionId is the ID of the _container_ where the Feast collection card is. // collectionCardId is the ID of the feast collection itself, which is a _card_ in terms of Fronts lookupCollection(collectionId) match { case None => NotFound case Some(sourceContainer) => val result = for { feastCollection <- getFeastCollectionContent( sourceContainer, collectionCardId ) updateData <- db.addCollectionToFront( frontId = frontId, user = req.user, now = OffsetDateTime.now(), name = feastCollection.title ) newCollection <- db .getCollections(List(GetCollectionsFilter(updateData._2, None))) .find(_.id == updateData._2) .toRight( Left( EditionsDB .InvariantError("Could not find created new collection") ) ) _ = db.updateCollection( newCollection.copy(items = feastCollection.collectionItems) ) updatedFront <- DB localTx { implicit session => db.getFront(frontId) .toRight( EditionsDB.InvariantError( "The front was deleted while processing" ) ) } } yield updatedFront result match { case Left(EditionsDB.NotFoundError(msg)) => NotFound(msg) case Left(EditionsDB.InvariantError(msg)) => Conflict(msg) case Left(err) => logger.error( s"Unexpected error when converting a Feast collection into a container: $err" ) InternalServerError("") case Right(updatedFront) => val collections = toClientCollections(updatedFront) Ok(Json.toJson(collections)) } } } }