Skip to content

Commit

Permalink
[SDBELGA-759] Feature: Sync Event metadata to Planning & Coverages (#…
Browse files Browse the repository at this point in the history
…1892)

* Config: Add new config for Event to Planning sync

* ui: Send `embedded_planning` from front-end

* api: Improve Planning ContentProfiles module and types

* api: Process `embedded_planning` and sync metadata

* fix(ui): Failed to get lock for recurring series

* fix(ui): Improve EmbeddedCoverageForm UI

* fix(ui): Event to Planning translations copies fields multiple times

* fix(e2e): Use chrome in CI to run tests

* Add behave tests for embedded planning

* Add behave tests for multilingual embedded planning

* Add CVs to behave test

* fix: Coverage Planning updated when processing Assignment details
Embedded/Sync wasn't working once above bug was fixed

* Add behave tests for event metadata sync
  • Loading branch information
MarkLark86 authored Jan 8, 2024
1 parent a04f9f0 commit b6663cd
Show file tree
Hide file tree
Showing 22 changed files with 1,837 additions and 101 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ Below sections include the config options that can be defined in settings.py.
* DEFAULT_CREATE_PLANNING_SERIES_WITH_EVENT_SERIES
* Default: False
* If true, will default to creating series of Planning items with a recurring series of Events,
* SYNC_EVENT_FIELDS_TO_PLANNING
* Default: ""
* Comma separated list of Planning & Coverage fields to keep in sync with the associated Event
* Supported Fields:
* slugline
* internal_note
* name
* place (list CVs)
* subject (list CVs, exclude items with scheme)
* custom_vocabularies (list CVs, inside subject with scheme)
* anpa_category (list CVs)
* ednote
* language (includes `languages` if multilingual is enabled)
* definition_short (copies to Planning item's `Description Text`)
* priority

### Assignments Config
* SLACK_BOT_TOKEN
Expand Down
116 changes: 73 additions & 43 deletions client/api/events.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import {
FILTER_TYPE,
IEventItem,
IPlanningAPI, IPlanningItem,
IPlanningAPI,
ISearchAPIParams,
ISearchParams,
ISearchSpikeState,
LOCK_STATE
IPlanningConfig,
} from '../interfaces';
import {appConfig as config} from 'appConfig';
import {IRestApiResponse} from 'superdesk-api';
import {planningApi, superdeskApi} from '../superdeskApi';
import {EVENTS, TEMP_ID_PREFIX} from '../constants';

import {arrayToString, convertCommonParams, cvsToString, searchRaw, searchRawGetAll} from './search';
import {eventUtils, planningUtils} from '../utils';
import {eventUtils} from '../utils';
import {eventProfile, eventSearchProfile} from '../selectors/forms';
import * as actions from '../actions';

const appConfig = config as IPlanningConfig;

function convertEventParams(params: ISearchParams): Partial<ISearchAPIParams> {
return {
reference: params.reference,
Expand Down Expand Up @@ -118,62 +121,89 @@ function getEventSearchProfile() {
return eventSearchProfile(planningApi.redux.store.getState());
}

function createOrUpdatePlannings(
event: IEventItem,
items: Array<Partial<IPlanningItem>>
): Promise<Array<IPlanningItem>> {
return Promise.all(
items.map(
(updates) => (
updates._id.startsWith(TEMP_ID_PREFIX) ?
planningApi.planning.createFromEvent(event, updates) :
planningApi.planning.getById(updates._id)
.then((original) => (
planningApi.planning.update(original, updates)
))
)
)
)
.then((newOrUpdatedItems) => {
newOrUpdatedItems.forEach(planningUtils.modifyForClient);

return newOrUpdatedItems;
});
}

function create(updates: Partial<IEventItem>): Promise<Array<IEventItem>> {
return superdeskApi.dataApi.create<IEventItem | IRestApiResponse<IEventItem>>('events', {
const url = appConfig.planning.default_create_planning_series_with_event_series === true ?
'events?add_to_series=true' :
'events';

return superdeskApi.dataApi.create<IEventItem | IRestApiResponse<IEventItem>>(url, {
...updates,
associated_plannings: undefined,
embedded_planning: updates.associated_plannings.map((planning) => ({
coverages: planning.coverages.map((coverage) => ({
coverage_id: coverage.coverage_id,
g2_content_type: coverage.planning.g2_content_type,
desk: coverage.assigned_to.desk,
user: coverage.assigned_to.user,
language: coverage.planning.language,
news_coverage_status: coverage.news_coverage_status.qcode,
scheduled: coverage.planning.scheduled,
genre: coverage.planning.genre?.qcode,
slugline: coverage.planning.slugline,
ednote: coverage.planning.ednote,
internal_note: coverage.planning.internal_note,
})),
})),
})
.then((response) => {
const events: Array<IEventItem> = modifySaveResponseForClient(response);

return createOrUpdatePlannings(events[0], updates.associated_plannings ?? [])
.then((plannings) => {
// Make sure to update the Redux Store with the latest Planning items
// So that the Editor can set the state with these latest items
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(plannings));
const events = modifySaveResponseForClient(response);

return events;
});
return planningApi.planning.searchGetAll({
recurrence_id: events[0].recurrence_id,
event_item: events[0].recurrence_id != null ? null : events.map((event) => event._id),
spike_state: 'both',
only_future: false,
}).then((planningItems) => {
// Make sure to update the Redux Store with the latest Planning items
// So that the Editor can set the state with these latest items
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(planningItems));

return events;
});
})
.catch((error) => {
console.error(error);

return Promise.reject(error);
});
}

function update(original: IEventItem, updates: Partial<IEventItem>): Promise<Array<IEventItem>> {
return superdeskApi.dataApi.patch<any>('events', original, {
return superdeskApi.dataApi.patch<IEventItem>('events', original, {
...updates,
associated_plannings: undefined,
embedded_planning: updates.associated_plannings.map((planning) => ({
planning_id: planning._id.startsWith(TEMP_ID_PREFIX) ? undefined : planning._id,
coverages: planning.coverages.map((coverage) => ({
coverage_id: coverage.coverage_id,
g2_content_type: coverage.planning.g2_content_type,
desk: coverage.assigned_to.desk,
user: coverage.assigned_to.user,
language: coverage.planning.language,
news_coverage_status: coverage.news_coverage_status.qcode,
scheduled: coverage.planning.scheduled,
genre: coverage.planning.genre?.qcode,
slugline: coverage.planning.slugline,
ednote: coverage.planning.ednote,
internal_note: coverage.planning.internal_note,
})),
})),
})
.then((response) => {
const events = modifySaveResponseForClient(response);

return createOrUpdatePlannings(events[0], updates.associated_plannings ?? [])
.then((plannings) => {
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(plannings));

return events;
});
return planningApi.planning.searchGetAll({
recurrence_id: events[0].recurrence_id,
event_item: events[0].recurrence_id != null ? null : events.map((event) => event._id),
spike_state: 'both',
only_future: false,
}).then((planningItems) => {
// Make sure to update the Redux Store with the latest Planning items
// So that the Editor can set the state with these latest items
planningApi.redux.store.dispatch<any>(actions.planning.api.receivePlannings(planningItems));

return events;
});
});
}

Expand Down
11 changes: 10 additions & 1 deletion client/api/locks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,16 @@ function unlockItem<T extends IAssignmentOrPlanningItem>(item: T, reloadLocksIfN
}
}

const lockedItemId = currentLock.item_id;
let lockedItemId: string;

if (item.type === 'event' && item.recurrence_id === currentLock.item_id) {
lockedItemId = item._id;
} else if (item.type === 'planning' && item.recurrence_id === currentLock.item_id) {
lockedItemId = item.event_item;
} else {
lockedItemId = currentLock.item_id;
}

const resource = getLockResourceName(currentLock.item_type);
const endpoint = `${resource}/${lockedItemId}/unlock`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,23 +171,21 @@ export class EmbeddedCoverageFormComponent extends React.PureComponent<IProps> {
<List.Row>
<Row
testId="user"
noPadding={true}
style={{padding: '2rem 0'}}
>
<Row style={{padding: '2rem 0'}}>
<SelectUser
onSelect={(user) => {
this.onUserChange(null, user);
}}
autoFocus={false}
horizontalSpacing={true}
clearable={true}
/>
</Row>
<SelectUser
onSelect={(user) => {
this.onUserChange(null, user);
}}
autoFocus={false}
horizontalSpacing={true}
clearable={true}
/>
</Row>
</List.Row>
<List.Row>
<Row>
{this.props.coverageProfile.language != null && (
{this.props.coverageProfile.language?.enabled !== true ? null : (
<List.Row>
<Row>
<Select
label={gettext('Language:')}
value={language}
Expand All @@ -205,9 +203,9 @@ export class EmbeddedCoverageFormComponent extends React.PureComponent<IProps> {
)
)}
</Select>
)}
</Row>
</List.Row>
</Row>
</List.Row>
)}
<List.Row>
<EditorFieldNewsCoverageStatus
testId="status"
Expand Down
17 changes: 17 additions & 0 deletions client/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,23 @@ export interface IEventItem extends IBaseRestApiResponse {
// Used only to add/modify Plannings/Coverages from the Event form
// These are only stored with the Autosave and not the actual Event
associated_plannings: Array<Partial<IPlanningItem>>;
embedded_planning: Array<{
planning_id?: IPlanningItem['_id'];
coverages: Array<{
coverage_id?: IPlanningCoverageItem['coverage_id'];
g2_content_type: ICoveragePlanningDetails['g2_content_type'];
desk: IPlanningAssignedTo['desk'];
user: IPlanningAssignedTo['user'];
language: ICoveragePlanningDetails['language'];
news_coverage_status: IPlanningNewsCoverageStatus['qcode'];
scheduled: ICoveragePlanningDetails['scheduled'];

genre: ICoveragePlanningDetails['genre']['qcode'];
slugline: ICoveragePlanningDetails['slugline'];
ednote: ICoveragePlanningDetails['ednote'];
internal_note: ICoveragePlanningDetails['internal_note'];
}>;
}>;

// Attributes added by API (removed via modifyForClient)
// The `_status` field is available when the item comes from a POST/PATCH request
Expand Down
2 changes: 1 addition & 1 deletion client/utils/strings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function convertStringFields<Src extends IEventOrPlanningItem, Dest exten
}));

if (translationsDest.length > 0) {
itemDest.translations = (itemSrc.translations ?? []).concat(translationsDest);
itemDest.translations = (itemDest.translations ?? []).concat(translationsDest);
}

return itemDest;
Expand Down
Loading

0 comments on commit b6663cd

Please sign in to comment.