diff --git a/client/components/Coverages/CoverageEditor/CoverageForm.jsx b/client/components/Coverages/CoverageEditor/CoverageForm.jsx index 24b099038..7ca500d5b 100644 --- a/client/components/Coverages/CoverageEditor/CoverageForm.jsx +++ b/client/components/Coverages/CoverageEditor/CoverageForm.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {get} from 'lodash'; import {getItemInArrayById, gettext, planningUtils, generateTempId} from '../../../utils'; import moment from 'moment'; -import {WORKFLOW_STATE} from '../../../constants'; +import {WORKFLOW_STATE, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; import {Button} from '../../UI'; import {Row, Label, LineInput} from '../../UI/Form'; import {ScheduledUpdate} from '../ScheduledUpdate'; @@ -400,6 +400,6 @@ CoverageForm.propTypes = { }; CoverageForm.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, }; diff --git a/client/components/Coverages/CoverageEditor/index.jsx b/client/components/Coverages/CoverageEditor/index.jsx index e74408121..34c8b9bf0 100644 --- a/client/components/Coverages/CoverageEditor/index.jsx +++ b/client/components/Coverages/CoverageEditor/index.jsx @@ -9,7 +9,7 @@ import {CoverageForm} from './CoverageForm'; import {CoverageFormHeader} from './CoverageFormHeader'; import {planningUtils, gettext, editorMenuUtils} from '../../../utils'; -import {COVERAGES} from '../../../constants'; +import {COVERAGES, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; export const CoverageEditor = ({ diff, @@ -249,6 +249,6 @@ CoverageEditor.propTypes = { }; CoverageEditor.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, }; diff --git a/client/components/Coverages/CoverageItem.jsx b/client/components/Coverages/CoverageItem.jsx index b1746207f..81cb553e1 100644 --- a/client/components/Coverages/CoverageItem.jsx +++ b/client/components/Coverages/CoverageItem.jsx @@ -15,7 +15,7 @@ import { planningUtils, } from '../../utils'; import {UserAvatar} from '../UserAvatar'; -import {WORKFLOW_STATE} from '../../constants'; +import {WORKFLOW_STATE, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../constants'; export const CoverageItem = ({ item, @@ -148,7 +148,7 @@ CoverageItem.propTypes = { }; CoverageItem.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, isPreview: false, }; diff --git a/client/components/Coverages/CoveragePreview/CoveragePreviewTopBar.jsx b/client/components/Coverages/CoveragePreview/CoveragePreviewTopBar.jsx index 107b7a3b8..ff10949a4 100644 --- a/client/components/Coverages/CoveragePreview/CoveragePreviewTopBar.jsx +++ b/client/components/Coverages/CoveragePreview/CoveragePreviewTopBar.jsx @@ -4,6 +4,7 @@ import {Row as PreviewRow} from '../../UI/Preview'; import moment from 'moment-timezone'; import {get} from 'lodash'; import {getCreator, getItemInArrayById, gettext} from '../../../utils'; +import {DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; import {StateLabel} from '../../index'; export const CoveragePreviewTopBar = ({ @@ -86,6 +87,6 @@ CoveragePreviewTopBar.propTypes = { CoveragePreviewTopBar.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, }; diff --git a/client/components/Coverages/CoveragePreview/index.jsx b/client/components/Coverages/CoveragePreview/index.jsx index 39092707e..e7e0fc237 100644 --- a/client/components/Coverages/CoveragePreview/index.jsx +++ b/client/components/Coverages/CoveragePreview/index.jsx @@ -5,7 +5,7 @@ import {CollapseBox} from '../../UI'; import {get} from 'lodash'; import {gettext, stringUtils, planningUtils} from '../../../utils'; import {ContactsPreviewList} from '../../Contacts/index'; -import {PLANNING, WORKFLOW_STATE} from '../../../constants'; +import {PLANNING, WORKFLOW_STATE, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; import {CoverageItem} from '../'; import {CoveragePreviewTopBar} from './CoveragePreviewTopBar'; @@ -205,6 +205,6 @@ CoveragePreview.propTypes = { CoveragePreview.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, }; diff --git a/client/components/Coverages/ScheduledUpdate/ScheduledUpdateForm.jsx b/client/components/Coverages/ScheduledUpdate/ScheduledUpdateForm.jsx index 542a6e56f..1776a29bb 100644 --- a/client/components/Coverages/ScheduledUpdate/ScheduledUpdateForm.jsx +++ b/client/components/Coverages/ScheduledUpdate/ScheduledUpdateForm.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {gettext, planningUtils} from '../../../utils'; -import {WORKFLOW_STATE} from '../../../constants'; +import {WORKFLOW_STATE, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; import { TextAreaInput, SelectInput, @@ -177,6 +177,6 @@ ScheduledUpdateForm.propTypes = { }; ScheduledUpdateForm.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, }; diff --git a/client/components/Coverages/ScheduledUpdate/index.jsx b/client/components/Coverages/ScheduledUpdate/index.jsx index 9c1ebe37d..6c573cb6a 100644 --- a/client/components/Coverages/ScheduledUpdate/index.jsx +++ b/client/components/Coverages/ScheduledUpdate/index.jsx @@ -12,7 +12,7 @@ import {CoverageFormHeader} from '../CoverageEditor/CoverageFormHeader'; import {CoveragePreviewTopBar} from '../CoveragePreview/CoveragePreviewTopBar'; import {planningUtils, gettext, stringUtils} from '../../../utils'; -import {PLANNING, COVERAGES} from '../../../constants'; +import {PLANNING, COVERAGES, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; export const ScheduledUpdate = ({ diff, @@ -255,7 +255,7 @@ ScheduledUpdate.propTypes = { }; ScheduledUpdate.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, openScheduledUpdates: [], }; diff --git a/client/components/Events/EventMetadata/index.jsx b/client/components/Events/EventMetadata/index.jsx index aa819faa4..60d74ead9 100644 --- a/client/components/Events/EventMetadata/index.jsx +++ b/client/components/Events/EventMetadata/index.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {get} from 'lodash'; -import {ICON_COLORS} from '../../../constants'; +import {ICON_COLORS, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; import {StateLabel} from '../..'; import {EventScheduleSummary} from '../'; import {ItemIcon} from '../../index'; @@ -250,8 +250,8 @@ EventMetadata.propTypes = { EventMetadata.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, scrollInView: true, showIcon: true, showBorder: true, diff --git a/client/components/Events/EventScheduleSummary/index.jsx b/client/components/Events/EventScheduleSummary/index.jsx index 7d824cbef..a043340ad 100644 --- a/client/components/Events/EventScheduleSummary/index.jsx +++ b/client/components/Events/EventScheduleSummary/index.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import {RepeatEventSummary} from '../RepeatEventSummary'; import {Row} from '../../UI/Preview'; import {gettext, eventUtils, timeUtils} from '../../../utils'; +import {DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../../../constants'; import {get} from 'lodash'; import './style.scss'; @@ -75,8 +76,8 @@ EventScheduleSummary.propTypes = { }; EventScheduleSummary.defaultProps = { - dateFormat: 'DD/MM/YYYY', - timeFormat: 'HH:mm', + dateFormat: DEFAULT_DATE_FORMAT, + timeFormat: DEFAULT_TIME_FORMAT, noPadding: false, useEventTimezone: false, }; diff --git a/client/constants/index.js b/client/constants/index.js index 4fcc1f568..8507d37bf 100644 --- a/client/constants/index.js +++ b/client/constants/index.js @@ -21,6 +21,9 @@ export {EVENTS} from './events'; export const WS_NOTIFICATION = 'WS_NOTIFICATION'; +export const DEFAULT_DATE_FORMAT = 'DD/MM/YYYY'; +export const DEFAULT_TIME_FORMAT = 'HH:mm'; + export const DATE_FORMATS = { COMPARE_FORMAT: 'YYYY-M-D', DISPLAY_DATE_FORMAT: 'D. MMMM YYYY HH:mm', diff --git a/client/index.js b/client/index.js index e0b347ad7..017d3d4c5 100644 --- a/client/index.js +++ b/client/index.js @@ -145,6 +145,13 @@ export default angular.module('superdesk-planning', []) assignments.onPublishFromAuthoring ); } + + if (get(deployConfig, 'config.planning_link_updates_to_coverage')) { + functionPoints.register( + 'archive:rewrite_after', + assignments.onArchiveRewrite + ); + } }); }, ]); diff --git a/client/services/AssignmentsService.js b/client/services/AssignmentsService.js index 7978ed575..8efa9fb3c 100644 --- a/client/services/AssignmentsService.js +++ b/client/services/AssignmentsService.js @@ -1,10 +1,11 @@ import {get} from 'lodash'; import React from 'react'; import {Provider} from 'react-redux'; +import moment from 'moment'; -import {gettext} from '../utils'; +import {gettext, planningUtils} from '../utils'; -import {WORKSPACE, MODALS, ASSIGNMENTS} from '../constants'; +import {WORKSPACE, MODALS, ASSIGNMENTS, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT} from '../constants'; import {ModalsContainer} from '../components'; import * as actions from '../actions'; @@ -19,6 +20,8 @@ export class AssignmentsService { this.config = config; this.onPublishFromAuthoring = this.onPublishFromAuthoring.bind(this); + this.onArchiveRewrite = this.onArchiveRewrite.bind(this); + this.onUnloadModal = this.onUnloadModal.bind(); } getAssignmentQuery(slugline, contentType) { @@ -82,6 +85,82 @@ export class AssignmentsService { }); } + onArchiveRewrite(item) { + if (!get(item, 'assignment_id')) { + return Promise.resolve(item); + } + + return this.api('assignments').getById(item.assignment_id) + .then((assignment) => ( + this.api('planning').query({ + source: JSON.stringify({ + query: {terms: {_id: [get(assignment, 'planning_item')]}}, + }), + }) + .then((data) => { + let items = get(data, '_items', []); + + items.forEach((item) => { + planningUtils.modifyForClient((item)); + }); + + const planning = get(items, '[0]'); + + if (!planning) { + return Promise.resolve(item); + } + // Check to see if there is a scheduled update following this assignment + // that is available for linking + const coverage = get(planning, 'coverages', []).find((c) => + c.coverage_id === assignment.coverage_item); + + if (!coverage || get(coverage, 'scheduled_updates.length', 0) <= 0) { + return Promise.resolve(item); + } + + let availableScheduledUpdate; + + if (item.assignment_id === get(coverage, 'assigned_to.assignment_id')) { + if (![ASSIGNMENTS.WORKFLOW_STATE.ASSIGNED, ASSIGNMENTS.WORKFLOW_STATE.SUBMITTED].includes( + get(coverage, 'scheduled_updates[0].assigned_to.state'))) { + return Promise.resolve(item); + } + + availableScheduledUpdate = coverage.scheduled_updates[0]; + } else { + const linkedScheduledUpdateIndex = coverage.scheduled_updates.findIndex( + (s) => assignment._id === get(s, 'assigned_to.assignment_id')); + + if (linkedScheduledUpdateIndex < 0 || + (linkedScheduledUpdateIndex === coverage.scheduled_updates.length - 1) || + ![ASSIGNMENTS.WORKFLOW_STATE.ASSIGNED, ASSIGNMENTS.WORKFLOW_STATE.SUBMITTED].includes( + get(coverage, + `scheduled_updates[${linkedScheduledUpdateIndex + 1}].assigned_to.state`))) { + return Promise.resolve(item); + } + + availableScheduledUpdate = coverage.scheduled_updates[linkedScheduledUpdateIndex + 1]; + } + + if (!availableScheduledUpdate) { + return Promise.resolve(item); + } + + return new Promise((resolve, reject) => this.showScheduleUpdatesConfirmationModal( + item, + availableScheduledUpdate, + resolve, + reject)); + }, (error) => + // At errors we leave the archive item as it is + Promise.resolve(item) + ) + ), (error) => + // At errors we leave the archive item as it is + Promise.resolve(item) + ); + } + getBySlugline(slugline, contentType) { return this.api('assignments').query({ source: JSON.stringify({ @@ -93,6 +172,63 @@ export class AssignmentsService { }); } + onUnloadModal(store, closeModal, action, res) { + closeModal(); + store.dispatch(actions.resetStore()); + return action(res); + } + + showScheduleUpdatesConfirmationModal(newsItem, scheduledUpdate, resolve, reject) { + let store; + + return this.sdPlanningStore.initWorkspace(WORKSPACE.AUTHORING, (newStore) => { + store = newStore; + return Promise.resolve(); + }) + .then(() => this.modal.createCustomModal() + .then(({openModal, closeModal}) => { + openModal( + + + + ); + + const $scope = { + resolve: () => + // link to the new assignment + this.api('assignments_link').save({}, { + assignment_id: get(scheduledUpdate, 'assigned_to.assignment_id'), + item_id: newsItem._id, + reassign: true, + force: true, + }) + .then((item) => { + newsItem.assignment_id = item.assignment_id; + this.onUnloadModal(store, closeModal, resolve, item); + }, () => { + this.notify.warning(gettext('Failed to link to requested assignment!')); + this.onUnloadModal(store, closeModal, resolve, newsItem); + }), + reject: () => { + this.onUnloadModal(store, closeModal, resolve, newsItem); + }, + }; + + const time = moment(scheduledUpdate.planning.scheduled).format( + DEFAULT_DATE_FORMAT + ' ' + DEFAULT_TIME_FORMAT); + const prompt = gettext('Do you want to link it to that assignment ?'); + + store.dispatch(actions.showModal({ + modalType: MODALS.CONFIRMATION, + modalProps: { + body: gettext(`There is an update assignment for this story due at '${time}'. ${prompt}`), + action: $scope.resolve, + onCancel: $scope.reject, + }, + })); + })); + } + showLinkAssignmentModal(item, resolve, reject) { let store; @@ -123,19 +259,12 @@ export class AssignmentsService { ) ); - const onUnload = () => { - closeModal(); - store.dispatch(actions.resetStore()); - }; - const $scope = { resolve: () => { - onUnload(); - resolve(); + this.onUnloadModal(store, closeModal, resolve); }, reject: () => { - onUnload(); - reject(); + this.onUnloadModal(store, closeModal, reject); }, }; diff --git a/server/features/assignments_content.feature b/server/features/assignments_content.feature index 5d94c0e9e..9c72f38b4 100644 --- a/server/features/assignments_content.feature +++ b/server/features/assignments_content.feature @@ -665,7 +665,564 @@ Feature: Assignment content @auth @notification - Scenario: Coverage should be linked if scheduled update is going to be linked + Scenario: Previous scheduled update should be linked if scheduled update is going to be linked + Given empty "planning" + When we post to "planning" + """ + [{ + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + Then we get OK response + Then we store coverage id in "firstcoverage" from coverage 0 + Then we store assignment id in "firstassignment" from coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "workflow_status": "draft", + "coverage_id": "#firstcoverage#", + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + When we get "assignments/#firstassignment#" + Then we get existing resource + """ + { "_id": "#firstassignment#" } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we store scheduled_update id in "firstscheduled" from scheduled_update 0 of coverage 0 + Then we store scheduled_update id in "secondscheduled" from scheduled_update 1 of coverage 0 + Then we store assignment id in "firstscheduledassignment" from scheduled_update 0 of coverage 0 + Then we store assignment id in "secondscheduledassignment" from scheduled_update 1 of coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00+0000" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00+0000" + } + }, + { + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "scheduled_update_id": "#firstscheduled#", + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "scheduled_update_id": "#secondscheduled#", + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we get OK response + When we post to "/assignments/content" + """ + [{"assignment_id": "#firstassignment#"}] + """ + Then we get OK response + When we post to "/assignments/content" + """ + [{"assignment_id": "#secondscheduledassignment#"}] + """ + Then we get error 400 + """ + {"_message": "Previous scheduled update not linked to news item yet."} + """ + + @auth + @notification + Scenario: Scheduled update story can be created if previous scheduled update is marked as 'complete' + Given empty "planning" + When we post to "planning" + """ + [{ + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + Then we get OK response + Then we store coverage id in "firstcoverage" from coverage 0 + Then we store assignment id in "firstassignment" from coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "workflow_status": "draft", + "coverage_id": "#firstcoverage#", + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + When we get "assignments/#firstassignment#" + Then we get existing resource + """ + { "_id": "#firstassignment#" } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we store scheduled_update id in "firstscheduled" from scheduled_update 0 of coverage 0 + Then we store scheduled_update id in "secondscheduled" from scheduled_update 1 of coverage 0 + Then we store assignment id in "firstscheduledassignment" from scheduled_update 0 of coverage 0 + Then we store assignment id in "secondscheduledassignment" from scheduled_update 1 of coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00+0000" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00+0000" + } + }, + { + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "scheduled_update_id": "#firstscheduled#", + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "scheduled_update_id": "#secondscheduled#", + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we get OK response + When we post to "/assignments/content" + """ + [{"assignment_id": "#firstassignment#"}] + """ + Then we get OK response + When we patch "/assignments/#firstscheduledassignment#" + """ + { + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "completed" + } + } + """ + Then we get OK response + When we post to "/assignments/content" + """ + [{"assignment_id": "#secondscheduledassignment#"}] + """ + Then we get OK response + + @auth + @notification + @link_updates + Scenario: Start working on scheduled update picks up the latest update of the story Given empty "planning" When we post to "planning" """ @@ -923,55 +1480,59 @@ Feature: Assignment content } """ Then we get OK response - When we post to "/archive" with success + When we post to "/assignments/content" """ - [{"type": "text", "headline": "test", "state": "fetched", - "task": {"desk": "#desks._id#", "stage": "#desks.incoming_stage#", "user": "#CONTEXT_USER_ID#"}, - "subject":[{"qcode": "17004000", "name": "Statistics"}], - "slugline": "test", - "body_html": "Test Document body", - "target_subscribers": [{"_id": "#subscribers._id#"}], - "dateline": { - "located" : { - "country" : "Afghanistan", - "tz" : "Asia/Kabul", - "city" : "Mazar-e Sharif", - "alt_name" : "", - "country_code" : "AF", - "city_code" : "Mazar-e Sharif", - "dateline" : "city", - "state" : "Balkh", - "state_code" : "AF.30" - }, - "text" : "MAZAR-E SHARIF, Dec 30 -", - "source": "AAP"} - }] + [{"assignment_id": "#firstassignment#"}] """ - And we publish "#archive._id#" with "publish" type and "published" state + Then we store "NEW_ITEM" from patch + When we get "/archive/#NEW_ITEM._id#" + Then we get existing resource + """ + { + "_id": "#NEW_ITEM._id#", + "assignment_id": "#firstassignment#" + } + """ + When we publish "#NEW_ITEM._id#" with "publish" type and "published" state Then we get OK response - When we rewrite "#archive._id#" + When we rewrite "#NEW_ITEM._id#" """ {"desk_id": "#desks._id#"} """ Then we get OK response - And we store "rewrite1" with value "#REWRITE_ID#" to context - When we publish "#REWRITE_ID#" with "publish" type and "published" state - Then we get OK response - When we rewrite "#rewrite1#" + When we get "/archive/#REWRITE_ID#" + Then we get existing resource """ - {"desk_id": "#desks._id#"} + { + "_id": "#REWRITE_ID#", + "assignment_id": "#firstassignment#" + } """ + When we publish "#REWRITE_ID#" with "publish" type and "published" state Then we get OK response - When we post to "/assignments/content" + When we patch "/assignments/#firstscheduledassignment#" """ - [{"assignment_id": "#firstassignment#"}] + { + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "completed" + } + } """ Then we get OK response When we post to "/assignments/content" """ [{"assignment_id": "#secondscheduledassignment#"}] """ - Then we get error 400 + Then we get OK response + And we store "REWRITE_ITEM" from patch + When we get "/archive/#REWRITE_ITEM._id#" + Then we get existing resource """ - {"_message": "Previous scheduled update not linked to news item yet."} + { + "_id": "#REWRITE_ITEM._id#", + "assignment_id": "#secondscheduledassignment#", + "rewrite_of": "#REWRITE_ID#" + } """ diff --git a/server/features/assignments_link.feature b/server/features/assignments_link.feature index d8ccd8f35..21671460e 100644 --- a/server/features/assignments_link.feature +++ b/server/features/assignments_link.feature @@ -19,7 +19,11 @@ Feature: Assignment link """ And "desks" """ - [{"name": "Sports", "content_expiry": 60}] + [{ + "name": "Sports", + "content_expiry": 60, + "members": [ {"user": "#CONTEXT_USER_ID#"} ] + }] """ @auth @notification @@ -1025,7 +1029,8 @@ Feature: Assignment link } """ - @auth @link_updates + @auth + @link_updates Scenario: Completed assignment remains completed when linked story is updated Given the "validators" """ @@ -1673,8 +1678,9 @@ Feature: Assignment link """ @auth - @notification @today - Scenario: Correct sequqnce rewrite can be linked to a scheduled update assignment + @notification + @link_updates + Scenario: Can't link scheduled update if coverage is not linked Given empty "planning" When we post to "planning" """ @@ -1979,4 +1985,1330 @@ Feature: Assignment link "reassign": true }] """ + Then we get error 400 + """ + {"_message": "Previous coverage is not linked to content."} + """ + + @auth + @notification + @link_updates + Scenario: Can't link scheduled update if previous scheduled update is not linked + Given empty "planning" + When we post to "planning" + """ + [{ + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ Then we get OK response + Then we store coverage id in "firstcoverage" from coverage 0 + Then we store assignment id in "firstassignment" from coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "workflow_status": "draft", + "coverage_id": "#firstcoverage#", + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + When we get "assignments/#firstassignment#" + Then we get existing resource + """ + { "_id": "#firstassignment#" } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we store scheduled_update id in "firstscheduled" from scheduled_update 0 of coverage 0 + Then we store scheduled_update id in "secondscheduled" from scheduled_update 1 of coverage 0 + Then we store assignment id in "firstscheduledassignment" from scheduled_update 0 of coverage 0 + Then we store assignment id in "secondscheduledassignment" from scheduled_update 1 of coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00+0000" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00+0000" + } + }, + { + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "scheduled_update_id": "#firstscheduled#", + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "scheduled_update_id": "#secondscheduled#", + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we get OK response + When we post to "/archive" with success + """ + [{"type": "text", "headline": "test", "state": "fetched", + "task": {"desk": "#desks._id#", "stage": "#desks.incoming_stage#", "user": "#CONTEXT_USER_ID#"}, + "subject":[{"qcode": "17004000", "name": "Statistics"}], + "slugline": "test", + "body_html": "Test Document body", + "target_subscribers": [{"_id": "#subscribers._id#"}], + "dateline": { + "located" : { + "country" : "Afghanistan", + "tz" : "Asia/Kabul", + "city" : "Mazar-e Sharif", + "alt_name" : "", + "country_code" : "AF", + "city_code" : "Mazar-e Sharif", + "dateline" : "city", + "state" : "Balkh", + "state_code" : "AF.30" + }, + "text" : "MAZAR-E SHARIF, Dec 30 -", + "source": "AAP"} + }] + """ + And we publish "#archive._id#" with "publish" type and "published" state + Then we get OK response + When we rewrite "#archive._id#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + And we store "rewrite1" with value "#REWRITE_ID#" to context + When we publish "#REWRITE_ID#" with "publish" type and "published" state + Then we get OK response + When we post to "assignments/link" + """ + [{ + "assignment_id": "#firstassignment#", + "item_id": "#rewrite1#", + "reassign": true + }] + """ + Then we get OK response + When we rewrite "#rewrite1#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + When we get "/archive/#rewrite1#" + Then we get existing resource + """ + { "assignment_id": "#firstassignment#" } + """ + When we post to "assignments/link" + """ + [{ + "assignment_id": "#secondscheduledassignment#", + "item_id": "#REWRITE_ID#", + "reassign": true, + "force": true + }] + """ + Then we get error 400 + """ + {"_message": "Previous scheduled-update pending content-linking/completion"} + """ + + @auth + @notification + @link_updates + Scenario: Content will link by default to latest in_progress/completed assignment + Given empty "planning" + When we post to "planning" + """ + [{ + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + Then we get OK response + Then we store coverage id in "firstcoverage" from coverage 0 + Then we store assignment id in "firstassignment" from coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "workflow_status": "draft", + "coverage_id": "#firstcoverage#", + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + When we get "assignments/#firstassignment#" + Then we get existing resource + """ + { "_id": "#firstassignment#" } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we store scheduled_update id in "firstscheduled" from scheduled_update 0 of coverage 0 + Then we store scheduled_update id in "secondscheduled" from scheduled_update 1 of coverage 0 + Then we store assignment id in "firstscheduledassignment" from scheduled_update 0 of coverage 0 + Then we store assignment id in "secondscheduledassignment" from scheduled_update 1 of coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00+0000" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00+0000" + } + }, + { + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "scheduled_update_id": "#firstscheduled#", + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "scheduled_update_id": "#secondscheduled#", + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we get OK response + When we post to "/archive" with success + """ + [{"type": "text", "headline": "test", "state": "fetched", + "task": {"desk": "#desks._id#", "stage": "#desks.incoming_stage#", "user": "#CONTEXT_USER_ID#"}, + "subject":[{"qcode": "17004000", "name": "Statistics"}], + "slugline": "test", + "body_html": "Test Document body", + "target_subscribers": [{"_id": "#subscribers._id#"}], + "dateline": { + "located" : { + "country" : "Afghanistan", + "tz" : "Asia/Kabul", + "city" : "Mazar-e Sharif", + "alt_name" : "", + "country_code" : "AF", + "city_code" : "Mazar-e Sharif", + "dateline" : "city", + "state" : "Balkh", + "state_code" : "AF.30" + }, + "text" : "MAZAR-E SHARIF, Dec 30 -", + "source": "AAP"} + }] + """ + And we publish "#archive._id#" with "publish" type and "published" state + Then we get OK response + When we rewrite "#archive._id#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + And we store "rewrite1" with value "#REWRITE_ID#" to context + When we publish "#REWRITE_ID#" with "publish" type and "published" state + Then we get OK response + When we post to "assignments/link" + """ + [{ + "assignment_id": "#firstassignment#", + "item_id": "#rewrite1#", + "reassign": true + }] + """ + Then we get OK response + When we patch "/assignments/#firstscheduledassignment#" + """ + { + "assigned_to": { + "desk": "#desks._id#", + "user": "#CONTEXT_USER_ID#", + "state": "completed" + } + } + """ + Then we get OK response + When we rewrite "#rewrite1#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + When we get "/archive/#REWRITE_ID#" + Then we get existing resource + """ + { "assignment_id": "#firstscheduledassignment#" } + """ + + @auth + @notification + @link_updates + Scenario: Using 'force' option will reassign to a new assignment + Given empty "planning" + When we post to "planning" + """ + [{ + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + Then we get OK response + Then we store coverage id in "firstcoverage" from coverage 0 + Then we store assignment id in "firstassignment" from coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "workflow_status": "draft", + "coverage_id": "#firstcoverage#", + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + When we get "assignments/#firstassignment#" + Then we get existing resource + """ + { "_id": "#firstassignment#" } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we store scheduled_update id in "firstscheduled" from scheduled_update 0 of coverage 0 + Then we store scheduled_update id in "secondscheduled" from scheduled_update 1 of coverage 0 + Then we store assignment id in "firstscheduledassignment" from scheduled_update 0 of coverage 0 + Then we store assignment id in "secondscheduledassignment" from scheduled_update 1 of coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00+0000" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00+0000" + } + }, + { + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "scheduled_update_id": "#firstscheduled#", + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "scheduled_update_id": "#secondscheduled#", + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we get OK response + When we post to "/archive" with success + """ + [{"type": "text", "headline": "test", "state": "fetched", + "task": {"desk": "#desks._id#", "stage": "#desks.incoming_stage#", "user": "#CONTEXT_USER_ID#"}, + "subject":[{"qcode": "17004000", "name": "Statistics"}], + "slugline": "test", + "body_html": "Test Document body", + "target_subscribers": [{"_id": "#subscribers._id#"}], + "dateline": { + "located" : { + "country" : "Afghanistan", + "tz" : "Asia/Kabul", + "city" : "Mazar-e Sharif", + "alt_name" : "", + "country_code" : "AF", + "city_code" : "Mazar-e Sharif", + "dateline" : "city", + "state" : "Balkh", + "state_code" : "AF.30" + }, + "text" : "MAZAR-E SHARIF, Dec 30 -", + "source": "AAP"} + }] + """ + And we publish "#archive._id#" with "publish" type and "published" state + Then we get OK response + When we rewrite "#archive._id#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + And we store "rewrite1" with value "#REWRITE_ID#" to context + When we publish "#REWRITE_ID#" with "publish" type and "published" state + Then we get OK response + When we post to "assignments/link" + """ + [{ + "assignment_id": "#firstassignment#", + "item_id": "#rewrite1#", + "reassign": true + }] + """ + Then we get OK response + When we patch "/assignments/#firstscheduledassignment#" + """ + { + "assigned_to": { + "desk": "#desks._id#", + "user": "#CONTEXT_USER_ID#", + "state": "completed" + } + } + """ + Then we get OK response + When we rewrite "#rewrite1#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + When we get "/archive/#REWRITE_ID#" + Then we get existing resource + """ + { "assignment_id": "#firstscheduledassignment#" } + """ + When we post to "assignments/link" + """ + [{ + "assignment_id": "#secondscheduledassignment#", + "item_id": "#REWRITE_ID#", + "reassign": true, + "force": true + }] + """ + Then we get OK response + When we get "/archive/#REWRITE_ID#" + Then we get existing resource + """ + { "assignment_id": "#secondscheduledassignment#" } + """ + + @auth + @notification + @link_updates + Scenario: Using 'force' option will still validate assignment being linked + Given empty "planning" + When we post to "planning" + """ + [{ + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + Then we get OK response + Then we store coverage id in "firstcoverage" from coverage 0 + Then we store assignment id in "firstassignment" from coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "workflow_status": "draft", + "coverage_id": "#firstcoverage#", + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + } + } + ] + } + """ + When we get "assignments/#firstassignment#" + Then we get existing resource + """ + { "_id": "#firstassignment#" } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "assigned_to": { + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we store scheduled_update id in "firstscheduled" from scheduled_update 0 of coverage 0 + Then we store scheduled_update id in "secondscheduled" from scheduled_update 1 of coverage 0 + Then we store assignment id in "firstscheduledassignment" from scheduled_update 0 of coverage 0 + Then we store assignment id in "secondscheduledassignment" from scheduled_update 1 of coverage 0 + Then we get existing resource + """ + { + "_id": "#planning._id#", + "guid": "123", + "item_class": "item class value", + "headline": "test headline", + "slugline": "test slugline", + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00+0000" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00+0000" + } + }, + { + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "draft" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "draft", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + When we patch "/planning/#planning._id#" + """ + { + "coverages": [ + { + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "ednote": "test coverage, I want 250 words", + "headline": "test headline", + "slugline": "test slugline", + "scheduled": "2029-11-21T14:00:00.000Z" + }, + "assigned_to": { + "assignment_id": "#firstassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb" + }, + "scheduled_updates": [{ + "scheduled_update_id": "#firstscheduled#", + "assigned_to": { + "assignment_id": "#firstscheduledassignment#", + "desk": "#desks._id#", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-27T14:00:00.000Z" + } + }, + { + "scheduled_update_id": "#secondscheduled#", + "assigned_to": { + "assignment_id": "#secondscheduledassignment#", + "desk": "desk_123", + "user": "507f191e810c19729de870eb", + "state": "active" + }, + "coverage_id": "#firstcoverage#", + "workflow_status": "active", + "news_coverage_status": { + "qcode": "ncostat:int" + }, + "planning": { + "internal_note": "Int. note", + "scheduled": "2029-11-28T14:00:00+0000" + } + }] + } + ] + } + """ + Then we get OK response + When we post to "/archive" with success + """ + [{"type": "text", "headline": "test", "state": "fetched", + "task": {"desk": "#desks._id#", "stage": "#desks.incoming_stage#", "user": "#CONTEXT_USER_ID#"}, + "subject":[{"qcode": "17004000", "name": "Statistics"}], + "slugline": "test", + "body_html": "Test Document body", + "target_subscribers": [{"_id": "#subscribers._id#"}], + "dateline": { + "located" : { + "country" : "Afghanistan", + "tz" : "Asia/Kabul", + "city" : "Mazar-e Sharif", + "alt_name" : "", + "country_code" : "AF", + "city_code" : "Mazar-e Sharif", + "dateline" : "city", + "state" : "Balkh", + "state_code" : "AF.30" + }, + "text" : "MAZAR-E SHARIF, Dec 30 -", + "source": "AAP"} + }] + """ + And we publish "#archive._id#" with "publish" type and "published" state + Then we get OK response + When we rewrite "#archive._id#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + And we store "rewrite1" with value "#REWRITE_ID#" to context + When we publish "#REWRITE_ID#" with "publish" type and "published" state + Then we get OK response + When we post to "assignments/link" + """ + [{ + "assignment_id": "#firstassignment#", + "item_id": "#rewrite1#", + "reassign": true + }] + """ + Then we get OK response + When we rewrite "#rewrite1#" + """ + {"desk_id": "#desks._id#"} + """ + Then we get OK response + When we get "/archive/#REWRITE_ID#" + Then we get existing resource + """ + { "assignment_id": "#firstassignment#" } + """ + When we post to "assignments/link" + """ + [{ + "assignment_id": "#secondscheduledassignment#", + "item_id": "#REWRITE_ID#", + "reassign": true, + "force": true + }] + """ + Then we get error 400 + """ + {"_message": "Previous scheduled-update pending content-linking/completion"} + """ \ No newline at end of file diff --git a/server/planning/__init__.py b/server/planning/__init__.py index f051392ce..1fc494b0c 100644 --- a/server/planning/__init__.py +++ b/server/planning/__init__.py @@ -18,7 +18,7 @@ from .planning_article_export import PlanningArticleExportResource, PlanningArticleExportService from .common import get_max_recurrent_events, get_street_map_url, get_event_max_multi_day_duration,\ planning_auto_assign_to_workflow, get_long_event_duration_threshold, get_planning_allow_scheduled_updates,\ - event_templates_enabled + event_templates_enabled, planning_link_updates_to_coverage from apps.common.components.utils import register_component from .item_lock import LockService from .planning_notifications import PlanningNotifications @@ -176,6 +176,7 @@ def init_app(app): app.client_config['long_event_duration_threshold'] = get_long_event_duration_threshold(app) app.client_config['planning_allow_scheduled_updates'] = get_planning_allow_scheduled_updates(app) app.client_config['event_templates_enabled'] = event_templates_enabled(app) + app.client_config['planning_link_updates_to_coverage'] = planning_link_updates_to_coverage(app) # Set up Celery task options if not app.config.get('CELERY_TASK_ROUTES'): diff --git a/server/planning/assignments/__init__.py b/server/planning/assignments/__init__.py index 8fdd0bc4f..ffe5d7673 100644 --- a/server/planning/assignments/__init__.py +++ b/server/planning/assignments/__init__.py @@ -81,7 +81,7 @@ def init_app(app): app.on_updated_events += assignments_publish_service.on_events_updated # Track updates for an assignment if it's news story was updated - if app.config.get('PLANNING_LINK_UPDATES_TO_COVERAGES', False): + if app.config.get('PLANNING_LINK_UPDATES_TO_COVERAGES', True): app.on_inserted_archive_rewrite += assignments_publish_service.create_delivery_for_content_update # Remove Assignment and Coverage upon deleting an Archive Rewrite diff --git a/server/planning/assignments/assignments.py b/server/planning/assignments/assignments.py index ee0aabf4c..7ebcbce3e 100644 --- a/server/planning/assignments/assignments.py +++ b/server/planning/assignments/assignments.py @@ -736,12 +736,6 @@ def create_delivery_for_content_update(self, items): if not original_item.get('assignment_id'): continue - assignment = self.find_one(req=None, _id=str(original_item['assignment_id'])) - if not assignment: - raise SuperdeskApiError.badRequestError( - 'Assignment not found.' - ) - delivery = delivery_service.find_one(req=None, item_id=original_item[config.ID_FIELD]) if not delivery: raise SuperdeskApiError.badRequestError( @@ -754,6 +748,7 @@ def create_delivery_for_content_update(self, items): 'Planning does not exist' ) + coverage = None coverages = planning.get('coverages') or [] try: coverage = next(c for c in coverages if c.get('coverage_id') == delivery.get('coverage_id')) @@ -763,12 +758,29 @@ def create_delivery_for_content_update(self, items): ) # Link only if linking updates are enabled - if not (coverage.get('flags') or {}).get('no_content_linking'): - assignment_link_service.post([{ - 'assignment_id': str(assignment[config.ID_FIELD]), - 'item_id': str(item[config.ID_FIELD]), - 'reassign': True - }]) + if (coverage.get('flags') or {}).get('no_content_linking'): + return + + # get latest assignment available to link + assignment_id = (coverage.get('assigned_to') or {}).get('assignment_id') + for s in coverage.get('scheduled_updates'): + if (s.get('assigned_to') or {}).get('state') in [ASSIGNMENT_WORKFLOW_STATE.IN_PROGRESS, + ASSIGNMENT_WORKFLOW_STATE.COMPLETED]: + assignment_id = (s.get('assigned_to') or {}).get('assignment_id') + + assignment = self.find_one(req=None, _id=str(assignment_id)) + if not assignment: + raise SuperdeskApiError.badRequestError( + 'Assignment not found.' + ) + + assignment_link_service.post([{ + 'assignment_id': str(assignment[config.ID_FIELD]), + 'item_id': str(item[config.ID_FIELD]), + 'reassign': True + }]) + + doc['assignment_id'] = assignment['_id'] def unlink_assignment_on_delete_archive_rewrite(self): # Because this is in response to a Resource level DELETE, we need to get the diff --git a/server/planning/assignments/assignments_content.py b/server/planning/assignments/assignments_content.py index f72728952..79deb9005 100644 --- a/server/planning/assignments/assignments_content.py +++ b/server/planning/assignments/assignments_content.py @@ -17,7 +17,7 @@ from planning.planning_article_export import get_desk_template from superdesk.errors import SuperdeskApiError from planning.common import ASSIGNMENT_WORKFLOW_STATE, get_coverage_type_name, get_next_assignment_status,\ - get_coverage_for_assignment + get_coverage_for_assignment, get_archive_items_for_assignment from superdesk.utc import utcnow from planning.planning_notifications import PlanningNotifications from superdesk import get_resource_service @@ -138,6 +138,7 @@ def create(self, docs): request.view_args['original_id'] = archive_item.get(config.ID_FIELD) ids = get_resource_service('archive_rewrite').post([{'desk_id': str(item.get('task').get('desk'))}]) item = archive_service.find_one(_id=ids[0], req=None) + item['task']['user'] = get_user_id() # link the rewrite get_resource_service('assignments_link').post([{ @@ -200,20 +201,22 @@ def create(self, docs): def get_latest_news_item_for_coverage(self, assignment): coverage = get_coverage_for_assignment(assignment) - assignment_id = (coverage.get('assigned_to') or {}).get('assignment_id') + previous_items = [] + assignment_id = (coverage.get('assigned_to') or {}).get('assignment_id') if len(coverage.get('scheduled_updates')) == 0: - return get_resource_service('archive').find_one(req=None, assignment_id=assignment_id) + previous_items = get_archive_items_for_assignment(assignment_id) else: - previous_item = get_resource_service('archive').find_one(req=None, assignment_id=assignment_id) + previous_items = get_archive_items_for_assignment(assignment_id) for s in coverage.get('scheduled_updates'): - if s['scheduled_update_id'] == assignment['scheduled_update_id']: - return previous_item + new_items = get_archive_items_for_assignment((s.get('assigned_to') or {}).get('assignment_id')) + if len(new_items) > 0: + previous_items = new_items + + if len(previous_items) > 0: + return previous_items[0] - assignment_id = (s.get('assigned_to') or {}).get('assignment_id') - new_item = get_resource_service('archive').find_one(req=None, assignment_id=assignment_id) - if new_item: - previous_item = new_item + return None def _validate(self, doc): """Validate the doc for content creation""" diff --git a/server/planning/assignments/assignments_link.py b/server/planning/assignments/assignments_link.py index ce12bbb30..b4ec4e3ac 100644 --- a/server/planning/assignments/assignments_link.py +++ b/server/planning/assignments/assignments_link.py @@ -10,7 +10,7 @@ from superdesk.errors import SuperdeskApiError from superdesk.metadata.item import ITEM_STATE, CONTENT_STATE from eve.utils import config -from planning.common import ASSIGNMENT_WORKFLOW_STATE, get_related_items, \ +from planning.common import ASSIGNMENT_WORKFLOW_STATE, get_related_items, get_coverage_for_assignment, \ update_assignment_on_link_unlink, get_next_assignment_status, get_delivery_publish_time from apps.archive.common import get_user, is_assigned_to_a_desk from apps.content import push_content_notification @@ -41,24 +41,30 @@ def create(self, docs): def link_archive_items_to_assignments(self, assignment, related_items, actioned_item, doc): assignments_service = get_resource_service('assignments') + delivery_service = get_resource_service('delivery') assignments_service.validate_assignment_action(assignment) items = [] ids = [] deliveries = [] published_updated_items = [] for item in related_items: - if not item.get('assignment_id'): - # Add a delivery for all items in published collection - deliveries.append({ - 'item_id': item[config.ID_FIELD], - 'assignment_id': assignment.get(config.ID_FIELD), - 'planning_id': assignment['planning_item'], - 'coverage_id': assignment['coverage_item'], - 'item_state': item.get('state'), - 'sequence_no': item.get('rewrite_sequence') or 0, - 'publish_time': get_delivery_publish_time(item), - 'scheduled_update_id': assignment.get('scheduled_update_id'), - }) + if not item.get('assignment_id') or (item['_id'] == actioned_item.get('_id') and doc.get('force')): + # Update the delivery for the item if one exists + delivery = delivery_service.find_one(req=None, item_id=item[config.ID_FIELD]) + if delivery: + delivery_service.patch(delivery['_id'], {'assignment_id': assignment['_id']}) + else: + # Add a delivery for the item + deliveries.append({ + 'item_id': item[config.ID_FIELD], + 'assignment_id': assignment.get(config.ID_FIELD), + 'planning_id': assignment['planning_item'], + 'coverage_id': assignment['coverage_item'], + 'item_state': item.get('state'), + 'sequence_no': item.get('rewrite_sequence') or 0, + 'publish_time': get_delivery_publish_time(item), + 'scheduled_update_id': assignment.get('scheduled_update_id'), + }) # Update archive/published collection with assignment linking update_assignment_on_link_unlink(assignment[config.ID_FIELD], item, published_updated_items) @@ -68,7 +74,7 @@ def link_archive_items_to_assignments(self, assignment, related_items, actioned_ # Create all deliveries if len(deliveries) > 0: - get_resource_service('delivery').post(deliveries) + delivery_service.post(deliveries) updates = {'assigned_to': deepcopy(assignment.get('assigned_to'))} already_completed = assignment['assigned_to']['state'] == ASSIGNMENT_WORKFLOW_STATE.COMPLETED @@ -115,7 +121,7 @@ def _validate(self, doc): if not item: raise SuperdeskApiError.badRequestError('Content item not found.') - if item.get('assignment_id'): + if not doc.get('force') and item.get('assignment_id'): raise SuperdeskApiError.badRequestError( 'Content is already linked to an assignment. Cannot link assignment and content.' ) @@ -139,6 +145,22 @@ def _validate(self, doc): if assignment.get('scheduled_update_id'): raise SuperdeskApiError.badRequestError('Only updates can be linked to a scheduled update assignment') + coverage = get_coverage_for_assignment(assignment) + allowed_states = [ASSIGNMENT_WORKFLOW_STATE.IN_PROGRESS, ASSIGNMENT_WORKFLOW_STATE.COMPLETED] + if (coverage and len(coverage.get('scheduled_updates')) > 0 and + str(assignment['_id']) != str((coverage.get('assigned_to') or {}).get('assignment_id'))): + if (coverage.get('assigned_to') or {}).get('state') not in allowed_states: + raise SuperdeskApiError('Previous coverage is not linked to content.') + + # Check all previous scheduled updated to be linked/completed + for s in coverage.get('scheduled_updates'): + assigned_to = (s.get('assigned_to') or {}) + if str(assigned_to.get('assignment_id')) == str(doc.get('assignment_id')): + break + + if assigned_to.get('state') not in allowed_states: + raise SuperdeskApiError('Previous scheduled-update pending content-linking/completion') + def update_assignment(self, updates, assignment, actioned_item, reassign, already_completed): # Update assignments, assignment history and publish planning # set the state to in progress if no item in the updates chain has ever been published @@ -186,7 +208,8 @@ class AssignmentsLinkResource(Resource): 'reassign': { 'type': 'boolean', 'required': True - } + }, + 'force': {'type': 'boolean'} } resource_methods = ['POST'] diff --git a/server/planning/common.py b/server/planning/common.py index d5be63938..461a01796 100644 --- a/server/planning/common.py +++ b/server/planning/common.py @@ -366,6 +366,36 @@ def set_actioned_date_to_event(updates, original): updates['actioned_date'] = original['dates']['start'] +def get_archive_items_for_assignment(assignment_id, descending_rewrite_seq=True): + if not assignment_id: + return [] + + req = ParsedRequest() + req.args = MultiDict() + must_not = [{'term': {'state': 'spiked'}}] + must = [{'term': {'assignment_id': str(assignment_id)}}, + {'term': {'type': 'text'}}] + + query = { + 'query': { + 'filtered': { + 'filter': { + 'bool': { + 'must': must, + 'must_not': must_not + } + } + } + } + } + query['sort'] = [{'rewrite_sequence': 'desc' if descending_rewrite_seq else 'asc'}] + query['size'] = 200 + + req.args['source'] = json.dumps(query) + req.args['repo'] = 'archive,published,archived' + return list(get_resource_service('search').get(req, None)) + + def get_related_items(item, assignment=None): # If linking updates is not configured, return just this item if not planning_link_updates_to_coverage(): @@ -393,6 +423,7 @@ def get_related_items(item, assignment=None): } } query['sort'] = [{'rewrite_sequence': 'asc'}] + query['size'] = 200 req.args['source'] = json.dumps(query) req.args['repo'] = 'archive,published,archived' @@ -433,8 +464,8 @@ def update_assignment_on_link_unlink(assignment_id, item, published_updated): get_resource_service('archive').system_update(item[config.ID_FIELD], {'assignment_id': assignment_id}, item) -def planning_link_updates_to_coverage(): - return app.config.get('PLANNING_LINK_UPDATES_TO_COVERAGES', False) +def planning_link_updates_to_coverage(current_app=None): + return (current_app if current_app else app).config.get('PLANNING_LINK_UPDATES_TO_COVERAGES', False) def is_valid_event_planning_reason(updates, original): diff --git a/server/planning/output_formatters/json_planning.py b/server/planning/output_formatters/json_planning.py index ae0bf0d00..b917cc9e8 100644 --- a/server/planning/output_formatters/json_planning.py +++ b/server/planning/output_formatters/json_planning.py @@ -15,7 +15,6 @@ from superdesk.utils import json_serialize_datetime_objectId from copy import deepcopy from superdesk import get_resource_service -from bson.objectid import ObjectId from planning.common import ASSIGNMENT_WORKFLOW_STATE, WORKFLOW_STATE from superdesk.metadata.item import CONTENT_STATE @@ -56,9 +55,7 @@ def _format_item(self, item): for f in self.remove_fields: output_item.pop(f, None) for coverage in output_item.get('coverages', []): - assigned_to = coverage.pop('assigned_to', None) or {} - coverage['coverage_provider'] = assigned_to.get('coverage_provider') - deliveries, workflow_state = self._expand_delivery(assigned_to.get('assignment_id')) + deliveries, workflow_state = self._expand_delivery(coverage) if workflow_state: coverage['workflow_status'] = self._get_coverage_workflow_state(workflow_state) @@ -95,12 +92,16 @@ def _expand_agendas(self, item): expanded.append(agenda_details) return expanded - def _expand_delivery(self, assignment_id): + def _expand_delivery(self, coverage): """Find any deliveries associated with the assignment :param assignment_id: :return: """ + assigned_to = coverage.pop('assigned_to', None) or {} + coverage['coverage_provider'] = assigned_to.get('coverage_provider') + assignment_id = assigned_to.get('assignment_id') + if not assignment_id: return [], None @@ -114,7 +115,7 @@ def _expand_delivery(self, assignment_id): delivery_service = get_resource_service('delivery') remove_fields = ('coverage_id', 'planning_id', '_created', '_updated', 'assignment_id', '_etag') - deliveries = list(delivery_service.get(req=None, lookup={'assignment_id': ObjectId(assignment_id)})) + deliveries = list(delivery_service.get(req=None, lookup={'coverage_id': coverage.get('coverage_id')})) # Check to see if in this delivery chain, whether the item has been published at least once item_never_published = True diff --git a/server/planning/tests/output_formatters/json_planning_test.py b/server/planning/tests/output_formatters/json_planning_test.py index 24fecb37d..109cfa94e 100644 --- a/server/planning/tests/output_formatters/json_planning_test.py +++ b/server/planning/tests/output_formatters/json_planning_test.py @@ -104,7 +104,7 @@ class JsonPlanningTestCase(TestCase): '_id': ObjectId('5b206de61d41c89c6659d5ec'), 'original_creator': '57bcfc5d1d41c82e8401dcc0', 'priority': 2, - 'coverage_item': 'urn:newsml:localhost:2018-06-08T11:52:44.100395:cba5e7dc-f832-4c85-bf6e-bbff98679fbe', + 'coverage_item': 'urn:newsml:localhost:2018-04-10T14:37:31.188619:e5da893e-8027-4923-8c39-868f11eee713', '_updated': '2018-06-08T01:53:06.000Z', 'type': 'assignment', 'planning_item': 'urn:newsml:localhost:2018-06-08T11:51:24.704360:447788f4-641f-4248-8837-cf3dc8a6ac9a', @@ -138,7 +138,7 @@ class JsonPlanningTestCase(TestCase): '_id': ObjectId('5b2079711d41c89c6659d6a0'), 'assignment_id': ObjectId('5b206de61d41c89c6659d5ec'), '_created': '2018-06-13T01:54:57.000Z', - 'coverage_id': 'urn:newsml:localhost:2018-06-13T11:05:42.447915:030c31c3-baaf-4f98-8401-fac332a2ef1c', + 'coverage_id': 'urn:newsml:localhost:2018-04-10T14:37:31.188619:e5da893e-8027-4923-8c39-868f11eee713', '_updated': '2018-06-13T01:54:57.000Z', 'item_id': 'urn:newsml:localhost:2018-06-13T11:54:57.477423:c944042d-f93b-4304-9732-e7b5798ee8f9', 'planning_id': 'urn:newsml:localhost:2018-06-13T11:05:42.040242:8d810c01-2c0e-403a-bd0d-b4e2d001b163',