SamplesView.folderitem()   F
last analyzed

Complexity

Conditions 29

Size

Total Lines 164
Code Lines 116

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 116
dl 0
loc 164
rs 0
c 0
b 0
f 0
cc 29
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like senaite.core.browser.samples.view.SamplesView.folderitem() 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-2025 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import collections
22
from string import Template
23
24
from bika.lims import _
25
from bika.lims import api
26
from bika.lims.api.security import check_permission
27
from bika.lims.config import PRIORITIES
28
from bika.lims.interfaces import IBatch
29
from bika.lims.interfaces import IClient
30
from bika.lims.utils import get_image
31
from bika.lims.utils import get_link_for
32
from bika.lims.utils import get_progress_bar_html
33
from bika.lims.utils import getUsers
34
from DateTime import DateTime
35
from plone.memoize import view
36
from senaite.app.listing import ListingView
37
from senaite.core.api import dtime
38
from senaite.core.catalog import SAMPLE_CATALOG
39
from senaite.core.i18n import translate as t
40
from senaite.core.interfaces import ISamples
41
from senaite.core.interfaces import ISamplesView
42
from senaite.core.permissions import AddAnalysisRequest
43
from senaite.core.permissions import TransitionSampleSample
44
from senaite.core.permissions.worksheet import can_add_worksheet
45
from zope.interface import implementer
46
47
ANALYSES_NUM_TPL = Template("$not_submitted/$to_be_verified/$verified/$total")
48
ANALYSES_NUM_TPL_HTML = Template("""<div class="d-flex flex-row">
49
  <span data-toggle="tooltip"
50
        title="$not_submitted_title"
51
        class="text-secondary cursor-pointer">
52
    $not_submitted
53
  </span>
54
  <span class="separator">/</span>
55
  <span data-toggle="tooltip"
56
        title="$to_be_verified_title"
57
        class="text-state-to_be_verified cursor-pointer">
58
    $to_be_verified
59
  </span>
60
  <span class="separator">/</span>
61
  <span data-toggle="tooltip"
62
        title="$verified_title"
63
        class="text-state-verified cursor-pointer">
64
    $verified
65
  </span>
66
  <span class="separator">/</span>
67
  <span data-toggle="tooltip"
68
        title="$total_title"
69
        class="text-black cursor-pointer">
70
    $total
71
  </span>
72
</div>
73
""")
74
75
76
@implementer(ISamplesView)
77
class SamplesView(ListingView):
78
    """Listing View for Samples (AnalysisRequest content type) in the System
79
    """
80
81
    def __init__(self, context, request):
82
        super(SamplesView, self).__init__(context, request)
83
84
        self.catalog = SAMPLE_CATALOG
85
        self.contentFilter = {
86
            "sort_on": "created",
87
            "sort_order": "descending",
88
            "isRootAncestor": True,  # only root ancestors
89
        }
90
91
        self.title = self.context.translate(_("Samples"))
92
        self.description = ""
93
94
        self.show_select_column = True
95
        self.form_id = "samples"
96
        self.context_actions = {}
97
        self.icon = "{}{}".format(
98
            self.portal_url, "/senaite_theme/icon/sample")
99
100
        self.url = api.get_url(self.context)
101
102
        # Toggle some columns if the sampling workflow is enabled
103
        sampling_enabled = api.get_setup().getSamplingWorkflowEnabled()
104
105
        now = DateTime().strftime("%Y-%m-%d %H:%M")
106
107
        self.columns = collections.OrderedDict((
108
            ("Priority", {
109
                "title": "",
110
                "index": "getPrioritySortkey",
111
                "sortable": True, }),
112
            ("Progress", {
113
                "title": _("Progress"),
114
                "index": "getProgress",
115
                "sortable": True,
116
                "toggle": True}),
117
            ("getId", {
118
                "title": _("Sample ID"),
119
                "attr": "getId",
120
                "replace_url": "getURL",
121
                "index": "getId"}),
122
            ("getClientOrderNumber", {
123
                "title": _("Client Order"),
124
                "sortable": True,
125
                "toggle": False}),
126
            ("Creator", {
127
                "title": _("Creator"),
128
                "index": "Creator",
129
                "sortable": True,
130
                "toggle": True}),
131
            ("Created", {
132
                "title": _("Date Registered"),
133
                "index": "created",
134
                "toggle": False}),
135
            ("SamplingDate", {
136
                "title": _("Expected Sampling Date"),
137
                "index": "getSamplingDate",
138
                "toggle": sampling_enabled}),
139
            ("getDateSampled", {
140
                "title": _("Date Sampled"),
141
                "toggle": True,
142
                "type": "datetime",
143
                "max": now,
144
                "sortable": True}),
145
            ("getDatePreserved", {
146
                "title": _("Date Preserved"),
147
                "toggle": False,
148
                "type": "datetime",
149
                "max": now,
150
                "sortable": False}),  # no datesort without index
151
            ("getDateReceived", {
152
                "title": _("Date Received"),
153
                "toggle": False}),
154
            ("getDueDate", {
155
                "title": _("Due Date"),
156
                "toggle": False}),
157
            ("getDateVerified", {
158
                "title": _("Date Verified"),
159
                "input_width": "10",
160
                "toggle": False}),
161
            ("getDatePublished", {
162
                "title": _("Date Published"),
163
                "toggle": False}),
164
            ("BatchID", {
165
                "title": _("Batch ID"),
166
                "index": "getBatchID",
167
                "sortable": True,
168
                "toggle": False}),
169
            ("Client", {
170
                "title": _("Client"),
171
                "index": "getClientTitle",
172
                "attr": "getClientTitle",
173
                "replace_url": "getClientURL",
174
                "toggle": True}),
175
            ("ClientID", {
176
                "title": _("Client ID"),
177
                "index": "getClientID",
178
                "attr": "getClientID",
179
                "replace_url": "getClientURL",
180
                "toggle": True}),
181
            ("Province", {
182
                "title": _("Province"),
183
                "sortable": True,
184
                "index": "getProvince",
185
                "attr": "getProvince",
186
                "toggle": False}),
187
            ("District", {
188
                "title": _("District"),
189
                "sortable": True,
190
                "index": "getDistrict",
191
                "attr": "getDistrict",
192
                "toggle": False}),
193
            ("getClientReference", {
194
                "title": _("Client Ref"),
195
                "sortable": True,
196
                "index": "getClientReference",
197
                "toggle": False}),
198
            ("getClientSampleID", {
199
                "title": _("Client SID"),
200
                "toggle": False}),
201
            ("ClientContact", {
202
                "title": _("Contact"),
203
                "sortable": False,
204
                "toggle": False}),
205
            ("getSampleTypeTitle", {
206
                "title": _("Sample Type"),
207
                "sortable": True,
208
                "toggle": True}),
209
            ("getSamplePointTitle", {
210
                "title": _("Sample Point"),
211
                "sortable": True,
212
                "index": "getSamplePointTitle",
213
                "toggle": False}),
214
            ("getStorageLocation", {
215
                "title": _("Storage Location"),
216
                "sortable": True,
217
                "index": "getStorageLocationTitle",
218
                "toggle": False}),
219
            ("SamplingDeviation", {
220
                "title": _("Sampling Deviation"),
221
                "sortable": True,
222
                "index": "getSamplingDeviationTitle",
223
                "toggle": False}),
224
            ("getSampler", {
225
                "title": _("Sampler"),
226
                "toggle": sampling_enabled}),
227
            ("getPreserver", {
228
                "title": _("Preserver"),
229
                "sortable": False,
230
                "toggle": False}),
231
            ("getProfilesTitle", {
232
                "title": _("Profile"),
233
                "sortable": False,
234
                "toggle": False}),
235
            ("getAnalysesNum", {
236
                "title": _("Number of Analyses"),
237
                "alt": _("Open / To be verified / Verified / Total"),
238
                "sortable": True,
239
                "index": "getAnalysesNum",
240
                "toggle": False}),
241
            ("getTemplateTitle", {
242
                "title": _("Template"),
243
                "sortable": True,
244
                "index": "getTemplateTitle",
245
                "toggle": False}),
246
            ("Printed", {
247
                "title": _("Printed"),
248
                "sortable": False,
249
                "index": "getPrinted",
250
                "toggle": False}),
251
            ("state_title", {
252
                "title": _("State"),
253
                "sortable": True,
254
                "index": "review_state"}),
255
        ))
256
257
        # custom print transition
258
        print_stickers = {
259
            "id": "print_stickers",
260
            "title": _("Print stickers"),
261
            "url": "{}/workflow_action?action=print_stickers".format(self.url)
262
        }
263
264
        self.review_states = [
265
            {
266
                "id": "default",
267
                "title": _("Active"),
268
                "contentFilter": {
269
                    "review_state": (
270
                        "sample_registered",
271
                        "scheduled_sampling",
272
                        "to_be_sampled",
273
                        "sample_due",
274
                        "sample_received",
275
                        "to_be_preserved",
276
                        "to_be_verified",
277
                        "verified",
278
                    ),
279
                    "sort_on": "created",
280
                    "sort_order": "descending",
281
                },
282
                "custom_transitions": [print_stickers],
283
                "columns": self.columns.keys(),
284
            }, {
285
                "id": "to_be_sampled",
286
                "title": _("To Be Sampled"),
287
                "contentFilter": {
288
                    "review_state": ("to_be_sampled",),
289
                    "sort_on": "created",
290
                    "sort_order": "descending"},
291
                "custom_transitions": [print_stickers],
292
                "columns": self.columns.keys()
293
            }, {
294
                "id": "to_be_preserved",
295
                "title": _("To Be Preserved"),
296
                "contentFilter": {
297
                    "review_state": ("to_be_preserved",),
298
                    "sort_on": "created",
299
                    "sort_order": "descending",
300
                },
301
                "custom_transitions": [print_stickers],
302
                "columns": self.columns.keys(),
303
            }, {
304
                "id": "scheduled_sampling",
305
                "title": _("Scheduled sampling"),
306
                "contentFilter": {
307
                    "review_state": ("scheduled_sampling",),
308
                    "sort_on": "created",
309
                    "sort_order": "descending",
310
                },
311
                "custom_transitions": [print_stickers],
312
                "columns": self.columns.keys(),
313
            }, {
314
                "id": "sample_due",
315
                "title": _("Due"),
316
                "contentFilter": {
317
                    "review_state": (
318
                        "to_be_sampled",
319
                        "to_be_preserved",
320
                        "sample_due"),
321
                    "sort_on": "created",
322
                    "sort_order": "descending"},
323
                "custom_transitions": [print_stickers],
324
                "columns": self.columns.keys(),
325
            }, {
326
                "id": "sample_received",
327
                "title": _("Received"),
328
                "contentFilter": {
329
                    "review_state": "sample_received",
330
                    "sort_on": "created",
331
                    "sort_order": "descending",
332
                },
333
                "custom_transitions": [print_stickers],
334
                "columns": self.columns.keys(),
335
            }, {
336
                "id": "to_be_verified",
337
                "title": _("To be verified"),
338
                "contentFilter": {
339
                    "review_state": "to_be_verified",
340
                    "sort_on": "created",
341
                    "sort_order": "descending",
342
                },
343
                "custom_transitions": [print_stickers],
344
                "columns": self.columns.keys(),
345
            }, {
346
                "id": "verified",
347
                "title": _("Verified"),
348
                "contentFilter": {
349
                    "review_state": "verified",
350
                    "sort_on": "created",
351
                    "sort_order": "descending",
352
                },
353
                "custom_transitions": [print_stickers],
354
                "columns": self.columns.keys(),
355
            }, {
356
                "id": "published",
357
                "title": _("Published"),
358
                "contentFilter": {
359
                    "review_state": ("published"),
360
                    "sort_on": "created",
361
                    "sort_order": "descending",
362
                },
363
                "custom_transitions": [],
364
                "columns": self.columns.keys(),
365
            }, {
366
                "id": "dispatched",
367
                "title": _("Dispatched"),
368
                "flat_listing": True,
369
                "confirm_transitions": ["restore"],
370
                "contentFilter": {
371
                    "review_state": ("dispatched"),
372
                    "sort_on": "created",
373
                    "sort_order": "descending",
374
                },
375
                "custom_transitions": [],
376
                "columns": self.columns.keys(),
377
            }, {
378
                "id": "cancelled",
379
                "title": _("Cancelled"),
380
                "contentFilter": {
381
                    "review_state": "cancelled",
382
                    "sort_on": "created",
383
                    "sort_order": "descending",
384
                },
385
                "custom_transitions": [],
386
                "columns": self.columns.keys(),
387
            }, {
388
                "id": "invalid",
389
                "title": _("Invalid"),
390
                "contentFilter": {
391
                    "review_state": "invalid",
392
                    "sort_on": "created",
393
                    "sort_order": "descending",
394
                },
395
                "custom_transitions": [print_stickers],
396
                "columns": self.columns.keys(),
397
            }, {
398
                "id": "all",
399
                "title": _("All"),
400
                "contentFilter": {
401
                    "sort_on": "created",
402
                    "sort_order": "descending",
403
                },
404
                "custom_transitions": [print_stickers],
405
                "columns": self.columns.keys(),
406
            }, {
407
                "id": "rejected",
408
                "title": _("Rejected"),
409
                "contentFilter": {
410
                    "review_state": "rejected",
411
                    "sort_on": "created",
412
                    "sort_order": "descending",
413
                },
414
                "custom_transitions": [print_stickers],
415
                "columns": self.columns.keys(),
416
            }, {
417
                "id": "assigned",
418
                "title": get_image("assigned.png",
419
                                   title=t(_("Assigned"))),
420
                "contentFilter": {
421
                    "assigned_state": "assigned",
422
                    "review_state": ("sample_received",),
423
                    "sort_on": "created",
424
                    "sort_order": "descending",
425
                },
426
                "custom_transitions": [print_stickers],
427
                "columns": self.columns.keys(),
428
            }, {
429
                "id": "unassigned",
430
                "title": get_image("unassigned.png",
431
                                   title=t(_("Unsassigned"))),
432
                "contentFilter": {
433
                    "assigned_state": "unassigned",
434
                    "review_state": (
435
                        "sample_received",
436
                    ),
437
                    "sort_on": "created",
438
                    "sort_order": "descending",
439
                },
440
                "custom_transitions": [print_stickers],
441
                "columns": self.columns.keys(),
442
            }, {
443
                "id": "late",
444
                "title": get_image("late.png",
445
                                   title=t(_("Late"))),
446
                "contentFilter": {
447
                    # Query only for unpublished ARs that are late
448
                    "review_state": (
449
                        "sample_received",
450
                        "to_be_verified",
451
                        "verified",
452
                    ),
453
                    "getDueDate": {
454
                        "query": DateTime(),
455
                        "range": "max",
456
                    },
457
                    "sort_on": "created",
458
                    "sort_order": "descending",
459
                },
460
                "custom_transitions": [print_stickers],
461
                "columns": self.columns.keys(),
462
            }
463
        ]
464
465
    def update(self):
466
        """Called before the listing renders
467
        """
468
        super(SamplesView, self).update()
469
470
        self.workflow = api.get_tool("portal_workflow")
471
        self.member = self.mtool.getAuthenticatedMember()
472
        self.roles = self.member.getRoles()
473
474
        # Remove unnecessary filters
475
        self.purge_review_states()
476
477
        # Remove unnecessary columns
478
        self.purge_columns()
479
480
        # Additional custom transitions
481
        self.add_custom_transitions()
482
483
    def before_render(self):
484
        """Before template render hook
485
        """
486
        super(SamplesView, self).before_render()
487
        # remove query filter for root samples when listing is flat
488
        if self.flat_listing:
489
            self.contentFilter.pop("isRootAncestor", None)
490
491
    def folderitem(self, obj, item, index):
492
        # Additional info from AnalysisRequest to be added in the item
493
        # generated by default by bikalisting.
494
        # Call the folderitem method from the base class
495
        item = super(SamplesView, self).folderitem(obj, item, index)
496
        if not item:
497
            return None
498
499
        item["Creator"] = self.user_fullname(obj.Creator)
500
        # If we redirect from the folderitems view we should check if the
501
        # user has permissions to medify the element or not.
502
        priority_sort_key = obj.getPrioritySortkey
503
        if not priority_sort_key:
504
            # Default priority is Medium = 3.
505
            # The format of PrioritySortKey is <priority>.<created>
506
            priority_sort_key = "3.%s" % obj.created.ISO8601()
507
        priority = priority_sort_key.split(".")[0]
508
        priority_text = PRIORITIES.getValue(priority)
509
        priority_div = """<div class="priority-ico priority-%s">
510
                          <span class="notext">%s</span><div>
511
                       """
512
        item["replace"]["Priority"] = priority_div % (priority, priority_text)
513
        item["replace"]["getProfilesTitle"] = obj.getProfilesTitleStr
514
515
        # returns a list of
516
        # [verified, total, not_submitted, to_be_verified]
517
        analysesnum = obj.getAnalysesNum
518
        if analysesnum:
519
            numbers = {
520
                "verified": analysesnum[0],
521
                "verified_title": t(_("Verified")),
522
                "total": analysesnum[1],
523
                "total_title": t(_("Total")),
524
                "not_submitted": analysesnum[2],
525
                "not_submitted_title": t(_("Open")),
526
                "to_be_verified": analysesnum[3],
527
                "to_be_verified_title": t(_("To be verified")),
528
            }
529
            item["getAnalysesNum"] = ANALYSES_NUM_TPL.safe_substitute(numbers)
530
            html = ANALYSES_NUM_TPL_HTML.safe_substitute(numbers)
531
            item["replace"]["getAnalysesNum"] = html
532
        else:
533
            item["getAnalysesNum"] = ""
534
535
        # Progress
536
        progress_perc = obj.getProgress
537
        item["Progress"] = progress_perc
538
        item["replace"]["Progress"] = get_progress_bar_html(progress_perc)
539
540
        item["BatchID"] = obj.getBatchID
541
        if obj.getBatchID:
542
            item['replace']['BatchID'] = "<a href='%s'>%s</a>" % \
543
                                         (obj.getBatchURL, obj.getBatchID)
544
        # TODO: SubGroup ???
545
        # val = obj.Schema().getField('SubGroup').get(obj)
546
        # item['SubGroup'] = val.Title() if val else ''
547
548
        item["SamplingDate"] = self.str_date(obj.getSamplingDate)
549
        item["getDateSampled"] = self.str_date(obj.getDateSampled)
550
        item["getDateReceived"] = self.str_date(obj.getDateReceived)
551
        item["getDueDate"] = self.str_date(obj.getDueDate)
552
        item["getDatePublished"] = self.str_date(obj.getDatePublished)
553
        item["getDateVerified"] = self.str_date(obj.getDateVerified)
554
555
        if self.is_printing_workflow_enabled:
556
            item["Printed"] = ""
557
            printed = obj.getPrinted if hasattr(obj, "getPrinted") else "0"
558
            print_icon = ""
559
            if printed == "0":
560
                print_icon = get_image("delete.png",
561
                                       title=t(_("Not printed yet")))
562
            elif printed == "1":
563
                print_icon = get_image("ok.png",
564
                                       title=t(_("Printed")))
565
            elif printed == "2":
566
                print_icon = get_image(
567
                    "exclamation.png",
568
                    title=t(_("Republished after last print")))
569
            item["after"]["Printed"] = print_icon
570
        item["SamplingDeviation"] = obj.getSamplingDeviationTitle
571
572
        item["getStorageLocation"] = obj.getStorageLocationTitle
573
574
        after_icons = ""
575
        if obj.assigned_state == 'assigned':
576
            after_icons += get_image("worksheet.png",
577
                                     title=t(_("All analyses assigned")))
578
        if item["review_state"] == 'invalid':
579
            after_icons += get_image("delete.png",
580
                                     title=t(_("Results have been withdrawn")))
581
582
        due_date = obj.getDueDate
583
        if due_date and due_date < (obj.getDatePublished or DateTime()):
584
            due_date_str = self.ulocalized_time(due_date)
585
            img_title = "{}: {}".format(t(_("Late Analyses")), due_date_str)
586
            after_icons += get_image("late.png", title=img_title)
587
588
        if obj.getSamplingDate and obj.getSamplingDate > DateTime():
589
            after_icons += get_image("calendar.png",
590
                                     title=t(_("Future dated sample")))
591
        if obj.getInvoiceExclude:
592
            after_icons += get_image("invoice_exclude.png",
593
                                     title=t(_("Exclude from invoice")))
594
        if obj.getHazardous:
595
            after_icons += get_image("hazardous.png",
596
                                     title=t(_("Hazardous")))
597
598
        if obj.getInternalUse:
599
            after_icons += get_image("locked.png", title=t(_("Internal use")))
600
601
        if after_icons:
602
            item['after']['getId'] = after_icons
603
604
        item['Created'] = self.ulocalized_time(obj.created, long_format=1)
605
        contact = self.get_object_by_uid(obj.getContactUID)
606
        if contact:
607
            item['ClientContact'] = contact.getFullname()
608
            item['replace']['ClientContact'] = get_link_for(contact)
609
        else:
610
            item["ClientContact"] = ""
611
        # TODO-performance: If SamplingWorkflowEnabled, we have to get the
612
        # full object to check the user permissions, so far this is
613
        # a performance hit.
614
        if obj.getSamplingWorkflowEnabled:
615
616
            sampler = obj.getSampler
617
            if sampler:
618
                item["getSampler"] = sampler
619
                item["replace"]["getSampler"] = self.user_fullname(sampler)
620
621
            # sampling workflow - inline edits for Sampler and Date Sampled
622
            if item["review_state"] == "to_be_sampled":
623
                # We need to get the full object in order to check
624
                # the permissions
625
                full_object = api.get_object(obj)
626
                if check_permission(TransitionSampleSample, full_object):
627
                    # make fields required and editable
628
                    item["required"] = ["getSampler", "getDateSampled"]
629
                    item["allow_edit"] = ["getSampler", "getDateSampled"]
630
                    date = obj.getDateSampled or DateTime()
631
                    # provide date and time in a valid input format
632
                    item["getDateSampled"] = self.to_datetime_input_value(date)
633
                    sampler_roles = ["Sampler", "LabManager", ""]
634
                    samplers = getUsers(full_object, sampler_roles)
635
                    users = [({
636
                        "ResultValue": u,
637
                        "ResultText": samplers.getValue(u)}) for u in samplers]
638
                    item["choices"] = {"getSampler": users}
639
                    # preselect the current user as sampler
640
                    if not sampler and "Sampler" in self.roles:
641
                        sampler = self.member.getUserName()
642
                        item["getSampler"] = sampler
643
644
        # These don't exist on ARs
645
        # XXX This should be a list of preservers...
646
        item["getPreserver"] = ""
647
        item["getDatePreserved"] = ""
648
649
        # Assign parent and children partitions of this sample
650
        if self.show_partitions:
651
            item["parent"] = obj.getRawParentAnalysisRequest
652
            item["children"] = obj.getDescendantsUIDs or []
653
654
        return item
655
656
    @view.memoize
657
    def get_object_by_uid(self, uid):
658
        """Returns the object for the given uid
659
        """
660
        return api.get_object_by_uid(uid, default=None)
661
662
    def purge_review_states(self):
663
        """Purges unnecessary review statuses
664
        """
665
        remove_filters = []
666
        setup = api.get_bika_setup()
667
        if not setup.getSamplingWorkflowEnabled():
668
            remove_filters.append("to_be_sampled")
669
        if not setup.getScheduleSamplingEnabled():
670
            remove_filters.append("scheduled_sampling")
671
        if not setup.getSamplePreservationEnabled():
672
            remove_filters.append("to_be_preserved")
673
        if not setup.getRejectionReasons():
674
            remove_filters.append("rejected")
675
676
        self.review_states = filter(lambda r: r.get("id") not in remove_filters,
677
                                    self.review_states)
678
679
    def purge_columns(self):
680
        """Purges unnecessary columns
681
        """
682
        remove_columns = []
683
        if not self.is_printing_workflow_enabled:
684
            remove_columns.append("Printed")
685
686
        for rv in self.review_states:
687
            cols = rv.get("columns", [])
688
            rv["columns"] = filter(lambda c: c not in remove_columns, cols)
689
690
    def add_custom_transitions(self):
691
        """Adds custom transitions as required
692
        """
693
        custom_transitions = []
694
        if self.is_printing_workflow_enabled:
695
            custom_transitions.append({
696
                "id": "print_sample",
697
                "title": _("Print"),
698
                "url": "{}/workflow_action?action={}".format(
699
                    self.url, "print_sample")
700
            })
701
702
        copy_to_new = self.get_copy_to_new_transition()
703
        if copy_to_new:
704
            custom_transitions.append(copy_to_new)
705
706
        # Allow to create a worksheet for the selected samples
707
        if self.can_create_worksheet():
708
            custom_transitions.append({
709
                "id": "modal_create_worksheet",
710
                "title": _("Create Worksheet"),
711
                "url": "{}/create_worksheet_modal".format(
712
                    api.get_url(self.context)),
713
                "css_class": "btn btn-outline-secondary",
714
                "help": _("Create a new worksheet for the selected samples")
715
            })
716
717
        for rv in self.review_states:
718
            rv.setdefault("custom_transitions", []).extend(custom_transitions)
719
720
    def get_copy_to_new_transition(self):
721
        """Returns the copy to new custom transition if the current has enough
722
        privileges. Returns None otherwise
723
        """
724
        base_url = None
725
        mtool = api.get_tool("portal_membership")
726
        if mtool.checkPermission(AddAnalysisRequest, self.context):
727
            base_url = self.url
728
        else:
729
            client = api.get_current_client()
730
            if client and mtool.checkPermission(AddAnalysisRequest, client):
731
                base_url = api.get_url(client)
732
733
        if base_url:
734
            return {
735
                "id": "copy_to_new",
736
                "title": _("Copy to new"),
737
                "url": "{}/workflow_action?action=copy_to_new".format(base_url)
738
            }
739
740
        return None
741
742
    def can_create_worksheet(self):
743
        """Checks if the create worksheet transition should be rendered or not
744
        """
745
        # check add permission for Worksheets
746
        if not can_add_worksheet(self.portal):
747
            return False
748
749
        # only available for samples in received state and with at least one
750
        # analysis in unassigned status
751
        for sample in self.get_selected_samples():
752
            state = api.get_workflow_status_of(sample)
753
            if state not in ["sample_received"]:
754
                return False
755
756
            # At least one analysis in unassigned status
757
            if not self.has_unassigned_analyses(sample):
758
                return False
759
760
        # restrict contexts to well known places
761
        if ISamples.providedBy(self.context):
762
            return True
763
        elif IBatch.providedBy(self.context):
764
            return True
765
        elif IClient.providedBy(self.context):
766
            return True
767
        else:
768
            return False
769
770
    def has_unassigned_analyses(self, sample):
771
        """Returns whether the sample passed in has at least one analysis in
772
        'unassigned' status
773
        """
774
        for analysis in sample.getAnalyses():
775
            status = api.get_review_status(analysis)
776
            if status == "unassigned":
777
                return True
778
        return False
779
780
    def get_selected_samples(self):
781
        """Returns the selected samples
782
        """
783
        payload = self.get_json()
784
        uids = payload.get("selected_uids", [])
785
        return map(api.get_object, uids)
786
787
    @property
788
    def is_printing_workflow_enabled(self):
789
        setup = api.get_setup()
790
        return setup.getPrintingWorkflowEnabled()
791
792
    def str_date(self, date, long_format=1, default=""):
793
        if not date:
794
            return default
795
        return self.ulocalized_time(date, long_format=long_format)
796
797
    def to_datetime_input_value(self, date):
798
        """Converts to a compatible datetime format
799
        """
800
        if not isinstance(date, DateTime):
801
            return ""
802
        return dtime.date_to_string(date, fmt="%Y-%m-%d %H:%M")
803
804
    def getDefaultAddCount(self):
805
        return self.context.bika_setup.getDefaultNumberOfARsToAdd()
806
807
    @property
808
    def show_partitions(self):
809
        if self.flat_listing:
810
            return False
811
        if api.get_current_client():
812
            # If current user is a client contact, delegate to ShowPartitions
813
            return api.get_setup().getShowPartitions()
814
        return True
815
816
    @property
817
    def flat_listing(self):
818
        return self.review_state.get("flat_listing", False)
819