Skip to content

Commit 2d33cd5

Browse files
authored
Merge pull request #8162 from 4teamwork/amo/TI-2000/extend_dossier_transactions_with_auth_close_tasks
Force finish dossier pending tasks
2 parents 4f69a9d + 399ddcf commit 2d33cd5

16 files changed

Lines changed: 541 additions & 28 deletions

File tree

changes/TI-2000-2.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Provide the auto_close_tasks parameter on the task transition action when closing a dossier. [amo]

opengever/api/tests/test_dossier_workflow.py

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from datetime import datetime
2+
from ftw.builder import Builder
3+
from ftw.builder import create
24
from ftw.testbrowser import browsing
35
from ftw.testing import freeze
46
from opengever.dossier.resolve import LockingResolveManager
57
from opengever.testing import IntegrationTestCase
68
from plone import api
9+
import json
710

811

912
class TestDossierWorkflowRESTAPITransitions(IntegrationTestCase):
@@ -21,12 +24,14 @@ def assert_state(self, expected_state, obj):
2124
self.assertEquals(expected_state,
2225
api.content.get_state(obj))
2326

24-
def api_transition(self, obj, transition, browser):
27+
def api_transition(self, obj, transition, browser, data=None):
2528
url = '/'.join((obj.absolute_url(), '@workflow', transition))
2629
browser.open(
2730
url,
28-
headers={'Accept': 'application/json'},
29-
method='POST')
31+
headers=self.api_headers,
32+
method='POST',
33+
data=data
34+
)
3035

3136
@browsing
3237
def test_resolve_via_restapi(self, browser):
@@ -199,3 +204,118 @@ def test_archive_offered_via_restapi_is_forbidden(self, browser):
199204
{u'message': u"Invalid transition 'dossier-transition-archive'.\nValid transitions are:\n",
200205
u'type': u'Bad Request'}},
201206
browser.json)
207+
208+
@browsing
209+
def test_resolve_dossier_auto_close_tasks(self, browser):
210+
211+
self.login(self.secretariat_user, browser)
212+
213+
open_task = create(
214+
Builder('task')
215+
.within(self.resolvable_subdossier)
216+
.in_state('task-state-open')
217+
.titled(u'Task 1')
218+
.having(
219+
issuer=self.secretariat_user.id,
220+
responsible=self.secretariat_user.id,
221+
responsible_client='fa',
222+
)
223+
)
224+
in_progress_task = create(
225+
Builder('task')
226+
.within(self.resolvable_subdossier)
227+
.in_state('task-state-in-progress')
228+
.titled(u'Task 2')
229+
.having(
230+
issuer=self.secretariat_user.id,
231+
responsible=self.secretariat_user.id,
232+
responsible_client='fa',
233+
task_type='approval',
234+
)
235+
)
236+
resolved_task = create(
237+
Builder('task')
238+
.within(self.resolvable_subdossier)
239+
.in_state('task-state-resolved')
240+
.titled(u'Task 3')
241+
.having(
242+
issuer=self.secretariat_user.id,
243+
responsible=self.secretariat_user.id,
244+
responsible_client='fa',
245+
)
246+
)
247+
with freeze(datetime(2018, 4, 30)):
248+
with browser.expect_http_error():
249+
self.api_transition(self.resolvable_subdossier, 'dossier-transition-resolve', browser)
250+
self.assertEqual(400, browser.status_code)
251+
self.assertDictEqual(
252+
{
253+
u'error': {
254+
u'message': u'',
255+
u'errors': [u'not all task are closed'],
256+
u'type': u'PreconditionsViolated',
257+
u'has_not_closed_tasks': True,
258+
}
259+
},
260+
browser.json)
261+
262+
# dossier and tasks state did not change
263+
self.assert_state('dossier-state-active', self.resolvable_subdossier)
264+
265+
self.assert_state('task-state-open', open_task)
266+
self.assert_state('task-state-in-progress', in_progress_task)
267+
self.assert_state('task-state-resolved', resolved_task)
268+
269+
with freeze(datetime(2018, 4, 30)):
270+
271+
self.api_transition(
272+
self.resolvable_subdossier,
273+
'dossier-transition-resolve',
274+
browser,
275+
data=json.dumps({"auto_close_tasks": True})
276+
)
277+
278+
self.assertEqual(200, browser.status_code)
279+
280+
# dossier and tasks states were changed
281+
self.assert_state('dossier-state-resolved', self.resolvable_subdossier)
282+
283+
self.assert_state('task-state-cancelled', open_task)
284+
self.assert_state('task-state-tested-and-closed', in_progress_task)
285+
self.assert_state('task-state-tested-and-closed', resolved_task)
286+
287+
@browsing
288+
def test_resolve_dossier_auto_close_tasks_raises_error_if_auto_close_is_not_possible(self, browser):
289+
self.login(self.secretariat_user, browser)
290+
291+
# A private task cannot be closed by the secretarian user.
292+
# The api should raise an error in this case.
293+
create(
294+
Builder('task')
295+
.within(self.resolvable_dossier)
296+
.in_state('task-state-open')
297+
.titled(u'Task 1')
298+
.having(
299+
issuer=self.regular_user.id,
300+
responsible=self.regular_user.id,
301+
responsible_client='fa',
302+
is_private=True,
303+
)
304+
)
305+
306+
with browser.expect_http_error(code=400):
307+
self.api_transition(
308+
self.resolvable_dossier,
309+
'dossier-transition-resolve',
310+
browser,
311+
data=json.dumps({"auto_close_tasks": True})
312+
)
313+
314+
self.assertEqual(
315+
{
316+
u'error': {
317+
u'errors': [u'Auto-close tasks is not possible. Please close the tasks manually.'],
318+
u'type': u'AutoCloseTasksNotPossible'
319+
}
320+
},
321+
browser.json)

opengever/api/transition.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
from opengever.dossier.deactivate import DossierDeactivator
77
from opengever.dossier.reactivate import Reactivator
88
from opengever.dossier.resolve import AlreadyBeingResolved
9+
from opengever.dossier.resolve import AutoCloseTasksNotPossible
910
from opengever.dossier.resolve import InvalidDates
1011
from opengever.dossier.resolve import LockingResolveManager
1112
from opengever.dossier.resolve import MSG_ALREADY_BEING_RESOLVED
13+
from opengever.dossier.resolve import NOT_CLOSED_TASKS
1214
from opengever.dossier.resolve import PreconditionsViolated
1315
from opengever.sign.sign import Signer
1416
from plone import api
@@ -189,6 +191,7 @@ def reply(self):
189191
return dict(error=dict(
190192
type='PreconditionsViolated',
191193
errors=map(self.translate, e.errors),
194+
has_not_closed_tasks=NOT_CLOSED_TASKS in e.errors,
192195
message=self.translate(str(e))))
193196

194197
except InvalidDates as e:
@@ -210,6 +213,12 @@ def reply(self):
210213
type='AlreadyBeingResolved',
211214
message=msg))
212215

216+
except AutoCloseTasksNotPossible as e:
217+
self.request.response.setStatus(400)
218+
return dict(error=dict(
219+
type='AutoCloseTasksNotPossible',
220+
errors=[self.translate(e.message)]))
221+
213222
except BadRequest as e:
214223
self.request.response.setStatus(400)
215224
return dict(error=dict(
@@ -238,14 +247,15 @@ def perform_custom_transition(self):
238247
# For now we also extract these, but we don't do anything with them
239248
# in the case of resolving a dossier.
240249
comment = data.get('comment', '')
250+
auto_close_tasks = data.get('auto_close_tasks', False)
241251
publication_dates = self.parse_publication_dates(data)
242252
args = [self.context], comment, publication_dates
243253

244254
if adapter and data:
245255
data = adapter.deserialize(data)
246256

247257
if self.transition == 'dossier-transition-resolve':
248-
self.resolve_dossier(*args, **data)
258+
self.resolve_dossier(*args, auto_close_tasks=auto_close_tasks, **data)
249259
elif self.transition == 'dossier-transition-activate':
250260
self.activate_dossier(*args)
251261
elif self.transition == 'dossier-transition-deactivate':

opengever/dossier/locales/de/LC_MESSAGES/opengever.dossier.po

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
msgid ""
22
msgstr ""
33
"Project-Id-Version: 1.0\n"
4-
"POT-Creation-Date: 2025-03-24 10:10+0000\n"
4+
"POT-Creation-Date: 2025-06-24 08:51+0000\n"
55
"PO-Revision-Date: 2017-05-23 04:53+0000\n"
66
"Last-Translator: Jacqueline Sposato <jacqueline.sposato@gmail.com>\n"
77
"Language-Team: German <https://translations.onegovgever.ch/projects/onegov-gever/opengever-dossier/de/>\n"
@@ -269,6 +269,11 @@ msgstr "Alle"
269269
msgid "any_role"
270270
msgstr "Alle"
271271

272+
#. Default: "Auto-close tasks is not possible. Please close the tasks manually."
273+
#: ./opengever/dossier/resolve.py
274+
msgid "auto_close_tasks_not_possible"
275+
msgstr "Die Aufgaben konnten nicht automatisch geschlossen werden. Bitte schliessen Sie die Aufgaben manuell."
276+
272277
#. Default: "Add"
273278
#: ./opengever/dossier/browser/forms.py
274279
msgid "button_add"

opengever/dossier/locales/en/LC_MESSAGES/opengever.dossier.po

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
msgid ""
22
msgstr ""
33
"Project-Id-Version: 1.0\n"
4-
"POT-Creation-Date: 2025-03-24 10:10+0000\n"
4+
"POT-Creation-Date: 2025-06-24 08:51+0000\n"
55
"PO-Revision-Date: 2017-05-23 04:53+0000\n"
66
"Last-Translator: Jacqueline Sposato <jacqueline.sposato@gmail.com>\n"
77
"Language-Team: German <https://translations.onegovgever.ch/projects/onegov-gever/opengever-dossier/de/>\n"
@@ -336,6 +336,11 @@ msgstr "Any participant"
336336
msgid "any_role"
337337
msgstr "Any role"
338338

339+
#. Default: "Auto-close tasks is not possible. Please close the tasks manually."
340+
#: ./opengever/dossier/resolve.py
341+
msgid "auto_close_tasks_not_possible"
342+
msgstr "Auto-close tasks is not possible. Please close the tasks manually."
343+
339344
#. German translation: Erstellen
340345
#. Default: "Add"
341346
#: ./opengever/dossier/browser/forms.py

opengever/dossier/locales/fr/LC_MESSAGES/opengever.dossier.po

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
msgid ""
22
msgstr ""
33
"Project-Id-Version: PACKAGE VERSION\n"
4-
"POT-Creation-Date: 2025-03-24 10:10+0000\n"
4+
"POT-Creation-Date: 2025-06-24 08:51+0000\n"
55
"PO-Revision-Date: 2017-12-03 11:16+0000\n"
66
"Last-Translator: Jacqueline Sposato <jacqueline.sposato@gmail.com>\n"
77
"Language-Team: French <https://translations.onegovgever.ch/projects/onegov-gever/opengever-dossier/fr/>\n"
@@ -267,6 +267,11 @@ msgstr "Tous"
267267
msgid "any_role"
268268
msgstr "Tous"
269269

270+
#. Default: "Auto-close tasks is not possible. Please close the tasks manually."
271+
#: ./opengever/dossier/resolve.py
272+
msgid "auto_close_tasks_not_possible"
273+
msgstr "La fermeture automatique des tâches n'est pas possible. Veuillez clôturer les tâches manuellement."
274+
270275
#. Default: "Add"
271276
#: ./opengever/dossier/browser/forms.py
272277
msgid "button_add"

opengever/dossier/locales/opengever.dossier.pot

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
msgid ""
55
msgstr ""
66
"Project-Id-Version: PACKAGE VERSION\n"
7-
"POT-Creation-Date: 2025-03-24 10:10+0000\n"
7+
"POT-Creation-Date: 2025-06-24 08:51+0000\n"
88
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
99
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1010
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -268,6 +268,11 @@ msgstr ""
268268
msgid "any_role"
269269
msgstr ""
270270

271+
#. Default: "Auto-close tasks is not possible. Please close the tasks manually."
272+
#: ./opengever/dossier/resolve.py
273+
msgid "auto_close_tasks_not_possible"
274+
msgstr ""
275+
271276
#. Default: "Add"
272277
#: ./opengever/dossier/browser/forms.py
273278
msgid "button_add"

opengever/dossier/resolve.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from opengever.dossier.statusmessage_mixin import DossierResolutionStatusmessageMixin
2020
from opengever.task.task import ITask
2121
from plone import api
22+
from plone.api.exc import InvalidParameterError
2223
from Products.CMFCore.Expression import createExprContext
2324
from Products.CMFCore.Expression import Expression
2425
from Products.CMFCore.utils import getToolByName
@@ -64,6 +65,11 @@ def __init__(self, invalid_dossier_titles):
6465
self.invalid_dossier_titles = invalid_dossier_titles
6566

6667

68+
class AutoCloseTasksNotPossible(Exception):
69+
"""Could not auto close the tasks. Please do it manually.
70+
"""
71+
72+
6773
def get_resolver(dossier):
6874
"""Return the currently configured dossier-resolver."""
6975

@@ -154,6 +160,11 @@ def resolve(self, **transition_params):
154160
# by acquring a lock, resolving, and then releasing the lock
155161
try:
156162
resolve_lock.acquire(commit=True)
163+
auto_close_tasks = transition_params.get("auto_close_tasks", False)
164+
if auto_close_tasks:
165+
pending_tasks = self._get_pending_tasks()
166+
self._auto_close_tasks(pending_tasks)
167+
157168
result = self.execute_recursive_resolve(**transition_params)
158169

159170
# We need to commit here so that a possible ConflictError already
@@ -187,6 +198,28 @@ def resolve(self, **transition_params):
187198
# this 'finally' block.
188199
resolve_lock.release(commit=True)
189200

201+
def _auto_close_tasks(self, tasks):
202+
"""Auto-close all pending tasks."""
203+
for brain in tasks:
204+
task = brain.getObject()
205+
try:
206+
task.force_finish_task()
207+
except InvalidParameterError:
208+
raise AutoCloseTasksNotPossible(
209+
_('auto_close_tasks_not_possible',
210+
default=u'Auto-close tasks is not possible. Please close the tasks manually.'))
211+
212+
def _get_pending_tasks(self):
213+
"""Get all pending tasks in dossier."""
214+
catalog = api.portal.get_tool('portal_catalog')
215+
path = '/'.join(self.context.getPhysicalPath())
216+
return catalog.searchResults(
217+
path=path,
218+
object_provides=ITask.__identifier__,
219+
is_subtask=False,
220+
review_state=['task-state-open', 'task-state-in-progress', 'task-state-resolved']
221+
)
222+
190223
def execute_recursive_resolve(self, **transition_params):
191224
self.resolver.raise_on_failed_preconditions()
192225
if is_archive_form_needed(self.context):
@@ -301,7 +334,7 @@ def are_enddates_valid(self):
301334
self.enddates_valid = True
302335
return errors
303336

304-
def resolve(self, end_date=None, **kwargs):
337+
def resolve(self, end_date=None, auto_close_tasks=False, **kwargs):
305338
if not self.enddates_valid or not self.preconditions_fulfilled:
306339
raise TypeError
307340

opengever/dossier/tests/test_activate.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ def assert_errors(self, dossier, browser, error_msgs):
138138
{u'error':
139139
{u'message': u'',
140140
u'errors': error_msgs,
141+
u'has_not_closed_tasks': False,
141142
u'type': u'PreconditionsViolated'}},
142143
browser.json)
143144
expected_url = dossier.absolute_url() + \

opengever/dossier/tests/test_archiver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ def test_precondition_violation_raises_error_already_on_resolve_view(self, brows
525525
{u'error': {
526526
u'message': u'',
527527
u'errors': [u'not all task are closed'],
528+
u'has_not_closed_tasks': True,
528529
u'type': u'PreconditionsViolated'}},
529530
browser.json)
530531

0 commit comments

Comments
 (0)