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/elasticsearch/ElasticSearchClient.scala b/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/ElasticSearchClient.scala index 229ec40117..ab2110676c 100644 --- a/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/ElasticSearchClient.scala +++ b/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/ElasticSearchClient.scala @@ -7,7 +7,11 @@ import com.sksamuel.elastic4s.http.JavaClient import com.sksamuel.elastic4s.requests.common.HealthStatus import com.sksamuel.elastic4s.requests.indexes.CreateIndexResponse import com.sksamuel.elastic4s.requests.indexes.admin.IndexExistsResponse +import org.apache.http.conn.ssl.NoopHostnameVerifier +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder +import org.elasticsearch.client.RestClientBuilder.HttpClientConfigCallback +import javax.net.ssl.HostnameVerifier import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.concurrent.{Await, Future} @@ -40,7 +44,11 @@ trait ElasticSearchClient extends ElasticSearchExecutions with GridLogging { lazy val client = { logger.info("Connecting to Elastic 8: " + url) - val client = JavaClient(ElasticProperties(url)) + //val client = JavaClient(ElasticProperties(url)) + val client = JavaClient( + props = ElasticProperties(url), + httpClientConfigCallback = (httpClientBuilder: HttpAsyncClientBuilder) => httpClientBuilder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE) + ) ElasticClient(client) } diff --git a/kahuna/public/js/common/usageRightsUtils.js b/kahuna/public/js/common/usageRightsUtils.js new file mode 100644 index 0000000000..d8d3bec9da --- /dev/null +++ b/kahuna/public/js/common/usageRightsUtils.js @@ -0,0 +1,51 @@ +// -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; + default: + startDate = undefined; + 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; +} diff --git a/kahuna/public/js/edits/image-editor.js b/kahuna/public/js/edits/image-editor.js index 337f15921d..cbd43a5356 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 } 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'; @@ -317,9 +318,24 @@ imageEditor.controller('ImageEditorCtrl', [ } 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..7fa4c846cd 100644 --- a/kahuna/public/js/leases/leases.js +++ b/kahuna/public/js/leases/leases.js @@ -122,6 +122,15 @@ 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'; + + //-handle rights cat assigned lease- + $scope.$on(rightsCatAddLeasesEvent, + (e, payload) => { + if (payload.catLeases[0].mediaId === ctrl.images[0].data.id || payload.batch) { + leaseService.replace(ctrl.images[0], payload.catLeases); + } + }); 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..2eae7d5033 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 } from '../common/usageRightsUtils.js'; + import template from './usage-rights-editor.html'; import './usage-rights-editor.css'; @@ -202,6 +204,7 @@ usageRightsEditor.controller( const resource = image.data.userMetadata.data.usageRights; return editsService.update(resource, data, image, true); }, + ({ image }) => setLeasesFromUsageRights(image), ({ image }) => setMetadataFromUsageRights(image, true), ({ image }) => image.get() ],'images-updated'); @@ -227,6 +230,20 @@ usageRightsEditor.controller( 'Unexpected error'; } + function setLeasesFromUsageRights(image) { + if (ctrl.category.leases.length === 0) { + return; + } + const catLeases = createCategoryLeases(ctrl.category.leases, image); + if (catLeases.length === 0) { + 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 + ) + + })) + } + +}