diff --git a/common-lib/src/main/resources/application.conf b/common-lib/src/main/resources/application.conf index bf85d91593..a7041862eb 100644 --- a/common-lib/src/main/resources/application.conf +++ b/common-lib/src/main/resources/application.conf @@ -89,6 +89,32 @@ usageRights.applicable = [ #----------------------------------------------------------------------------------------- usageRights.stdUserExcluded = [] +#-------------------------------------------------------------------------------------------- +# List of leases that should be associated with an image when a rights category is selected +# (on upload or image edit) +# Format should be: +# usageRights.leases = [ (array) +# { +# category: "<>", +# type: "allow-use | deny-use | allow-syndication | deny-syndication", +# startDate: "TODAY | UPLOAD | TAKEN | TXDATE", <- other than today all entries map to image metadata field +# duration: <>, <- optional and will be indefinite if excluded +# notes: "<>" <- optional +# }, +# ... +# ] +#-------------------------------------------------------------------------------------------- +usageRights.leases = [ + { + category: "screengrab", + type: "allow-use", + startDate: "UPLOAD", + duration: 5, + notes: "test lease" + } +] + + usageRightsConfigProvider = { className: "com.gu.mediaservice.lib.config.RuntimeUsageRightsConfig" config { @@ -123,9 +149,13 @@ usageRightsConfigProvider = { # } # can be left blank or excluded if not required # ------------------------------------------------------- -usageInstructions { -} usageRestrictions { + contract-photographer = "This image has restrictions - see special instructions for details" + handout = "This image can only be used in that context from which it originates - or you'll get told off!" +} +usageInstructions { + contract-photographer = "You'll need to ask the photographer nicely if you want to use this image" + obituary = "Make sure the person is dead before you use this image" } # ------------------------------------------------------------- diff --git a/common-lib/src/main/scala/com/gu/mediaservice/lib/metrics/CloudWatchMetrics.scala b/common-lib/src/main/scala/com/gu/mediaservice/lib/metrics/CloudWatchMetrics.scala index 1f49306329..a6ad21148e 100644 --- a/common-lib/src/main/scala/com/gu/mediaservice/lib/metrics/CloudWatchMetrics.scala +++ b/common-lib/src/main/scala/com/gu/mediaservice/lib/metrics/CloudWatchMetrics.scala @@ -13,6 +13,7 @@ import java.util.Date import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration.{DurationInt, FiniteDuration} +import scala.util.Random trait Metric[A] { def recordOne(value: A, dimensions: List[Dimension] = Nil): Unit @@ -45,7 +46,8 @@ abstract class CloudWatchMetrics( private val client: AmazonCloudWatch = config.withAWSCredentials(AmazonCloudWatchClientBuilder.standard()).build() - private[CloudWatchMetrics] val metricsActor = actorSystem.actorOf(MetricsActor.props(namespace, client), "metricsactor") + private val random = new Random() + private[CloudWatchMetrics] val metricsActor = actorSystem.actorOf(MetricsActor.props(namespace, client), s"metricsactor-${random.alphanumeric.take(8).mkString}") applicationLifecycle.addStopHook(() => (metricsActor ? MetricsActor.Shutdown)(Timeout(5.seconds))) diff --git a/kahuna/public/js/common/usageRightsUtils.js b/kahuna/public/js/common/usageRightsUtils.js new file mode 100644 index 0000000000..c67aa254f4 --- /dev/null +++ b/kahuna/public/js/common/usageRightsUtils.js @@ -0,0 +1,71 @@ +// -using config lease definitions to create leases for image based on chosen rights category- +export function createCategoryLeases(leaseDefs, image) { + const leaseTypes = ["allow-use", "deny-use", "allow-syndication", "deny-syndication"]; + const leases = []; + leaseDefs.forEach((leaseDef) => { + //-establish start date: TODAY | UPLOAD | TAKEN | TXDATE- + const startDteType = leaseDef.startDate ?? "NONE"; + let startDate = undefined; + switch (startDteType) { + case ("TODAY"): + startDate = new Date(); + break; + case ("UPLOAD"): + startDate = new Date(image.data.uploadTime); + break; + case ("TAKEN"): + if (image.data.metadata.dateTaken) { + startDate = new Date(image.data.metadata.dateTaken); + } + break; + case ("TXDATE"): + if (image.data.metadata.domainMetadata && + image.data.metadata.domainMetadata.programmes && + image.data.metadata.domainMetadata.programmes.originalTxDate) { + startDate = new Date(image.data.metadata.domainMetadata.programmes.originalTxDate); + } + break; + } + // -check we have acceptable type and startDate- + if (leaseTypes.includes(leaseDef.type ?? "") && startDate) { + const lease = {}; + lease["access"] = leaseDef.type; + lease["createdAt"] = (new Date()).toISOString(); + lease["leasedBy"] = "Usage_Rights_Category"; + lease["startDate"] = startDate.toISOString(); + lease["notes"] = leaseDef.notes ?? ""; + + if (leaseDef.duration) { + let endDate = startDate; + endDate.setFullYear(endDate.getFullYear() + leaseDef.duration); + lease["endDate"] = endDate.toISOString(); + } + lease["mediaId"] = image.data.id; + leases.push(lease); + } + }); + return leases; +} + +/* ****************************************************************************** +Remove any leases from image that have same type as any rights-cat applied leases +********************************************************************************* */ +export function removeCategoryLeases(categories, image, removeRights) { + const mtchCats = categories.filter(cat => cat.value === removeRights); + if (mtchCats.length === 0) { + return []; + } + const removeCat = mtchCats[0]; + if (removeCat.leases.length === 0) { + return []; + } + const removeLeases = []; + image.data.leases.data.leases.forEach(lease => { + const mtches = removeCat.leases.filter(catLease => catLease.type === lease.access); + if (mtches.length > 0) { + removeLeases.push(lease); + } + }); + + return removeLeases; +} diff --git a/kahuna/public/js/edits/image-editor.js b/kahuna/public/js/edits/image-editor.js index 337f15921d..5ab6364074 100644 --- a/kahuna/public/js/edits/image-editor.js +++ b/kahuna/public/js/edits/image-editor.js @@ -7,6 +7,7 @@ import {imageService} from '../image/service'; import '../services/label'; import {imageAccessor} from '../services/image-accessor'; import {usageRightsEditor} from '../usage-rights/usage-rights-editor'; +import { createCategoryLeases, removeCategoryLeases } from '../common/usageRightsUtils.js'; import {metadataTemplates} from "../metadata-templates/metadata-templates"; import {leases} from '../leases/leases'; import {archiver} from '../components/gr-archiver-status/gr-archiver-status'; @@ -273,6 +274,7 @@ imageEditor.controller('ImageEditorCtrl', [ const image = ctrl.image; const resource = image.data.userMetadata.data.usageRights; editsService.update(resource, data, image); + batchSetLeasesFromUsageRights(image, data.category); }); } @@ -316,10 +318,65 @@ imageEditor.controller('ImageEditorCtrl', [ ctrl.showUsageRights = ctrl.usageRightsCategory === undefined; } + function batchSetLeasesFromUsageRights(image, rightsCat) { + const category = ctrl.categories.find(cat => cat.value === rightsCat); + if (!category || image.data.usageRights.category === rightsCat) { + return; + } + if (category.leases.length === 0) { + // possibility of removal only + if (!image.data.usageRights.category) { + return; + } + const removeLeases = removeCategoryLeases(ctrl.categories, image, image.data.usageRights.category); + if (removeLeases.length > 0) { + $rootScope.$broadcast('events:rights-category:delete-leases', { + catLeases: removeLeases, + batch: false + }); + } + return; + } + const catLeases = createCategoryLeases(category.leases, image); + if (catLeases.length === 0) { + // possibility of remove only of leases due to missing date info on image + if (!image.data.usageRights.category) { + return; + } + const removeLeases = removeCategoryLeases(ctrl.categories, image, image.data.usageRights.category); + if (removeLeases.length > 0) { + $rootScope.$broadcast('events:rights-category:delete-leases', { + catLeases: removeLeases, + batch: false + }); + } + return; + } + $rootScope.$broadcast('events:rights-category:add-leases', { + catLeases: catLeases, + batch: false + }); + } + function batchApplyUsageRights() { - $rootScope.$broadcast(batchApplyUsageRightsEvent, { - data: ctrl.usageRights.data - }); + $rootScope.$broadcast(batchApplyUsageRightsEvent, { + data: ctrl.usageRights.data + }); + + //-rights category derived leases- + const mtchingRightsCats = ctrl.categories.filter(c => c.value == ctrl.usageRights.data.category); + if (mtchingRightsCats.length > 0) { + const rightsCat = mtchingRightsCats[0]; + if (rightsCat.leases.length > 0) { + const catLeases = createCategoryLeases(rightsCat.leases, ctrl.image); + if (catLeases.length > 0) { + $rootScope.$broadcast('events:rights-category:add-leases', { + catLeases: catLeases, + batch: true + }); + } + } + } } function openCollectionTree() { diff --git a/kahuna/public/js/leases/leases.js b/kahuna/public/js/leases/leases.js index 2a5f2405a9..67b06ce13b 100644 --- a/kahuna/public/js/leases/leases.js +++ b/kahuna/public/js/leases/leases.js @@ -122,6 +122,32 @@ leases.controller('LeasesCtrl', [ // which also isn't ideal, but isn't quadratic either. const batchAddLeasesEvent = 'events:batch-apply:add-leases'; const batchRemoveLeasesEvent = 'events:batch-apply:remove-leases'; + const rightsCatAddLeasesEvent = 'events:rights-category:add-leases'; + const rightsCatDeleteLeasesEvent = 'events:rights-category:delete-leases'; + + //-handle rights cat assigned lease- + $scope.$on(rightsCatAddLeasesEvent, + (e, payload) => { + let matchImages = ctrl.images.filter(img => img.data.id === payload.catLeases[0].mediaId); + if (angular.isDefined(matchImages.toArray)) { + matchImages = matchImages.toArray(); + }; + if (matchImages.length || payload.batch) { + leaseService.replace(matchImages[0], payload.catLeases); + } + } + ); + + //-handle deletion of leases from previous rights category- + $scope.$on(rightsCatDeleteLeasesEvent, + (e, payload) => { + if (payload.catLeases && 0 < payload.catLeases.length) { + payload.catLeases.forEach(lease => { + leaseService.deleteLease(lease, ctrl.images); + }); + } + } + ); if (Boolean(ctrl.withBatch)) { $scope.$on(batchAddLeasesEvent, diff --git a/kahuna/public/js/usage-rights/usage-rights-editor.js b/kahuna/public/js/usage-rights/usage-rights-editor.js index 1b4225854c..608a9003ef 100644 --- a/kahuna/public/js/usage-rights/usage-rights-editor.js +++ b/kahuna/public/js/usage-rights/usage-rights-editor.js @@ -8,6 +8,8 @@ import {List} from 'immutable'; import '../services/image-list'; +import { createCategoryLeases, removeCategoryLeases } from '../common/usageRightsUtils.js'; + import template from './usage-rights-editor.html'; import './usage-rights-editor.css'; @@ -202,6 +204,10 @@ usageRightsEditor.controller( const resource = image.data.userMetadata.data.usageRights; return editsService.update(resource, data, image, true); }, + ({ image }) => { + const prevRights = (0 < ctrl.usageRights.size) ? ctrl.usageRights.first().data.category : ""; + return setLeasesFromUsageRights(image, prevRights); + }, ({ image }) => setMetadataFromUsageRights(image, true), ({ image }) => image.get() ],'images-updated'); @@ -227,6 +233,36 @@ usageRightsEditor.controller( 'Unexpected error'; } + function setLeasesFromUsageRights(image, prevRights) { + if (ctrl.category.leases.length === 0) { + // possibility of removal only + const removeLeases = removeCategoryLeases(ctrl.categories, image, prevRights); + if (removeLeases && removeLeases.length > 0) { + $rootScope.$broadcast('events:rights-category:delete-leases', { + catLeases: removeLeases, + batch: false + }); + } + return; + } + const catLeases = createCategoryLeases(ctrl.category.leases, image); + if (catLeases.length === 0) { + // possibility of removal only - missing tx date etc. + const removeLeases = removeCategoryLeases(ctrl.categories, image, prevRights); + if (removeLeases && removeLeases.length > 0) { + $rootScope.$broadcast('events:rights-category:delete-leases', { + catLeases: removeLeases, + batch: false + }); + } + return; + } + $rootScope.$broadcast('events:rights-category:add-leases', { + catLeases: catLeases, + batch: false + }); + } + // HACK: This should probably live somewhere else, but it's the least intrusive // here. This updates the metadata based on the usage rights to stop users having // to enter content twice. diff --git a/metadata-editor/app/controllers/EditsApi.scala b/metadata-editor/app/controllers/EditsApi.scala index ad30aaac5e..6e8d9b32f9 100644 --- a/metadata-editor/app/controllers/EditsApi.scala +++ b/metadata-editor/app/controllers/EditsApi.scala @@ -9,6 +9,7 @@ import com.gu.mediaservice.lib.config.{RuntimeUsageRightsConfig, UsageRightsConf import com.gu.mediaservice.model._ import lib.EditsConfig import model.UsageRightsProperty +import model.UsageRightsLease import play.api.libs.json._ import play.api.mvc.Security.AuthenticatedRequest import play.api.mvc.{AnyContent, BaseController, ControllerComponents} @@ -75,6 +76,7 @@ case class CategoryResponse( defaultRestrictions: Option[String], caution: Option[String], properties: List[UsageRightsProperty] = List(), + leases: Seq[UsageRightsLease] = Seq(), usageRestrictions: Option[String], usageSpecialInstructions: Option[String] ) @@ -90,6 +92,7 @@ object CategoryResponse { defaultRestrictions = u.defaultRestrictions, caution = u.caution, properties = UsageRightsProperty.getPropertiesForSpec(u, config.usageRightsConfig), + leases = UsageRightsLease.getLeasesForSpec(u, config.usageRightsLeases), usageRestrictions = config.customUsageRestrictions.get(u.category), usageSpecialInstructions = config.customSpecialInstructions.get(u.category) ) diff --git a/metadata-editor/app/lib/EditsConfig.scala b/metadata-editor/app/lib/EditsConfig.scala index 561451562d..04643bb538 100644 --- a/metadata-editor/app/lib/EditsConfig.scala +++ b/metadata-editor/app/lib/EditsConfig.scala @@ -2,7 +2,7 @@ package lib import com.amazonaws.regions.{Region, RegionUtils} import com.gu.mediaservice.lib.config.{CommonConfig, GridConfigResources} - +import model.UsageRightsLease class EditsConfig(resources: GridConfigResources) extends CommonConfig(resources) { val dynamoRegion: Region = RegionUtils.getRegion(string("aws.region")) @@ -19,6 +19,8 @@ class EditsConfig(resources: GridConfigResources) extends CommonConfig(resources val kahunaUri: String = services.kahunaBaseUri val loginUriTemplate: String = services.loginUriTemplate + val usageRightsLeases: Seq[UsageRightsLease] = configuration.getOptional[Seq[UsageRightsLease]]("usageRights.leases").getOrElse(Seq.empty) + val customSpecialInstructions: Map[String, String] = configuration.getOptional[Map[String, String]]("usageInstructions").getOrElse(Map.empty) diff --git a/metadata-editor/app/model/UsageRightsLease.scala b/metadata-editor/app/model/UsageRightsLease.scala new file mode 100644 index 0000000000..445cf10f70 --- /dev/null +++ b/metadata-editor/app/model/UsageRightsLease.scala @@ -0,0 +1,59 @@ +package model + +import com.gu.mediaservice.model._ +import play.api.ConfigLoader +import play.api.libs.json._ +import scala.collection.JavaConverters._ +import scala.util.{Failure, Success, Try} +import java.time.{LocalDate, Period} + +case class UsageRightsLease( + category: String, + `type`: String, + startDate: String, + duration: Option[Int], + notes: Option[String] +) + +object UsageRightsLease { + + def getLeasesForSpec(u: UsageRightsSpec, leases: Seq[UsageRightsLease]): Seq[UsageRightsLease] = leases.filter(_.category == u.category) + + implicit val writes: Writes[UsageRightsLease] = Json.writes[UsageRightsLease] + + implicit val configLoader: ConfigLoader[Seq[UsageRightsLease]] = { + ConfigLoader(_.getConfigList).map( + _.asScala.map(config => { + + val categoryId = if (config.hasPath("category")) { + config.getString("category") + } else "" + + val leaseType = if (config.hasPath("type")) { + config.getString("type") + } else "" + + val startDate = if (config.hasPath("startDate")) { + config.getString("startDate") + } else "" + + val duration = if (config.hasPath("duration")) { + Some(config.getInt("duration")) + } else None + + val notes = if (config.hasPath("notes")) { + Some(config.getString("notes")) + } else None + + UsageRightsLease ( + category = categoryId, + `type` = leaseType, + startDate = startDate, + duration = duration, + notes = notes + ) + + })) + } + +}