diff --git a/client/actions/planning/api.js b/client/actions/planning/api.js
index bd9ba168e..e9b9101fb 100644
--- a/client/actions/planning/api.js
+++ b/client/actions/planning/api.js
@@ -960,15 +960,16 @@ const fetchPlanningFiles = (planning) => (
return Promise.resolve();
}
+ const filesToFetch = planningUtils.getPlanningFiles(planning);
const filesInStore = selectors.general.files(getState());
- if (every(planning.files, (f) => f in filesInStore)) {
+ if (every(filesToFetch, (f) => f in filesInStore)) {
return Promise.resolve();
}
return api('planning_files').query(
{
- where: {$and: [{_id: {$in: planning.files}}]},
+ where: {$and: [{_id: {$in: filesToFetch}}]},
}
)
.then((data) => {
diff --git a/client/components/Assignments/AssignmentPreviewContainer/AssignmentPreview.jsx b/client/components/Assignments/AssignmentPreviewContainer/AssignmentPreview.jsx
index 63598fe0d..1c5e40d92 100644
--- a/client/components/Assignments/AssignmentPreviewContainer/AssignmentPreview.jsx
+++ b/client/components/Assignments/AssignmentPreviewContainer/AssignmentPreview.jsx
@@ -7,6 +7,7 @@ import {gettext, stringUtils, assignmentUtils} from '../../../utils';
import {InternalNoteLabel} from '../../';
import {ContactsPreviewList} from '../../Contacts/index';
import {Row} from '../../UI/Preview';
+import {FileReadOnlyList} from '../../UI';
// eslint-disable-next-line complexity
export const AssignmentPreview = ({
@@ -15,6 +16,8 @@ export const AssignmentPreview = ({
coverageFormProfile,
planningFormProfile,
planningItem,
+ files,
+ createLink,
}) => {
const planning = get(assignment, 'planning', {});
@@ -86,11 +89,25 @@ export const AssignmentPreview = ({
{stringUtils.convertNewlineToBreak(planning.internal_note || '-')}
+
+
+
+
+
+
);
};
@@ -101,4 +118,6 @@ AssignmentPreview.propTypes = {
coverageFormProfile: PropTypes.object,
planningFormProfile: PropTypes.object,
planningItem: PropTypes.object,
+ files: PropTypes.array,
+ createLink: PropTypes.func,
};
diff --git a/client/components/Assignments/AssignmentPreviewContainer/EventPreview.jsx b/client/components/Assignments/AssignmentPreviewContainer/EventPreview.jsx
index 17189c540..5a163588d 100644
--- a/client/components/Assignments/AssignmentPreviewContainer/EventPreview.jsx
+++ b/client/components/Assignments/AssignmentPreviewContainer/EventPreview.jsx
@@ -1,18 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {connect} from 'react-redux';
import {get} from 'lodash';
-import * as selectors from '../../../selectors';
import {gettext, stringUtils, timeUtils} from '../../../utils';
-
import {Datetime} from '../../';
import {Location} from '../../Location';
+import {FileReadOnlyList} from '../../UI';
import {Row} from '../../UI/Preview';
-import {FileInput, LinkInput} from '../../UI/Form';
+import {LinkInput} from '../../UI/Form';
import {ContactsPreviewList} from '../../Contacts';
-export const EventPreviewComponent = ({item, formProfile, createLink, streetMapUrl, files}) => {
+export const EventPreview = ({item, formProfile, createLink, streetMapUrl, files}) => {
if (!item) {
return null;
}
@@ -115,22 +113,12 @@ export const EventPreviewComponent = ({item, formProfile, createLink, streetMapU
enabled={get(formProfile, 'editor.files.enabled')}
label={gettext('Attachments')}
>
- {get(item, 'files.length', 0) > 0 ? (
-
- {get(item, 'files').map((file, index) =>
- (-
-
-
)
- )}
-
- ) : (
- {gettext('No attached files added.')}
- )}
+
({
- createLink: (f) => (selectors.config.getServerUrl(state) + '/upload/' + f.filemeta.media_id + '/raw'),
- streetMapUrl: selectors.config.getStreetMapUrl(state),
- files: selectors.general.files(state),
-});
-
-
-export const EventPreview = connect(mapStateToProps)(EventPreviewComponent);
\ No newline at end of file
diff --git a/client/components/Assignments/AssignmentPreviewContainer/index.jsx b/client/components/Assignments/AssignmentPreviewContainer/index.jsx
index 73dcdd114..cce27106c 100644
--- a/client/components/Assignments/AssignmentPreviewContainer/index.jsx
+++ b/client/components/Assignments/AssignmentPreviewContainer/index.jsx
@@ -5,7 +5,7 @@ import {get} from 'lodash';
import * as selectors from '../../../selectors';
import * as actions from '../../../actions';
-import {assignmentUtils, gettext, eventUtils} from '../../../utils';
+import {assignmentUtils, gettext, eventUtils, planningUtils} from '../../../utils';
import {ASSIGNMENTS, WORKSPACE} from '../../../constants';
import {AssignmentPreviewHeader} from './AssignmentPreviewHeader';
@@ -20,6 +20,10 @@ class AssignmentPreviewContainerComponent extends React.Component {
if (eventUtils.shouldFetchFilesForEvent(this.props.eventItem)) {
this.props.fetchEventFiles(this.props.eventItem);
}
+
+ if (planningUtils.shouldFetchFilesForPlanning(this.props.planningItem)) {
+ this.props.fetchPlanningFiles(this.props.planningItem);
+ }
}
getItemActions() {
@@ -82,6 +86,9 @@ class AssignmentPreviewContainerComponent extends React.Component {
contentTypes,
session,
privileges,
+ createLink,
+ streetMapUrl,
+ files,
} = this.props;
if (!assignment) {
@@ -129,6 +136,8 @@ class AssignmentPreviewContainerComponent extends React.Component {
coverageFormProfile={formProfile.coverage}
planningFormProfile={formProfile.planning}
planningItem={planningItem}
+ createLink={createLink}
+ files={files}
/>
@@ -161,6 +170,9 @@ class AssignmentPreviewContainerComponent extends React.Component {
@@ -202,6 +214,10 @@ AssignmentPreviewContainerComponent.propTypes = {
fetchEventFiles: PropTypes.func,
currentWorkspace: PropTypes.string,
contentTypes: PropTypes.array,
+ fetchPlanningFiles: PropTypes.func,
+ createLink: PropTypes.func,
+ streetMapUrl: PropTypes.string,
+ files: PropTypes.array,
};
const mapStateToProps = (state) => ({
@@ -223,6 +239,9 @@ const mapStateToProps = (state) => ({
agendas: selectors.general.agendas(state),
currentWorkspace: selectors.general.currentWorkspace(state),
contentTypes: selectors.general.contentTypes(state),
+ createLink: (f) => (selectors.config.getServerUrl(state) + '/upload/' + f.filemeta.media_id + '/raw'),
+ streetMapUrl: selectors.config.getStreetMapUrl(state),
+ files: selectors.general.files(state),
});
const mapDispatchToProps = (dispatch) => ({
@@ -235,6 +254,7 @@ const mapDispatchToProps = (dispatch) => ({
removeAssignment: (assignment) => dispatch(actions.assignments.ui.showRemoveAssignmentModal(assignment)),
openArchivePreview: (assignment) => dispatch(actions.assignments.ui.openArchivePreview(assignment)),
fetchEventFiles: (event) => dispatch(actions.events.api.fetchEventFiles(event)),
+ fetchPlanningFiles: (planning) => dispatch(actions.planning.api.fetchPlanningFiles(planning)),
});
export const AssignmentPreviewContainer = connect(
diff --git a/client/components/Coverages/CoverageEditor/CoverageForm.jsx b/client/components/Coverages/CoverageEditor/CoverageForm.jsx
index 7212bee89..3cfc72adf 100644
--- a/client/components/Coverages/CoverageEditor/CoverageForm.jsx
+++ b/client/components/Coverages/CoverageEditor/CoverageForm.jsx
@@ -4,8 +4,8 @@ import {get} from 'lodash';
import {getItemInArrayById, gettext, planningUtils, generateTempId, assignmentUtils} from '../../../utils';
import moment from 'moment';
import {WORKFLOW_STATE, DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT, TO_BE_CONFIRMED_FIELD} from '../../../constants';
-import {Button} from '../../UI';
-import {Row, Label, LineInput} from '../../UI/Form';
+import {Button, ToggleBox} from '../../UI';
+import {Row, Label, LineInput, FileInput} from '../../UI/Form';
import {ScheduledUpdate} from '../ScheduledUpdate';
@@ -32,11 +32,18 @@ export class CoverageForm extends React.Component {
this.onRemoveScheduledUpdate = this.onRemoveScheduledUpdate.bind(this);
this.onScheduledUpdateClose = this.onScheduledUpdateClose.bind(this);
this.onScheduledUpdateOpen = this.onScheduledUpdateOpen.bind(this);
+ this.onAddFiles = this.onAddFiles.bind(this);
+ this.onRemoveFile = this.onRemoveFile.bind(this);
this.dom = {
contentType: null,
popupContainer: null,
};
- this.state = {openScheduledUpdates: []};
+ this.state = {
+ openScheduledUpdates: [],
+ uploading: false,
+ };
+ this.filePath = 'value.planning.files';
+ this.fullFilePath = `coverages[${this.props.index}].planning.files`;
}
componentDidUpdate(prevProps) {
@@ -142,6 +149,34 @@ export class CoverageForm extends React.Component {
)});
}
+ onAddFiles(fileList) {
+ const files = Array.from(fileList).map((f) => [f]);
+
+ this.setState({uploading: true});
+ this.props.uploadFiles(files)
+ .then((newFiles) => {
+ this.props.onChange(this.fullFilePath,
+ [
+ ...get(this.props, this.filePath, []),
+ ...newFiles.map((f) => f._id),
+ ]);
+ this.setState({uploading: false});
+ }, () => {
+ this.props.notifyValidationErrors(['Failed to upload files']);
+ this.setState({uploading: false});
+ });
+ }
+
+ onRemoveFile(file) {
+ const promise = !get(this.props, this.filePath, []).includes(file._id) ?
+ this.props.removeFile(file) : Promise.resolve();
+
+ promise.then(() =>
+ this.props.onChange(this.fullFilePath,
+ get(this.props, this.filePath, []).filter((f) => f !== file._id))
+ );
+ }
+
render() {
const {
field,
@@ -170,6 +205,8 @@ export class CoverageForm extends React.Component {
planningAllowScheduledUpdates,
onRemoveAssignment,
setCoverageDefaultDesk,
+ createUploadLink,
+ files,
...props
} = this.props;
@@ -315,6 +352,29 @@ export class CoverageForm extends React.Component {
{...fieldProps}
/>
+ {get(formProfile, 'editor.files.enabled') &&
+
+
+ {!this.state.uploading && }
+
+ }
+
{
const coverageStatus = get(coverage, 'news_coverage_status.qcode', '') ===
PLANNING.NEWS_COVERAGE_CANCELLED_STATUS.qcode ? PLANNING.NEWS_COVERAGE_CANCELLED_STATUS :
@@ -146,6 +148,14 @@ export const CoveragePreview = ({
/>
}
+ {get(formProfile, 'editor.files.enabled') &&
+
+ }
+
{
- this.notifyValidationErrors('Failed to upload files');
+ this.props.notifyValidationErrors(['Failed to upload files']);
this.setState({uploading: false});
});
}
@@ -518,6 +518,7 @@ EventEditorComponent.propTypes = {
onPopupClose: PropTypes.func,
itemManager: PropTypes.object,
original: PropTypes.object,
+ notifyValidationErrors: PropTypes.func,
};
EventEditorComponent.defaultProps = {
diff --git a/client/components/Events/EventPreviewContent.jsx b/client/components/Events/EventPreviewContent.jsx
index 45e8effb3..93755b7c6 100644
--- a/client/components/Events/EventPreviewContent.jsx
+++ b/client/components/Events/EventPreviewContent.jsx
@@ -12,9 +12,9 @@ import {
StateLabel,
} from '../index';
import {EventScheduleSummary} from './';
-import {ToggleBox} from '../UI';
+import {ToggleBox, FileReadOnlyList} from '../UI';
import {ContentBlock} from '../UI/SidePanel';
-import {LinkInput, FileInput} from '../UI/Form';
+import {LinkInput} from '../UI/Form';
import {Location} from '../Location';
import * as actions from '../../actions';
import {ContactsPreviewList} from '../Contacts/index';
@@ -180,26 +180,13 @@ export class EventPreviewContentComponent extends React.Component {
value={stringUtils.convertNewlineToBreak(item.ednote || '-')}
/>
- {get(formProfile, 'editor.files.enabled') &&
- 0 ? item.files.length : null}>
- {get(item, 'files.length') > 0 ?
-
- {get(item, 'files', []).map((file, index) => (
- -
-
-
- ))}
-
:
- {gettext('No attached files added.')}}
-
- }
+
+
+
{get(formProfile, 'editor.links.enabled') &&
{
- this.notifyValidationErrors('Failed to upload files');
+ this.props.notifyValidationErrors('Failed to upload files');
this.setState({uploading: false});
});
}
@@ -761,6 +761,10 @@ export class PlanningEditorComponent extends React.Component {
planningAllowScheduledUpdates={planningAllowScheduledUpdates}
coverageAddAdvancedMode={this.props.coverageAddAdvancedMode}
setCoverageAddAdvancedMode={this.props.setCoverageAddAdvancedMode}
+ files={files}
+ createUploadLink={createUploadLink}
+ uploadFiles={this.props.uploadFiles}
+ notifyValidationErrors={this.props.notifyValidationErrors}
/>
);
@@ -823,6 +827,7 @@ PlanningEditorComponent.propTypes = {
planningAllowScheduledUpdates: PropTypes.bool,
coverageAddAdvancedMode: PropTypes.bool,
setCoverageAddAdvancedMode: PropTypes.func,
+ notifyValidationErrors: PropTypes.func,
};
PlanningEditorComponent.defaultProps = {
diff --git a/client/components/Planning/PlanningPreviewContent.jsx b/client/components/Planning/PlanningPreviewContent.jsx
index 6992fb6ba..b00f1f2d9 100644
--- a/client/components/Planning/PlanningPreviewContent.jsx
+++ b/client/components/Planning/PlanningPreviewContent.jsx
@@ -240,6 +240,8 @@ export class PlanningPreviewContentComponent extends React.Component {
timeFormat={timeFormat}
formProfile={formProfile.coverage}
inner={inner}
+ files={files}
+ createLink={createUploadLink}
planningAllowScheduledUpdates={planningAllowScheduledUpdates} />)
)
}
diff --git a/client/components/UI/FileReadOnlyList.jsx b/client/components/UI/FileReadOnlyList.jsx
new file mode 100644
index 000000000..ca2e9249e
--- /dev/null
+++ b/client/components/UI/FileReadOnlyList.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {get} from 'lodash';
+
+import {FileInput} from './Form';
+import {ToggleBox} from './index';
+
+/**
+ * @ngdoc react
+ * @name FileReadOnlyList
+ * @description Generic read-only file list
+ */
+const FileReadOnlyList = ({formProfile, item, field, createLink, files, noToggle}) => {
+ if (!get(formProfile, 'editor.files.enabled')) {
+ return null;
+ }
+
+ const fileList = get(item, `${field}.length`, 0) > 0 ?
+ (
+ {item[field].map((file, index) => (
+ -
+
+
+ ))}
+
) :
+ ({gettext('No attached files added.')}
);
+
+ if (noToggle) {
+ return fileList;
+ }
+
+ return (
+ 0 ? item[field].length : null}>
+ {fileList}
+ );
+};
+
+FileReadOnlyList.propTypes = {
+ formProfile: PropTypes.object,
+ item: PropTypes.object,
+ createLink: PropTypes.func,
+ files: PropTypes.array,
+ field: PropTypes.string,
+ noToggle: PropTypes.bool,
+};
+
+FileReadOnlyList.defaultProps = {field: 'files'};
+
+export default FileReadOnlyList;
diff --git a/client/components/UI/index.jsx b/client/components/UI/index.jsx
index 469cc8da9..b0bc291d0 100644
--- a/client/components/UI/index.jsx
+++ b/client/components/UI/index.jsx
@@ -21,6 +21,7 @@ import ButtonList from './ButtonList';
import IconButton from './IconButton';
import Icon from './Icon';
import IconMix from './IconMix';
+import FileReadOnlyList from './FileReadOnlyList';
export {
List,
@@ -46,4 +47,5 @@ export {
IconButton,
Icon,
IconMix,
+ FileReadOnlyList,
};
diff --git a/client/utils/planning.js b/client/utils/planning.js
index 70e4930ed..271b4399d 100644
--- a/client/utils/planning.js
+++ b/client/utils/planning.js
@@ -637,6 +637,7 @@ const getCoverageReadOnlyFields = (
newsCoverageStatus: true,
scheduled: readOnly || get(addNewsItemToPlanning, 'state') === 'published',
flags: scheduledUpdatesExist,
+ files: true,
};
}
@@ -661,6 +662,7 @@ const getCoverageReadOnlyFields = (
newsCoverageStatus: true,
scheduled: readOnly,
flags: true,
+ files: readOnly,
};
case ASSIGNMENTS.WORKFLOW_STATE.IN_PROGRESS:
case ASSIGNMENTS.WORKFLOW_STATE.SUBMITTED:
@@ -674,6 +676,7 @@ const getCoverageReadOnlyFields = (
newsCoverageStatus: true,
scheduled: readOnly,
flags: true,
+ files: readOnly,
};
case ASSIGNMENTS.WORKFLOW_STATE.COMPLETED:
return {
@@ -686,6 +689,7 @@ const getCoverageReadOnlyFields = (
newsCoverageStatus: true,
scheduled: readOnly,
flags: true,
+ files: readOnly,
};
case ASSIGNMENTS.WORKFLOW_STATE.CANCELLED:
return {
@@ -698,6 +702,7 @@ const getCoverageReadOnlyFields = (
newsCoverageStatus: true,
scheduled: true,
flags: true,
+ files: readOnly,
};
case null:
default:
@@ -711,6 +716,7 @@ const getCoverageReadOnlyFields = (
newsCoverageStatus: readOnly,
scheduled: readOnly,
flags: scheduledUpdatesExist,
+ files: readOnly,
};
}
};
@@ -988,7 +994,7 @@ const isFeaturedPlanningUpdatedAfterPosting = (item) => {
};
const shouldFetchFilesForPlanning = (planning) => (
- get(planning, 'files', []).filter((f) => typeof (f) === 'string'
+ self.getPlanningFiles(planning).filter((f) => typeof (f) === 'string'
|| f instanceof String).length > 0
);
@@ -1035,6 +1041,21 @@ const getActiveCoverage = (updatedCoverage, newsCoverageStatus) => {
return coverage;
};
+const getPlanningFiles = (planning) => {
+ let filesToFetch = get(planning, 'files') || [];
+
+ (get(planning, 'coverages') || []).forEach((c) => {
+ if ((c.planning.files || []).length) {
+ filesToFetch = [
+ ...filesToFetch,
+ ...c.planning.files,
+ ];
+ }
+ });
+
+ return filesToFetch;
+};
+
// eslint-disable-next-line consistent-this
const self = {
canSpikePlanning,
@@ -1086,6 +1107,7 @@ const self = {
getActiveCoverage,
canAddScheduledUpdateToWorkflow,
getDefaultCoverageStatus,
+ getPlanningFiles,
};
export default self;
diff --git a/server/features/planning_files.feature b/server/features/planning_files.feature
new file mode 100644
index 000000000..cf4736019
--- /dev/null
+++ b/server/features/planning_files.feature
@@ -0,0 +1,95 @@
+Feature: Planning Files
+
+ @auth
+ Scenario: Delete fails if a file is used by planning item
+ When we upload a file "bike.jpg" to "/planning_files"
+ Then we get an event file reference
+ When we get "planning_files/"
+ Then we get list with 1 items
+ """
+ {"_items": [{ "_id": "#planning_files._id#" }]}
+ """
+ When we post to "planning"
+ """
+ {
+ "slugline": "file test",
+ "planning_date": "2016-01-02",
+ "type": "planning",
+ "files": ["#planning_files._id#"]
+ }
+ """
+ Then we get OK response
+ When we get "/planning/#planning._id#"
+ Then we get existing resource
+ """
+ {
+ "_id": "#planning._id#",
+ "slugline": "file test",
+ "type": "planning",
+ "files": ["#planning_files._id#"]
+ }
+ """
+ When we delete "planning_files/#planning_files._id#"
+ Then we get error 403
+
+ @auth
+ Scenario: Delete fails if a file is used by a coverage
+ When we upload a file "bike.jpg" to "/planning_files"
+ Then we get an event file reference
+ When we get "planning_files/"
+ Then we get list with 1 items
+ """
+ {"_items": [{ "_id": "#planning_files._id#" }]}
+ """
+ When we post to "planning"
+ """
+ {
+ "slugline": "file test",
+ "planning_date": "2016-01-02",
+ "type": "planning",
+ "files": ["#planning_files._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",
+ "files": ["#planning_files._id#"]
+ }
+ }
+ ]
+ }
+ """
+ Then we get OK response
+ When we get "/planning/#planning._id#"
+ Then we get existing resource
+ """
+ {
+ "_id": "#planning._id#",
+ "slugline": "file test",
+ "type": "planning",
+ "files": ["#planning_files._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",
+ "files": ["#planning_files._id#"]
+ }
+ }
+ ]
+
+ }
+ """
+ When we delete "planning_files/#planning_files._id#"
+ Then we get error 403
+
diff --git a/server/planning/output_formatters/json_planning.py b/server/planning/output_formatters/json_planning.py
index b917cc9e8..8d08c82ca 100644
--- a/server/planning/output_formatters/json_planning.py
+++ b/server/planning/output_formatters/json_planning.py
@@ -38,6 +38,7 @@ def __init__(self):
# fields to be removed from coverage
remove_coverage_fields = ('original_creator', 'version_creator', 'assigned_to', 'flags')
+ remove_coverage_planning_fields = ('contact_info', 'files')
def can_format(self, format_type, article):
if article.get('flags', {}).get('marked_for_not_publication', False):
@@ -63,8 +64,9 @@ def _format_item(self, item):
for f in self.remove_coverage_fields:
coverage.pop(f, None)
- # Remove contacts field in coverage
- coverage.get('planning').pop('contact_info', None)
+ for key in (coverage.get('planning') or {}).keys():
+ if key in self.remove_coverage_planning_fields:
+ coverage['planning'].pop(key, None)
output_item['agendas'] = self._expand_agendas(item)
return output_item
diff --git a/server/planning/planning/planning.py b/server/planning/planning/planning.py
index 59beb5d65..cf7939579 100644
--- a/server/planning/planning/planning.py
+++ b/server/planning/planning/planning.py
@@ -1067,6 +1067,12 @@ def on_event_converted_to_recurring(self, updates, original):
'item_class': {'type': 'string', 'mapping': not_analyzed},
'item_count': {'type': 'string', 'mapping': not_analyzed},
'scheduled': {'type': 'datetime'},
+ 'files': {
+ 'type': 'list',
+ 'nullable': True,
+ 'schema': Resource.rel('planning_files'),
+ 'mapping': not_analyzed,
+ },
'service': {
'type': 'list',
'mapping': {
diff --git a/server/planning/planning/planning_files.py b/server/planning/planning/planning_files.py
index 51d210acf..0d4a0614a 100644
--- a/server/planning/planning/planning_files.py
+++ b/server/planning/planning/planning_files.py
@@ -53,6 +53,10 @@ def on_created(self, docs):
doc['media']['name'] = doc['media']['name'].split('/')[1]
def on_delete(self, doc):
- plannings_using_file = get_resource_service("planning").find(where={'files': doc.get("_id")})
+ find_clause = {
+ '$or': [{'files': doc.get("_id")},
+ {'coverages.planning.files': doc.get("_id")}],
+ }
+ plannings_using_file = get_resource_service("planning").find(where=find_clause)
if plannings_using_file.count() > 0:
- raise SuperdeskApiError.forbiddenError('Delete failed. File still used by other events.')
+ raise SuperdeskApiError.forbiddenError('Delete failed. File still used by other planning items.')
diff --git a/server/planning/planning/planning_types.py b/server/planning/planning/planning_types.py
index 28dbecc0a..118bf52fe 100644
--- a/server/planning/planning/planning_types.py
+++ b/server/planning/planning/planning_types.py
@@ -198,6 +198,7 @@ class CoverageSchema(BaseSchema):
'news_coverage_status': {'enabled': True},
'contact_info': {'enabled': False},
'flags': {'enabled': True},
+ 'files': {'enabled': True},
},
'schema': dict(CoverageSchema)
}, {
diff --git a/server/planning/planning_notifications.py b/server/planning/planning_notifications.py
index b59d54e4f..e19f937cb 100644
--- a/server/planning/planning_notifications.py
+++ b/server/planning/planning_notifications.py
@@ -269,6 +269,14 @@ def _send_user_email(user_id, contact_id, text_message, html_message, data):
fp = media.read()
attachments.append(Attachment(filename=media.name, content_type=media.content_type, data=fp))
+ if data.get('assignment') and (data['assignment'].get('planning', {})).get('files'):
+ for file_id in data['assignment']['planning']['files']:
+ assignment_file = superdesk.get_resource_service('planning_files').find_one(req=None, _id=file_id)
+ if assignment_file:
+ media = app.media.get(assignment_file['media'], resource='planning_files')
+ fp = media.read()
+ attachments.append(Attachment(filename=media.name, content_type=media.content_type, data=fp))
+
send_email(subject='Superdesk assignment' + ': {}'.format(data.get('slugline') if data.get('slugline') else ''),
sender=admins[0],
recipients=[email_address],