diff --git a/client/utils/history.tsx b/client/utils/history.tsx index f3c2aaaca..794d4fe21 100644 --- a/client/utils/history.tsx +++ b/client/utils/history.tsx @@ -61,7 +61,7 @@ const getHistoryRowElement = (text, historyItem, users) => { if (text) { return (
- {text}{gettext(' by ')} + {text}{historyItem.user_id != null ? gettext(' by ') : null} {self.getDisplayUser(historyItem.user_id, users)}
@@ -74,50 +74,20 @@ const getPostedHistoryElement = (index, historyItems, users) => { const historyItem = historyItems[index]; const itemType = 'event_id' in historyItem ? gettext('Event ') : gettext('Planning '); - for (let i = index - 1; i >= 0; i--) { - const item = historyItems[i]; - - if (item.operation !== HISTORY_OPERATIONS.POST || - [HISTORY_OPERATIONS.SPIKED, HISTORY_OPERATIONS.UNSPIKED].includes(historyItem.operation)) { - continue; - } - - if (get(item, 'update.pubstatus') === POST_STATE.USABLE) { - // Current history item happened when the item was in posted state - - if (get(historyItem, 'update.pubstatus') === POST_STATE.USABLE && - historyItem.operation !== HISTORY_OPERATIONS.EDITED) { - // If it is an edit and update operation don't show as a separate item - return; - } - - if (get(historyItem, 'update.pubstatus') !== POST_STATE.CANCELLED) { - text = gettext('Updated'); - break; - } - - if (get(historyItem, 'update.pubstatus') === POST_STATE.CANCELLED) { - text = itemType + gettext('unposted'); - break; - } - } else if (get(historyItem, 'update.pubstatus') === POST_STATE.USABLE) { - // Posted when the item was in unposted state - text = itemType + gettext('re-posted'); - break; - } + if (historyItem.operation !== HISTORY_OPERATIONS.POST && + historyItem.operation !== HISTORY_OPERATIONS.EVENTS_CANCEL && + historyItem.operation !== HISTORY_OPERATIONS.PLANNING_CANCEL + ) { + return; // not post operation } - // Item posted for the first time - if (!text && historyItem.operation === HISTORY_OPERATIONS.POST) { - text = itemType + gettext('posted'); + text = itemType + gettext('posted'); + + if (get(historyItem, 'update.pubstatus') === POST_STATE.CANCELLED) { + text = itemType + gettext('unposted'); } - return text && text.includes('unposted') ? ( -
- {self.getHistoryRowElement(gettext('Updated'), historyItem, users)} - {self.getHistoryRowElement(text, historyItem, users)} -
- ) : self.getHistoryRowElement(text, historyItem, users); + return self.getHistoryRowElement(text, historyItem, users); }; // eslint-disable-next-line consistent-this diff --git a/server/planning/common.py b/server/planning/common.py index d98e04793..25cd0fabc 100644 --- a/server/planning/common.py +++ b/server/planning/common.py @@ -8,7 +8,7 @@ # AUTHORS and LICENSE files distributed with this source code, or # at https://www.sourcefabric.org/superdesk/license -from typing import NamedTuple, Dict, Any, Set, Optional +from typing import NamedTuple, Dict, Any, Set, Optional, Union import re import time @@ -30,7 +30,7 @@ import json from bson import ObjectId -from planning.types import Planning, Coverage +from planning.types import Planning, Coverage, Event ITEM_STATE = "state" ITEM_EXPIRY = "expiry" @@ -379,6 +379,8 @@ def update_post_item(updates, original): # Save&Post or Save&Unpost if updates.get("pubstatus"): pub_status = updates["pubstatus"] + elif updates.get("ingest_pubstatus"): + pub_status = updates["ingest_pubstatus"] elif original.get("pubstatus") == POST_STATE.USABLE: # From item actions pub_status = POST_STATE.USABLE @@ -854,7 +856,7 @@ def update_ingest_on_patch(updates: Dict[str, Any], original: Dict[str, Any]): # The local version has not been published yet # So remove the provided ``pubstatus`` updates.pop("pubstatus", None) - elif original.get("pubstatus") == updates.get("pubstatus"): + elif original.get("pubstatus") == updates.get("ingest_pubstatus") or original.get("pubstatus"): # The local version has been published # and no change to ``pubstatus`` on ingested item updates.pop("state") @@ -865,3 +867,8 @@ def get_coverage_from_planning(planning_item: Planning, coverage_id: str) -> Opt (coverage for coverage in planning_item.get("coverages") or [] if coverage.get("coverage_id") == coverage_id), None, ) + + +def prepare_ingested_item_for_storage(doc: Union[Event, Planning]) -> None: + doc.setdefault("state", "ingested") + doc["ingest_pubstatus"] = doc.pop("pubstatus", "usable") # pubstatus is set when posted diff --git a/server/planning/events/events.py b/server/planning/events/events.py index 4c6bec981..2fc13aa97 100644 --- a/server/planning/events/events.py +++ b/server/planning/events/events.py @@ -54,6 +54,7 @@ get_max_recurrent_events, WORKFLOW_STATE, ITEM_STATE, + prepare_ingested_item_for_storage, remove_lock_information, format_address, update_post_item, @@ -135,6 +136,7 @@ def post_in_mongo(self, docs, **kwargs): """Post an ingested item(s)""" for doc in docs: + prepare_ingested_item_for_storage(doc) self._resolve_defaults(doc) set_ingest_version_datetime(doc) @@ -146,6 +148,9 @@ def post_in_mongo(self, docs, **kwargs): def patch_in_mongo(self, id, document, original) -> Optional[Dict[str, Any]]: """Patch an ingested item onto an existing item locally""" + prepare_ingested_item_for_storage(document) + events_history = get_resource_service("events_history") + events_history.on_item_updated(document, original, "ingested") set_planning_schedule(document) update_ingest_on_patch(document, original) diff --git a/server/planning/events/events_schema.py b/server/planning/events/events_schema.py index d3b5bf9e3..07ff3dc2a 100644 --- a/server/planning/events/events_schema.py +++ b/server/planning/events/events_schema.py @@ -64,6 +64,7 @@ "ingest_provider_sequence": metadata_schema["ingest_provider_sequence"], "ingest_firstcreated": metadata_schema["versioncreated"], "ingest_versioncreated": metadata_schema["versioncreated"], + "ingest_pubstatus": metadata_schema["pubstatus"], "event_created": {"type": "datetime"}, "event_lastmodified": {"type": "datetime"}, # Event Details diff --git a/server/planning/events/events_tests.py b/server/planning/events/events_tests.py index 46229ce71..78778cfd0 100644 --- a/server/planning/events/events_tests.py +++ b/server/planning/events/events_tests.py @@ -157,7 +157,7 @@ def test_create_cancelled_event(self): event = service.find_one(req=None, guid="test") assert event is not None - assert event["pubstatus"] == "cancelled" + assert event["ingest_pubstatus"] == "cancelled" class EventLocationFormatAddress(TestCase): diff --git a/server/planning/io/ingest_rule_handler.py b/server/planning/io/ingest_rule_handler.py index 0a0462210..6f10948e9 100644 --- a/server/planning/io/ingest_rule_handler.py +++ b/server/planning/io/ingest_rule_handler.py @@ -74,15 +74,18 @@ def apply_rule(self, rule: Dict[str, Any], ingest_item: Dict[str, Any], routing_ if updates is not None: ingest_item.update(updates) - - if attributes.get("autopost", False): + elif attributes.get("autopost", False): self.process_autopost(ingest_item) def _is_original_posted(self, ingest_item: Dict[str, Any]): service = get_resource_service("events" if ingest_item[ITEM_TYPE] == CONTENT_TYPE.EVENT else "planning") original = service.find_one(req=None, _id=ingest_item.get(config.ID_FIELD)) - return original is not None and original.get("pubstatus") in [POST_STATE.USABLE, POST_STATE.CANCELLED] + return ( + original is not None + and original.get("pubstatus") in [POST_STATE.USABLE, POST_STATE.CANCELLED] + and original["pubstatus"] == ingest_item.get("ingest_pubstatus") + ) def add_event_calendars(self, ingest_item: Dict[str, Any], attributes: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Add Event Calendars from Routing Rule Action onto the ingested item""" @@ -164,7 +167,6 @@ def add_planning_agendas(self, ingest_item: Dict[str, Any], attributes: Dict[str def process_autopost(self, ingest_item: Dict[str, Any]): """Automatically post this item""" - if self._is_original_posted(ingest_item): # No need to autopost this item # As the original is already posted @@ -174,7 +176,7 @@ def process_autopost(self, ingest_item: Dict[str, Any]): item_id = ingest_item.get(config.ID_FIELD) update_post_item( { - "pubstatus": ingest_item.get("pubstatus") or POST_STATE.USABLE, + "pubstatus": ingest_item.get("ingest_pubstatus") or POST_STATE.USABLE, "_etag": ingest_item.get("_etag"), }, ingest_item, diff --git a/server/planning/io/ingest_rule_handler_test.py b/server/planning/io/ingest_rule_handler_test.py index 98a246133..e41c87114 100644 --- a/server/planning/io/ingest_rule_handler_test.py +++ b/server/planning/io/ingest_rule_handler_test.py @@ -9,6 +9,8 @@ # at https://www.sourcefabric.org/superdesk/license from bson import ObjectId +from datetime import datetime +from superdesk import get_resource_service from superdesk.metadata.item import ITEM_TYPE, CONTENT_TYPE from .ingest_rule_handler import PlanningRoutingRuleHandler from planning.tests import TestCase @@ -30,6 +32,8 @@ }, } +AUTOPOST_RULE = {"actions": {"extra": {"autopost": True}}} + class IngestRuleHandlerTestCase(TestCase): calendars = [ @@ -44,10 +48,11 @@ class IngestRuleHandlerTestCase(TestCase): { "_id": "event1", "dates": { - "start": "2022-07-02T14:00:00+0000", - "end": "2022-07-03T14:00:00+0000", + "start": datetime.fromisoformat("2022-07-02T14:00:00+00:00"), + "end": datetime.fromisoformat("2022-07-03T14:00:00+00:00"), }, "type": "event", + "pubstatus": "usable", }, { "_id": "event2", @@ -92,7 +97,7 @@ def test_adds_event_calendars(self): } ], ) - event = self.event_items[0] + event = self.event_items[0].copy() self.app.data.insert("events", [event]) original = self.app.data.find_one("events", req=None, _id=event["_id"]) @@ -115,7 +120,7 @@ def test_skips_disabled_and_existing_calendars(self): } ], ) - event = self.event_items[1] + event = self.event_items[1].copy() self.app.data.insert("events", [event]) original = self.app.data.find_one("events", req=None, _id=event["_id"]) @@ -159,3 +164,53 @@ def test_skips_disabled_and_existing_agendas(self): self.assertEqual(len(updated["agendas"]), 1) self.assertEqual(updated["agendas"][0], self.agendas[0]["_id"]) + + def test_autopost(self): + event = self.event_items[0].copy() + events_service = get_resource_service("events") + events_service.post_in_mongo([event]) + + history = self.get_event_history() + assert len(history) == 1 + assert history[0]["operation"] == "ingested" + + self.handler.apply_rule(AUTOPOST_RULE, event, {}) + + history = self.get_event_history() + assert len(history) == 2 + assert history[-1]["operation"] == "post" + + original = events_service.find_one(req=None, _id=event["_id"]) + assert original["pubstatus"] == "usable" + + event["pubstatus"] = "cancelled" + event["versioncreated"] = datetime.now() + events_service.patch_in_mongo(event["_id"], event, original) + + self.handler.apply_rule(AUTOPOST_RULE, event, {}) + + history = self.get_event_history() + assert len(history) == 4 + assert history[-2]["operation"] == "ingested" + assert history[-1]["operation"] == "post" + + original = events_service.find_one(req=None, _id=event["_id"]) + assert original["pubstatus"] == "cancelled" + + def test_autopost_cancelled(self): + event = self.event_items[0].copy() + event["pubstatus"] = "cancelled" + events_service = get_resource_service("events") + events_service.post_in_mongo([event]) + + self.handler.apply_rule(AUTOPOST_RULE, event, {}) + + history = self.get_event_history() + assert len(history) == 2 + assert history[-1]["operation"] == "post" + + original = events_service.find_one(req=None, _id=event["_id"]) + assert original["pubstatus"] == "cancelled" + + def get_event_history(self): + return list(self.app.data.find_all("events_history")) diff --git a/server/planning/planning/planning.py b/server/planning/planning/planning.py index 14de66af6..0a3a2dac2 100644 --- a/server/planning/planning/planning.py +++ b/server/planning/planning/planning.py @@ -38,6 +38,7 @@ get_coverage_status_from_cv, WORKFLOW_STATE, ASSIGNMENT_WORKFLOW_STATE, + prepare_ingested_item_for_storage, update_post_item, get_coverage_type_name, set_original_creator, @@ -91,8 +92,8 @@ def post_in_mongo(self, docs, **kwargs): """Post an ingested item(s)""" for doc in docs: + prepare_ingested_item_for_storage(doc) self._resolve_defaults(doc) - doc.pop("pubstatus", None) set_ingest_version_datetime(doc) self.on_create(docs) @@ -105,7 +106,7 @@ def post_in_mongo(self, docs, **kwargs): def patch_in_mongo(self, id, document, original): """Patch an ingested item onto an existing item locally""" - + prepare_ingested_item_for_storage(document) update_ingest_on_patch(document, original) response = self.backend.update_in_mongo(self.datasource, id, document, original) self.on_updated(document, original, from_ingest=True) @@ -1813,6 +1814,7 @@ def _iter_recurring_plannings_to_update(self, updates, original, update_method): "ingest_provider_sequence": metadata_schema["ingest_provider_sequence"], "ingest_firstcreated": metadata_schema["versioncreated"], "ingest_versioncreated": metadata_schema["versioncreated"], + "ingest_pubstatus": metadata_schema["pubstatus"], # Agenda Item details "agendas": { "type": "list",