dspace-api-toolkit 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dspace_api/__init__.py +75 -0
- dspace_api/_base.py +407 -0
- dspace_api/_containers.py +217 -0
- dspace_api/_version.py +4 -0
- dspace_api/auth.py +467 -0
- dspace_api/authorizations.py +486 -0
- dspace_api/bundles.py +1193 -0
- dspace_api/collections.py +1069 -0
- dspace_api/communities.py +363 -0
- dspace_api/config.py +254 -0
- dspace_api/discovery.py +920 -0
- dspace_api/entity_types.py +127 -0
- dspace_api/epersons.py +647 -0
- dspace_api/exceptions.py +110 -0
- dspace_api/groups.py +599 -0
- dspace_api/items.py +549 -0
- dspace_api/metadata.py +742 -0
- dspace_api/permissions.py +1052 -0
- dspace_api/projection.py +292 -0
- dspace_api/relationships.py +694 -0
- dspace_api/session.py +538 -0
- dspace_api/site.py +98 -0
- dspace_api/utils.py +519 -0
- dspace_api/versions.py +292 -0
- dspace_api/vocabulary.py +365 -0
- dspace_api/workflow.py +893 -0
- dspace_api/workspaces.py +1404 -0
- dspace_api_toolkit-0.1.1.dist-info/METADATA +211 -0
- dspace_api_toolkit-0.1.1.dist-info/RECORD +32 -0
- dspace_api_toolkit-0.1.1.dist-info/WHEEL +5 -0
- dspace_api_toolkit-0.1.1.dist-info/licenses/LICENSE +28 -0
- dspace_api_toolkit-0.1.1.dist-info/top_level.txt +1 -0
dspace_api/workflow.py
ADDED
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Review process (configurable workflow) for DSpace API (DSpace 7+ / DSpace-CRIS)
|
|
4
|
+
|
|
5
|
+
A deposit into a collection with workflow groups becomes a workflow item.
|
|
6
|
+
Each step (reviewstep, editstep, finaleditstep by default) offers a *pool
|
|
7
|
+
task* to the members of the step's group; a member *claims* it (claimed
|
|
8
|
+
task) and performs an option: approve, reject (back to the submitter with a
|
|
9
|
+
reason), edit metadata, ... Supervision orders give a group access to an
|
|
10
|
+
in-progress submission.
|
|
11
|
+
|
|
12
|
+
GET|DELETE /workflow/workflowitems[/{id}] DELETE: back to workspace, ?expunge=true: delete
|
|
13
|
+
GET /workflow/workflowitems/{id}/item|collection|submissionDefinition|step
|
|
14
|
+
GET /workflow/workflowitems/search/findBySubmitter?uuid= /search/item?uuid=
|
|
15
|
+
PATCH /workflow/workflowitems/{id} section PATCH (edit_metadata option)
|
|
16
|
+
GET /workflow/pooltasks/{id}[/workflowitem|eperson|group|step|action]
|
|
17
|
+
GET /workflow/pooltasks/search/findByUser|findAllByItem|findByItem?uuid=
|
|
18
|
+
POST /workflow/claimedtasks text/uri-list: claim a pool task
|
|
19
|
+
GET /workflow/claimedtasks/{id}[/workflowitem|owner|step|action]
|
|
20
|
+
GET /workflow/claimedtasks/search/findByUser|findAllByItem|findByItem?uuid=
|
|
21
|
+
POST /workflow/claimedtasks/{id} form data: submit_approve, submit_reject&reason, ...
|
|
22
|
+
DELETE /workflow/claimedtasks/{id} unclaim (back to the pool)
|
|
23
|
+
GET /config/workflowdefinitions[/{name}[/collections|/steps]] /search/findByCollection?uuid=
|
|
24
|
+
GET /config/workflowsteps/{name} /config/workflowactions/{name}
|
|
25
|
+
GET|POST|DELETE /core/supervisionorders[/{id}] /search/byItem?uuid=
|
|
26
|
+
|
|
27
|
+
wf = client.workflow
|
|
28
|
+
for task in wf.pending_tasks(): # what is waiting for me
|
|
29
|
+
print(task["kind"], task["title"], task["step"], task["options"])
|
|
30
|
+
wf.review(item_uuid, "approve") # claim if needed, then approve
|
|
31
|
+
wf.review(item_uuid, "reject", reason="Please add an abstract")
|
|
32
|
+
wf.approve_all_steps(item_uuid) # walk through every step (bulk approval)
|
|
33
|
+
|
|
34
|
+
Claiming needs membership in the step's group (collection role
|
|
35
|
+
workflowGroups/<role>); being an administrator alone is not enough.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import logging
|
|
39
|
+
from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, Union
|
|
40
|
+
|
|
41
|
+
from ._base import BaseManager
|
|
42
|
+
from .exceptions import DSpaceError
|
|
43
|
+
from .metadata import MetadataPatch, as_operations
|
|
44
|
+
from .projection import EmbedArg
|
|
45
|
+
|
|
46
|
+
# Logger for the module
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
# Workflow API endpoints and related constants
|
|
50
|
+
WORKFLOW_ITEMS = "/workflow/workflowitems"
|
|
51
|
+
POOL_TASKS = "/workflow/pooltasks"
|
|
52
|
+
CLAIMED_TASKS = "/workflow/claimedtasks"
|
|
53
|
+
DEFINITIONS = "/config/workflowdefinitions"
|
|
54
|
+
STEPS = "/config/workflowsteps"
|
|
55
|
+
ACTIONS = "/config/workflowactions"
|
|
56
|
+
SUPERVISION_ORDERS = "/core/supervisionorders"
|
|
57
|
+
|
|
58
|
+
# Links and embed constants for workflow items and tasks
|
|
59
|
+
POOL_TASK_LINKS = ("workflowitem", "eperson", "group", "step", "action")
|
|
60
|
+
CLAIMED_TASK_LINKS = ("workflowitem", "owner", "step", "action")
|
|
61
|
+
WORKFLOW_ITEM_LINKS = ("item", "collection", "submissionDefinition", "step")
|
|
62
|
+
SUPERVISION_TYPES = ("NONE", "EDITOR", "OBSERVER")
|
|
63
|
+
# tasks come with their workflow item and its item (title, uuid) in one request
|
|
64
|
+
TASK_EMBED = ["workflowitem", "workflowitem/item"]
|
|
65
|
+
|
|
66
|
+
# Type alias for references to workflow items, tasks, or other objects
|
|
67
|
+
Ref = Union[str, int, Dict[str, Any]]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class WorkflowError(DSpaceError):
|
|
71
|
+
"""A review action could not be performed (no task, not authorized, validation)."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def submit_param(option: str) -> str:
|
|
75
|
+
"""Option name -> request parameter: "approve" -> "submit_approve" (already prefixed names are kept).
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
option: The option name to convert to a request parameter
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
str: The corresponding request parameter name
|
|
82
|
+
"""
|
|
83
|
+
return option if option.startswith("submit_") else f"submit_{option}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class WorkflowManager(BaseManager):
|
|
87
|
+
"""Workflow items, pool / claimed tasks, workflow configuration and supervision orders"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, auth):
|
|
90
|
+
"""Initialize the workflow manager with the given authentication context.
|
|
91
|
+
|
|
92
|
+
Parameters:
|
|
93
|
+
auth: The authentication context to use for API requests
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
super().__init__(auth)
|
|
97
|
+
self._epersons = None
|
|
98
|
+
self._action_cache: Dict[str, Dict[str, Any]] = {}
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def epersons(self):
|
|
102
|
+
"""Get the EPerson manager, initializing it if necessary.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
EPersonManager: The EPerson manager instance
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
if self._epersons is None:
|
|
109
|
+
from .epersons import EPersonManager
|
|
110
|
+
self._epersons = EPersonManager(self.auth)
|
|
111
|
+
return self._epersons
|
|
112
|
+
|
|
113
|
+
def _id_of(self, ref: Ref) -> str:
|
|
114
|
+
"""Get the ID of the given reference, which can be a string, integer, or dictionary containing an 'id' or 'uuid'.
|
|
115
|
+
|
|
116
|
+
Parameters:
|
|
117
|
+
ref: The reference to get the ID from
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
str: The ID of the object
|
|
121
|
+
|
|
122
|
+
"""
|
|
123
|
+
if isinstance(ref, dict):
|
|
124
|
+
ident = ref.get("id", ref.get("uuid"))
|
|
125
|
+
if ident is None:
|
|
126
|
+
raise ValueError(f"Object has no id: {ref!r}")
|
|
127
|
+
return str(ident)
|
|
128
|
+
return str(ref)
|
|
129
|
+
|
|
130
|
+
def _uuid(self, ref: Ref, what: str = "object") -> str:
|
|
131
|
+
"""Get the UUID of the given reference, which can be a dictionary containing a 'uuid' or an integer (not allowed).
|
|
132
|
+
|
|
133
|
+
Parameters:
|
|
134
|
+
ref: The reference to get the UUID from
|
|
135
|
+
what: A description of the object type for error messages
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
str: The UUID of the object
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
uuid = self._uuid_of(ref) if not isinstance(ref, int) else None
|
|
142
|
+
if not uuid:
|
|
143
|
+
raise ValueError(f"Not a {what} UUID or dict: {ref!r}")
|
|
144
|
+
return uuid
|
|
145
|
+
|
|
146
|
+
def _user_uuid(self, eperson: Optional[Ref]) -> str:
|
|
147
|
+
"""Get the UUID of the given EPerson reference, or the current user if None is provided.
|
|
148
|
+
|
|
149
|
+
Parameters:
|
|
150
|
+
eperson: The EPerson reference, or None to use the current user
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
str: The UUID of the EPerson
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
if eperson is not None:
|
|
157
|
+
return self._uuid(eperson, "eperson")
|
|
158
|
+
me = self.epersons.get_current_user()
|
|
159
|
+
if not me:
|
|
160
|
+
raise WorkflowError("Not logged in - pass an eperson")
|
|
161
|
+
return me["uuid"]
|
|
162
|
+
|
|
163
|
+
# ==================================================================
|
|
164
|
+
# Workflow items
|
|
165
|
+
# ==================================================================
|
|
166
|
+
|
|
167
|
+
def iter_workflow_items(self, page_size: Optional[int] = None, embed: EmbedArg = None) -> Iterator[Dict[str, Any]]:
|
|
168
|
+
"""All workflow items (administrators). A generator.
|
|
169
|
+
|
|
170
|
+
Parameters:
|
|
171
|
+
page_size: The number of items per page for pagination
|
|
172
|
+
embed: The embed parameter to include related entities in the response
|
|
173
|
+
|
|
174
|
+
Yields:
|
|
175
|
+
Dict[str, Any]: A workflow item dictionary
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
return self._iter_embedded(self._url(WORKFLOW_ITEMS), "workflowitems", "List workflow items",
|
|
179
|
+
page_size=page_size, embed=embed)
|
|
180
|
+
|
|
181
|
+
def get_workflow_item(self, workflow_item: Ref, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
182
|
+
"""Retrieve a workflow item by its reference.
|
|
183
|
+
|
|
184
|
+
Parameters:
|
|
185
|
+
workflow_item: The reference to the workflow item
|
|
186
|
+
embed: The embed parameter to include related entities in the response
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Dict[str, Any]: A workflow item dictionary
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
ident = self._id_of(workflow_item)
|
|
194
|
+
return self._get_json(self._url(WORKFLOW_ITEMS, f"/{ident}"), f"Get workflow item {ident}", embed=embed)
|
|
195
|
+
|
|
196
|
+
def get_workflow_item_link(self, workflow_item: Ref, link: str, all_languages: Optional[bool] = None
|
|
197
|
+
) -> Optional[Dict[str, Any]]:
|
|
198
|
+
"""Linked entity: "item", "collection", "submissionDefinition" or "step".
|
|
199
|
+
|
|
200
|
+
Parameters:
|
|
201
|
+
workflow_item: The reference to the workflow item
|
|
202
|
+
link: The linked entity to retrieve ("item", "collection", "submissionDefinition" or "step")
|
|
203
|
+
all_languages: Whether to include all language variants
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Optional[Dict[str, Any]]: The linked entity dictionary, or None if not found
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
if link not in WORKFLOW_ITEM_LINKS:
|
|
210
|
+
raise ValueError(f"link must be one of {WORKFLOW_ITEM_LINKS}")
|
|
211
|
+
ident = self._id_of(workflow_item)
|
|
212
|
+
return self._get_optional(self._url(WORKFLOW_ITEMS, f"/{ident}/{link}"),
|
|
213
|
+
f"Get {link} of workflow item {ident}", all_languages=all_languages)
|
|
214
|
+
|
|
215
|
+
def find_by_submitter(self, eperson: Ref, embed: EmbedArg = None) -> List[Dict[str, Any]]:
|
|
216
|
+
"""Find workflow items by the submitter.
|
|
217
|
+
|
|
218
|
+
Parameters:
|
|
219
|
+
eperson: The reference to the submitter (EPerson)
|
|
220
|
+
embed: The embed parameter to include related entities in the response
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List[Dict[str, Any]]: A list of workflow item dictionaries
|
|
224
|
+
|
|
225
|
+
"""
|
|
226
|
+
uuid = self._uuid(eperson, "eperson")
|
|
227
|
+
return list(self._iter_embedded(self._url(WORKFLOW_ITEMS, "/search/findBySubmitter"), "workflowitems",
|
|
228
|
+
f"Workflow items of submitter {uuid}", [("uuid", uuid)], embed=embed))
|
|
229
|
+
|
|
230
|
+
def find_by_item(self, item: Ref, embed: EmbedArg = None) -> Optional[Dict[str, Any]]:
|
|
231
|
+
"""The workflow item of an item, or None (not in workflow).
|
|
232
|
+
|
|
233
|
+
Parameters:
|
|
234
|
+
item: The reference to the item
|
|
235
|
+
embed: The embed parameter to include related entities in the response
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Optional[Dict[str, Any]]: The workflow item dictionary, or None if not found
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
uuid = self._uuid(item, "item")
|
|
242
|
+
return self._get_optional(self._url(WORKFLOW_ITEMS, "/search/item"), f"Find workflow item of {uuid}",
|
|
243
|
+
params={"uuid": uuid}, embed=embed)
|
|
244
|
+
|
|
245
|
+
def return_to_workspace(self, workflow_item: Ref) -> bool:
|
|
246
|
+
"""Stop the workflow and send the submission back to the submitter's workspace (administrators)."""
|
|
247
|
+
ident = self._id_of(workflow_item)
|
|
248
|
+
self._request("DELETE", self._url(WORKFLOW_ITEMS, f"/{ident}"), f"Return workflow item {ident} to workspace",
|
|
249
|
+
expected=(204,))
|
|
250
|
+
logger.info("Workflow item %s returned to the workspace", ident)
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
def delete_workflow_item(self, workflow_item: Ref) -> bool:
|
|
254
|
+
"""Delete the submission while in workflow (?expunge=true, administrators).
|
|
255
|
+
|
|
256
|
+
Parameters:
|
|
257
|
+
workflow_item: The reference to the workflow item to delete
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
bool: True if the workflow item was successfully deleted
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
ident = self._id_of(workflow_item)
|
|
264
|
+
self._request("DELETE", self._url(WORKFLOW_ITEMS, f"/{ident}"), f"Delete workflow item {ident}",
|
|
265
|
+
expected=(204,), params={"expunge": "true"})
|
|
266
|
+
logger.info("Workflow item %s deleted", ident)
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
def patch_workflow_item(self, workflow_item: Ref,
|
|
270
|
+
operations: Union[MetadataPatch, Iterable[Dict[str, Any]]]) -> Dict[str, Any]:
|
|
271
|
+
"""
|
|
272
|
+
PATCH the sections of a workflow item (needs a claimed task whose
|
|
273
|
+
action offers "edit_metadata"). Same operations as for workspace items.
|
|
274
|
+
|
|
275
|
+
Parameters:
|
|
276
|
+
workflow_item: The reference to the workflow item to patch
|
|
277
|
+
operations: The metadata patch operations to apply
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Dict[str, Any]: The updated workflow item dictionary
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
ops = as_operations(operations)
|
|
284
|
+
if not ops:
|
|
285
|
+
raise ValueError("Empty patch")
|
|
286
|
+
ident = self._id_of(workflow_item)
|
|
287
|
+
return self._request("PATCH", self._url(WORKFLOW_ITEMS, f"/{ident}"), f"Patch workflow item {ident}",
|
|
288
|
+
headers=self._headers("application/json-patch+json"), json=ops).json()
|
|
289
|
+
|
|
290
|
+
def patch_section_metadata(self, workflow_item: Ref, section: str,
|
|
291
|
+
metadata: Union[MetadataPatch, Dict[str, Any]]) -> Dict[str, Any]:
|
|
292
|
+
"""Patch the metadata of a specific section of a workflow item.
|
|
293
|
+
|
|
294
|
+
The metadata can be provided as a dictionary of {field: values} (each replaces the field) or a MetadataPatch.for_section(section).
|
|
295
|
+
|
|
296
|
+
Parameters:
|
|
297
|
+
workflow_item: The reference to the workflow item to patch
|
|
298
|
+
section: The section of the workflow item to patch
|
|
299
|
+
metadata: The metadata patch or dictionary of field values
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Dict[str, Any]: The updated workflow item dictionary
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
if isinstance(metadata, dict):
|
|
306
|
+
patch = MetadataPatch.for_section(section)
|
|
307
|
+
for field, values in metadata.items():
|
|
308
|
+
patch.set(field, values)
|
|
309
|
+
metadata = patch
|
|
310
|
+
return self.patch_workflow_item(workflow_item, metadata)
|
|
311
|
+
|
|
312
|
+
# ==================================================================
|
|
313
|
+
# Tasks
|
|
314
|
+
# ==================================================================
|
|
315
|
+
|
|
316
|
+
def _task_search(self, base: str, key: str, method: str, uuid: str) -> List[Dict[str, Any]]:
|
|
317
|
+
"""Search for tasks based on a specific method and UUID.
|
|
318
|
+
|
|
319
|
+
Parameters:
|
|
320
|
+
base: The base URL for the task type
|
|
321
|
+
key: The key to identify the tasks in the response
|
|
322
|
+
method: The search method to use
|
|
323
|
+
uuid: The UUID to search for
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
List[Dict[str, Any]]: A list of task dictionaries
|
|
327
|
+
|
|
328
|
+
"""
|
|
329
|
+
return list(self._iter_embedded(self._url(base, f"/search/{method}"), key, f"{key} {method} {uuid}",
|
|
330
|
+
[("uuid", uuid)], embed=TASK_EMBED))
|
|
331
|
+
|
|
332
|
+
def _task_link(self, base: str, links: Sequence[str], task: Ref, link: str) -> Optional[Dict[str, Any]]:
|
|
333
|
+
"""Retrieve a linked entity of a task.
|
|
334
|
+
|
|
335
|
+
Parameters:
|
|
336
|
+
base: The base URL for the task type
|
|
337
|
+
links: The valid links for the task type
|
|
338
|
+
task: The reference to the task
|
|
339
|
+
link: The linked entity to retrieve
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Optional[Dict[str, Any]]: The linked entity dictionary, or None if not found
|
|
343
|
+
|
|
344
|
+
"""
|
|
345
|
+
if link not in links:
|
|
346
|
+
raise ValueError(f"link must be one of {tuple(links)}")
|
|
347
|
+
ident = self._id_of(task)
|
|
348
|
+
return self._get_optional(self._url(base, f"/{ident}/{link}"), f"Get {link} of task {ident}")
|
|
349
|
+
|
|
350
|
+
# -- pool tasks --------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
def get_pool_task(self, task: Ref, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
353
|
+
"""Retrieve a pool task by its reference.
|
|
354
|
+
|
|
355
|
+
Parameters:
|
|
356
|
+
task: The reference to the pool task
|
|
357
|
+
embed: Optional embedding instructions for the response
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Dict[str, Any]: The pool task dictionary
|
|
361
|
+
|
|
362
|
+
"""
|
|
363
|
+
ident = self._id_of(task)
|
|
364
|
+
return self._get_json(self._url(POOL_TASKS, f"/{ident}"), f"Get pool task {ident}", embed=embed)
|
|
365
|
+
|
|
366
|
+
def get_pool_task_link(self, task: Ref, link: str) -> Optional[Dict[str, Any]]:
|
|
367
|
+
"""Retrieve a linked entity of a pool task.
|
|
368
|
+
|
|
369
|
+
The link can be one of "workflowitem", "eperson", "group", "step" or "action".
|
|
370
|
+
|
|
371
|
+
Parameters:
|
|
372
|
+
task: The reference to the pool task
|
|
373
|
+
link: The linked entity to retrieve
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Optional[Dict[str, Any]]: The linked entity dictionary, or None if not found
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
return self._task_link(POOL_TASKS, POOL_TASK_LINKS, task, link)
|
|
380
|
+
|
|
381
|
+
def pool_tasks_for_user(self, eperson: Optional[Ref] = None) -> List[Dict[str, Any]]:
|
|
382
|
+
"""Tasks the user can claim (default: the current user).
|
|
383
|
+
|
|
384
|
+
Parameters:
|
|
385
|
+
eperson: Optional reference to the user (default: the current user)
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
List[Dict[str, Any]]: A list of pool task dictionaries
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
return self._task_search(POOL_TASKS, "pooltasks", "findByUser", self._user_uuid(eperson))
|
|
392
|
+
|
|
393
|
+
def pool_tasks_for_item(self, item: Ref) -> List[Dict[str, Any]]:
|
|
394
|
+
"""All pool tasks of an item, for any user (administrators).
|
|
395
|
+
|
|
396
|
+
Parameters:
|
|
397
|
+
item: The reference to the item
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
List[Dict[str, Any]]: A list of pool task dictionaries
|
|
401
|
+
|
|
402
|
+
"""
|
|
403
|
+
return self._task_search(POOL_TASKS, "pooltasks", "findAllByItem", self._uuid(item, "item"))
|
|
404
|
+
|
|
405
|
+
def find_pool_task(self, item: Ref) -> Optional[Dict[str, Any]]:
|
|
406
|
+
"""The pool task of an item the current user may claim, or None.
|
|
407
|
+
|
|
408
|
+
Parameters:
|
|
409
|
+
item: The reference to the item
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Optional[Dict[str, Any]]: The pool task dictionary, or None if not found
|
|
413
|
+
|
|
414
|
+
"""
|
|
415
|
+
uuid = self._uuid(item, "item")
|
|
416
|
+
return self._get_optional(self._url(POOL_TASKS, "/search/findByItem"), f"Find pool task of {uuid}",
|
|
417
|
+
params={"uuid": uuid}, embed=TASK_EMBED)
|
|
418
|
+
|
|
419
|
+
def claim(self, pool_task: Ref) -> Dict[str, Any]:
|
|
420
|
+
"""Claim a pool task.
|
|
421
|
+
|
|
422
|
+
The pool task disappears and is replaced by a claimed task.
|
|
423
|
+
|
|
424
|
+
Parameters:
|
|
425
|
+
pool_task: The reference to the pool task to claim
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Dict[str, Any]: The claimed task dictionary
|
|
429
|
+
|
|
430
|
+
"""
|
|
431
|
+
ident = self._id_of(pool_task)
|
|
432
|
+
task = self._request("POST", self._url(CLAIMED_TASKS), f"Claim pool task {ident}", expected=(200, 201),
|
|
433
|
+
headers=self._headers("text/uri-list"), data=self._url(POOL_TASKS, f"/{ident}")).json()
|
|
434
|
+
logger.info("Claimed pool task %s -> claimed task %s", ident, task.get("id"))
|
|
435
|
+
return task
|
|
436
|
+
|
|
437
|
+
# -- claimed tasks -------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
def get_claimed_task(self, task: Ref, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
440
|
+
"""Retrieve a claimed task by its reference.
|
|
441
|
+
|
|
442
|
+
Parameters:
|
|
443
|
+
task: The reference to the claimed task
|
|
444
|
+
embed: Optional embedding instructions for the response
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Dict[str, Any]: The claimed task dictionary
|
|
448
|
+
|
|
449
|
+
"""
|
|
450
|
+
ident = self._id_of(task)
|
|
451
|
+
return self._get_json(self._url(CLAIMED_TASKS, f"/{ident}"), f"Get claimed task {ident}", embed=embed)
|
|
452
|
+
|
|
453
|
+
def get_claimed_task_link(self, task: Ref, link: str) -> Optional[Dict[str, Any]]:
|
|
454
|
+
"""Retrieve a linked entity of a claimed task.
|
|
455
|
+
|
|
456
|
+
Parameters:
|
|
457
|
+
task: The reference to the claimed task
|
|
458
|
+
link: The linked entity to retrieve ("workflowitem", "owner", "step" or "action")
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
Optional[Dict[str, Any]]: The linked entity dictionary, or None if not found
|
|
462
|
+
|
|
463
|
+
"""
|
|
464
|
+
return self._task_link(CLAIMED_TASKS, CLAIMED_TASK_LINKS, task, link)
|
|
465
|
+
|
|
466
|
+
def claimed_tasks_for_user(self, eperson: Optional[Ref] = None) -> List[Dict[str, Any]]:
|
|
467
|
+
"""Tasks the user has claimed (default: the current user).
|
|
468
|
+
|
|
469
|
+
Parameters:
|
|
470
|
+
eperson: Optional reference to the user (default: the current user)
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
List[Dict[str, Any]]: A list of claimed task dictionaries
|
|
474
|
+
|
|
475
|
+
"""
|
|
476
|
+
return self._task_search(CLAIMED_TASKS, "claimedtasks", "findByUser", self._user_uuid(eperson))
|
|
477
|
+
|
|
478
|
+
def claimed_tasks_for_item(self, item: Ref) -> List[Dict[str, Any]]:
|
|
479
|
+
"""All claimed tasks of an item, for any user (administrators).
|
|
480
|
+
|
|
481
|
+
Parameters:
|
|
482
|
+
item: The reference to the item
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
List[Dict[str, Any]]: A list of claimed task dictionaries
|
|
486
|
+
|
|
487
|
+
"""
|
|
488
|
+
return self._task_search(CLAIMED_TASKS, "claimedtasks", "findAllByItem", self._uuid(item, "item"))
|
|
489
|
+
|
|
490
|
+
def find_claimed_task(self, item: Ref) -> Optional[Dict[str, Any]]:
|
|
491
|
+
"""The current user's claimed task of an item, or None.
|
|
492
|
+
|
|
493
|
+
Parameters:
|
|
494
|
+
item: The reference to the item
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Optional[Dict[str, Any]]: The claimed task dictionary, or None if not found
|
|
498
|
+
|
|
499
|
+
"""
|
|
500
|
+
uuid = self._uuid(item, "item")
|
|
501
|
+
return self._get_optional(self._url(CLAIMED_TASKS, "/search/findByItem"), f"Find claimed task of {uuid}",
|
|
502
|
+
params={"uuid": uuid}, embed=TASK_EMBED)
|
|
503
|
+
|
|
504
|
+
def task_options(self, task: Ref) -> List[str]:
|
|
505
|
+
"""Options of the task's current action, e.g. ["approve", "reject", "edit_metadata"].
|
|
506
|
+
|
|
507
|
+
Parameters:
|
|
508
|
+
task: The reference to the task
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
List[str]: A list of available options for the task's current action
|
|
512
|
+
|
|
513
|
+
"""
|
|
514
|
+
if isinstance(task, dict) and task.get("action"):
|
|
515
|
+
return list(self.get_workflow_action(task["action"]).get("options") or [])
|
|
516
|
+
getter = self.get_claimed_task_link if (isinstance(task, dict) and task.get("type") == "claimedtask") \
|
|
517
|
+
or not isinstance(task, dict) else self.get_pool_task_link
|
|
518
|
+
action = getter(task, "action") or {}
|
|
519
|
+
return list(action.get("options") or [])
|
|
520
|
+
|
|
521
|
+
def perform(self, claimed_task: Ref, option: str, **params: Any) -> bool:
|
|
522
|
+
"""
|
|
523
|
+
Perform an option of a claimed task: perform(task, "approve"),
|
|
524
|
+
perform(task, "reject", reason="..."). Extra keyword arguments become
|
|
525
|
+
form parameters; list values are repeated (eperson=[a, b]).
|
|
526
|
+
|
|
527
|
+
Parameters:
|
|
528
|
+
claimed_task: The reference to the claimed task
|
|
529
|
+
option: The option to perform
|
|
530
|
+
**params: Extra form parameters for the action
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
bool: True if the action was successfully performed
|
|
534
|
+
"""
|
|
535
|
+
ident = self._id_of(claimed_task)
|
|
536
|
+
form: List[Tuple[str, Any]] = [(submit_param(option), "true")]
|
|
537
|
+
for key, value in params.items():
|
|
538
|
+
if value is None:
|
|
539
|
+
continue
|
|
540
|
+
for v in (value if isinstance(value, (list, tuple)) else [value]):
|
|
541
|
+
form.append((key, v))
|
|
542
|
+
try:
|
|
543
|
+
self._request("POST", self._url(CLAIMED_TASKS, f"/{ident}"), f"Perform {option} on task {ident}",
|
|
544
|
+
expected=(200, 201, 204), headers=self._headers("application/x-www-form-urlencoded"),
|
|
545
|
+
data=form)
|
|
546
|
+
except DSpaceError as exc:
|
|
547
|
+
if getattr(exc, "status_code", None) in (403, 404, 422):
|
|
548
|
+
raise WorkflowError(f"Could not {option} claimed task {ident}: HTTP {exc.status_code} "
|
|
549
|
+
f"{(exc.response_text or '')[:300]}", status_code=exc.status_code,
|
|
550
|
+
response_text=exc.response_text) from exc
|
|
551
|
+
raise
|
|
552
|
+
logger.info("Performed %s on claimed task %s", option, ident)
|
|
553
|
+
return True
|
|
554
|
+
|
|
555
|
+
def approve(self, claimed_task: Ref) -> bool:
|
|
556
|
+
"""
|
|
557
|
+
Approve a claimed task.
|
|
558
|
+
|
|
559
|
+
Parameters:
|
|
560
|
+
claimed_task: The reference to the claimed task
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
bool: True if the action was successfully performed
|
|
564
|
+
"""
|
|
565
|
+
return self.perform(claimed_task, "approve")
|
|
566
|
+
|
|
567
|
+
def reject(self, claimed_task: Ref, reason: str) -> bool:
|
|
568
|
+
"""
|
|
569
|
+
Send the submission back to the submitter's workspace with a reason (required).
|
|
570
|
+
|
|
571
|
+
Parameters:
|
|
572
|
+
claimed_task: The reference to the claimed task
|
|
573
|
+
reason: The reason for rejection
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
bool: True if the action was successfully performed
|
|
577
|
+
"""
|
|
578
|
+
if not reason or not reason.strip():
|
|
579
|
+
raise ValueError("A rejection needs a reason")
|
|
580
|
+
return self.perform(claimed_task, "reject", reason=reason)
|
|
581
|
+
|
|
582
|
+
def select_reviewers(self, claimed_task: Ref, epersons: Sequence[Ref]) -> bool:
|
|
583
|
+
"""
|
|
584
|
+
selectrevieweraction: choose who performs the next step (at least one eperson).
|
|
585
|
+
Sends submit_select_reviewer with eperson=<uuid> values (current DSpace);
|
|
586
|
+
for a single reviewer also the contract's submit_select_reviewer_<uuid>=true
|
|
587
|
+
form, which older versions read.
|
|
588
|
+
|
|
589
|
+
Parameters:
|
|
590
|
+
claimed_task: The reference to the claimed task
|
|
591
|
+
epersons: A sequence of references to the reviewers to select
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
bool: True if the action was successfully performed
|
|
595
|
+
"""
|
|
596
|
+
uuids = [self._uuid(e, "eperson") for e in epersons]
|
|
597
|
+
if not uuids:
|
|
598
|
+
raise ValueError("At least one reviewer is required")
|
|
599
|
+
extra = {f"submit_select_reviewer_{uuids[0]}": "true"} if len(uuids) == 1 else {}
|
|
600
|
+
return self.perform(claimed_task, "select_reviewer", eperson=uuids, **extra)
|
|
601
|
+
|
|
602
|
+
def score(self, claimed_task: Ref, score: int, review: Optional[str] = None) -> bool:
|
|
603
|
+
"""
|
|
604
|
+
scorereviewaction: record a score (and a review text, required if the
|
|
605
|
+
action is configured with descriptionRequired). The contract names the
|
|
606
|
+
text "description"; DSpace's ScoreReviewAction reads "review" - both are sent.
|
|
607
|
+
|
|
608
|
+
Parameters:
|
|
609
|
+
claimed_task: The reference to the claimed task
|
|
610
|
+
score: The score to record
|
|
611
|
+
review: Optional review text
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
bool: True if the action was successfully performed
|
|
615
|
+
"""
|
|
616
|
+
return self.perform(claimed_task, "score", score=int(score), review=review, description=review)
|
|
617
|
+
|
|
618
|
+
def unclaim(self, claimed_task: Ref) -> bool:
|
|
619
|
+
"""Give a claimed task back to the pool.
|
|
620
|
+
|
|
621
|
+
Parameters:
|
|
622
|
+
claimed_task: The reference to the claimed task
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
bool: True if the action was successfully performed
|
|
626
|
+
"""
|
|
627
|
+
ident = self._id_of(claimed_task)
|
|
628
|
+
self._request("DELETE", self._url(CLAIMED_TASKS, f"/{ident}"), f"Unclaim task {ident}", expected=(204,))
|
|
629
|
+
logger.info("Claimed task %s returned to the pool", ident)
|
|
630
|
+
return True
|
|
631
|
+
|
|
632
|
+
# ==================================================================
|
|
633
|
+
# Higher-level flows
|
|
634
|
+
# ==================================================================
|
|
635
|
+
|
|
636
|
+
def claim_for_item(self, item: Ref) -> Dict[str, Any]:
|
|
637
|
+
"""The current user's claimed task of the item, claiming the pool task if needed.
|
|
638
|
+
|
|
639
|
+
Parameters:
|
|
640
|
+
item: The reference to the item
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Dict[str, Any]: The claimed task for the item
|
|
644
|
+
"""
|
|
645
|
+
claimed = self.find_claimed_task(item)
|
|
646
|
+
if claimed:
|
|
647
|
+
return claimed
|
|
648
|
+
pool_task = self.find_pool_task(item)
|
|
649
|
+
if not pool_task:
|
|
650
|
+
raise WorkflowError(f"No task for item {self._uuid(item, 'item')} that the current user can claim "
|
|
651
|
+
"(not in workflow, or not a member of the step's group)")
|
|
652
|
+
return self.claim(pool_task)
|
|
653
|
+
|
|
654
|
+
def review(self, item: Ref, decision: str, reason: Optional[str] = None, **params: Any) -> Dict[str, Any]:
|
|
655
|
+
"""
|
|
656
|
+
Claim (if needed) and decide on the item's current step:
|
|
657
|
+
decision "approve", "reject" (reason required) or any option of the
|
|
658
|
+
step's action. Returns {"task", "decision", "workflow_item"} -
|
|
659
|
+
workflow_item is the item's workflow item afterwards (None once archived
|
|
660
|
+
or returned to the submitter).
|
|
661
|
+
|
|
662
|
+
Parameters:
|
|
663
|
+
item: The reference to the item
|
|
664
|
+
decision: The decision to make ("approve", "reject", or any step action)
|
|
665
|
+
reason: Optional reason for the decision (required for "reject")
|
|
666
|
+
**params: Extra form parameters for the action
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
Dict[str, Any]: The result of the review, including "task", "decision", and "workflow_item"
|
|
670
|
+
"""
|
|
671
|
+
task = self.claim_for_item(item)
|
|
672
|
+
if decision == "reject":
|
|
673
|
+
self.reject(task, reason or "")
|
|
674
|
+
else:
|
|
675
|
+
self.perform(task, decision, reason=reason, **params)
|
|
676
|
+
return {"task": task, "decision": decision, "workflow_item": self.find_by_item(item)}
|
|
677
|
+
|
|
678
|
+
def approve_all_steps(self, item: Ref, max_steps: int = 10) -> int:
|
|
679
|
+
"""
|
|
680
|
+
Approve step after step until the item leaves the workflow (archived).
|
|
681
|
+
Returns the number of approvals. Stops with WorkflowError if a step
|
|
682
|
+
cannot be claimed by the current user.
|
|
683
|
+
|
|
684
|
+
Parameters:
|
|
685
|
+
item: The reference to the item
|
|
686
|
+
max_steps: The maximum number of steps to approve before stopping
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
int: The number of approvals performed
|
|
690
|
+
"""
|
|
691
|
+
approvals = 0
|
|
692
|
+
while self.find_by_item(item) is not None:
|
|
693
|
+
if approvals >= max_steps:
|
|
694
|
+
raise WorkflowError(f"Item still in workflow after {max_steps} approvals")
|
|
695
|
+
self.approve(self.claim_for_item(item))
|
|
696
|
+
approvals += 1
|
|
697
|
+
return approvals
|
|
698
|
+
|
|
699
|
+
def pending_tasks(self, eperson: Optional[Ref] = None, claimed: bool = True, pool: bool = True
|
|
700
|
+
) -> List[Dict[str, Any]]:
|
|
701
|
+
"""
|
|
702
|
+
Pending tasks for the user, either claimed or in the pool.
|
|
703
|
+
|
|
704
|
+
Parameters:
|
|
705
|
+
eperson: The reference to the user (default: current user)
|
|
706
|
+
claimed: Include claimed tasks (default: True)
|
|
707
|
+
pool: Include pool tasks (default: True)
|
|
708
|
+
|
|
709
|
+
Returns:
|
|
710
|
+
List[Dict[str, Any]]: The list of pending tasks for the user
|
|
711
|
+
|
|
712
|
+
What waits for a user (default: current user), as a flat list:
|
|
713
|
+
[{"kind": "claimed"|"pool", "task_id", "step", "action", "options",
|
|
714
|
+
"workflow_item_id", "item_uuid", "title"}].
|
|
715
|
+
"""
|
|
716
|
+
user = self._user_uuid(eperson)
|
|
717
|
+
tasks = [("claimed", t) for t in (self.claimed_tasks_for_user(user) if claimed else [])]
|
|
718
|
+
tasks += [("pool", t) for t in (self.pool_tasks_for_user(user) if pool else [])]
|
|
719
|
+
result = []
|
|
720
|
+
for kind, task in tasks:
|
|
721
|
+
wfi = (task.get("_embedded") or {}).get("workflowitem") or {}
|
|
722
|
+
item = (wfi.get("_embedded") or {}).get("item") or {}
|
|
723
|
+
titles = (item.get("metadata") or {}).get("dc.title") or []
|
|
724
|
+
result.append({
|
|
725
|
+
"kind": kind, "task_id": task.get("id"), "step": task.get("step"), "action": task.get("action"),
|
|
726
|
+
"options": self.task_options(task) if task.get("action") else [],
|
|
727
|
+
"workflow_item_id": wfi.get("id"), "item_uuid": item.get("uuid"),
|
|
728
|
+
"title": titles[0].get("value") if titles else item.get("name"),
|
|
729
|
+
})
|
|
730
|
+
return result
|
|
731
|
+
|
|
732
|
+
# ==================================================================
|
|
733
|
+
# Workflow configuration
|
|
734
|
+
# ==================================================================
|
|
735
|
+
|
|
736
|
+
def get_workflow_definitions(self) -> List[Dict[str, Any]]:
|
|
737
|
+
"""List all workflow definitions.
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
List[Dict[str, Any]]: The list of workflow definitions
|
|
741
|
+
"""
|
|
742
|
+
return list(self._iter_embedded(self._url(DEFINITIONS), "workflowdefinitions", "List workflow definitions"))
|
|
743
|
+
|
|
744
|
+
def get_workflow_definition(self, name: str) -> Dict[str, Any]:
|
|
745
|
+
"""Get a specific workflow definition by name.
|
|
746
|
+
|
|
747
|
+
Parameters:
|
|
748
|
+
name: The name of the workflow definition
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Dict[str, Any]: The workflow definition
|
|
752
|
+
"""
|
|
753
|
+
return self._get_json(self._url(DEFINITIONS, f"/{name}"), f"Get workflow definition {name}")
|
|
754
|
+
|
|
755
|
+
def definition_for_collection(self, collection: Ref) -> Dict[str, Any]:
|
|
756
|
+
"""The workflow definition a collection uses (falls back to the default one).
|
|
757
|
+
|
|
758
|
+
Parameters:
|
|
759
|
+
collection: The reference to the collection
|
|
760
|
+
|
|
761
|
+
Returns:
|
|
762
|
+
Dict[str, Any]: The workflow definition for the collection
|
|
763
|
+
"""
|
|
764
|
+
uuid = self._uuid(collection, "collection")
|
|
765
|
+
return self._get_json(self._url(DEFINITIONS, "/search/findByCollection"),
|
|
766
|
+
f"Workflow definition of {uuid}", params={"uuid": uuid})
|
|
767
|
+
|
|
768
|
+
def definition_collections(self, name: str) -> List[Dict[str, Any]]:
|
|
769
|
+
"""Collections that explicitly use a workflow definition.
|
|
770
|
+
|
|
771
|
+
Parameters:
|
|
772
|
+
name: The name of the workflow definition
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
List[Dict[str, Any]]: The list of collections using the workflow definition
|
|
776
|
+
"""
|
|
777
|
+
return list(self._iter_embedded(self._url(DEFINITIONS, f"/{name}/collections"), "collections",
|
|
778
|
+
f"Collections of workflow definition {name}"))
|
|
779
|
+
|
|
780
|
+
def definition_steps(self, name: str) -> List[Dict[str, Any]]:
|
|
781
|
+
"""Steps of a workflow definition.
|
|
782
|
+
|
|
783
|
+
Parameters:
|
|
784
|
+
name: The name of the workflow definition
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
List[Dict[str, Any]]: The list of steps for the workflow definition
|
|
788
|
+
"""
|
|
789
|
+
return list(self._iter_embedded(self._url(DEFINITIONS, f"/{name}/steps"), ("steps", "workflowsteps"),
|
|
790
|
+
f"Steps of workflow definition {name}"))
|
|
791
|
+
|
|
792
|
+
def get_workflow_step(self, name: str) -> Dict[str, Any]:
|
|
793
|
+
"""Get a specific workflow step by name.
|
|
794
|
+
|
|
795
|
+
Parameters:
|
|
796
|
+
name: The name of the workflow step
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
Dict[str, Any]: The workflow step details, including embedded workflow actions
|
|
800
|
+
"""
|
|
801
|
+
return self._get_json(self._url(STEPS, f"/{name}"), f"Get workflow step {name}")
|
|
802
|
+
|
|
803
|
+
def get_workflow_action(self, name: str) -> Dict[str, Any]:
|
|
804
|
+
"""Get a specific workflow action by name.
|
|
805
|
+
|
|
806
|
+
Parameters:
|
|
807
|
+
name: The name of the workflow action
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
Dict[str, Any]: The workflow action details (cached)
|
|
811
|
+
"""
|
|
812
|
+
if name not in self._action_cache:
|
|
813
|
+
self._action_cache[name] = self._get_json(self._url(ACTIONS, f"/{name}"), f"Get workflow action {name}")
|
|
814
|
+
return self._action_cache[name]
|
|
815
|
+
|
|
816
|
+
# ==================================================================
|
|
817
|
+
# Supervision orders
|
|
818
|
+
# ==================================================================
|
|
819
|
+
|
|
820
|
+
def iter_supervision_orders(self, embed: EmbedArg = ("item", "group")) -> Iterator[Dict[str, Any]]:
|
|
821
|
+
"""All supervision orders (administrators). A generator.
|
|
822
|
+
|
|
823
|
+
Parameters:
|
|
824
|
+
embed: The related entities to embed (default: ("item", "group"))
|
|
825
|
+
|
|
826
|
+
Returns:
|
|
827
|
+
Iterator[Dict[str, Any]]: The supervision orders
|
|
828
|
+
"""
|
|
829
|
+
return self._iter_embedded(self._url(SUPERVISION_ORDERS), "supervisionorders", "List supervision orders",
|
|
830
|
+
embed=list(embed) if embed else None)
|
|
831
|
+
|
|
832
|
+
def get_supervision_order(self, order_id, embed: EmbedArg = None) -> Dict[str, Any]:
|
|
833
|
+
"""Get a specific supervision order by ID.
|
|
834
|
+
|
|
835
|
+
Parameters:
|
|
836
|
+
order_id: The ID of the supervision order
|
|
837
|
+
embed: The related entities to embed (default: None)
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
Dict[str, Any]: The supervision order details
|
|
841
|
+
"""
|
|
842
|
+
return self._get_json(self._url(SUPERVISION_ORDERS, f"/{order_id}"), f"Get supervision order {order_id}",
|
|
843
|
+
embed=embed)
|
|
844
|
+
|
|
845
|
+
def supervision_orders_for_item(self, item: Ref) -> List[Dict[str, Any]]:
|
|
846
|
+
"""Get all supervision orders for a specific item.
|
|
847
|
+
|
|
848
|
+
Parameters:
|
|
849
|
+
item: The reference to the item
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
List[Dict[str, Any]]: The supervision orders for the item
|
|
853
|
+
"""
|
|
854
|
+
uuid = self._uuid(item, "item")
|
|
855
|
+
return list(self._iter_embedded(self._url(SUPERVISION_ORDERS, "/search/byItem"), "supervisionorders",
|
|
856
|
+
f"Supervision orders of {uuid}", [("uuid", uuid)], embed=["group"]))
|
|
857
|
+
|
|
858
|
+
def create_supervision_order(self, item: Ref, group: Ref, order_type: str = "NONE") -> Dict[str, Any]:
|
|
859
|
+
"""
|
|
860
|
+
Let a group supervise an in-progress submission (workspace or workflow).
|
|
861
|
+
order_type: NONE (no extra rights), EDITOR (READ + WRITE on the item and
|
|
862
|
+
its files) or OBSERVER (READ). 422: item not in progress, or the group
|
|
863
|
+
already has an order for it.
|
|
864
|
+
|
|
865
|
+
Parameters:
|
|
866
|
+
item: The reference to the item
|
|
867
|
+
group: The reference to the group
|
|
868
|
+
order_type: The type of supervision order (default: "NONE")
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
Dict[str, Any]: The created supervision order
|
|
872
|
+
"""
|
|
873
|
+
order_type = order_type.upper()
|
|
874
|
+
if order_type not in SUPERVISION_TYPES:
|
|
875
|
+
raise ValueError(f"order_type must be one of {SUPERVISION_TYPES}")
|
|
876
|
+
params = {"uuid": self._uuid(item, "item"), "group": self._uuid(group, "group"), "type": order_type}
|
|
877
|
+
order = self._request("POST", self._url(SUPERVISION_ORDERS), "Create supervision order", expected=(200, 201),
|
|
878
|
+
params=params, headers=self._headers()).json()
|
|
879
|
+
logger.info("Created %s supervision order %s on item %s", order_type, order.get("id"), params["uuid"])
|
|
880
|
+
return order
|
|
881
|
+
|
|
882
|
+
def delete_supervision_order(self, order_id) -> bool:
|
|
883
|
+
"""Delete a specific supervision order by ID.
|
|
884
|
+
|
|
885
|
+
Parameters:
|
|
886
|
+
order_id: The ID of the supervision order
|
|
887
|
+
|
|
888
|
+
Returns:
|
|
889
|
+
bool: True if the deletion was successful
|
|
890
|
+
"""
|
|
891
|
+
self._request("DELETE", self._url(SUPERVISION_ORDERS, f"/{order_id}"), f"Delete supervision order {order_id}",
|
|
892
|
+
expected=(204,))
|
|
893
|
+
return True
|