diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7277c2b54c..ac94192ae5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,7 +5,7 @@ on:
push:
branches:
- main
- - 'pr**'
+ - pr*
jobs:
CI:
runs-on: ubuntu-latest
diff --git a/.github/workflows/push-pr.yml b/.github/workflows/push-pr.yml
index 5a293fb19b..4c4cceec2e 100644
--- a/.github/workflows/push-pr.yml
+++ b/.github/workflows/push-pr.yml
@@ -1,4 +1,5 @@
name: push-pr
+run-name: Create/Update pr${{ inputs.prNumber }} branch
on:
workflow_dispatch:
inputs:
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/MappingTest.scala b/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/MappingTest.scala
index 0f27e25963..022e63ce0b 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/MappingTest.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/MappingTest.scala
@@ -175,7 +175,8 @@ object MappingTest {
composerUrl = Some(new URI("https://composer/api/2345678987654321345678"))
)),
syndicationUsageMetadata = Some(SyndicationUsageMetadata(
- partnerName = "friends of ours"
+ partnerName = "friends of ours",
+ syndicatedBy = Some("Bob")
)),
frontUsageMetadata = Some(FrontUsageMetadata(
addedBy = "me",
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/Mappings.scala b/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/Mappings.scala
index ff5d86e755..0b126e1839 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/Mappings.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/lib/elasticsearch/Mappings.scala
@@ -281,7 +281,8 @@ object Mappings {
))
def syndicationUsageMetadata(name: String): ObjectField = nonDynamicObjectField(name).copy(properties = Seq(
- keywordField("partnerName")
+ keywordField("partnerName"),
+ keywordField("syndicatedBy")
))
def frontUsageMetadata(name: String): ObjectField = nonDynamicObjectField(name).copy(properties = Seq(
@@ -293,7 +294,7 @@ object Mappings {
keywordField("downloadedBy")
))
- def usagesMapping(name: String): NestedField = nestedField(name).copy( properties = Seq(
+ def usagesMapping(name: String): NestedField = nestedField(name).copy(properties = Seq(
keywordField("id"),
sStemmerAnalysed("title"),
usageReference("references"),
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/ItemToMediaUsage.scala b/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/ItemToMediaUsage.scala
index c7fdf7bb69..dc9a84100d 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/ItemToMediaUsage.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/ItemToMediaUsage.scala
@@ -1,7 +1,6 @@
package com.gu.mediaservice.lib.usage
import java.net.URI
-
import com.amazonaws.services.dynamodbv2.document.Item
import com.gu.mediaservice.model.usage._
import org.joda.time.DateTime
@@ -52,7 +51,8 @@ object ItemToMediaUsage {
private def buildSyndication(metadataMap: Map[String, Any]): Option[SyndicationUsageMetadata] = {
Try {
SyndicationUsageMetadata(
- metadataMap("partnerName").asInstanceOf[String]
+ metadataMap("partnerName").asInstanceOf[String],
+ metadataMap.get("syndicatedBy").map(x => x.asInstanceOf[String])
)
}.toOption
}
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/UsageBuilder.scala b/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/UsageBuilder.scala
index d015333dea..4d422309f8 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/UsageBuilder.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/lib/usage/UsageBuilder.scala
@@ -64,7 +64,7 @@ object UsageBuilder {
private def buildSyndicationUsageReference(usage: MediaUsage): List[UsageReference] = usage.syndicationUsageMetadata.map (metadata => {
List(
UsageReference(
- SyndicationUsageReference, None, Some(metadata.partnerName)
+ SyndicationUsageReference, None, metadata.syndicatedBy.map(_ => s"${metadata.partnerName}, ${metadata.syndicatedBy.get}").orElse(Some(metadata.partnerName))
)
)
}).getOrElse(
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/model/ThrallMessage.scala b/common-lib/src/main/scala/com/gu/mediaservice/model/ThrallMessage.scala
index 40a8bfc8a6..41370db7d5 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/model/ThrallMessage.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/model/ThrallMessage.scala
@@ -76,6 +76,7 @@ object ExternalThrallMessage{
implicit val updateImagePhotoshootMetadataMessage = Json.format[UpdateImagePhotoshootMetadataMessage]
implicit val deleteUsagesMessage = Json.format[DeleteUsagesMessage]
implicit val deleteSingleUsageMessage = Json.format[DeleteSingleUsageMessage]
+ implicit val updateUsageStatusMessage = Json.format[UpdateUsageStatusMessage]
implicit val updateImageUsagesMessage = Json.format[UpdateImageUsagesMessage]
implicit val addImageLeaseMessage = Json.format[AddImageLeaseMessage]
implicit val removeImageLeaseMessage = Json.format[RemoveImageLeaseMessage]
@@ -124,6 +125,8 @@ case class DeleteSingleUsageMessage(id: String, lastModified: DateTime, usageId:
case class DeleteUsagesMessage(id: String, lastModified: DateTime) extends ExternalThrallMessage
+case class UpdateUsageStatusMessage(id: String, usageNotice: UsageNotice, lastModified: DateTime) extends ExternalThrallMessage
+
object DeleteUsagesMessage {
implicit val yourJodaDateReads = JodaReads.DefaultJodaDateTimeReads.map(d => d.withZone(DateTimeZone.UTC))
implicit val yourJodaDateWrites = JodaWrites.JodaDateTimeWrites
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/model/usage/SyndicationUsageMetadata.scala b/common-lib/src/main/scala/com/gu/mediaservice/model/usage/SyndicationUsageMetadata.scala
index 4ede81dd58..2804b82884 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/model/usage/SyndicationUsageMetadata.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/model/usage/SyndicationUsageMetadata.scala
@@ -3,11 +3,12 @@ package com.gu.mediaservice.model.usage
import play.api.libs.json._
case class SyndicationUsageMetadata(
- partnerName: String
+ partnerName: String,
+ syndicatedBy: Option[String] = None
) extends UsageMetadata {
override def toMap: Map[String, Any] = Map(
"partnerName" -> partnerName
- )
+ ) ++ syndicatedBy.map("syndicatedBy" -> _)
}
object SyndicationUsageMetadata {
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/model/usage/UsageStatus.scala b/common-lib/src/main/scala/com/gu/mediaservice/model/usage/UsageStatus.scala
index cbafb45fe7..3a356c1634 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/model/usage/UsageStatus.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/model/usage/UsageStatus.scala
@@ -9,6 +9,7 @@ sealed trait UsageStatus {
case RemovedUsageStatus => "removed"
case SyndicatedUsageStatus => "syndicated"
case DownloadedUsageStatus => "downloaded"
+ case FailedUsageStatus => "failed"
case UnknownUsageStatus => "unknown"
}
}
@@ -20,7 +21,9 @@ object UsageStatus {
case "removed" => RemovedUsageStatus
case "syndicated" => SyndicatedUsageStatus
case "downloaded" => DownloadedUsageStatus
+ case "failed" => FailedUsageStatus
case "unknown" => UnknownUsageStatus
+ case _ => throw new IllegalArgumentException("Invalid usage status")
}
implicit val reads: Reads[UsageStatus] = JsPath.read[String].map(UsageStatus(_))
@@ -35,6 +38,7 @@ object PublishedUsageStatus extends UsageStatus
object RemovedUsageStatus extends UsageStatus
object SyndicatedUsageStatus extends UsageStatus
object DownloadedUsageStatus extends UsageStatus
+object FailedUsageStatus extends UsageStatus
// For Fronts usages as we don't know if a front is in draft or is live
// TODO remove this once we do!
diff --git a/common-lib/src/main/scala/com/gu/mediaservice/syntax/MessageSubjects.scala b/common-lib/src/main/scala/com/gu/mediaservice/syntax/MessageSubjects.scala
index 8713d58a4a..a490f427f5 100644
--- a/common-lib/src/main/scala/com/gu/mediaservice/syntax/MessageSubjects.scala
+++ b/common-lib/src/main/scala/com/gu/mediaservice/syntax/MessageSubjects.scala
@@ -16,6 +16,7 @@ trait MessageSubjects {
val AddImageLease = "add-image-lease"
val RemoveImageLease = "remove-image-lease"
val SetImageCollections = "set-image-collections"
+ val UpdateUsageStatus = "update-usage-status"
val DeleteUsages = "delete-usages"
val DeleteSingleUsage = "delete-single-usage"
val UpdateImageSyndicationMetadata = "update-image-syndication-metadata"
diff --git a/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.html b/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.html
index 11fb29cdbe..ffe0d8c7bd 100644
--- a/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.html
+++ b/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.html
@@ -104,6 +104,7 @@
{{ctrl.metadata.title}}
@@ -112,6 +113,7 @@
{{ctrl.metadata.title}}
@@ -322,6 +324,7 @@
{{ctrl.metadata.byline}}
@@ -370,6 +373,7 @@
{{ctrl.metadata.credit}}
@@ -400,6 +404,7 @@
{{ctrl.metadata[prop]}}
{{ctrl.metadata.copyright}}
@@ -559,6 +565,7 @@
{{ctrl.extraInfo.uploadedBy | stripEmailDomain}}
@@ -639,6 +646,7 @@
{{key | spaceWords}}
{{value}}
@@ -647,6 +655,7 @@
{{key}}
{{metadata.value}}
@@ -832,6 +841,7 @@
ng-repeat="collection in ctrl.singleImage.data.collections"
ng-switch-default>
{{collection.data.path.join(' ▸ ')}}
diff --git a/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.js b/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.js
index 6cf4f31079..fbccf1b0ec 100644
--- a/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.js
+++ b/kahuna/public/js/components/gr-image-metadata/gr-image-metadata.js
@@ -12,6 +12,8 @@ import { editOptions, overwrite } from '../../util/constants/editOptions';
import '../../services/image-accessor';
import '../../services/image-list';
import '../../services/label';
+import '../../search/query-filter';
+
import { List } from 'immutable';
export const module = angular.module('gr.imageMetadata', [
@@ -34,8 +36,7 @@ module.controller('grImageMetadataCtrl', [
'inject$',
'labelService',
'storage',
-
-
+ 'searchWithModifiers',
function ($rootScope,
$scope,
$window,
@@ -47,7 +48,8 @@ module.controller('grImageMetadataCtrl', [
imageAccessor,
inject$,
labelService,
- storage) {
+ storage,
+ searchWithModifiers) {
let ctrl = this;
@@ -58,14 +60,14 @@ module.controller('grImageMetadataCtrl', [
ctrl.metadataUpdatedByTemplate = [];
ctrl.$onInit = () => {
- $scope.$watchCollection('ctrl.selectedImages', function() {
+ $scope.$watchCollection('ctrl.selectedImages', function () {
ctrl.singleImage = singleImage();
ctrl.selectedLabels = selectedLabels();
ctrl.usageRights = selectedUsageRights();
inject$($scope, Rx.Observable.fromPromise(selectedUsageCategory(ctrl.usageRights)), ctrl, 'usageCategory');
ctrl.rawMetadata = rawMetadata();
ctrl.metadata = displayMetadata();
- ctrl.metadata.dateTaken = ctrl.displayDateTakenMetadata();
+ ctrl.metadata.dateTaken = ctrl.displayDateTakenMetadata();
ctrl.newPeopleInImage = "";
ctrl.newKeywords = "";
ctrl.extraInfo = extraInfo();
@@ -76,7 +78,7 @@ module.controller('grImageMetadataCtrl', [
});
const freeUpdateListener = $rootScope.$on('images-updated',
- (e, updatedImages) => updateHandler(updatedImages));
+ (e, updatedImages) => updateHandler(updatedImages));
const updateHandler = (updatedImages) => {
ctrl.selectedImages = new List(updatedImages);
@@ -84,18 +86,20 @@ module.controller('grImageMetadataCtrl', [
ctrl.hasMultipleValues = (val) => Array.isArray(val) && val.length > 1;
- ctrl.displayDateTakenMetadata = function() {
+ ctrl.displayDateTakenMetadata = function () {
let dateTaken = ctrl.metadata.dateTaken ? new Date(ctrl.metadata.dateTaken) : undefined;
- if (dateTaken) { dateTaken.setSeconds(0, 0); }
+ if (dateTaken) {
+ dateTaken.setSeconds(0, 0);
+ }
return dateTaken;
};
- ctrl.credits = function(searchText) {
+ ctrl.credits = function (searchText) {
return ctrl.metadataSearch('credit', searchText);
};
ctrl.metadataSearch = (field, q) => {
- return mediaApi.metadataSearch(field, { q }).then(resource => {
+ return mediaApi.metadataSearch(field, {q}).then(resource => {
return resource.data.map(d => d.key);
});
};
@@ -104,11 +108,11 @@ module.controller('grImageMetadataCtrl', [
ctrl.descriptionOptions = editOptions;
- ctrl.updateDescriptionField = function() {
+ ctrl.updateDescriptionField = function () {
ctrl.updateMetadataField('description', ctrl.metadata.description);
};
- ctrl.updateLocationField = function(data, value) {
+ ctrl.updateLocationField = function (data, value) {
Object.keys(value).forEach(key => {
if (value[key] === undefined) {
delete value[key];
@@ -120,7 +124,7 @@ module.controller('grImageMetadataCtrl', [
ctrl.updateMetadataField = function (field, value) {
var imageArray = Array.from(ctrl.selectedImages);
if (field === 'dateTaken') {
- value = value.toISOString();
+ value = value.toISOString();
}
if (field === 'peopleInImage') {
ctrl.addPersonToImages(imageArray, value);
@@ -138,7 +142,7 @@ module.controller('grImageMetadataCtrl', [
);
};
- ctrl.updateDomainMetadataField = function(name, field, value) {
+ ctrl.updateDomainMetadataField = function (name, field, value) {
return editsService.updateDomainMetadataField(ctrl.singleImage, name, field, value)
.then((updatedImage) => {
if (updatedImage) {
@@ -240,7 +244,7 @@ module.controller('grImageMetadataCtrl', [
.map(([key, value]) => {
let fieldAlias = ctrl.fieldAliases.find(_ => _.alias === key);
if (fieldAlias && fieldAlias.displayInAdditionalMetadata === true) {
- return [fieldAlias.label, { value, alias: fieldAlias.alias}];
+ return [fieldAlias.label, {value, alias: fieldAlias.alias}];
}
})
.filter(_ => _ !== undefined));
@@ -250,7 +254,7 @@ module.controller('grImageMetadataCtrl', [
ctrl.domainMetadata = ctrl.domainMetadataSpecs
.filter(domainMetadataSpec => domainMetadataSpec.fields.length > 0)
.reduce((acc, domainMetadataSpec) => {
- let domainMetadata = { ...domainMetadataSpec };
+ let domainMetadata = {...domainMetadataSpec};
if (ctrl.singleImage.data.metadata) {
const imageDomainMetadata = ctrl.singleImage.data.metadata.domainMetadata ? ctrl.singleImage.data.metadata.domainMetadata : {};
@@ -284,7 +288,7 @@ module.controller('grImageMetadataCtrl', [
field.selectOptions = field.options
.filter(option => option)
.map(option => {
- return { value: option, text: option };
+ return {value: option, text: option};
});
}
@@ -332,7 +336,7 @@ module.controller('grImageMetadataCtrl', [
ctrl.hasLocationInformation = hasLocationInformation;
function singleImage() {
- if (ctrl.selectedImages.size === 1){
+ if (ctrl.selectedImages.size === 1) {
return ctrl.selectedImages.first();
}
}
@@ -371,9 +375,12 @@ module.controller('grImageMetadataCtrl', [
function rawMetadata() {
return selectedMetadata().map((values) => {
switch (values.size) {
- case 0: return undefined;
- case 1: return values.first();
- default: return Array.from(values);
+ case 0:
+ return undefined;
+ case 1:
+ return values.first();
+ default:
+ return Array.from(values);
}
}).toObject();
}
@@ -381,8 +388,10 @@ module.controller('grImageMetadataCtrl', [
function displayMetadata() {
return selectedMetadata().map((values) => {
switch (values.size) {
- case 1: return values.first();
- default: return undefined;
+ case 1:
+ return values.first();
+ default:
+ return undefined;
}
}).toObject();
}
@@ -392,9 +401,12 @@ module.controller('grImageMetadataCtrl', [
const properties = imageList.getSetOfProperties(info);
return properties.map((values) => {
switch (values.size) {
- case 0: return undefined;
- case 1: return values.first();
- default: return Array.from(values);
+ case 0:
+ return undefined;
+ case 1:
+ return values.first();
+ default:
+ return Array.from(values);
}
}).toObject();
}
@@ -421,11 +433,11 @@ module.controller('grImageMetadataCtrl', [
ctrl.removeImageFromCollection = (collection) => {
ctrl.removingCollection = collection;
collections.removeImageFromCollection(collection, ctrl.singleImage)
- .then(() => ctrl.removingCollection = false);
+ .then(() => ctrl.removingCollection = false);
};
- $scope.$on('$destroy', function() {
- freeUpdateListener();
+ $scope.$on('$destroy', function () {
+ freeUpdateListener();
});
ctrl.onMetadataTemplateSelected = (metadata, usageRights, collection, leasesWithConfig) => {
@@ -498,16 +510,18 @@ module.controller('grImageMetadataCtrl', [
};
ctrl.isDomainMetadataEmpty = (key) => {
- return ctrl.domainMetadata.find(obj => obj.name === key ).fields.every(field => field.value === undefined );
+ return ctrl.domainMetadata.find(obj => obj.name === key).fields.every(field => field.value === undefined);
};
ctrl.isAdditionalMetadataEmpty = () => {
const totalAdditionalMetadataCount = Object.keys(ctrl.metadata).filter(key => ctrl.isUsefulMetadata(key)).length +
- Object.keys(ctrl.additionalMetadata).length +
- Object.keys(ctrl.identifiers).length;
+ Object.keys(ctrl.additionalMetadata).length +
+ Object.keys(ctrl.identifiers).length;
return totalAdditionalMetadataCount == 0;
};
+
+ ctrl.searchWithModifiers = searchWithModifiers;
};
}
]);
diff --git a/kahuna/public/js/edits/list-editor-compact.html b/kahuna/public/js/edits/list-editor-compact.html
index 1851bbcdf2..326ae0e8d3 100644
--- a/kahuna/public/js/edits/list-editor-compact.html
+++ b/kahuna/public/js/edits/list-editor-compact.html
@@ -8,6 +8,7 @@
{{element}}
diff --git a/kahuna/public/js/edits/list-editor-info-panel.html b/kahuna/public/js/edits/list-editor-info-panel.html
index 9fcf8c4e1f..81208af2fa 100644
--- a/kahuna/public/js/edits/list-editor-info-panel.html
+++ b/kahuna/public/js/edits/list-editor-info-panel.html
@@ -8,6 +8,7 @@
library_add
diff --git a/kahuna/public/js/edits/list-editor.js b/kahuna/public/js/edits/list-editor.js
index cf86f91f9d..c26f84e547 100644
--- a/kahuna/public/js/edits/list-editor.js
+++ b/kahuna/public/js/edits/list-editor.js
@@ -22,13 +22,15 @@ listEditor.controller('ListEditorCtrl', [
'imageLogic',
'imageList',
'storage',
+ 'searchWithModifiers',
function($rootScope,
$scope,
$window,
$timeout,
imageLogic,
imageList,
- storage) {
+ storage,
+ searchWithModifiers) {
var ctrl = this;
ctrl.$onInit = () => {
@@ -147,6 +149,8 @@ listEditor.controller('ListEditorCtrl', [
$scope.$on('$destroy', function() {
updateListener();
});
+
+ ctrl.searchWithModifiers = searchWithModifiers;
};
}]);
diff --git a/kahuna/public/js/preview/image.html b/kahuna/public/js/preview/image.html
index 02ed16870c..285c010118 100644
--- a/kahuna/public/js/preview/image.html
+++ b/kahuna/public/js/preview/image.html
@@ -79,6 +79,7 @@
gr-tooltip-position="top">
{{::collection.data.description}}
diff --git a/kahuna/public/js/preview/image.js b/kahuna/public/js/preview/image.js
index 7983968c18..7909ff5d14 100644
--- a/kahuna/public/js/preview/image.js
+++ b/kahuna/public/js/preview/image.js
@@ -17,6 +17,7 @@ import '../components/gr-add-label/gr-add-label';
import '../components/gr-archiver-status/gr-archiver-status';
import '../components/gr-syndication-icon/gr-syndication-icon';
import {graphicImageBlurService} from "../services/graphic-image-blur";
+import '../search/query-filter';
export var image = angular.module('kahuna.preview.image', [
'gr.image.service',
@@ -43,6 +44,7 @@ image.controller('uiPreviewImageCtrl', [
'imageAccessor',
'storage',
'graphicImageBlurService',
+ 'searchWithModifiers',
function (
$scope,
inject$,
@@ -53,7 +55,8 @@ image.controller('uiPreviewImageCtrl', [
labelService,
imageAccessor,
storage,
- graphicImageBlurService) {
+ graphicImageBlurService,
+ searchWithModifiers) {
var ctrl = this;
ctrl.$onInit = () => {
@@ -131,6 +134,8 @@ image.controller('uiPreviewImageCtrl', [
return $window._clientConfig.imagePreviewFlagAlertCopy;
}
};
+
+ ctrl.searchWithModifiers = searchWithModifiers;
};
}]);
diff --git a/kahuna/public/js/search/query-filter.js b/kahuna/public/js/search/query-filter.js
index aeafcb017b..04de18c597 100644
--- a/kahuna/public/js/search/query-filter.js
+++ b/kahuna/public/js/search/query-filter.js
@@ -21,6 +21,37 @@ export function fieldFilter(field, value) {
return `${field}:${valueMaybeQuoted}`;
}
+queryFilters.factory('searchWithModifiers',
+ ['$state', '$stateParams', 'storage',
+ function($state, $stateParams, storage) {
+ function updateQueryWithModifiers(field, fieldValue, alt, shift, prevQuery) {
+ if (alt && prevQuery) {
+ return `${prevQuery} -${fieldFilter(field, fieldValue)}`;
+ }
+ if (alt) {
+ return `-${fieldFilter(field, fieldValue)}`;
+ }
+ if (shift && prevQuery) {
+ return `${prevQuery} ${fieldFilter(field, fieldValue)}`;
+ }
+ return fieldFilter(field, fieldValue);
+ }
+
+ return ($event, fieldName, fieldValue) => {
+ const alt = $event.getModifierState('Alt');
+ const shift = $event.getModifierState('Shift');
+ if (alt || shift) {
+ $event.preventDefault();
+ const nonFree = storage.getJs("isNonFree", true) ? true : undefined;
+
+ return $state.go('search.results', {
+ query: updateQueryWithModifiers(fieldName, fieldValue, alt, shift, $stateParams.query),
+ nonFree: nonFree
+ });
+ }
+ };
+ }]);
+
queryFilters.filter('queryFilter', function() {
return (value, field) => fieldFilter(field, value);
});
diff --git a/media-api/app/lib/querysyntax/QuerySyntax.scala b/media-api/app/lib/querysyntax/QuerySyntax.scala
index 1f5abd2930..2770e5e9ba 100644
--- a/media-api/app/lib/querysyntax/QuerySyntax.scala
+++ b/media-api/app/lib/querysyntax/QuerySyntax.scala
@@ -34,7 +34,8 @@ class QuerySyntax(val input: ParserInput) extends Parser with ImageFields {
DateConstraintMatch |
DateRangeMatch ~> Match | AtMatch |
FileTypeMatch ~> Match |
- ScopedMatch ~> Match | HashMatch | CollectionRule |
+ CollectionRule |
+ ScopedMatch ~> Match | HashMatch |
AnyMatch
}
@@ -83,7 +84,7 @@ class QuerySyntax(val input: ParserInput) extends Parser with ImageFields {
)
)}
- def CollectionRule = rule { '~' ~ ExactMatchValue ~> (
+ def CollectionRule = rule { ("~" | "collection:") ~ ExactMatchValue ~> (
collection => Match(
HierarchyField,
Phrase(collection.string.toLowerCase)
@@ -120,7 +121,6 @@ class QuerySyntax(val input: ParserInput) extends Parser with ImageFields {
"supplier" |
"specialInstructions" |
"title" |
- "collection" |
"keyword" |
"label" |
"croppedBy" |
@@ -134,7 +134,6 @@ class QuerySyntax(val input: ParserInput) extends Parser with ImageFields {
case "illustrator" => "credit"
case "uploader" => "uploadedBy"
case "label" => "labels"
- case "collection" => "suppliersCollection"
case "subject" => "subjects"
case "location" => "subLocation"
case "by" | "photographer" => "byline"
diff --git a/thrall/app/lib/elasticsearch/ElasticSearch.scala b/thrall/app/lib/elasticsearch/ElasticSearch.scala
index d212300151..a0c0156f43 100644
--- a/thrall/app/lib/elasticsearch/ElasticSearch.scala
+++ b/thrall/app/lib/elasticsearch/ElasticSearch.scala
@@ -8,7 +8,7 @@ import com.gu.mediaservice.lib.formatting.printDateTime
import com.gu.mediaservice.lib.logging.{LogMarker, MarkerMap}
import com.gu.mediaservice.model._
import com.gu.mediaservice.model.leases.MediaLease
-import com.gu.mediaservice.model.usage.Usage
+import com.gu.mediaservice.model.usage.{Usage, UsageNotice}
import com.gu.mediaservice.syntax._
import com.sksamuel.elastic4s.ElasticDsl._
import com.sksamuel.elastic4s.requests.script.Script
@@ -20,6 +20,7 @@ import com.sksamuel.elastic4s.requests.update.UpdateRequest
import com.sksamuel.elastic4s.{ElasticDsl, Executor, Functor, Handler, Response}
import lib.{BatchDeletionIds, ThrallMetrics}
import org.joda.time.DateTime
+import play.api.libs.json.JsValue.jsValueToJsLookup
import play.api.libs.json._
import scala.annotation.nowarn
@@ -503,6 +504,33 @@ class ElasticSearch(
}))
}
+ def updateUsageStatus(id: String, usages: Seq[Usage], lastModified: DateTime)
+ (implicit ex: ExecutionContext, logMarker: LogMarker): List[Future[ElasticSearchUpdateResponse]] = {
+
+ val updateUsageStatusScript =
+ s"""
+ | for(int i = 0; i < ctx._source.usages.size(); i++) {
+ | if(ctx._source.usages[i].id == params.usage.id) {
+ | ctx._source.usages[i].status = params.usage.status;
+ | ctx._source.usages[i].lastModified = params.lastModified;
+ | ctx._source.usagesLastModified = params.lastModified;
+ | }
+ | }
+ |""".stripMargin
+
+ val scriptSource = loadUpdatingModificationPainless(updateUsageStatusScript)
+
+ val usageParameters = JsDefined(Json.toJson(usages.head)).toOption.map(_.as[Usage]).map(i => asNestedMap(Json.toJson(i))).orNull
+
+ List(migrationAwareUpdater(
+ requestFromIndexName = indexName =>
+ prepareUpdateRequest(indexName, id, scriptSource, lastModified, ("usage", usageParameters)),
+ logMessageFromIndexName = indexName =>
+ s"ES6 updating usagesRights on image $id and usages id ${usageParameters.get("id")} " +
+ s"in index $indexName with usage $usageParameters"
+ ).map(_ => ElasticSearchUpdateResponse()))
+ }
+
def deleteSyndicationRights(id: String, lastModified: DateTime)
(implicit ex: ExecutionContext, logMarker: LogMarker): List[Future[ElasticSearchUpdateResponse]] = {
val deleteSyndicationRightsScript = s"""
@@ -777,4 +805,5 @@ class ElasticSearch(
image.transform(removeUploadInformation()).get
}
+
}
diff --git a/thrall/app/lib/kinesis/MessageProcessor.scala b/thrall/app/lib/kinesis/MessageProcessor.scala
index f86ccebddb..03c7b4616b 100644
--- a/thrall/app/lib/kinesis/MessageProcessor.scala
+++ b/thrall/app/lib/kinesis/MessageProcessor.scala
@@ -49,6 +49,7 @@ class MessageProcessor(
case message: CreateMigrationIndexMessage => createMigrationIndex(message, logMarker)
case message: MigrateImageMessage => migrateImage(message, logMarker)
case message: UpsertFromProjectionMessage => upsertImageFromProjection(message, logMarker)
+ case message: UpdateUsageStatusMessage => updateUsageStatus(message, logMarker)
case _: CompleteMigrationMessage => completeMigration(logMarker)
}
}
@@ -182,6 +183,14 @@ class MessageProcessor(
Future.sequence(es.deleteSingleImageUsage(message.id, message.usageId, message.lastModified)(ec, logMarker))
}
+ private def updateUsageStatus(message: UpdateUsageStatusMessage, logMarker: LogMarker)(implicit ec: ExecutionContext): Future[List[ElasticSearchUpdateResponse]] = {
+ implicit val lm: LogMarker = combineMarkers(message, logMarker)
+ val usage = message.usageNotice.usageJson.as[Seq[Usage]]
+ Future.traverse(es.updateUsageStatus(message.id, usage, message.lastModified ))(_.recoverWith {
+ case ElasticNotFoundException => Future.successful(ElasticSearchUpdateResponse())
+ })
+ }
+
def upsertSyndicationRightsOnly(message: UpdateImageSyndicationMetadataMessage, logMarker: LogMarker)(implicit ec: ExecutionContext): Future[Any] = {
implicit val marker: LogMarker = logMarker ++ imageIdMarker(ImageId(message.id))
es.getImage(message.id) map {
diff --git a/thrall/app/lib/kinesis/MessageTranslator.scala b/thrall/app/lib/kinesis/MessageTranslator.scala
index 8b4ae60a1c..77cd14ff08 100644
--- a/thrall/app/lib/kinesis/MessageTranslator.scala
+++ b/thrall/app/lib/kinesis/MessageTranslator.scala
@@ -73,6 +73,10 @@ object MessageTranslator extends GridLogging {
case (Some(id), Some(edits)) => Right(UpdateImagePhotoshootMetadataMessage(id, updateMessage.lastModified, edits))
case _ => Left(MissingFieldsException(updateMessage.subject))
}
+ case UpdateUsageStatus => (updateMessage.id, updateMessage.usageNotice ) match {
+ case (Some(id), Some(usageNotice)) => Right(UpdateUsageStatusMessage(id, usageNotice, updateMessage.lastModified))
+ case _ => Left(MissingFieldsException(updateMessage.subject))
+ }
case _ => Left(ProcessorNotFoundException(updateMessage.subject))
}
}
diff --git a/thrall/test/lib/elasticsearch/ElasticSearchTest.scala b/thrall/test/lib/elasticsearch/ElasticSearchTest.scala
index 7b32633eda..0416fb5e57 100644
--- a/thrall/test/lib/elasticsearch/ElasticSearchTest.scala
+++ b/thrall/test/lib/elasticsearch/ElasticSearchTest.scala
@@ -5,6 +5,7 @@ import com.gu.mediaservice.lib.logging.{LogMarker, MarkerMap}
import com.gu.mediaservice.model
import com.gu.mediaservice.model._
import com.gu.mediaservice.model.leases.{LeasesByMedia, MediaLease}
+import com.gu.mediaservice.model.usage.{PublishedUsageStatus, SyndicatedUsageStatus}
import com.gu.mediaservice.model.usage.Usage
import com.sksamuel.elastic4s.ElasticDsl
import com.sksamuel.elastic4s.ElasticDsl._
@@ -534,6 +535,18 @@ class ElasticSearchTest extends ElasticSearchTestBase {
reloadedImage(id).get.usages.head.id shouldEqual ("recent")
}
+
+ "can update usage status for single image" in {
+ val id = UUID.randomUUID().toString
+ val imageWithUsages = createImageForSyndication(id = UUID.randomUUID().toString, true, Some(now), None).copy(usages = List(usage(), usage()))
+ val usageWithUpdatedUsageStatus = imageWithUsages.usages.head.copy(status = SyndicatedUsageStatus)
+
+ Await.result(ES.migrationAwareIndexImage(id, imageWithUsages, now), fiveSeconds)
+
+ Await.result(Future.sequence(ES.updateUsageStatus(id, List(usageWithUpdatedUsageStatus), now)), fiveSeconds)
+ reloadedImage(id).get.usages.head.status shouldBe (SyndicatedUsageStatus)
+ reloadedImage(id).get.usages.last.status shouldBe (PublishedUsageStatus)
+ }
}
"syndication rights" - {
diff --git a/usage/app/controllers/UsageApi.scala b/usage/app/controllers/UsageApi.scala
index 934837bc4f..60ed5c0baf 100644
--- a/usage/app/controllers/UsageApi.scala
+++ b/usage/app/controllers/UsageApi.scala
@@ -9,11 +9,11 @@ import com.gu.mediaservice.lib.aws.UpdateMessage
import com.gu.mediaservice.lib.logging.{LogMarker, MarkerMap}
import com.gu.mediaservice.lib.play.RequestLoggingFilter
import com.gu.mediaservice.lib.usage.UsageBuilder
-import com.gu.mediaservice.model.usage.{MediaUsage, Usage}
+import com.gu.mediaservice.model.usage.{MediaUsage, SyndicatedUsageStatus, Usage, UsageNotice, UsageStatus}
import com.gu.mediaservice.syntax.MessageSubjects
import lib._
import model._
-import play.api.libs.json.{JsError, JsValue}
+import play.api.libs.json.{JsArray, JsError, JsValue, Json}
import play.api.mvc._
import play.utils.UriEncoding
import rx.lang.scala.Subject
@@ -256,6 +256,45 @@ class UsageApi(
)
}}
+ def updateUsageStatus(mediaId: String, usageId: String) = auth.async(parse.json) {req => {
+ val request = (req.body \ "data").validate[UsageStatus]
+ request.fold(
+ e => Future.successful(
+ respondError(
+ BadRequest,
+ errorKey = "update-image-usage-status-failed",
+ errorMessage = JsError.toJson(e).toString()
+ )
+ ),
+ usageStatus => {
+ implicit val logMarker: LogMarker = MarkerMap(
+ "requestType" -> "update-usage-status",
+ "requestId" -> RequestLoggingFilter.getRequestId(req),
+ "usageStatus" -> usageStatus.toString,
+ "image-id" -> mediaId,
+ "usage-id" -> usageId,
+ ) ++ apiKeyMarkers(req.user.accessor)
+ logger.info(logMarker, "recording usage status update")
+
+ usageTable.queryByUsageId(usageId).map {
+ case Some(mediaUsage) =>
+ val updatedStatusMediaUsage = mediaUsage.copy(status = usageStatus)
+ usageTable.update(updatedStatusMediaUsage)
+ val usageNotice = UsageNotice(mediaId,
+ JsArray(Seq(Json.toJson(UsageBuilder.build(updatedStatusMediaUsage)))))
+ val updateMessage = UpdateMessage(
+ subject = UpdateUsageStatus, id = Some(mediaId),
+ usageNotice = Some(usageNotice)
+ )
+ notifications.publish(updateMessage)
+ Ok
+ case None =>
+ NotFound
+ }
+ }
+ )
+ }}
+
def deleteSingleUsage(mediaId: String, usageId: String) = AuthenticatedAndAuthorisedToDelete.async { req =>
implicit val logMarker: LogMarker = MarkerMap(
"requestType" -> "delete-usage",
diff --git a/usage/app/model/SyndicationUsageRequest.scala b/usage/app/model/SyndicationUsageRequest.scala
index ff05282034..4b900ef81e 100644
--- a/usage/app/model/SyndicationUsageRequest.scala
+++ b/usage/app/model/SyndicationUsageRequest.scala
@@ -1,16 +1,21 @@
package model
-import com.gu.mediaservice.model.usage.{SyndicatedUsageStatus, SyndicationUsageMetadata, UsageStatus}
+import com.gu.mediaservice.model.usage.{PendingUsageStatus, SyndicatedUsageStatus, SyndicationUsageMetadata, UsageStatus}
import org.joda.time.DateTime
import play.api.libs.json._
case class SyndicationUsageRequest (
partnerName: String,
+ syndicatedBy: Option[String],
+ startPending: Option[Boolean],
mediaId: String,
dateAdded: DateTime
) {
- val status: UsageStatus = SyndicatedUsageStatus
- val metadata: SyndicationUsageMetadata = SyndicationUsageMetadata(partnerName)
+ val status: UsageStatus = startPending match {
+ case Some(true) => PendingUsageStatus
+ case _ => SyndicatedUsageStatus
+ }
+ val metadata: SyndicationUsageMetadata = SyndicationUsageMetadata(partnerName, syndicatedBy)
}
object SyndicationUsageRequest {
import JodaWrites._
diff --git a/usage/app/model/UsageGroup.scala b/usage/app/model/UsageGroup.scala
index bee9878745..77e5124ace 100644
--- a/usage/app/model/UsageGroup.scala
+++ b/usage/app/model/UsageGroup.scala
@@ -29,6 +29,7 @@ class UsageGroupOps(config: UsageConfig, mediaWrapperOps: MediaWrapperOps)
def buildId(syndicationUsageRequest: SyndicationUsageRequest): String = s"syndication/${
MD5.hash(List(
syndicationUsageRequest.metadata.partnerName,
+ syndicationUsageRequest.metadata.syndicatedBy,
syndicationUsageRequest.mediaId
).mkString("_"))
}"
diff --git a/usage/app/model/UsageIdBuilder.scala b/usage/app/model/UsageIdBuilder.scala
index b61b947991..fa5f8cf92a 100644
--- a/usage/app/model/UsageIdBuilder.scala
+++ b/usage/app/model/UsageIdBuilder.scala
@@ -23,6 +23,7 @@ object UsageIdBuilder {
def build(syndicationUsageRequest: SyndicationUsageRequest) = buildId(List(
Some(syndicationUsageRequest.mediaId),
Some(syndicationUsageRequest.metadata.partnerName),
+ syndicationUsageRequest.metadata.syndicatedBy,
Some(syndicationUsageRequest.status)
))
diff --git a/usage/conf/routes b/usage/conf/routes
index f8886bdc04..8bc0ac8e50 100644
--- a/usage/conf/routes
+++ b/usage/conf/routes
@@ -8,7 +8,7 @@ POST /usages/print controllers.UsageApi.set
POST /usages/syndication controllers.UsageApi.setSyndicationUsages
POST /usages/front controllers.UsageApi.setFrontUsages
POST /usages/download controllers.UsageApi.setDownloadUsages
-
+PUT /usages/status/update/:mediaId/*usageId controllers.UsageApi.updateUsageStatus(mediaId: String, usageId: String)
GET /usages/digital/content/*contentId/reindex controllers.UsageApi.reindexForContent(contentId: String)
# Management