Skip to content

Commit

Permalink
[STTNHUB-374] Unlink content on Event/Planning sttinstruct:remove sig…
Browse files Browse the repository at this point in the history
…nal (#118)

* [STTNHUB-374] Unlink content on Event/Planning sttinstruct:remove signal

* fix system_update call

* delete delivery item(s) by planning_id

* continue spike/cancel if unlink content fails

* add behave tests
  • Loading branch information
MarkLark86 authored Nov 8, 2024
1 parent 50522cf commit b98af91
Show file tree
Hide file tree
Showing 4 changed files with 372 additions and 4 deletions.
125 changes: 125 additions & 0 deletions server/features/ingest_planning.feature
Original file line number Diff line number Diff line change
Expand Up @@ -495,3 +495,128 @@ Feature: Ingest STT Planning items
}
]}
"""

@auth
@stt_cvs
@stt_providers
Scenario: Unlinks content from planning on remove signal
When we fetch from "STTNewsML" ingest "stt_newsml_link_content.xml" using routing_scheme
"""
#routing_schemes._id#
"""
When we fetch from "STTPlanningML" ingest "planning_ml_link_content.xml"
When we get "/assignments"
Then we get list with 1 items
Then we store "assignment" with first item
When we get "/planning"
Then we get list with 1 items
"""
{"_items": [{
"_id": "urn:newsml:stt.fi:437036",
"coverages": [{
"coverage_id": "ID_TEXT_120123822",
"workflow_status": "active",
"assigned_to": {
"assignment_id": "#assignment._id#",
"desk": "#desks._id#",
"user": null,
"state": "completed",
"priority": 6
}
}]
}]}
"""
When we get "published"
Then we get list with 1 items
"""
{"_items": [{
"uri": "urn:newsml:stt.fi:101801633",
"assignment_id": "#assignment._id#"
}]}
"""
When we fetch from "STTPlanningML" ingest "planning_ml_link_content_delete.xml"
When we get "/assignments"
Then we get list with 0 items
When we get "/planning"
Then we get list with 1 items
"""
{"_items": [{
"_id": "urn:newsml:stt.fi:437036",
"coverages": [{
"coverage_id": "ID_TEXT_120123822",
"assigned_to": "__empty__",
"workflow_status": "draft"
}]
}]}
"""
When we get "published"
Then we get list with 1 items
"""
{"_items": [{
"uri": "urn:newsml:stt.fi:101801633",
"assignment_id": null
}]}
"""

@auth
@stt_cvs
@stt_providers
Scenario: Unlinks content from event on remove signal
When we fetch from "STTNewsML" ingest "stt_newsml_link_content.xml" using routing_scheme
"""
#routing_schemes._id#
"""
When we fetch from "STTEventsML" ingest "events_ml_259431.xml"
When we fetch from "STTPlanningML" ingest "planning_ml_437036_link_content_and_event.xml"
When we get "/assignments"
Then we get list with 1 items
Then we store "assignment" with first item
When we get "/planning"
Then we get list with 1 items
"""
{"_items": [{
"_id": "urn:newsml:stt.fi:437036",
"coverages": [{
"coverage_id": "ID_TEXT_120123822",
"workflow_status": "active",
"assigned_to": {
"assignment_id": "#assignment._id#",
"desk": "#desks._id#",
"user": null,
"state": "completed",
"priority": 6
}
}]
}]}
"""
When we get "published"
Then we get list with 1 items
"""
{"_items": [{
"uri": "urn:newsml:stt.fi:101801633",
"assignment_id": "#assignment._id#"
}]}
"""
When we fetch from "STTEventsML" ingest "events_ml_259431_delete.xml"
When we get "/assignments"
Then we get list with 0 items
When we get "/planning"
Then we get list with 1 items
"""
{"_items": [{
"_id": "urn:newsml:stt.fi:437036",
"coverages": [{
"coverage_id": "ID_TEXT_120123822",
"assigned_to": "__empty__",
"workflow_status": "draft"
}]
}]}
"""
When we get "published"
Then we get list with 1 items
"""
{"_items": [{
"uri": "urn:newsml:stt.fi:101801633",
"assignment_id": null
}]}
"""
83 changes: 79 additions & 4 deletions server/stt/common.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from typing import Dict, Any
from typing import Dict, Any, Union
import logging
from copy import deepcopy

from xml.etree.ElementTree import Element
from eve.utils import config

from superdesk import get_resource_service
from superdesk.metadata.item import ITEM_TYPE, ITEM_STATE
from planning.common import WORKFLOW_STATE, POST_STATE, update_post_item
from planning.common import WORKFLOW_STATE, POST_STATE, update_post_item, update_assignment_on_link_unlink


logger = logging.getLogger(__name__)


def planning_xml_contains_remove_signal(xml: Element) -> bool:
Expand All @@ -17,9 +22,19 @@ def planning_xml_contains_remove_signal(xml: Element) -> bool:
return False


def unpost_or_spike_event_or_planning(item: Dict[str, Any]):
def unpost_or_spike_event_or_planning(item: Dict[str, Any]) -> None:
item_resource = "events" if item.get(ITEM_TYPE) == "event" else "planning"
original: Dict[str, Any] = get_resource_service(item_resource).find_one(req=None, _id=item["guid"]) or {}
original: Union[Dict[str, Any], None] = get_resource_service(item_resource).find_one(req=None, _id=item["guid"])

if not original:
logger.error("Failed to spike/cancel ingested item: item not found", extra={"item_id": item["guid"]})
return

# Wrap ``unlink_item_from_all_content`` in a try...except, so if it fails the item is still spiked/cancelled
try:
unlink_item_from_all_content(original)
except Exception:
logger.exception("Failed to unlink content from item", extra={"item_id": item["guid"]})

if not original.get("pubstatus") and original.get(ITEM_STATE) in [
WORKFLOW_STATE.INGESTED,
Expand All @@ -32,6 +47,66 @@ def unpost_or_spike_event_or_planning(item: Dict[str, Any]):
update_post_item({"pubstatus": POST_STATE.CANCELLED, "_etag": original["_etag"]}, original)


def unlink_item_from_all_content(item: Dict[str, Any]) -> None:
"""Attempts to unlink all content/assignments from the provided item
Performs the following actions:
* If this is an Event, re-runs this function with any linked Planning items
* Removes ``assignment_id`` from content linked to this item, using 'archived', 'published' or 'archive' collection
* Deletes all items in ``delivery`` collection, that match any coverage in the Planning item
* Deletes all items in ``assignments`` collection, that match the Planning item's ID
* Updates the Planning item's coverages, to remove ``assigned_to`` field and set ``workflow_status`` to ``DRAFT``
The above actions are performed directly to avoid validation logic in the Planning module. As some of the services,
such as Assignments service, assumes an unlink is being performed from the front-end and not via ingest.
So instead we directly delete the items from their respective collections.
"""

item_id = item["_id"]
planning_service = get_resource_service("planning")
if item.get(ITEM_TYPE) == "event":
for planning_item in planning_service.find(where={"event_item": item_id}):
unlink_item_from_all_content(planning_item)
else:
delivery_service = get_resource_service("delivery")
archive_service = get_resource_service("search")

coverages = deepcopy(item.get("coverages") or [])
if not len(coverages):
# No coverages on this Planning item, no need to continue
return

for coverage in coverages:
# Remove assignee information and set state to DRAFT
coverage.pop("assigned_to", None)
coverage["workflow_status"] = WORKFLOW_STATE.DRAFT

for content_link in delivery_service.find(where={"coverage_id": coverage["coverage_id"]}):
content_id = content_link.get("item_id")
if not content_id:
# Content ID not on this delivery, no need to unlink
continue

content_item = archive_service.find_one(req=None, _id=content_id)
if not content_item or not content_item.get("assignment_id"):
# Either content not found, or does not contain the ``assignment_id``
# Nothing to do for this one
continue

# Update the content item to remove the ``assignment_id``
update_assignment_on_link_unlink(None, content_item)

# Delete all delivery entries for this Planning item
delivery_service.delete_action(lookup={"planning_id": item_id})

# Delete all assignments for this Planning item directly
# Note: skips ``on_delete`` and ``on_deleted`` hooks, due to validation issues
get_resource_service("assignments").delete(lookup={"planning_item": item_id})

# Update the Planning item, to update its coverage assignee and workflow_status
planning_service.system_update(item_id, {"coverages": coverages}, item)


def remove_date_portion_from_id(item_id: str) -> str:
"""Removes the date portion from an ingested Event or Planning ID
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<planningItem
xmlns="http://iptc.org/std/nar/2006-10-01/"
xmlns:stt="http://www.stt-lehtikuva.fi/NewsML"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://iptc.org/std/nar/2006-10-01/ http://www.iptc.org/std/NewsML-G2/2.12/specification/NewsML-G2_2.12-spec-All-Power.xsd http://www.stt-lehtikuva.fi/NewsML http://www.stt-lehtikuva.fi/newsml/schema/STT-Lehtikuva_NewsML_G2.xsd"
guid="urn:newsml:stt.fi:20220330:437036"
version="2"
standard="NewsML-G2"
standardversion="2.12"
conformance="power"
xml:lang="fi"
>
<catalogRef href="http://www.iptc.org/std/catalog/catalog.IPTC-G2-Standards_18.xml"/>
<catalogRef href="http://www.stt-lehtikuva.fi/newsml/doc/stt-NewsCodesCatalog_1.xml"/>
<itemMeta>
<itemClass qcode="plinat:newscoverage"/>
<provider literal="STT"/>
<versionCreated>2022-03-22T14:54:59+02:00</versionCreated>
<pubStatus qcode="stat:usable"/>
<edNote role="sttdescription:additionalinfo">Miten taistelut etenevät? Millaisia kansainvälisiä reaktioita syntyy? Entä miten tilanne Venäjällä elää? Seuraamme päivän tapahtumia ja tarkennamme paketointia.</edNote>
</itemMeta>
<contentMeta>
<contentCreated>2022-03-22T14:54:59+02:00</contentCreated>
<contentModified>2022-03-22T14:54:59+02:00</contentModified>
<headline>Miten tilanne Ukrainan sodan ympärillä ja Ukrainassa kehittyy?</headline>
<description role="drol:summary"/>
<subject qcode="stt-topics:437036">
<related rel="sttrel:assigneddate" value="2022-03-30" valuedatatype="Date"/>
</subject>
<subject type="cpnat:department" qcode="sttdepartment:14">
<name>Ulkomaat</name>
</subject>
</contentMeta>
<assert qcode="stt-topics:437036">
<newsCoverageStatus qcode="ncostat:int"/>
</assert>
<newsCoverageSet>
<newsCoverage id="ID_EVENT_259431" modified="2022-03-30T09:25:05+02:00">
<planning>
<g2contentType>application/vnd.iptc.g2.newsitem+xml</g2contentType>
<itemClass qcode="ninat:text"/>
<headline>SDP:n puoluevaltuuston kokous</headline>
<description>Puoluevaltuuston kokouksen avaa klo 13.00 puoluevaltuuston puheenjohtaja, kuntaministeri Sirpa Paatero. Poliittisen tilannekatsauksen pitää SDP:n puheenjohtaja, pääministeri Sanna Marin. Tämän jälkeen kokous jatkuu suljettuna.</description>
<subject type="cpnat:event" qcode="urn:newsml:stt.fi:20220402:259431">
<name>SDP:n puoluevaltuuston kokous</name>
</subject>
</planning>
</newsCoverage>
<newsCoverage id="ID_TEXT_120123822">
<planning>
<g2contentType>application/vnd.iptc.g2.newsitem+xml</g2contentType>
<itemClass qcode="ninat:text"/>
<headline>UKRAINA // Yön seurantaa</headline>
<description/>
<subject qcode="stt-topics:437036" type="cpnat:abstract"/>
<genre qcode="sttgenre:1">
<name>Pääjuttu</name>
<narrower qcode="sttversion:3"/>
<narrower qcode="sttversion:4"/>
<narrower qcode="sttversion:6"/>
<narrower qcode="sttversion:7"/>
<related rel="sttrel:plannedlength" value="3150"/>
</genre>
</planning>
<delivery>
<deliveredItemRef guidref="urn:newsml:stt.fi:20171219:101801633">
<modified>2022-03-30T09:15:40+02:00</modified>
<version/>
</deliveredItemRef>
</delivery>
</newsCoverage>
<newsCoverage id="ID_WORKREQUEST_159700">
<planning>
<g2contentType>application/vnd.iptc.g2.newsitem+xml</g2contentType>
<itemClass qcode="ninat:picture"/>
<headline>Miten tilanne Ukrainan sodan ympärillä ja Ukrainassa kehittyy?</headline>
<subject type="ninat:text" qcode="urn:newsml:stt.fi:20220330000000:159700">
<stt:workstartdate>2022-03-30T00:00:00+02:00</stt:workstartdate>
<stt:workenddate>2022-03-30T00:00:00+02:00</stt:workenddate>
</subject>
<dateline>Ukraina</dateline>
<genre qcode="sttimage:27">
<name>Kv. kuvaa</name>
</genre>
</planning>
</newsCoverage>
</newsCoverageSet>
</planningItem>
Loading

0 comments on commit b98af91

Please sign in to comment.