metadata-editor/app/controllers/EditsController.scala (182 lines of code) (raw):

package controllers import java.net.URI import java.net.URLDecoder.decode import com.amazonaws.AmazonServiceException import com.gu.mediaservice.GridClient import com.gu.mediaservice.lib.argo.ArgoHelpers import com.gu.mediaservice.lib.argo.model._ import com.gu.mediaservice.lib.aws.DynamoDB import com.gu.mediaservice.lib.auth.Authentication.Principal import com.gu.mediaservice.lib.auth.Permissions.EditMetadata import com.gu.mediaservice.lib.auth.{Authentication, Authorisation} import com.gu.mediaservice.lib.aws.NoItemFound import com.gu.mediaservice.lib.config.{ServiceHosts, Services} import com.gu.mediaservice.model._ import com.gu.mediaservice.syntax.MessageSubjects import lib._ import lib.Edit import org.joda.time.DateTime import play.api.libs.json._ import play.api.libs.ws.WSClient import play.api.mvc.{BaseController, ControllerComponents} import scala.concurrent.{ExecutionContext, Future} import scala.collection.compat._ // FIXME: the argoHelpers are all returning `Ok`s (200) // Some of these responses should be `Accepted` (202) // TODO: Look at adding actions e.g. to collections / sets where we could `PUT` // a singular collection item e.g. // { // "labels": { // "uri": "...", // "data": [], // "actions": [ // { // "method": "PUT", // "rel": "create", // "href": "../metadata/{id}/labels/{label}" // } // ] // } // } class EditsController( auth: Authentication, val editsStore: EditsStore, val notifications: Notifications, val config: EditsConfig, ws: WSClient, authorisation: Authorisation, override val controllerComponents: ControllerComponents )(implicit val ec: ExecutionContext) extends BaseController with ArgoHelpers with EditsResponse with MessageSubjects with Edit { import com.gu.mediaservice.lib.metadata.UsageRightsMetadataMapper.usageRightsToMetadata val services: Services = new Services(config.domainRoot, config.serviceHosts, Set.empty) val gridClient: GridClient = GridClient(services)(ws) val metadataBaseUri = config.services.metadataBaseUri private val AuthenticatedAndAuthorised = auth andThen authorisation.CommonActionFilters.authorisedForArchive private def getUploader(imageId: String, user: Principal): Future[Option[String]] = gridClient.getUploadedBy(imageId, auth.getOnBehalfOfPrincipal(user)) private def authorisedForEditMetadataOrUploader(imageId: String) = authorisation.actionFilterForUploaderOr(imageId, EditMetadata, getUploader) def decodeUriParam(param: String): String = decode(param, "UTF-8") // TODO: Think about calling this `overrides` or something that isn't metadata def getAllMetadata(id: String) = auth.async { val emptyResponse = respond(Edits.getEmpty)(editsEntity(id)) editsStore.get(id) map { dynamoEntry => dynamoEntry.asOpt[Edits] .map(respond(_)(editsEntity(id))) .getOrElse(emptyResponse) } recover { case NoItemFound => emptyResponse } } def getEdits(id: String) = auth.async { editsStore.get(id) map { dynamoEntry => val edits = dynamoEntry.asOpt[Edits] respond(data = edits) } recover { case NoItemFound => NotFound } } def getArchived(id: String) = auth.async { editsStore.booleanGet(id, Edits.Archived) map { archived => respond(archived.getOrElse(false)) } recover { case NoItemFound => respond(false) } } def setArchived(id: String) = AuthenticatedAndAuthorised.async(parse.json) { implicit req => (req.body \ "data").validate[Boolean].fold( errors => Future.successful(BadRequest(errors.toString())), archived => editsStore.booleanSetOrRemove(id, "archived", archived) .map(publish(id, UpdateImageUserMetadata)) .map(edits => respond(edits.archived)) ) } def unsetArchived(id: String) = auth.async { editsStore.removeKey(id, Edits.Archived) .map(publish(id, UpdateImageUserMetadata)) .map(_ => respond(false)) } def getLabels(id: String) = auth.async { editsStore.setGet(id, Edits.Labels) .map(labelsCollection(id, _)) .map {case (uri, labels) => respondCollection(labels)} recover { case NoItemFound => respond(Array[String]()) } } def addLabels(id: String) = auth.async(parse.json) { req => (req.body \ "data").validate[List[String]].fold( errors => Future.successful(BadRequest(errors.toString())), labels => editsStore .setAdd(id, Edits.Labels, labels) .map(publish(id, UpdateImageUserMetadata)) .map(edits => labelsCollection(id, edits.labels.toSet)) .map { case (uri, l) => respondCollection(l) } recover { case _: AmazonServiceException => BadRequest } ) } def removeLabel(id: String, label: String) = auth.async { editsStore.setDelete(id, Edits.Labels, decodeUriParam(label)) .map(publish(id, UpdateImageUserMetadata)) .map(edits => labelsCollection(id, edits.labels.toSet)) .map {case (uri, labels) => respondCollection(labels, uri=Some(uri))} } def getMetadata(id: String) = auth.async { editsStore.jsonGet(id, Edits.Metadata).map { dynamoEntry => val metadata = (dynamoEntry \ Edits.Metadata).as[ImageMetadata] respond(metadata) } recover { case NoItemFound => respond(Json.toJson(JsObject(Nil))) } } def setMetadata(id: String) = (auth andThen authorisedForEditMetadataOrUploader(id)).async(parse.json) { req => (req.body \ "data").validate[ImageMetadata].fold( errors => Future.successful(BadRequest(errors.toString())), metadata => { val specsAsMap = config.domainMetadataSpecs.groupBy(_.name).view.mapValues(_.flatMap(_.fields.map(_.name))).toMap val validatedDomainMetadata = metadata.domainMetadata .view .filterKeys(specsAsMap.keySet) .toMap .flatMap(specData => { val fields = specsAsMap.getOrElse(specData._1, List()) Map(specData._1 -> specData._2.view.filterKeys(fields.toSet).toMap) }) val validatedMetadata = metadata.copy(domainMetadata = validatedDomainMetadata) editsStore.jsonAdd(id, Edits.Metadata, metadataAsMap(validatedMetadata)) .map(publish(id, UpdateImageUserMetadata)) .map(edits => respond(edits.metadata)) } ) } def setMetadataFromUsageRights(id: String) = (auth andThen authorisedForEditMetadataOrUploader(id)).async { req => editsStore.get(id) flatMap { dynamoEntry => gridClient.getMetadata(id, auth.getOnBehalfOfPrincipal(req.user)) flatMap { imageMetadata => val edits = dynamoEntry.as[Edits] val originalUserMetadata = edits.metadata val staffPhotographerPublications: Set[String] = config.usageRightsConfig.staffPhotographers.map(_.name).toSet val metadataOpt = edits.usageRights.flatMap(ur => usageRightsToMetadata(ur, imageMetadata, staffPhotographerPublications)) metadataOpt map { metadata => val mergedMetadata = originalUserMetadata.copy( byline = metadata.byline orElse originalUserMetadata.byline, credit = metadata.credit orElse originalUserMetadata.credit, copyright = metadata.copyright orElse originalUserMetadata.copyright, imageType = metadata.imageType orElse originalUserMetadata.imageType ) editsStore.jsonAdd(id, Edits.Metadata, metadataAsMap(mergedMetadata)) .map(publish(id, UpdateImageUserMetadata)) .map(edits => respond(edits.metadata, uri = Some(metadataUri(id)))) } getOrElse { // just return the unmodified Future.successful(respond(edits.metadata, uri = Some(metadataUri(id)))) } } } recover { case NoItemFound => respondError(NotFound, "item-not-found", "Could not find image") } } def getUsageRights(id: String) = auth.async { editsStore.jsonGet(id, Edits.UsageRights).map { dynamoEntry => val usageRights = (dynamoEntry \ Edits.UsageRights).as[UsageRights] respond(usageRights) } recover { case NoItemFound => respondNotFound("No usage rights overrides found") } } def setUsageRights(id: String) = auth.async(parse.json) { req => (req.body \ "data").asOpt[UsageRights].map(usageRight => { editsStore.jsonAdd(id, Edits.UsageRights, DynamoDB.caseClassToMap(usageRight)) .map(publish(id, UpdateImageUserMetadata)) .map(_ => respond(usageRight)) }).getOrElse(Future.successful(respondError(BadRequest, "invalid-form-data", "Invalid form data"))) } def deleteUsageRights(id: String) = auth.async { req => editsStore.removeKey(id, Edits.UsageRights).map(publish(id, UpdateImageUserMetadata)).map(edits => Accepted) } def labelsCollection(id: String, labels: Set[String]): (URI, Seq[EmbeddedEntity[String]]) = (labelsUri(id), labels.map(setUnitEntity(id, Edits.Labels, _)).toSeq) def metadataAsMap(metadata: ImageMetadata) = { (Json.toJson(metadata).as[JsObject]).as[Map[String, JsValue]] } } case class EditsValidationError(key: String, message: String) extends Throwable