metadata-editor/app/lib/Syndication.scala (142 lines of code) (raw):
package lib
import com.gu.mediaservice.lib.aws.{DynamoDB, NoItemFound, UpdateMessage}
import com.gu.mediaservice.lib.logging.GridLogging
import com.gu.mediaservice.model.{Edits, Photoshoot, SyndicationRights}
import com.gu.mediaservice.syntax.MessageSubjects
import org.joda.time.DateTime
import play.api.libs.json.{JsNull, JsString, Reads}
import scala.concurrent.{ExecutionContext, Future}
trait Syndication extends Edit with MessageSubjects with GridLogging {
def syndicationStore: SyndicationStore
private val syndicationRightsFieldName = "syndicationRights"
private def timedFuture[T](label: String, f : => Future[T])(implicit ec: ExecutionContext): Future[T] = {
val timeBefore = DateTime.now().getMillis
val result = f
result.onComplete { _ =>
val timeAfter = DateTime.now().getMillis
logger.info(s"$label took ${timeAfter - timeBefore} milliseconds")
}
result
}
private[lib] def publishChangedSyndicationRightsForPhotoshoot[T](id: String, unchangedPhotoshoot: Boolean = false, photoshoot: Option[Photoshoot] = None)(f: () => Future[T])
(implicit ec: ExecutionContext): Future[T] =
for {
oldPhotoshootMaybe <- getPhotoshootForImage(id)
newPhotoshootMaybe = if (unchangedPhotoshoot) None else photoshoot // if None, the get rights calls (below) will return none.
allImageRightsInOldPhotoshootBefore <- timedFuture("Get old photoshoot rights before", getAllImageRightsInPhotoshoot(oldPhotoshootMaybe))
allImageRightsInNewPhotoshootBefore <- timedFuture("Get new photoshoot rights before", getAllImageRightsInPhotoshoot(newPhotoshootMaybe))
result <- f()
allImageRightsInOldPhotoshootAfter <- timedFuture("Get old photoshoot rights after", getAllImageRightsInPhotoshoot(oldPhotoshootMaybe))
allImageRightsInNewPhotoshootAfter <- timedFuture("Get new photoshoot rights after", getAllImageRightsInPhotoshoot(newPhotoshootMaybe))
oldChangedRights = getChangedRights(allImageRightsInOldPhotoshootBefore, allImageRightsInOldPhotoshootAfter)
newChangedRights = getChangedRights(allImageRightsInNewPhotoshootBefore, allImageRightsInNewPhotoshootAfter)
_ <- timedFuture("Publish the photoshoot rights updates", publish(oldChangedRights ++ newChangedRights, UpdateImageSyndicationMetadata))
} yield {
logger.info(s"Changed rights on old photoshoot ($oldPhotoshootMaybe): ${oldChangedRights.size}")
logger.info(s"Changed rights on new photoshoot ($newPhotoshootMaybe): ${newChangedRights.size}")
result
}
def deletePhotoshootAndPublish(id: String)
(implicit ec: ExecutionContext): Future[Unit] =
publishChangedSyndicationRightsForPhotoshoot[Unit](id, unchangedPhotoshoot = false) { () =>
for {
edits <- editsStore.removeKey(id, Edits.Photoshoot)
_ <- editsStore.removeKey(id, Edits.PhotoshootTitle)
_ = publish(id, UpdateImagePhotoshootMetadata)(edits)
} yield ()
}
def setPhotoshootAndPublish(id: String, newPhotoshoot: Photoshoot)
(implicit ec: ExecutionContext): Future[Photoshoot] = {
publishChangedSyndicationRightsForPhotoshoot[Photoshoot](id, photoshoot = Some(newPhotoshoot)) { () =>
for {
editsAsJsonResponse <- editsStore.jsonAdd(id, Edits.Photoshoot, DynamoDB.caseClassToMap(newPhotoshoot))
_ <- editsStore.stringSet(id, Edits.PhotoshootTitle, JsString(newPhotoshoot.title)) // store - don't care about return
_ = publish(id, UpdateImagePhotoshootMetadata)(editsAsJsonResponse)
} yield newPhotoshoot
}
}
def deleteSyndicationAndPublish(id: String)
(implicit ec: ExecutionContext): Future[Unit] = {
publishChangedSyndicationRightsForPhotoshoot[Unit](id, unchangedPhotoshoot = true) { () =>
syndicationStore.deleteItem(id)
// Always publish, in case there is no photoshoot
publish(Map(id -> None), UpdateImageSyndicationMetadata)
}
}
def setSyndicationAndPublish(id: String, syndicationRight: SyndicationRights)
(implicit ec: ExecutionContext): Future[SyndicationRights] =
publishChangedSyndicationRightsForPhotoshoot[SyndicationRights](id, unchangedPhotoshoot = true) { () =>
val result = syndicationStore.jsonAdd (id, syndicationRightsFieldName, DynamoDB.caseClassToMap (syndicationRight)).map(_=>syndicationRight)
// Always publish, in case there is no photoshoot
publish(Map(id -> Some(syndicationRight)), UpdateImageSyndicationMetadata)
result
}
def getSyndicationForImage(id: String)
(implicit ec: ExecutionContext): Future[Option[SyndicationRights]] = {
syndicationStore.jsonGet(id, syndicationRightsFieldName)
// It's OK for the image to _not_ exist in the syndication store, so this needs to be recovered
.recover { case NoItemFound => JsNull }
.flatMap(dynamoEntry => (dynamoEntry \ syndicationRightsFieldName).toOption match {
// If image has its own rights, return those rights, with isInferred false
case Some(rights) => Future.successful(Some(rights.as[SyndicationRights].copy(isInferred = false)))
// If the image does not have it's own rights, get rights for the photoshoot
case None => getRightsByPhotoshoot(id)
})
}
private def getRightsByPhotoshoot(id: String)
(implicit ec: ExecutionContext): Future[Option[SyndicationRights]] =
getPhotoshootForImage(id)
// It's ok for the image to _not_ exist in the edits store - it may have no photoshoot (or any other edit)
.recover { case NoItemFound => None }
.flatMap { photoshootMaybe: Option[Photoshoot] =>
photoshootMaybe match {
// If image is not in a photoshoot, return no rights
case None => Future.successful(None)
// If image is in a photo shoot, find the most recently published image and return those rights, with isInferred true
case Some(photoshoot) =>
getAllImageRightsInPhotoshoot(photoshoot)
.map ( m => getMostRecentInferrableSyndicationRights(m.values.toList))
}
}
private def getRightsForImages(ids: List[String], nonInferredRights: Map[String, SyndicationRights], inferrableRights: Option[SyndicationRights])
(implicit ec: ExecutionContext, rjs: Reads[SyndicationRights]): Map[String, SyndicationRights] = {
inferrableRights match {
case Some(rights) =>
val inferredRights = (ids.toSet -- nonInferredRights.keySet)
.map(id => id -> rights)
.toMap
inferredRights ++ nonInferredRights
case None => nonInferredRights
}
}
def getMostRecentInferrableSyndicationRights(list: List[SyndicationRights]): Option[SyndicationRights] = list
.filter(_.published.nonEmpty).sortBy(_.published.map(-_.getMillis)).headOption.map(r => r.copy(isInferred = true))
def getAllImageRightsInPhotoshoot(photoshootMaybe: Option[Photoshoot])
(implicit ec: ExecutionContext): Future[Map[String, SyndicationRights]] =
photoshootMaybe.map(photoshoot => getAllImageRightsInPhotoshoot(photoshoot))
.getOrElse(Future.successful(Map.empty[String, SyndicationRights]))
def getAllImageRightsInPhotoshoot(photoshoot: Photoshoot)
(implicit ec: ExecutionContext): Future[Map[String, SyndicationRights]] = for {
imageIds <- getImagesInPhotoshoot(photoshoot)
allNonInferredRights <- syndicationStore.batchGet(imageIds, syndicationRightsFieldName)
} yield {
logger.info(s"Found non-inferred rights for ${allNonInferredRights.size} of ${imageIds.size} images in photoshoot ${photoshoot.title}")
val mostRecentInferrableRightsMaybe = getMostRecentInferrableSyndicationRights(allNonInferredRights.values.toList)
getRightsForImages(imageIds, allNonInferredRights, mostRecentInferrableRightsMaybe)
}
def getImagesInPhotoshoot(photoshoot: Photoshoot)
(implicit ec: ExecutionContext): Future[List[String]] =
editsStore.scanForId(config.editsTablePhotoshootIndex, Edits.PhotoshootTitle, photoshoot.title)
.recover { case NoItemFound => Nil }
def getChangedRights(before: Map[String, SyndicationRights], after: Map[String, SyndicationRights]): Map[String, Option[SyndicationRights]] = {
// Rights in 'after' which do not have an exact equal in 'before'
// Rights in 'before' which are not present at all in 'after', so have no inferred rights now
(after.toSet -- before.toSet).toMap.map(kv => kv._1 -> Some(kv._2)) ++
(before.keySet -- after.keySet).map(id => id -> None)
}
def getPhotoshootForImage(id: String)
(implicit ec: ExecutionContext): Future[Option[Photoshoot]] =
editsStore.jsonGet(id, Edits.Photoshoot)
.map(dynamoEntry => (dynamoEntry \ Edits.Photoshoot).toOption map {
photoshootJson => photoshootJson.as[Photoshoot]
})
.recover { case NoItemFound => None }
def publish(imagesInPhotoshoot: Map[String, Option[SyndicationRights]], subject: String)
(implicit ec: ExecutionContext): Future[Unit] = Future {
for (kv <- imagesInPhotoshoot) {
val (k, v) = kv
val updateMessage = UpdateMessage(subject = subject, id = Some(k), syndicationRights = v)
notifications.publish(updateMessage)
}
}
}