diff --git a/README.md b/README.md index 0c12b9e8e..4940dfebf 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,9 @@ Below sections include the config options that can be defined in settings.py. * PLANNING_JSON_ASSIGNED_INFO_EXTENDED * Defaults to `false` * If `true`, it will add to planning JSON output additional info for coverages like assigned desk name/email and assigned user name/email. +* ASSIGNMENT_MANUAL_REASSIGNMENT_ONLY + * Default: False (preserves the current behavior where automatic user assignment occurs) + * If true, Disables automatic user assignment for coverage, ensuring that assignments are updated only through explicit manual reassignment ### Authoring Config * PLANNING_CHECK_FOR_ASSIGNMENT_ON_PUBLISH diff --git a/server/features/assignments.feature b/server/features/assignments.feature index 67ad25d85..624576b57 100644 --- a/server/features/assignments.feature +++ b/server/features/assignments.feature @@ -2084,3 +2084,251 @@ Feature: Assignments ] } """ + + @auth + @vocabularies + Scenario: Reassignment to In-Progress but User Restricted by Manual Config instead of null + When we post to "/archive" + """ + [{ + "type": "text", + "headline": "test headline", + "slugline": "test slugline", + "task": { + "desk": "#desks._id#", + "stage": "#desks.incoming_stage#" + } + }] + """ + When we post to "/planning" + """ + [{ + "item_class": "item class value", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we patch "/planning/#planning._id#" + """ + { + "coverages": [{ + "planning": { + "ednote": "test coverage, I want 250 words", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "user": "#CONTEXT_USER_ID#", + "assignor_user": "#CONTEXT_USER_ID#" + }, + "workflow_status": "active" + }] + } + """ + Then we get OK response + Then we store assignment id in "firstassignment" from coverage 0 + When we post to "assignments/link" + """ + [{ + "assignment_id": "#firstassignment#", + "item_id": "#archive._id#", + "reassign": true + }] + """ + Then we get OK response + When we get "/archive/#archive._id#" + When we get "/assignments/#firstassignment#" + Then we get OK response + Then we get existing resource + """ + { + "_id": "#firstassignment#", + "planning": { + "ednote": "test coverage, I want 250 words", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "user": "#CONTEXT_USER_ID#", + "state": "in_progress", + "assignor_user": "#CONTEXT_USER_ID#" + } + } + """ + When we post to "/desks" with "FINANCE_DESK_ID" and success + """ + [{"name": "Finance", "desk_type": "production" }] + """ + And we set config assignment manual reassignment only to True + And we post to "/archive/#archive._id#/move" + """ + [{"task": {"desk": "#desks._id#", "stage": "#desks.incoming_stage#"}}] + """ + Then we get OK response + When we get "/assignments/#firstassignment#" + Then we get OK response + Then we get existing resource + """ + { + "_id": "#firstassignment#", + "planning": { + "ednote": "test coverage, I want 250 words", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "user": "#CONTEXT_USER_ID#", + "state": "submitted", + "assignor_user": "#CONTEXT_USER_ID#" + } + } + """ + + @auth + @vocabularies + @notification + Scenario: Assignee can not changed as the author of content changes based on Manual config + Given empty "assignments_history" + When we post to "/archive" + """ + [{ + "type": "text", + "headline": "test headline", + "slugline": "test slugline", + "task": { + "desk": "#desks._id#", + "stage": "#desks.incoming_stage#" + } + }] + """ + When we post to "/planning" + """ + [{ + "item_class": "item class value", + "slugline": "test slugline", + "planning_date": "2016-01-02" + }] + """ + Then we get OK response + When we reset notifications + When we patch "/planning/#planning._id#" + """ + { + "coverages": [{ + "planning": { + "ednote": "test coverage, I want 250 words", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "user": "#CONTEXT_USER_ID#" + }, + "workflow_status": "active" + }] + } + """ + Then we get OK response + Then we store coverage id in "firstcoverage" from coverage 0 + Then we store assignment id in "firstassignment" from coverage 0 + And we get notifications + """ + [{ + "event": "assignments:created", + "extra": { + "item": "#firstassignment#", + "coverage": "#firstcoverage#", + "planning": "#planning._id#", + "assignment_state": "assigned", + "assigned_user": "#CONTEXT_USER_ID#", + "assigned_desk": "#desks._id#", + "lock_user": null, + "user": "#CONTEXT_USER_ID#", + "original_assigned_desk": null, + "original_assigned_user": null + } + }] + """ + Then we store assignment id in "firstassignment" from coverage 0 + When we patch "/archive/#archive._id#" + """ + {"headline": "test headline 2"} + """ + Then we get OK response + When we reset notifications + When we post to "assignments/link" + """ + [{ + "assignment_id": "#firstassignment#", + "item_id": "#archive._id#", + "reassign": true + }] + """ + Then we get OK response + And we get notifications + """ + [{ + "event": "assignments:updated", + "extra": { + "item": "#firstassignment#", + "coverage": "#firstcoverage#", + "planning": "#planning._id#", + "assignment_state": "in_progress", + "assigned_user": "#CONTEXT_USER_ID#", + "assigned_desk": "#desks._id#", + "lock_user": null, + "user": "#CONTEXT_USER_ID#", + "original_assigned_desk": "#desks._id#", + "original_assigned_user": "#CONTEXT_USER_ID#" + } + }] + """ + When we get "/archive/#archive._id#" + Then we get existing resource + """ + { + "assignment_id": "#firstassignment#" + } + """ + When we get "/assignments/#firstassignment#" + Then we get OK response + Then we get existing resource + """ + { + "_id": "#firstassignment#", + "planning": { + "ednote": "test coverage, I want 250 words", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "user": "#CONTEXT_USER_ID#", + "state": "in_progress" + } + } + """ + When we set config assignment manual reassignment only to True + Then we store "old_assignee_id" with value "#CONTEXT_USER_ID#" to context + When we switch user + When we patch "/archive/#archive._id#" + """ + {"slugline": "I'm changing the user"} + """ + Then we get OK response + When we get "/assignments/#firstassignment#" + Then we get OK response + Then we get existing resource + """ + { + "_id": "#firstassignment#", + "planning": { + "ednote": "test coverage, I want 250 words", + "slugline": "test slugline" + }, + "assigned_to": { + "desk": "#desks._id#", + "state": "in_progress", + "user": "#old_assignee_id#" + } + } + """ diff --git a/server/features/steps/steps.py b/server/features/steps/steps.py index ee9ac5541..9b745dccf 100644 --- a/server/features/steps/steps.py +++ b/server/features/steps/steps.py @@ -399,6 +399,11 @@ def then_set_auto_workflow(context): context.app.config["PLANNING_AUTO_ASSIGN_TO_WORKFLOW"] = True +@when("we set config assignment manual reassignment only to True") +def then_set_assignment_manual_reassignment_only(context): + context.app.config["ASSIGNMENT_MANUAL_REASSIGNMENT_ONLY"] = True + + @when("we set PLANNING_USE_XMP_FOR_PIC_ASSIGNMENTS") def then_set_use_xmp_for_pic_assignments(context): ABS_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) diff --git a/server/planning/assignments/assignments.py b/server/planning/assignments/assignments.py index f285f6f41..b53df0be9 100644 --- a/server/planning/assignments/assignments.py +++ b/server/planning/assignments/assignments.py @@ -60,6 +60,7 @@ update_assignment_on_link_unlink, get_notify_self_on_assignment, planning_auto_assign_to_workflow, + get_config_assignment_manual_reassignment_only, ) from icalendar import Calendar, Event from flask import request, json, current_app as app @@ -862,10 +863,12 @@ def _get_empty_updates_for_assignment(self, assignment): def _set_user_for_assignment(self, assignment, assignee, assignor=None): updates = self._get_empty_updates_for_assignment(assignment) - updates["assigned_to"]["user"] = assignee - if assignor: - updates["assigned_to"]["assignor_user"] = assignor + if not get_config_assignment_manual_reassignment_only(): + updates["assigned_to"]["user"] = assignee + + if assignor: + updates["assigned_to"]["assignor_user"] = assignor return updates diff --git a/server/planning/assignments/assignments_history.py b/server/planning/assignments/assignments_history.py index 51198a9d5..2f184ccb5 100644 --- a/server/planning/assignments/assignments_history.py +++ b/server/planning/assignments/assignments_history.py @@ -108,30 +108,32 @@ def on_item_updated(self, updates, original, operation=None): self._save_history(item, diff, operation) return - # Split an update to two actions if needed - planning_history_service = get_resource_service("planning_history") - cov_diff = {"coverage_id": original.get("coverage_item"), "assigned_to": {}} - if "priority" in diff.keys(): - cov_diff["assigned_to"]["priority"] = diff.pop("priority") - self._save_history( - item, - {"priority": cov_diff["assigned_to"]["priority"]}, - ASSIGNMENT_HISTORY_ACTIONS.EDIT_PRIORITY, - ) - planning_history_service._save_history( - {"_id": original.get("planning_item")}, - cov_diff, - ASSIGNMENT_HISTORY_ACTIONS.EDIT_PRIORITY, - ) - - if "assigned_to" in diff.keys(): - cov_diff["assigned_to"] = diff["assigned_to"] - self._save_history(item, diff, ASSIGNMENT_HISTORY_ACTIONS.REASSIGNED) - planning_history_service._save_history( - {"_id": original.get("planning_item")}, - cov_diff, - ASSIGNMENT_HISTORY_ACTIONS.REASSIGNED, - ) + if diff: + # Split an update to two actions if needed + planning_history_service = get_resource_service("planning_history") + cov_diff = {"coverage_id": original.get("coverage_item"), "assigned_to": {}} + + if "priority" in diff.keys(): + cov_diff["assigned_to"]["priority"] = diff.pop("priority") + self._save_history( + item, + {"priority": cov_diff["assigned_to"]["priority"]}, + ASSIGNMENT_HISTORY_ACTIONS.EDIT_PRIORITY, + ) + planning_history_service._save_history( + {"_id": original.get("planning_item")}, + cov_diff, + ASSIGNMENT_HISTORY_ACTIONS.EDIT_PRIORITY, + ) + + if "assigned_to" in diff.keys(): + cov_diff["assigned_to"] = diff["assigned_to"] + self._save_history(item, diff, ASSIGNMENT_HISTORY_ACTIONS.REASSIGNED) + planning_history_service._save_history( + {"_id": original.get("planning_item")}, + cov_diff, + ASSIGNMENT_HISTORY_ACTIONS.REASSIGNED, + ) def on_item_deleted(self, doc): planning = {"_id": doc.get("planning_item")} diff --git a/server/planning/common.py b/server/planning/common.py index 717dcbdc0..d98e04793 100644 --- a/server/planning/common.py +++ b/server/planning/common.py @@ -271,6 +271,10 @@ def get_config_planning_duplicate_retain_assignee_details(current_app=None): return (current_app or app).config.get("PLANNING_DUPLICATE_RETAIN_ASSIGNEE_DETAILS", False) +def get_config_assignment_manual_reassignment_only(current_app=None): + return (current_app or app).config.get("ASSIGNMENT_MANUAL_REASSIGNMENT_ONLY", False) + + def get_coverage_status_from_cv(qcode: str): coverage_states = get_resource_service("vocabularies").find_one(req=None, _id="newscoveragestatus") diff --git a/server/settings.py b/server/settings.py index 4c1064807..1f787439a 100644 --- a/server/settings.py +++ b/server/settings.py @@ -191,3 +191,5 @@ def env(variable, fallback_value=None): PLANNING_DEFAULT_COVERAGE_STATUS_ON_INGEST = "ncostat:int" PLANNING_DUPLICATE_RETAIN_ASSIGNEE_DETAILS = False + +ASSIGNMENT_MANUAL_REASSIGNMENT_ONLY = False