diff --git a/kahuna/public/js/image/controller.js b/kahuna/public/js/image/controller.js
index c9a42b0b55..b52d10fcf0 100644
--- a/kahuna/public/js/image/controller.js
+++ b/kahuna/public/js/image/controller.js
@@ -169,6 +169,8 @@ image.controller('ImageCtrl', [
ctrl.canUserEdit = editable;
});
+ ctrl.objectHasEntries = obj => obj && Object.keys(obj).length > 0;
+
const usages = imageUsagesService.getUsages(ctrl.image);
const usagesCount$ = usages.count$;
diff --git a/kahuna/public/js/image/view.html b/kahuna/public/js/image/view.html
index 7b4b3c1dbb..01710b9bdb 100644
--- a/kahuna/public/js/image/view.html
+++ b/kahuna/public/js/image/view.html
@@ -151,6 +151,32 @@
class="image-details__delete-crops"
gr-image="ctrl.image"
gr-on-delete="ctrl.onCropsDeleted()">
+
+
diff --git a/kahuna/public/stylesheets/main.css b/kahuna/public/stylesheets/main.css
index fb9811489a..2ceb0c897a 100644
--- a/kahuna/public/stylesheets/main.css
+++ b/kahuna/public/stylesheets/main.css
@@ -2102,7 +2102,8 @@ FIXME: what to do with touch devices
}
.image-details__delete-crops {
- width: 100%;
+ width: 100%;
+ outline: 1px solid #565656;
}
.image-details:after {
@@ -2122,6 +2123,11 @@ FIXME: what to do with touch devices
border-bottom: 1px solid #565656;
}
+.image-info__group--bottom {
+ border-bottom: 0;
+ border-top: 1px solid #565656;
+}
+
.image-info__group--last {
border-bottom: 0;
clear: both;
diff --git a/media-api/app/controllers/MediaApi.scala b/media-api/app/controllers/MediaApi.scala
index 940e73fa67..a891247613 100644
--- a/media-api/app/controllers/MediaApi.scala
+++ b/media-api/app/controllers/MediaApi.scala
@@ -2,6 +2,7 @@ package controllers
import org.apache.pekko.stream.scaladsl.StreamConverters
import com.google.common.net.HttpHeaders
+import com.gu.mediaservice.lib.ImageStorageProps
import com.gu.mediaservice.{GridClient, JsonDiff}
import com.gu.mediaservice.lib.argo._
import com.gu.mediaservice.lib.argo.model.{Action, _}
@@ -15,6 +16,7 @@ import com.gu.mediaservice.lib.logging.{LogMarker, MarkerMap}
import com.gu.mediaservice.lib.metadata.SoftDeletedMetadataTable
import com.gu.mediaservice.lib.play.RequestLoggingFilter
import com.gu.mediaservice.model._
+import com.gu.mediaservice.model.usage.{DerivativeUsageStatus, ReplacedUsageStatus}
import com.gu.mediaservice.syntax.MessageSubjects
import lib._
import lib.elasticsearch._
@@ -573,6 +575,16 @@ class MediaApi(
val deleteImagePermission = authorisation.isUploaderOrHasPermission(request.user, source.instance.uploadedBy, DeleteImagePermission)
val deleteCropsOrUsagePermission = canUserDeleteCropsOrUsages(request.user)
+ import JodaWrites._
+ implicit val jsonDetailsWrites: OWrites[RelationDetail] = Json.writes[RelationDetail]
+ val getRelationDetails = elasticSearch.getRelationDetails(id, imageResponse.getSecureThumbUrl)_
+ val relationDetails = Map(
+ "Replacement for" -> source.instance.identifiers.get(ImageStorageProps.replacesMediaIdIdentifierKey).map(getRelationDetails),
+ "Replaced by" -> source.instance.usages.filter(_.status == ReplacedUsageStatus).flatMap(_.childUsageMetadata.map(_.childMediaId)).map(getRelationDetails),
+ "Derivative of" -> source.instance.identifiers.get(ImageStorageProps.derivativeOfMediaIdsIdentifierKey).toList.flatMap(_.split(",").map(_.trim)).map(getRelationDetails),
+ "Derivatives" -> source.instance.usages.filter(_.status == DerivativeUsageStatus).flatMap(_.childUsageMetadata.map(_.childMediaId)).map(getRelationDetails)
+ ).view.mapValues(_.iterator.toMap).toMap
+
val (imageData, imageLinks, imageActions) = imageResponse.create(
id,
source,
@@ -583,8 +595,12 @@ class MediaApi(
request.user.accessor.tier
)
- Some((source.instance, imageData, imageLinks, imageActions))
-
+ Some((
+ source.instance,
+ imageData.asInstanceOf[JsObject] + ("parentAndChildDetails" -> Json.toJson(relationDetails)),
+ imageLinks,
+ imageActions
+ ))
case _ => None
}
}
diff --git a/media-api/app/lib/ImageResponse.scala b/media-api/app/lib/ImageResponse.scala
index 357a8a295a..2eef915b3f 100644
--- a/media-api/app/lib/ImageResponse.scala
+++ b/media-api/app/lib/ImageResponse.scala
@@ -75,17 +75,11 @@ class ImageResponse(config: MediaApiConfig, s3Client: S3Client, usageQuota: Usag
val pngFileUri = image.optimisedPng.map(_.file)
- val fileUri = image.source.file
-
- val imageUrl = s3Client.signUrl(config.imageBucket, fileUri, image, imageType = Source)
+ val imageUrl = s3Client.signUrl(config.imageBucket, image.source.file, image, imageType = Source)
val pngUrl: Option[String] = pngFileUri
.map(s3Client.signUrl(config.imageBucket, _, image, imageType = OptimisedPng))
- def s3SignedThumbUrl = s3Client.signUrl(config.thumbBucket, fileUri, image, imageType = Thumbnail)
-
- val thumbUrl = config.cloudFrontDomainThumbBucket
- .flatMap(s3Client.signedCloudFrontUrl(_, fileUri.getPath.drop(1)))
- .getOrElse(s3SignedThumbUrl)
+ val thumbUrl: String = getSecureThumbUrl(image)
val validityMap = checkUsageRestrictions(source, ImageExtras.validityMap(image, withWritePermission))
val valid = ImageExtras.isValid(validityMap)
@@ -130,6 +124,16 @@ class ImageResponse(config: MediaApiConfig, s3Client: S3Client, usageQuota: Usag
(data, links, actions)
}
+ def getSecureThumbUrl(image: Image) = {
+ val fileUri: URI = image.source.file
+
+ def s3SignedThumbUrl = s3Client.signUrl(config.thumbBucket, fileUri, image, imageType = Thumbnail)
+
+ config.cloudFrontDomainThumbBucket
+ .flatMap(s3Client.signedCloudFrontUrl(_, fileUri.getPath.drop(1)))
+ .getOrElse(s3SignedThumbUrl)
+ }
+
private def downloadLink(id: String) = Link("download", s"${config.rootUri}/images/$id/download")
private def downloadOptimisedLink(id: String) = Link("downloadOptimised", s"${config.rootUri}/images/$id/downloadOptimised?{&width,height,quality}")
diff --git a/media-api/app/lib/elasticsearch/ElasticSearch.scala b/media-api/app/lib/elasticsearch/ElasticSearch.scala
index 0ea248661b..559589d5c2 100644
--- a/media-api/app/lib/elasticsearch/ElasticSearch.scala
+++ b/media-api/app/lib/elasticsearch/ElasticSearch.scala
@@ -7,7 +7,7 @@ import com.gu.mediaservice.lib.auth.Authentication.Principal
import com.gu.mediaservice.lib.elasticsearch.{CompletionPreview, ElasticSearchClient, ElasticSearchConfig, MigrationStatusProvider, Running}
import com.gu.mediaservice.lib.logging.{GridLogging, LogMarker, MarkerMap}
import com.gu.mediaservice.lib.metrics.FutureSyntax
-import com.gu.mediaservice.model.{Agencies, Agency, AwaitingReviewForSyndication, Image}
+import com.gu.mediaservice.model.{Agencies, Agency, AwaitingReviewForSyndication, Dimensions, Image}
import com.sksamuel.elastic4s.ElasticDsl
import com.sksamuel.elastic4s.ElasticDsl._
import com.sksamuel.elastic4s.requests.get.{GetRequest, GetResponse}
@@ -27,8 +27,8 @@ import scalaz.NonEmptyList
import scalaz.syntax.std.list._
import java.util.concurrent.TimeUnit
-import scala.concurrent.duration.FiniteDuration
-import scala.concurrent.{ExecutionContext, Future}
+import scala.concurrent.duration.{DurationInt, FiniteDuration}
+import scala.concurrent.{Await, ExecutionContext, Future, TimeoutException}
class ElasticSearch(
val config: MediaApiConfig,
@@ -280,6 +280,25 @@ class ElasticSearch(
}
}
+ def getRelationDetails(mediaIdThisIsFor: String, getSecureThumbUrl: Image => String)(
+ id: String
+ )(implicit ex: ExecutionContext, logMarker:MarkerMap = MarkerMap()): (String, Option[RelationDetail]) = {
+ try {
+ id -> Await.result(getImageById(id), 5.seconds).map{image =>
+ RelationDetail(
+ thumbnail = getSecureThumbUrl(image),
+ addedBy = image.uploadedBy,
+ addedAt = image.uploadTime,
+ dimensions = image.source.orientedDimensions.orElse(image.source.dimensions)
+ )
+ }
+ } catch {
+ case e: TimeoutException =>
+ logger.error(logMarker, s"Timeout getting image $id (when finding relation details for $mediaIdThisIsFor)", e)
+ id -> None
+ }
+ }
+
def usageForSupplier(id: String, numDays: Int)(implicit ex: ExecutionContext, logMarker: LogMarker): Future[SupplierUsageSummary] = {
val supplier = Agencies.get(id)
val supplierName = supplier.supplier
diff --git a/media-api/app/lib/elasticsearch/ElasticSearchModel.scala b/media-api/app/lib/elasticsearch/ElasticSearchModel.scala
index 50b5499104..da1052f633 100644
--- a/media-api/app/lib/elasticsearch/ElasticSearchModel.scala
+++ b/media-api/app/lib/elasticsearch/ElasticSearchModel.scala
@@ -3,7 +3,7 @@ package lib.elasticsearch
import com.gu.mediaservice.lib.auth.{Authentication, Tier}
import com.gu.mediaservice.lib.formatting.{parseDateFromQuery, printDateTime}
import com.gu.mediaservice.model.usage.UsageStatus
-import com.gu.mediaservice.model.{Image, PrintUsageFilters, SyndicationStatus}
+import com.gu.mediaservice.model.{Dimensions, Image, PrintUsageFilters, SyndicationStatus}
import lib.querysyntax.{Condition, Parser}
import org.joda.time.DateTime
import play.api.libs.json.{Json, OWrites}
@@ -54,6 +54,12 @@ object AggregateSearchParams {
)
}
}
+case class RelationDetail(
+ thumbnail: String,
+ addedBy: String,
+ addedAt: DateTime,
+ dimensions: Option[Dimensions]
+)
case class SearchParams(
query: Option[String] = None,