bika.lims.workflow.analysis.guards   F
last analyzed

Complexity

Total Complexity 90

Size/Duplication

Total Lines 393
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 90
eloc 185
dl 0
loc 393
rs 2
c 0
b 0
f 0

25 Functions

Rating   Name   Duplication   Size   Complexity  
A guard_initialize() 0 7 2
A guard_unassign() 0 12 3
A guard_reinstate() 0 6 1
A is_worksheet_context() 0 16 4
A guard_cancel() 0 6 1
A guard_assign() 0 16 4
C guard_multi_verify() 0 31 10
A is_multi_verification_allowed() 0 5 1
A guard_retract() 0 16 5
A is_consecutive_multi_verification_allowed() 0 5 1
F guard_submit() 0 47 18
A was_verified_by_current_user() 0 4 1
A user_has_super_roles() 0 7 2
A guard_retest() 0 13 3
A current_user_was_last_verifier() 0 5 1
A was_submitted_by_current_user() 0 4 1
A user_can_manage_worksheets() 0 9 2
D guard_verify() 0 37 13
A guard_publish() 0 6 1
A is_transition_allowed() 0 11 5
A guard_reject() 0 5 1
A is_submitted_or_submittable() 0 8 3
A is_verified_or_verifiable() 0 10 4
A cached_is_transition_allowed() 0 13 2
A _transition_cache_key() 0 14 1

How to fix   Complexity   

Complexity

Complex classes like bika.lims.workflow.analysis.guards often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2024 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from bika.lims import api
22
from bika.lims import logger
23
from bika.lims import workflow as wf
24
from bika.lims.api import security
25
from bika.lims.interfaces import ISubmitted
26
from bika.lims.interfaces import IVerified
27
from bika.lims.interfaces import IWorksheet
28
from bika.lims.interfaces.analysis import IRequestAnalysis
29
from plone.memoize.request import cache
30
31
32
def is_worksheet_context():
33
    """Returns whether the current context from the request is a Worksheet
34
    """
35
    request = api.get_request()
36
    parents = request.get("PARENTS", [])
37
    portal_types_names = map(lambda p: getattr(p, "portal_type", None), parents)
38
    if "Worksheet" in portal_types_names:
39
        return True
40
41
    # Check if the worksheet is declared in request explicitly
42
    ws_uid = request.get("ws_uid", "")
43
    obj = api.get_object_by_uid(ws_uid, None)
44
    if IWorksheet.providedBy(obj):
45
        return True
46
47
    return False
48
49
50
def guard_initialize(analysis):
51
    """Return whether the transition "initialize" can be performed or not
52
    """
53
    request = analysis.getRequest()
54
    if request.getDateReceived():
55
        return True
56
    return False
57
58
59
def guard_assign(analysis):
60
    """Return whether the transition "assign" can be performed or not
61
    """
62
    # Only if the request was done from worksheet context.
63
    if not is_worksheet_context():
64
        return False
65
66
    # Cannot assign if the Sample has not been received
67
    if not analysis.isSampleReceived():
68
        return False
69
70
    # Cannot assign if the analysis has a worksheet assigned already
71
    if analysis.getWorksheet():
72
        return False
73
74
    return True
75
76
77
def guard_unassign(analysis):
78
    """Return whether the transition "unassign" can be performed or not
79
    """
80
    # Only if the request was done from worksheet context.
81
    if not is_worksheet_context():
82
        return False
83
84
    # Cannot unassign if the analysis is not assigned to any worksheet
85
    if not analysis.getWorksheet():
86
        return False
87
88
    return True
89
90
91
def guard_cancel(analysis):
92
    """Return whether the transition "cancel" can be performed or not. Returns
93
    True only when the Analysis Request the analysis belongs to is in cancelled
94
    state. Otherwise, returns False.
95
    """
96
    return not api.is_active(analysis.getRequest())
97
98
99
def guard_reinstate(analysis):
100
    """Return whether the transition "reinstate" can be performed or not.
101
    Returns True only when the Analysis Request the analysis belongs to is in a
102
    non-cancelled state. Otherwise, returns False.
103
    """
104
    return api.is_active(analysis.getRequest())
105
106
107
def guard_submit(analysis):
108
    """Return whether the transition "submit" can be performed or not
109
    """
110
    # Cannot submit without a result
111
    if not analysis.getResult():
112
        return False
113
114
    # Cannot submit with interims without value
115
    for interim in analysis.getInterimFields():
116
        true_values = ("true", "1", "on", "True", True, 1)
117
        if interim.get("allow_empty", False) in true_values:
118
            continue
119
120
        if not interim.get("value", ""):
121
            return False
122
123
    # Cannot submit if attachment not set, but is required
124
    if not analysis.getAttachment():
125
        if analysis.getAttachmentRequired():
126
            return False
127
128
    # Check if can submit based on the Analysis Request state
129
    if IRequestAnalysis.providedBy(analysis):
130
        point_of_capture = analysis.getPointOfCapture()
131
        # Cannot submit if the Sample has not been received
132
        if point_of_capture == "lab" and not analysis.isSampleReceived():
133
            return False
134
        # Cannot submit if the Sample has not been sampled
135
        if point_of_capture == "field" and not analysis.isSampleSampled():
136
            return False
137
138
    # Check if the current user can submit if is not assigned
139
    if not analysis.bika_setup.getAllowToSubmitNotAssigned():
140
        if not user_has_super_roles():
141
            # Cannot submit if unassigned
142
            analyst = analysis.getAnalyst()
143
            if not analyst:
144
                return False
145
            # Cannot submit if assigned analyst is not the current user
146
            if analyst != security.get_user_id():
147
                return False
148
149
    # Cannot submit unless all dependencies are submitted or can be submitted
150
    for dependency in analysis.getDependencies():
151
        if not is_submitted_or_submittable(dependency):
152
            return False
153
    return True
154
155
156
def guard_multi_verify(analysis):
157
    """Return whether the transition "multi_verify" can be performed or not
158
    The transition multi_verify will only take place if multi-verification of
159
    results is enabled.
160
    """
161
    # Cannot multiverify if there is only one remaining verification
162
    remaining_verifications = analysis.getNumberOfRemainingVerifications()
163
    if remaining_verifications <= 1:
164
        return False
165
166
    # Cannot verify if the user submitted and self-verification is not allowed
167
    if was_submitted_by_current_user(analysis):
168
        if not analysis.isSelfVerificationEnabled():
169
            return False
170
171
    # Cannot verify if the user verified and multi verification is not allowed
172
    if was_verified_by_current_user(analysis):
173
        if not is_multi_verification_allowed(analysis):
174
            return False
175
176
    # Cannot verify if the user was last verifier and consecutive verification
177
    # by same user is not allowed
178
    if current_user_was_last_verifier(analysis):
179
        if not is_consecutive_multi_verification_allowed(analysis):
180
            return False
181
182
    # Cannot verify unless all dependencies are verified or can be verified
183
    for dependency in analysis.getDependencies():
184
        if not is_verified_or_verifiable(dependency):
185
            return False
186
    return True
187
188
189
def guard_verify(analysis):
190
    """Return whether the transition "verify" can be performed or not
191
    """
192
    # Cannot verify if the number of remaining verifications is > 1
193
    remaining_verifications = analysis.getNumberOfRemainingVerifications()
194
    if remaining_verifications > 1:
195
        return False
196
197
    # Cannot verify if the user submitted and self-verification is not allowed
198
    if was_submitted_by_current_user(analysis):
199
        if not analysis.isSelfVerificationEnabled():
200
            return False
201
202
    # Cannot verify unless dependencies have been verified or can be verified
203
    if analysis.getNumberOfRequiredVerifications() <= 1:
204
        for dependency in analysis.getDependencies():
205
            if not is_verified_or_verifiable(dependency):
206
                return False
207
        return True
208
209
    # This analysis has multi-verification enabled
210
    # Cannot verify if the user verified and multi verification is not allowed
211
    if was_verified_by_current_user(analysis):
212
        if not is_multi_verification_allowed(analysis):
213
            return False
214
215
    # Cannot verify if the user was last verifier and consecutive verification
216
    # by same user is not allowed
217
    if current_user_was_last_verifier(analysis):
218
        if not is_consecutive_multi_verification_allowed(analysis):
219
            return False
220
221
    # Cannot verify unless all dependencies are verified or can be verified
222
    for dependency in analysis.getDependencies():
223
        if not is_verified_or_verifiable(dependency):
224
            return False
225
    return True
226
227
228
def guard_retract(analysis):
229
    """ Return whether the transition "retract" can be performed or not
230
    """
231
    # Cannot retract if there are dependents that cannot be retracted
232
    if not is_transition_allowed(analysis.getDependents(), "retract"):
233
        return False
234
235
    dependencies = analysis.getDependencies()
236
    if not dependencies:
237
        return True
238
239
    # Cannot retract if all dependencies have been verified
240
    if all(map(lambda an: IVerified.providedBy(an), dependencies)):
241
        return False
242
243
    return True
244
245
246
def guard_retest(analysis, check_dependents=True):
247
    """Return whether the transition "retest" can be performed or not
248
    """
249
    # Retest transition does an automatic verify transition, so the analysis
250
    # should be verifiable first
251
    if not is_transition_allowed(analysis, "verify"):
252
        return False
253
254
    # Cannot retest if there are dependents that cannot be retested
255
    if not is_transition_allowed(analysis.getDependents(), "retest"):
256
        return False
257
258
    return True
259
260
261
def guard_reject(analysis):
262
    """Return whether the transition "reject" can be performed or not
263
    """
264
    # Cannot reject if there are dependents that cannot be rejected
265
    return is_transition_allowed(analysis.getDependents(), "reject")
266
267
268
def guard_publish(analysis):
269
    """Return whether the transition "publish" can be performed or not. Returns
270
    True only when the Analysis Request the analysis belongs to is in published
271
    state. Otherwise, returns False.
272
    """
273
    return api.get_workflow_status_of(analysis.getRequest()) == "published"
274
275
276
def user_can_manage_worksheets():
277
    """Return whether the current user has privileges to manage worksheets
278
    """
279
    if not api.get_setup().getRestrictWorksheetManagement():
280
        # There is no restriction, everybody can manage worksheets
281
        return True
282
283
    # Only Labmanager and Manager roles can manage worksheets
284
    return user_has_super_roles()
285
286
287
def user_has_super_roles():
288
    """Return whether the current belongs to superuser roles
289
    """
290
    member = api.get_current_user()
291
    super_roles = ["LabManager", "Manager"]
292
    diff = filter(lambda role: role in super_roles, member.getRoles())
293
    return len(diff) > 0
294
295
296
def was_submitted_by_current_user(analysis):
297
    """Returns whether the analysis was submitted by current user or not
298
    """
299
    return analysis.getSubmittedBy() == api.get_current_user().getId()
300
301
302
def was_verified_by_current_user(analysis):
303
    """Returns whether the analysis was verified by current user or not
304
    """
305
    return api.get_current_user().getId() in analysis.getVerificators()
306
307
308
def current_user_was_last_verifier(analysis):
309
    """Returns whether the current user was the last verifier or not
310
    """
311
    verifiers = analysis.getVerificators()
312
    return verifiers and verifiers[:-1] == api.get_current_user().getId()
313
314
315
def is_consecutive_multi_verification_allowed(analysis):
316
    """Returns whether multiple verification and consecutive verification is
317
    allowed or not"""
318
    multi_type = api.get_setup().getTypeOfmultiVerification()
319
    return multi_type != "self_multi_not_cons"
320
321
322
def is_multi_verification_allowed(analysis):
323
    """Returns whether multi verification is allowed or not
324
    """
325
    multi_type = api.get_setup().getTypeOfmultiVerification()
326
    return multi_type != "self_multi_disabled"
327
328
329
def is_transition_allowed(analyses, transition_id):
330
    """Returns whether all analyses can be transitioned or not
331
    """
332
    if not analyses:
333
        return True
334
    if not isinstance(analyses, list):
335
        return is_transition_allowed([analyses], transition_id)
336
    for analysis in analyses:
337
        if not cached_is_transition_allowed(analysis, transition_id):
338
            return False
339
    return True
340
341
342
def _transition_cache_key(fun, obj, action):
343
    """Cache key generator for the request cache
344
345
    This function generates cache keys like this:
346
    >>> from bika.lims import api
347
    >>> from zope.annotation.interfaces import IAnnotations
348
    >>> request = api.get_request()
349
    >>> IAnnotations(request)
350
    {'bika.lims.workflow.analysis.guards.check_analysis_allows_transition:3ff02762c70f4a56b1b30c1b74d32bf6-retract': True,
351
     'bika.lims.workflow.analysis.guards.check_analysis_allows_transition:0390c16ddec14a04b87ff8408e2aa229-retract': True,
352
     ...
353
    }
354
    """
355
    return "%s-%s" % (api.get_uid(obj), action)
356
357
358
@cache(get_key=_transition_cache_key, get_request="analysis.REQUEST")
359
def cached_is_transition_allowed(analysis, transition_id):
360
    """Check if the transition is allowed for the given analysis and cache the
361
    value on the request.
362
363
    Note: The request is obtained by the given expression from the `locals()`,
364
          which includes the given arguments.
365
    """
366
    logger.debug("cached_is_transition_allowed: analyis=%r transition=%s"
367
                 % (analysis, transition_id))
368
    if wf.isTransitionAllowed(analysis, transition_id):
369
        return True
370
    return False
371
372
373
def is_submitted_or_submittable(analysis):
374
    """Returns whether the analysis is submittable or has already been submitted
375
    """
376
    if ISubmitted.providedBy(analysis):
377
        return True
378
    if is_transition_allowed(analysis, "submit"):
379
        return True
380
    return False
381
382
383
def is_verified_or_verifiable(analysis):
384
    """Returns whether the analysis is verifiable or has already been verified
385
    """
386
    if IVerified.providedBy(analysis):
387
        return True
388
    if is_transition_allowed(analysis, "verify"):
389
        return True
390
    if is_transition_allowed(analysis, "multi_verify"):
391
        return True
392
    return False
393