Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically add/remove leases when a rights category is selected for an image/s based on application level config #4358

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
34 changes: 32 additions & 2 deletions common-lib/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<<category-id>>",
# 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: <<int nos years>>, <- optional and will be indefinite if excluded
# notes: "<<text string>>" <- optional
# },
# ...
# ]
#--------------------------------------------------------------------------------------------
usageRights.leases = [
AndyKilmory marked this conversation as resolved.
Show resolved Hide resolved
{
category: "screengrab",
type: "allow-use",
startDate: "UPLOAD",
duration: 5,
notes: "test lease"
}
]


usageRightsConfigProvider = {
className: "com.gu.mediaservice.lib.config.RuntimeUsageRightsConfig"
config {
Expand Down Expand Up @@ -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"
}

# -------------------------------------------------------------
Expand Down
74 changes: 74 additions & 0 deletions kahuna/public/js/common/usageRightsUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startDate is already intialised as undefined - does it need to be redeclared as undefined in the default case as well?

Also maybe not relevant here but object literals look like a good alternative to switch cases in javascript: https://dev.to/mhmdjaw/an-alternative-to-the-javascript-switch-statement-1kah

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;
}
52 changes: 49 additions & 3 deletions kahuna/public/js/edits/image-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
}

Expand Down Expand Up @@ -316,10 +318,54 @@ 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 && image.data.usageRights.category === rightsCat )) {
AndyKilmory marked this conversation as resolved.
Show resolved Hide resolved
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 && 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) {
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() {
Expand Down
26 changes: 26 additions & 0 deletions kahuna/public/js/leases/leases.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
AndyKilmory marked this conversation as resolved.
Show resolved Hide resolved
payload.catLeases.forEach(lease => {
leaseService.deleteLease(lease, ctrl.images);
});
}
}
);

if (Boolean(ctrl.withBatch)) {
$scope.$on(batchAddLeasesEvent,
Expand Down
28 changes: 28 additions & 0 deletions kahuna/public/js/usage-rights/usage-rights-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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');
Expand All @@ -227,6 +233,28 @@ 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) {
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.
Expand Down
3 changes: 3 additions & 0 deletions metadata-editor/app/controllers/EditsApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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]
)
Expand All @@ -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)
)
Expand Down
4 changes: 3 additions & 1 deletion metadata-editor/app/lib/EditsConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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)

Expand Down
59 changes: 59 additions & 0 deletions metadata-editor/app/model/UsageRightsLease.scala
Original file line number Diff line number Diff line change
@@ -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
)

}))
}

}
Loading