From 706491f32f6c89ecfebccc3bce768a10c19be588 Mon Sep 17 00:00:00 2001 From: Petr Jasek Date: Tue, 23 Jul 2024 17:07:17 +0200 Subject: [PATCH] add config for different linking methods STT-30 --- client/actions/agenda.ts | 1 - client/actions/tests/agenda_test.ts | 1 - client/api/planning.ts | 1 - client/interfaces.ts | 2 +- server/features/assignments_delete.feature | 3 +- server/features/combined_export.feature | 2 +- server/features/events.feature | 89 ++++++++++++++++++- server/features/events_cancel.feature | 28 +++--- server/features/events_post.feature | 10 ++- server/features/events_postpone.feature | 4 + server/features/events_reschedule.feature | 4 + server/features/events_spike.feature | 4 + server/features/planning_lock.feature | 4 + .../planning/commands/export_to_newsroom.py | 1 - server/planning/events/events.py | 12 ++- server/planning/planning/planning.py | 30 ++++++- server/planning/planning/planning_schema.py | 2 - server/planning/types/__init__.py | 1 + server/planning/utils.py | 5 ++ server/settings.py | 4 + 20 files changed, 176 insertions(+), 32 deletions(-) diff --git a/client/actions/agenda.ts b/client/actions/agenda.ts index 8b10fa4a8..ce0019d83 100644 --- a/client/actions/agenda.ts +++ b/client/actions/agenda.ts @@ -245,7 +245,6 @@ export function convertEventToPlanningItem(event: IEventItem): Partial { coverages: [], related_events: [{ _id: events[0]._id, - link_type: 'primary', }], planning_date: events[0].dates.start, slugline: events[0].slugline, diff --git a/client/api/planning.ts b/client/api/planning.ts index 98cfc1092..cd4234403 100644 --- a/client/api/planning.ts +++ b/client/api/planning.ts @@ -164,7 +164,6 @@ function createFromEvent(event: IEventItem, updates: Partial): Pr const eventLink: IPlanningRelatedEventLink = { _id: event._id, - link_type: 'primary', }; if (event.recurrence_id != null) { diff --git a/client/interfaces.ts b/client/interfaces.ts index 76551680b..a96bba9f1 100644 --- a/client/interfaces.ts +++ b/client/interfaces.ts @@ -701,7 +701,7 @@ export type IPlanningRelatedEventLinkType = 'primary' | 'secondary'; export interface IPlanningRelatedEventLink { _id: string; - link_type: IPlanningRelatedEventLinkType; + link_type?: IPlanningRelatedEventLinkType; recurrence_id?: string; } diff --git a/server/features/assignments_delete.feature b/server/features/assignments_delete.feature index 1b6e279e0..b7c6cffac 100644 --- a/server/features/assignments_delete.feature +++ b/server/features/assignments_delete.feature @@ -292,7 +292,7 @@ Feature: Assignments Delete "workflow_status": "active" }], "related_events": [ - {"_id": "#events._id#", "link_type": "primary"} + {"_id": "#events._id#"} ] } """ @@ -351,7 +351,6 @@ Feature: Assignments Delete }], "related_events": [{ "_id": "#EVENT2._id#", - "link_type": "primary", "recurrence_id": "#EVENT2.recurrence_id#" }], "recurrence_id": "#EVENT2.recurrence_id#" diff --git a/server/features/combined_export.feature b/server/features/combined_export.feature index bcd73aeb0..ee4835c7c 100644 --- a/server/features/combined_export.feature +++ b/server/features/combined_export.feature @@ -59,7 +59,7 @@ Feature: Export combined Planning and Event items with default template "slugline": "planning-1", "description_text": "desc", "related_events": [ - {"_id": "#events._id#", "link_type": "primary"} + {"_id": "#events._id#"} ], "ednote": "Ed. note 1", "coverages": [{ diff --git a/server/features/events.feature b/server/features/events.feature index bff343d72..da5a4c999 100644 --- a/server/features/events.feature +++ b/server/features/events.feature @@ -721,6 +721,10 @@ Feature: Events @auth Scenario: Link new Event as secondary to a Planning item + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "one_primary_many_secondary"} + """ Given "planning" """ [{ @@ -1667,4 +1671,87 @@ Feature: Events "lock_session": "#SESSION_ID#" } """ - Then we get OK response \ No newline at end of file + Then we get OK response + + @auth + Scenario: Link new Event with many_secondary link method + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "many_secondary"} + """ + Given "planning" + """ + [{ + "_id": "plan1", + "guid": "plan1", + "slugline": "TestEvent", + "state": "draft", + "lock_user": "#CONTEXT_USER_ID#", + "lock_session": "#SESSION_ID#", + "lock_action": "add_as_event", + "lock_time": "#DATE#", + "planning_date": "2016-01-02" + }] + """ + When we post to "events" + """ + { + "guid": "event_1", + "name": "Primary Event 1", + "slugline": "event-1", + "_planning_item": "plan1", + "dates": { + "start": "2029-11-21T12:00:00.000Z", + "end": "2029-11-21T14:00:00.000Z", + "tz": "Australia/Sydney" + } + } + """ + Then we get OK response + When we get "/planning/plan1" + Then we get existing resource + """ + {"related_events": [{"_id": "#events._id#", "link_type": "secondary"}]} + """ + + @auth + Scenario: Link new Event with one_primary link method + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "one_primary"} + """ + Given "planning" + """ + [{ + "_id": "plan1", + "guid": "plan1", + "slugline": "TestEvent", + "state": "draft", + "lock_user": "#CONTEXT_USER_ID#", + "lock_session": "#SESSION_ID#", + "lock_action": "add_as_event", + "lock_time": "#DATE#", + "planning_date": "2016-01-02" + }] + """ + When we post to "events" + """ + { + "guid": "event_1", + "name": "Primary Event 1", + "slugline": "event-1", + "_planning_item": "plan1", + "dates": { + "start": "2029-11-21T12:00:00.000Z", + "end": "2029-11-21T14:00:00.000Z", + "tz": "Australia/Sydney" + } + } + """ + Then we get OK response + When we get "/planning/plan1" + Then we get existing resource + """ + {"related_events": [{"_id": "#events._id#", "link_type": "primary"}]} + """ + \ No newline at end of file diff --git a/server/features/events_cancel.feature b/server/features/events_cancel.feature index bb2bcfe7f..23ea482e1 100644 --- a/server/features/events_cancel.feature +++ b/server/features/events_cancel.feature @@ -164,7 +164,7 @@ Feature: Events Cancel "_id": "plan1", "guid": "plan1", "slugline": "TestPlan 1", - "related_events": [{"_id": "event1", "link_type": "primary"}], + "related_events": [{"_id": "event1"}], "state": "draft", "planning_date": "2016-01-02" }, @@ -172,7 +172,7 @@ Feature: Events Cancel "_id": "plan2", "guid": "plan2", "slugline": "TestPlan 2", - "related_events": [{"_id": "event1", "link_type": "primary"}], + "related_events": [{"_id": "event1"}], "state": "draft", "planning_date": "2016-01-02" }] @@ -311,7 +311,7 @@ Feature: Events Cancel [{ "slugline": "Weekly Meetings", "headline": "Friday Club", - "related_events": [{"_id": "#EVENT3._id#", "link_type": "primary"}], + "related_events": [{"_id": "#EVENT3._id#"}], "planning_date": "2016-01-02" }] """ @@ -394,7 +394,7 @@ Feature: Events Cancel "guid": "plan1", "slugline": "TestPlan 1", "related_events": [ - {"_id": "event1", "link_type": "primary"} + {"_id": "event1"} ], "ednote": "We're covering this Event", "state": "draft", @@ -870,7 +870,7 @@ Feature: Events Cancel "slugline": "Weekly Meetings", "headline": "Friday Club", "related_events": [ - {"_id": "#EVENT3._id#", "link_type": "primary"} + {"_id": "#EVENT3._id#"} ], "planning_date": "2016-01-02" }] @@ -904,6 +904,10 @@ Feature: Events Cancel @auth @vocabulary Scenario: Cancelling an Event does not cancel Planning item with secondary link + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "many_secondary"} + """ Given we have sessions "/sessions" And "events" """ @@ -924,24 +928,20 @@ Feature: Events Cancel And "planning" """ [{ - "guid": "plan1", - "slugline": "test-plan", - "planning_date": "2029-05-29T12:00:00+0000", - "related_events": [{"_id": "event1", "link_type": "primary"}] - }, { "guid": "plan2", "slugline": "test-plan", "planning_date": "2029-05-29T12:00:00+0000", - "related_events": [{"_id": "event1", "link_type": "secondary"}] + "related_events": [{"_id": "event1"}] }] """ When we perform cancel on events "event1" Then we get OK response When we get "/planning" - Then we get list with 2 items + Then we get list with 1 items """ {"_items": [ - {"_id": "plan1", "state": "cancelled"}, - {"_id": "plan2", "state": "draft"} + {"_id": "plan2", "state": "draft", "related_events": [ + {"_id": "event1", "link_type": "secondary"} + ]} ]} """ diff --git a/server/features/events_post.feature b/server/features/events_post.feature index 74e2b260f..9ffc04702 100644 --- a/server/features/events_post.feature +++ b/server/features/events_post.feature @@ -297,7 +297,7 @@ Feature: Events Post "headline": "test headline", "slugline": "test slugline", "planning_date": "2016-01-02", - "related_events": [{"_id": "#events._id#", "link_type": "primary"}] + "related_events": [{"_id": "#events._id#"}] } """ Then we get OK response @@ -439,7 +439,7 @@ Feature: Events Post "headline": "test headline", "slugline": "test slugline", "planning_date": "2016-01-02", - "related_events": [{"_id": "#events._id#", "link_type": "primary"}] + "related_events": [{"_id": "#events._id#"}] } """ Then we get OK response @@ -580,7 +580,7 @@ Feature: Events Post "headline": "test headline", "slugline": "test slugline", "planning_date": "2016-01-02", - "related_events": [{"_id": "#events._id#", "link_type": "primary"}] + "related_events": [{"_id": "#events._id#"}] } """ Then we get OK response @@ -1262,6 +1262,10 @@ Feature: Events Post @auth Scenario: Posting an Event will not post Planning item with secondary link + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "one_primary_many_secondary"} + """ # Configure auto-posting of primary linked Events Given "planning_types" """ diff --git a/server/features/events_postpone.feature b/server/features/events_postpone.feature index 5035128a8..e4aa6c9bb 100644 --- a/server/features/events_postpone.feature +++ b/server/features/events_postpone.feature @@ -798,6 +798,10 @@ Feature: Events Postpone @auth @vocabulary Scenario: Postponing an Event does not postpone Planning item with secondary link + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "one_primary_many_secondary"} + """ Given we have sessions "/sessions" And "events" """ diff --git a/server/features/events_reschedule.feature b/server/features/events_reschedule.feature index 29e88cc2f..f60d37f04 100644 --- a/server/features/events_reschedule.feature +++ b/server/features/events_reschedule.feature @@ -1708,6 +1708,10 @@ Feature: Events Reschedule @auth @vocabulary Scenario: Rescheduling an Event does not modify Planning item with secondary link + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "one_primary_many_secondary"} + """ Given we have sessions "/sessions" And "events" """ diff --git a/server/features/events_spike.feature b/server/features/events_spike.feature index 5b74df83d..e2598651c 100644 --- a/server/features/events_spike.feature +++ b/server/features/events_spike.feature @@ -1079,6 +1079,10 @@ Feature: Events Spike @auth @vocabulary Scenario: Spiking an Event does not spike Planning item with secondary link + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "one_primary_many_secondary"} + """ Given we have sessions "/sessions" And "events" """ diff --git a/server/features/planning_lock.feature b/server/features/planning_lock.feature index 19f658304..69cb57d13 100644 --- a/server/features/planning_lock.feature +++ b/server/features/planning_lock.feature @@ -492,6 +492,10 @@ Feature: Planning Item Locking @auth Scenario: Can lock Planning while related secondary Event is locked + Given config update + """ + {"PLANNING_EVENT_LINK_METHOD": "one_primary_many_secondary"} + """ Given "events" """ [{ diff --git a/server/planning/commands/export_to_newsroom.py b/server/planning/commands/export_to_newsroom.py index aceb8f22c..1d4fd5c50 100644 --- a/server/planning/commands/export_to_newsroom.py +++ b/server/planning/commands/export_to_newsroom.py @@ -73,7 +73,6 @@ def run(self, resource_url, assets_url, size=None): logger.info("Completed export events and planning.") def _fetch_items(self, fetch_callback): - """""" query = { "query": { "bool": { diff --git a/server/planning/events/events.py b/server/planning/events/events.py index 6d3b4e788..aca0e9c2d 100644 --- a/server/planning/events/events.py +++ b/server/planning/events/events.py @@ -75,7 +75,11 @@ update_ingest_on_patch, TEMP_ID_PREFIX, ) -from planning.utils import get_related_planning_for_events, get_related_event_ids_for_planning +from planning.utils import ( + get_planning_event_link_method, + get_related_planning_for_events, + get_related_event_ids_for_planning, +) from .events_base_service import EventsBaseService from .events_schema import events_schema from .events_sync import sync_event_metadata_with_planning_items @@ -751,8 +755,12 @@ def _link_to_planning(event: Event): raise SuperdeskApiError.badRequestError("Planning item not found") updates = {"related_events": planning_item.get("related_events") or []} + event_link_method = get_planning_event_link_method() link_type: PLANNING_RELATED_EVENT_LINK_TYPE = ( - "primary" if not len(get_related_event_ids_for_planning(planning_item, "primary")) else "secondary" + "primary" + if not len(get_related_event_ids_for_planning(planning_item, "primary")) + and event_link_method in ("one_primary", "one_primary_many_secondary") + else "secondary" ) related_planning = PlanningRelatedEventLink(_id=event_id, link_type=link_type) updates["related_events"].append(related_planning) diff --git a/server/planning/planning/planning.py b/server/planning/planning/planning.py index 343e3f153..cf7686089 100644 --- a/server/planning/planning/planning.py +++ b/server/planning/planning/planning.py @@ -11,6 +11,7 @@ """Superdesk Planning""" from typing import Dict, Any, Optional, List +from typing_extensions import assert_never from copy import deepcopy import logging from datetime import datetime @@ -74,6 +75,7 @@ from planning.signals import planning_created, planning_ingested from .planning_schema import planning_schema from planning.utils import ( + get_planning_event_link_method, get_related_planning_for_events, get_related_event_links_for_planning, get_related_event_ids_for_planning, @@ -319,8 +321,7 @@ def validate_planning(self, updates, original=None): sanitize_input_data(updates) - if len(get_related_event_ids_for_planning(updates, "primary")) > 1: - raise SuperdeskApiError.badRequestError("Only 1 primary linked event is allowed") + self._validate_events_links(updates) # Validate if agendas being added are enabled agendas agenda_service = get_resource_service("agenda") @@ -362,6 +363,31 @@ def validate_planning(self, updates, original=None): if next_schedule and next_schedule["planning"]["scheduled"] > scheduled_update["planning"]["scheduled"]: raise SuperdeskApiError(message="Scheduled updates of a coverage must be after the previous update") + def _validate_events_links(self, updates) -> None: + ONLY_ONE_PRIMARY_LINKED_EVENT_ERROR = "Only 1 primary linked event is allowed" + event_link_method = get_planning_event_link_method() + + if updates.get("related_events"): + related_events_links = updates.get("related_events") + if event_link_method == "one_primary": + assert 1 == len(related_events_links), ONLY_ONE_PRIMARY_LINKED_EVENT_ERROR + link = related_events_links[0] + link.setdefault("link_type", "primary") + assert link["link_type"] == "primary", "Only primary event links are allowed" + elif event_link_method == "many_secondary": + for link in related_events_links: + link.setdefault("link_type", "secondary") + assert link["link_type"] == "secondary", "Only secondary event links are allowed" + elif event_link_method == "one_primary_many_secondary": + primary_links = get_related_event_links_for_planning(updates, "primary") + secondary_links = get_related_event_links_for_planning(updates, "secondary") + assert len(primary_links) <= 1, ONLY_ONE_PRIMARY_LINKED_EVENT_ERROR + assert len(primary_links) + len(secondary_links) == len( + related_events_links + ), "Missing events link type" + else: + assert_never(event_link_method) + def _set_planning_event_info(self, doc: Planning, planning_type: ContentProfile) -> Optional[Event]: """Set the planning event date diff --git a/server/planning/planning/planning_schema.py b/server/planning/planning/planning_schema.py index 4d7442662..041ff46e3 100644 --- a/server/planning/planning/planning_schema.py +++ b/server/planning/planning/planning_schema.py @@ -194,8 +194,6 @@ }, "link_type": { "type": "string", - "required": True, - "default": "primary", "allowed": ["primary", "secondary"], }, }, diff --git a/server/planning/types/__init__.py b/server/planning/types/__init__.py index a00215154..01ac7a15f 100644 --- a/server/planning/types/__init__.py +++ b/server/planning/types/__init__.py @@ -16,6 +16,7 @@ UPDATE_METHOD = Literal["single", "future", "all"] PLANNING_RELATED_EVENT_LINK_TYPE = Literal["primary", "secondary"] +PLANNING_EVENT_LINK_METHOD = Literal["one_primary", "many_secondary", "one_primary_many_secondary"] class StringFieldTranslation(TypedDict): diff --git a/server/planning/utils.py b/server/planning/utils.py index b0d3bd4c5..2ccfcc91d 100644 --- a/server/planning/utils.py +++ b/server/planning/utils.py @@ -20,6 +20,7 @@ import arrow import pytz +from planning import types from superdesk import get_resource_service from superdesk.json_utils import cast_item @@ -213,3 +214,7 @@ def get_first_event_item_for_planning_id( return None return get_resource_service("events").find_one(req=None, _id=first_event_id) + + +def get_planning_event_link_method() -> types.PLANNING_EVENT_LINK_METHOD: + return app.config.get("PLANNING_EVENT_LINK_METHOD", "one_primary") diff --git a/server/settings.py b/server/settings.py index 3518d27a4..80698dc8d 100644 --- a/server/settings.py +++ b/server/settings.py @@ -12,6 +12,8 @@ import os import json +import planning.types as planning_types + from urllib.parse import urlparse @@ -189,3 +191,5 @@ def env(variable, fallback_value=None): ELASTICSEARCH_AUTO_AGGREGATIONS = False PLANNING_DEFAULT_COVERAGE_STATUS_ON_INGEST = "ncostat:int" + +PLANNING_EVENT_LINK_METHOD: planning_types.PLANNING_EVENT_LINK_METHOD = "one_primary"