Skip to content

Commit

Permalink
autopost ingested events on creation via rule handler (#2182)
Browse files Browse the repository at this point in the history
* autopost ingested events on creation via rule handler

SDCP-903

* handle ingest pubstatus via new `ingest_pubstatus` field

* fix test

* fix planning test

* add to history when item is updated via ingest

* fix history widget UI

* add `ingest_pubstatus` to planning schema
  • Loading branch information
petrjasek authored Jan 17, 2025
1 parent fa5da65 commit 5aff1bf
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 56 deletions.
52 changes: 11 additions & 41 deletions client/utils/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const getHistoryRowElement = (text, historyItem, users) => {
if (text) {
return (
<div>
<span><strong>{text}</strong>{gettext(' by ')}</span>
<span><strong>{text}</strong>{historyItem.user_id != null ? gettext(' by ') : null}</span>
<span className="user-name">{self.getDisplayUser(historyItem.user_id, users)}</span>
<em> <AbsoluteDate date={historyItem._created} /> </em>
</div>
Expand All @@ -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') ? (
<div>
{self.getHistoryRowElement(gettext('Updated'), historyItem, users)}
{self.getHistoryRowElement(text, historyItem, users)}
</div>
) : self.getHistoryRowElement(text, historyItem, users);
return self.getHistoryRowElement(text, historyItem, users);
};

// eslint-disable-next-line consistent-this
Expand Down
13 changes: 10 additions & 3 deletions server/planning/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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
5 changes: 5 additions & 0 deletions server/planning/events/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions server/planning/events/events_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion server/planning/events/events_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
12 changes: 7 additions & 5 deletions server/planning/io/ingest_rule_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
63 changes: 59 additions & 4 deletions server/planning/io/ingest_rule_handler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,6 +32,8 @@
},
}

AUTOPOST_RULE = {"actions": {"extra": {"autopost": True}}}


class IngestRuleHandlerTestCase(TestCase):
calendars = [
Expand All @@ -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",
Expand Down Expand Up @@ -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"])

Expand All @@ -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"])

Expand Down Expand Up @@ -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"))
6 changes: 4 additions & 2 deletions server/planning/planning/planning.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 5aff1bf

Please sign in to comment.