From bc4dd5b233ff9af35410ada25cf01bffef0bba73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ja=C5=A1ek?= Date: Thu, 9 Jan 2025 09:28:04 +0100 Subject: [PATCH 1/2] avoid overriding user changes via event ingest (#2175) * avoid overriding user changes via event ingest check event history what keys were updated by users and ignore those when updating from ingest. SDCP-861 * avoid using `id` --- server/planning/events/events.py | 31 ++++++++++++++-- server/planning/events/events_history.py | 25 +++++++++++++ server/planning/events/events_ingest_tests.py | 35 +++++++++++++++++++ setup.cfg | 2 +- 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 server/planning/events/events_ingest_tests.py diff --git a/server/planning/events/events.py b/server/planning/events/events.py index 57e24f13e..62d1d4ddd 100644 --- a/server/planning/events/events.py +++ b/server/planning/events/events.py @@ -86,6 +86,16 @@ "eorol:venue": "Venue organiser", } +# based on onclusive provided content fields for now +CONTENT_FIELDS = { + "name", + "definition_short", + "definition_long", + "links", + "ednote", + "subject", +} + def get_events_embedded_planning(event: Event) -> List[EmbeddedPlanning]: def get_coverage_id(coverage: EmbeddedCoverageItem) -> str: @@ -128,6 +138,18 @@ def is_event_updated(new_item: Event, old_item: Event) -> bool: return False +def get_user_updated_keys(event_id: str) -> set[str]: + history_service = get_resource_service("events_history") + updates = history_service.get_by_id(event_id) + updated_keys = set() + for update in updates: + if not update.get("user_id"): + continue + if update.get("update"): + updated_keys.update(update["update"].keys()) + return updated_keys + + class EventsService(superdesk.Service): """Service class for the events model.""" @@ -144,12 +166,17 @@ def post_in_mongo(self, docs, **kwargs): self.on_created(docs) return ids - def patch_in_mongo(self, id, document, original) -> Optional[Dict[str, Any]]: + def patch_in_mongo(self, _id: str, document, original) -> Optional[Dict[str, Any]]: """Patch an ingested item onto an existing item locally""" + content_fields = app.config.get("EVENT_INGEST_CONTENT_FIELDS", CONTENT_FIELDS) + updated_keys = get_user_updated_keys(_id) + for key in updated_keys: + if key in document and key in content_fields and original.get(key): + document[key] = original[key] set_planning_schedule(document) update_ingest_on_patch(document, original) - response = self.backend.update_in_mongo(self.datasource, id, document, original) + response = self.backend.update_in_mongo(self.datasource, _id, document, original) self.on_updated(document, original, from_ingest=True) return response diff --git a/server/planning/events/events_history.py b/server/planning/events/events_history.py index 365b6138b..34db104b4 100644 --- a/server/planning/events/events_history.py +++ b/server/planning/events/events_history.py @@ -8,6 +8,7 @@ """Superdesk Files""" +from typing import Any, TypedDict from superdesk import Resource, get_resource_service from planning.history import HistoryService import logging @@ -22,6 +23,7 @@ class EventsHistoryResource(Resource): endpoint_name = "events_history" resource_methods = ["GET"] item_methods = ["GET"] + schema = { "event_id": {"type": "string"}, "user_id": Resource.rel("users", True), @@ -29,6 +31,17 @@ class EventsHistoryResource(Resource): "update": {"type": "dict", "nullable": True}, } + mongo_indexes = { + "event_id_1": ([("event_id", 1)], {"background": True}), + } + + +class EventHistoryRecord(TypedDict): + event_id: str + user_id: str + operation: str + update: dict[str, Any] + class EventsHistoryService(HistoryService): def on_item_created(self, items, operation=None): @@ -85,3 +98,15 @@ def on_update_repetitions(self, updates, event_id, operation): def on_update_time(self, updates, original): self.on_item_updated(updates, original, "update_time") + + def get_by_id(self, _id: str) -> list[EventHistoryRecord]: + records = self.find(where={"event_id": _id}) + return [ + { + "event_id": record.get("event_id"), + "user_id": record.get("user_id"), + "operation": record.get("operation"), + "update": record.get("update"), + } + for record in records + ] diff --git a/server/planning/events/events_ingest_tests.py b/server/planning/events/events_ingest_tests.py new file mode 100644 index 000000000..ea890dfbb --- /dev/null +++ b/server/planning/events/events_ingest_tests.py @@ -0,0 +1,35 @@ +from superdesk import get_resource_service +from datetime import datetime, timedelta +from planning.tests import TestCase +from unittest.mock import patch +from flask import g + + +class EventIngestTestCase(TestCase): + def test_ingest_updated_event(self): + events_service = get_resource_service("events") + events_history_service = get_resource_service("events_history") + dates = {"start": datetime.now(), "end": datetime.now() + timedelta(days=1)} + old_event = {"guid": "1", "name": "bar", "ednote": "ednote1", "dates": dates} + + # event is created + events_service.post_in_mongo([old_event]) + + # user updates the event + updates = {"definition_short": "manual", "ednote": "manual"} + with self.app.test_request_context(): + g.user = {"_id": "test"} + events_service.patch(old_event["_id"], updates) + events_history_service.on_item_updated(updates, old_event) + + new_event = {"guid": "1", "name": "updated", "ednote": "updated", "dates": dates, "definition_short": "updated"} + old_event = events_service.find_one(req=None, guid="1") + + # event is updated via ingest + events_service.patch_in_mongo(new_event["guid"], new_event, old_event) + + updated_event = events_service.find_one(req=None, _id="1") + assert updated_event is not None + assert updated_event["name"] == "updated" + assert updated_event["ednote"] == "manual" + assert updated_event["definition_short"] == "manual" diff --git a/setup.cfg b/setup.cfg index 1d8d579fb..fe55bd00a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ input_file = po/server.pot output_dir = server/planning/translations [mypy] -python_version = 3.8 +python_version = 3.10 warn_unused_configs = True allow_untyped_globals = True ignore_missing_imports = True From cb87d9d4e6f6167032e70ba5b749b5f19d2cd251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ja=C5=A1ek?= Date: Thu, 16 Jan 2025 12:23:01 +0100 Subject: [PATCH 2/2] allow updates of events edited by user (#2183) SDCP-861 --- server/planning/events/events.py | 1 - server/planning/events/events_ingest_tests.py | 28 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/server/planning/events/events.py b/server/planning/events/events.py index 62d1d4ddd..6b141ebad 100644 --- a/server/planning/events/events.py +++ b/server/planning/events/events.py @@ -851,7 +851,6 @@ def delete_event_files(self, updates, original): def should_update(self, old_item, new_item, provider): return old_item is None or not any( [ - old_item.get("version_creator"), old_item.get("pubstatus") == "cancelled", old_item.get("state") == "killed", ] diff --git a/server/planning/events/events_ingest_tests.py b/server/planning/events/events_ingest_tests.py index ea890dfbb..8619d04e2 100644 --- a/server/planning/events/events_ingest_tests.py +++ b/server/planning/events/events_ingest_tests.py @@ -1,7 +1,6 @@ from superdesk import get_resource_service from datetime import datetime, timedelta from planning.tests import TestCase -from unittest.mock import patch from flask import g @@ -10,21 +9,42 @@ def test_ingest_updated_event(self): events_service = get_resource_service("events") events_history_service = get_resource_service("events_history") dates = {"start": datetime.now(), "end": datetime.now() + timedelta(days=1)} - old_event = {"guid": "1", "name": "bar", "ednote": "ednote1", "dates": dates} + old_event = { + "guid": "1", + "name": "bar", + "ednote": "ednote1", + "dates": dates, + "state": "ingested", + "versioncreated": datetime.now() - timedelta(hours=1), + } # event is created events_service.post_in_mongo([old_event]) + history = events_history_service.get_by_id("1") + assert 1 == len(history) # user updates the event updates = {"definition_short": "manual", "ednote": "manual"} with self.app.test_request_context(): g.user = {"_id": "test"} events_service.patch(old_event["_id"], updates) - events_history_service.on_item_updated(updates, old_event) + events_history_service.on_item_updated(updates, old_event, "edited") - new_event = {"guid": "1", "name": "updated", "ednote": "updated", "dates": dates, "definition_short": "updated"} + history = events_history_service.get_by_id("1") + assert 2 == len(history) + + new_event = { + "guid": "1", + "name": "updated", + "ednote": "updated", + "dates": dates, + "definition_short": "updated", + "versioncreated": datetime.now() - timedelta(minutes=30), + } old_event = events_service.find_one(req=None, guid="1") + assert events_service.should_update(old_event, new_event, {}) + # event is updated via ingest events_service.patch_in_mongo(new_event["guid"], new_event, old_event)