Passed
Push — 2.x ( ac9ec3...712fca )
by Jordi
07:17
created

ajaxAnalysisRequestAddView.create_samples()   B

Complexity

Conditions 7

Size

Total Lines 40
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 40
rs 7.904
c 0
b 0
f 0
cc 7
nop 2
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 json
22
from collections import OrderedDict
23
from datetime import datetime
24
from datetime import timedelta
25
26
import six
27
import transaction
28
from bika.lims import POINTS_OF_CAPTURE
29
from bika.lims import api
30
from bika.lims import bikaMessageFactory as _
31
from bika.lims import logger
32
from bika.lims.api.analysisservice import get_calculation_dependencies_for
33
from bika.lims.api.security import check_permission
34
from bika.lims.decorators import returns_json
35
from bika.lims.interfaces import IAddSampleConfirmation
36
from bika.lims.interfaces import IAddSampleFieldsFlush
37
from bika.lims.interfaces import IAddSampleObjectInfo
38
from bika.lims.interfaces import IAddSampleRecordsValidator
39
from bika.lims.interfaces import IGetDefaultFieldValueARAddHook
40
from bika.lims.interfaces.field import IUIDReferenceField
41
from bika.lims.utils.analysisrequest import create_analysisrequest as crar
42
from BTrees.OOBTree import OOBTree
43
from DateTime import DateTime
44
from plone import protect
45
from plone.memoize import view as viewcache
46
from plone.memoize.volatile import DontCache
47
from plone.memoize.volatile import cache
48
from plone.protect.interfaces import IDisableCSRFProtection
49
from Products.Archetypes.interfaces import IField
50
from Products.CMFPlone.utils import safe_unicode
51
from Products.Five.browser import BrowserView
52
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
53
from senaite.core.api import dtime
54
from senaite.core.catalog import CONTACT_CATALOG
55
from senaite.core.catalog import SETUP_CATALOG
56
from senaite.core.interfaces import IAfterCreateSampleHook
57
from senaite.core.p3compat import cmp
58
from senaite.core.permissions import TransitionMultiResults
59
from senaite.core.registry import get_registry_record
60
from zope.annotation.interfaces import IAnnotations
61
from zope.component import getAdapters
62
from zope.component import queryAdapter
63
from zope.component import subscribers
64
from zope.i18n.locales import locales
65
from zope.i18nmessageid import Message
66
from zope.interface import alsoProvides
67
from zope.interface import implements
68
from zope.publisher.interfaces import IPublishTraverse
69
70
AR_CONFIGURATION_STORAGE = "bika.lims.browser.analysisrequest.manage.add"
71
SKIP_FIELD_ON_COPY = ["Sample", "PrimaryAnalysisRequest", "Remarks",
72
                      "NumSamples", "_ARAttachment"]
73
NO_COPY_FIELDS = ["_ARAttachment"]
74
ALLOW_MULTI_PASTE_WIDGET_TYPES = [
75
    # disable paste functionality for date fields, see:
76
    # https://github.com/senaite/senaite.core/pull/2658#discussion_r1946229751
77
    # "senaite.core.browser.widgets.datetimewidget.DateTimeWidget",
78
    "senaite.core.browser.widgets.referencewidget.ReferenceWidget",
79
    "Products.Archetypes.Widget.StringWidget",
80
    "Products.Archetypes.Widget.BooleanWidget",
81
    "bika.lims.browser.widgets.priorityselectionwidget.PrioritySelectionWidget",  # noqa
82
    "bika.lims.browser.widgets.remarkswidget.RemarksWidget",
83
    "bika.lims.browser.widgets.selectionwidget.SelectionWidget",
84
]
85
86
87
def cache_key(method, self, obj):
88
    if obj is None:
89
        raise DontCache
90
    return api.get_cache_key(obj)
91
92
93
class AnalysisRequestAddView(BrowserView):
94
    """AR Add view
95
    """
96
    template = ViewPageTemplateFile("templates/ar_add2.pt")
97
98
    def __init__(self, context, request):
99
        super(AnalysisRequestAddView, self).__init__(context, request)
100
        # disable CSRF protection
101
        alsoProvides(request, IDisableCSRFProtection)
102
        self.request = request
103
        self.context = context
104
        self.fieldvalues = {}
105
        self.tmp_ar = None
106
107
    def __call__(self):
108
        self.portal = api.get_portal()
109
        self.portal_url = self.portal.absolute_url()
110
        self.setup = api.get_setup()
111
        self.came_from = "add"
112
        self.tmp_ar = self.get_ar()
113
        self.ar_count = self.get_ar_count()
114
        self.fieldvalues = self.generate_fieldvalues(self.ar_count)
115
        self.ShowPrices = self.setup.getShowPrices()
116
        self.theme = api.get_view("senaite_theme")
117
        self.icon = self.theme.icon_url("Sample")
118
        logger.debug("*** Prepared data for {} ARs ***".format(self.ar_count))
119
        return self.template()
120
121
    def get_view_url(self):
122
        """Return the current view url including request parameters
123
        """
124
        request = self.request
125
        url = request.getURL()
126
        qs = request.getHeader("query_string")
127
        if not qs:
128
            return url
129
        return "{}?{}".format(url, qs)
130
131
    # N.B.: We are caching here persistent objects!
132
    #       It should be safe to do this but only on the view object,
133
    #       because it get recreated per request (transaction border).
134
    @viewcache.memoize
135
    def get_object_by_uid(self, uid):
136
        """Get the object by UID
137
        """
138
        logger.debug("get_object_by_uid::UID={}".format(uid))
139
        obj = api.get_object_by_uid(uid, None)
140
        if obj is None:
141
            logger.warn("!! No object found for UID '%s' !!" % uid)
142
        return obj
143
144
    @viewcache.memoize
145
    def analyses_required(self):
146
        """Check if analyses are required
147
        """
148
        setup = api.get_setup()
149
        return setup.getSampleAnalysesRequired()
150
151
    def get_currency(self):
152
        """Returns the configured currency
153
        """
154
        setup = api.get_setup()
155
        currency = setup.getCurrency()
156
        currencies = locales.getLocale("en").numbers.currencies
157
        return currencies[currency]
158
159
    def get_ar_count(self):
160
        """Return the ar_count request paramteter
161
        """
162
        ar_count = 1
163
        try:
164
            ar_count = int(self.request.form.get("ar_count", 1))
165
        except (TypeError, ValueError):
166
            ar_count = 1
167
        return ar_count
168
169
    def get_ar(self):
170
        """Create a temporary AR to fetch the fields from
171
        """
172
        if not self.tmp_ar:
173
            logger.debug("*** CREATING TEMPORARY AR ***")
174
            self.tmp_ar = self.context.restrictedTraverse(
175
                "portal_factory/AnalysisRequest/Request new analyses")
176
        return self.tmp_ar
177
178
    def get_ar_schema(self):
179
        """Return the AR schema
180
        """
181
        logger.debug("*** GET AR SCHEMA ***")
182
        ar = self.get_ar()
183
        return ar.Schema()
184
185
    def get_ar_fields(self):
186
        """Return the AR schema fields (including extendend fields)
187
        """
188
        logger.debug("*** GET AR FIELDS ***")
189
        schema = self.get_ar_schema()
190
        return schema.fields()
191
192
    def get_fieldname(self, field, arnum):
193
        """Generate a new fieldname with a '-<arnum>' suffix
194
        """
195
        name = field.getName()
196
        # ensure we have only *one* suffix
197
        base_name = name.split("-")[0]
198
        suffix = "-{}".format(arnum)
199
        return "{}{}".format(base_name, suffix)
200
201
    def get_input_widget(self, fieldname, arnum=0, **kw):
202
        """Get the field widget of the AR in column <arnum>
203
204
        :param fieldname: The base fieldname
205
        :type fieldname: string
206
        """
207
208
        # temporary AR Context
209
        context = self.get_ar()
210
        # request = self.request
211
        schema = context.Schema()
212
213
        # get original field in the schema from the base_fieldname
214
        base_fieldname = fieldname.split("-")[0]
215
        field = context.getField(base_fieldname)
216
217
        # fieldname with -<arnum> suffix
218
        new_fieldname = self.get_fieldname(field, arnum)
219
        new_field = field.copy(name=new_fieldname)
220
221
        # get the default value for this field
222
        fieldvalues = self.fieldvalues
223
        field_value = fieldvalues.get(new_fieldname)
224
        # request_value = request.form.get(new_fieldname)
225
        # value = request_value or field_value
226
        value = field_value
227
228
        def getAccessor(instance):
229
            def accessor(**kw):
230
                return value
231
            return accessor
232
233
        # inject the new context for the widget renderer
234
        # see: Products.Archetypes.Renderer.render
235
        kw["here"] = context
236
        kw["context"] = context
237
        kw["fieldName"] = new_fieldname
238
239
        # make the field available with this name
240
        # XXX: This is a hack to make the widget available in the template
241
        schema._fields[new_fieldname] = new_field
242
        new_field.getAccessor = getAccessor
243
        new_field.getEditAccessor = getAccessor
244
245
        # set the default value
246
        form = dict()
247
        form[new_fieldname] = value
248
        self.request.form.update(form)
249
        logger.debug("get_input_widget: fieldname={} arnum={} "
250
                     "-> new_fieldname={} value={}".format(
251
                         fieldname, arnum, new_fieldname, value))
252
        widget = context.widget(new_fieldname, **kw)
253
        return widget
254
255
    def get_copy_from(self):
256
        """Returns a mapping of UID index -> AR object
257
        """
258
        # Create a mapping of source ARs for copy
259
        copy_from = self.request.form.get("copy_from", "").split(",")
260
        # clean out empty strings
261
        copy_from_uids = filter(lambda x: x, copy_from)
262
        out = dict().fromkeys(range(len(copy_from_uids)))
263
        for n, uid in enumerate(copy_from_uids):
264
            ar = self.get_object_by_uid(uid)
265
            if ar is None:
266
                continue
267
            out[n] = ar
268
        logger.info("get_copy_from: uids={}".format(copy_from_uids))
269
        return out
270
271
    def get_default_value(self, field, context, arnum):
272
        """Get the default value of the field
273
        """
274
        name = field.getName()
275
        default = field.getDefault(context)
276
        if name == "Batch":
277
            batch = self.get_batch()
278
            if batch is not None:
279
                default = batch
280
        if name == "Client":
281
            client = self.get_client()
282
            if client is not None:
283
                default = client
284
        # only set default contact for first column
285
        if name == "Contact" and arnum == 0:
286
            contact = self.get_default_contact()
287
            if contact is not None:
288
                default = contact
289
        if name == "Sample":
290
            sample = self.get_sample()
291
            if sample is not None:
292
                default = sample
293
        # Querying for adapters to get default values from add-ons':
294
        # We don't know which fields the form will render since
295
        # some of them may come from add-ons. In order to obtain the default
296
        # value for those fields we take advantage of adapters. Adapters
297
        # registration should have the following format:
298
        # < adapter
299
        #   factory = ...
300
        #   for = "*"
301
        #   provides = "bika.lims.interfaces.IGetDefaultFieldValueARAddHook"
302
        #   name = "<fieldName>_default_value_hook"
303
        # / >
304
        hook_name = name + '_default_value_hook'
305
        adapter = queryAdapter(
306
            self.request,
307
            name=hook_name,
308
            interface=IGetDefaultFieldValueARAddHook)
309
        if adapter is not None:
310
            default = adapter(self.context)
311
        logger.debug("get_default_value: context={} field={} value={} arnum={}"
312
                     .format(context, name, default, arnum))
313
        return default
314
315
    def get_field_value(self, field, context):
316
        """Get the stored value of the field
317
        """
318
        name = field.getName()
319
        value = context.getField(name).get(context)
320
        logger.debug("get_field_value: context={} field={} value={}".format(
321
            context, name, value))
322
        return value
323
324
    def get_client(self):
325
        """Returns the Client
326
        """
327
        context = self.context
328
        parent = api.get_parent(context)
329
        if context.portal_type == "Client":
330
            return context
331
        elif parent.portal_type == "Client":
332
            return parent
333
        elif context.portal_type == "Batch":
334
            return context.getClient()
335
        elif parent.portal_type == "Batch":
336
            return context.getClient()
337
        return None
338
339
    def get_sample(self):
340
        """Returns the Sample
341
        """
342
        context = self.context
343
        if context.portal_type == "Sample":
344
            return context
345
        return None
346
347
    def get_batch(self):
348
        """Returns the Batch
349
        """
350
        context = self.context
351
        parent = api.get_parent(context)
352
        if context.portal_type == "Batch":
353
            return context
354
        elif parent.portal_type == "Batch":
355
            return parent
356
        return None
357
358
    def get_parent_ar(self, ar):
359
        """Returns the parent AR
360
        """
361
        parent = ar.getParentAnalysisRequest()
362
363
        # Return immediately if we have no parent
364
        if parent is None:
365
            return None
366
367
        # Walk back the chain until we reach the source AR
368
        while True:
369
            pparent = parent.getParentAnalysisRequest()
370
            if pparent is None:
371
                break
372
            # remember the new parent
373
            parent = pparent
374
375
        return parent
376
377
    def generate_fieldvalues(self, count=1):
378
        """Returns a mapping of '<fieldname>-<count>' to the default value
379
        of the field or the field value of the source AR
380
        """
381
        ar_context = self.get_ar()
382
383
        # mapping of UID index to AR objects {1: <AR1>, 2: <AR2> ...}
384
        copy_from = self.get_copy_from()
385
386
        out = {}
387
        # the original schema fields of an AR (including extended fields)
388
        fields = self.get_ar_fields()
389
390
        # generate fields for all requested ARs
391
        for arnum in range(count):
392
            source = copy_from.get(arnum)
393
            parent = None
394
            if source is not None:
395
                parent = self.get_parent_ar(source)
396
397
            for field in fields:
398
                value = None
399
                fieldname = field.getName()
400
                if source and fieldname not in SKIP_FIELD_ON_COPY:
401
                    # get the field value stored on the source
402
                    context = parent or source
403
                    value = self.get_field_value(field, context)
404
                else:
405
                    # get the default value of this field
406
                    value = self.get_default_value(
407
                        field, ar_context, arnum=arnum)
408
409
                # Filter out analyses in certain workflow states when copying
410
                if fieldname == "Analyses" and value:
411
                    skip_states = self.get_skip_analyses_states()
412
                    value = self.filter_objs_with_states(
413
                        value, filter_states=skip_states)
414
415
                    # Filter out partition analyses if configured
416
                    if self.get_skip_partition_analyses() and source:
417
                        value = self.filter_partition_analyses(value, source)
418
419
                # store the value on the new fieldname
420
                new_fieldname = self.get_fieldname(field, arnum)
421
                out[new_fieldname] = value
422
423
        return out
424
425
    def get_default_contact(self, client=None):
426
        """Logic refactored from JavaScript:
427
428
        * If client only has one contact, and the analysis request comes from
429
        * a client, then Auto-complete first Contact field.
430
        * If client only has one contect, and the analysis request comes from
431
        * a batch, then Auto-complete all Contact field.
432
433
        :returns: The default contact for the AR
434
        :rtype: Client object or None
435
        """
436
        catalog = api.get_tool(CONTACT_CATALOG)
437
        client = client or self.get_client()
438
        path = api.get_path(self.context)
439
        if client:
440
            path = api.get_path(client)
441
        query = {
442
            "portal_type": "Contact",
443
            "path": {
444
                "query": path,
445
                "depth": 1
446
            },
447
            "is_active": True,
448
        }
449
        contacts = catalog(query)
450
        if len(contacts) == 1:
451
            return api.get_object(contacts[0])
452
        elif client == api.get_current_client():
453
            # Current user is a Client contact. Use current contact
454
            current_user = api.get_current_user()
455
            return api.get_user_contact(current_user,
456
                                        contact_types=["Contact"])
457
458
        return None
459
460
    def getMemberDiscountApplies(self):
461
        """Return if the member discount applies for this client
462
463
        :returns: True if member discount applies for the client
464
        :rtype: bool
465
        """
466
        client = self.get_client()
467
        if client is None:
468
            return False
469
        return client.getMemberDiscountApplies()
470
471
    def is_field_visible(self, field):
472
        """Check if the field is visible
473
        """
474
        context = self.context
475
        fieldname = field.getName()
476
477
        # hide the Client field on client and batch contexts
478
        if fieldname == "Client" and context.portal_type in ("Client", ):
479
            return False
480
481
        # hide the Batch field on batch contexts
482
        if fieldname == "Batch" and context.portal_type in ("Batch", ):
483
            return False
484
485
        return True
486
487
    def get_fields_with_visibility(self, visibility, mode="add"):
488
        """Return the AR fields with the current visibility
489
        """
490
        ar = self.get_ar()
491
        mv = api.get_view("ar_add_manage", context=ar)
492
        mv.get_field_order()
493
494
        out = []
495
        for field in mv.get_fields_with_visibility(visibility, mode):
496
            # check custom field condition
497
            visible = self.is_field_visible(field)
498
            if visible is False and visibility != "hidden":
499
                continue
500
            out.append(field)
501
        return out
502
503
    def get_service_categories(self, restricted=True):
504
        """Return all service categories in the right order
505
506
        :param restricted: Client settings restrict categories
507
        :type restricted: bool
508
        :returns: Category catalog results
509
        :rtype: brains
510
        """
511
        bsc = api.get_tool("senaite_catalog_setup")
512
        query = {
513
            "portal_type": "AnalysisCategory",
514
            "is_active": True,
515
            "sort_on": "sortable_title",
516
        }
517
        categories = bsc(query)
518
        client = self.get_client()
519
        if client and restricted:
520
            restricted_categories = client.getRestrictedCategories()
521
            restricted_category_ids = map(
522
                lambda c: c.getId(), restricted_categories)
523
            # keep correct order of categories
524
            if restricted_category_ids:
525
                categories = filter(
526
                    lambda c: c.getId in restricted_category_ids, categories)
0 ignored issues
show
introduced by
The variable restricted_category_ids does not seem to be defined in case client and restricted on line 519 is False. Are you sure this can never be the case?
Loading history...
527
        return categories
528
529
    def get_points_of_capture(self):
530
        items = POINTS_OF_CAPTURE.items()
531
        return OrderedDict(items)
532
533
    def get_services(self, poc="lab"):
534
        """Return all Services
535
536
        :param poc: Point of capture (lab/field)
537
        :type poc: string
538
        :returns: Mapping of category -> list of services
539
        :rtype: dict
540
        """
541
        bsc = api.get_tool("senaite_catalog_setup")
542
        query = {
543
            "portal_type": "AnalysisService",
544
            "point_of_capture": poc,
545
            "is_active": True,
546
            "sort_on": "sortable_title",
547
        }
548
        services = bsc(query)
549
        categories = self.get_service_categories(restricted=False)
550
        analyses = {key: [] for key in map(lambda c: c.Title, categories)}
551
552
        # append the empty category as well
553
        analyses[""] = []
554
555
        for brain in services:
556
            category = self.get_category_title(brain)
557
            if category in analyses:
558
                analyses[category].append(brain)
559
        return analyses
560
561
    def get_category_title(self, service):
562
        """Return the title of the category the service is assigned to
563
        """
564
        service = api.get_object(service)
565
        cat_uid = service.getRawCategory()
566
        if not cat_uid:
567
            return ""
568
        cat = self.get_object_by_uid(cat_uid)
569
        return api.get_title(cat)
570
571
    @cache(cache_key)
572
    def get_service_uid_from(self, analysis):
573
        """Return the service from the analysis
574
        """
575
        analysis = api.get_object(analysis)
576
        return api.get_uid(analysis.getAnalysisService())
577
578
    def is_service_selected(self, service):
579
        """Checks if the given service is selected by one of the ARs.
580
        This is used to make the whole line visible or not.
581
        """
582
        service_uid = api.get_uid(service)
583
        for arnum in range(self.ar_count):
584
            analyses = self.fieldvalues.get("Analyses-{}".format(arnum))
585
            if not analyses:
586
                continue
587
            service_uids = map(self.get_service_uid_from, analyses)
588
            if service_uid in service_uids:
589
                return True
590
        return False
591
592
    def show_copy_button_for(self, field=None):
593
        """ Show copy/paste button for field
594
        """
595
        if self.ar_count <= 1:
596
            return False
597
        if field and field.getName() in NO_COPY_FIELDS:
598
            return False
599
        return True
600
601
    def show_paste_button_for(self, field=None):
602
        """ Show paste button for field
603
        """
604
        allowed = self.get_allowed_multi_paste_fields()
605
        if allowed:
606
            field_name = field.getName()
607
            return field_name in allowed
608
609
        # fallback to widget type based lookup
610
        try:
611
            widget_type = field.widget.getType()
612
        except AttributeError:
613
            widget_type = None
614
        return widget_type in ALLOW_MULTI_PASTE_WIDGET_TYPES
615
616
    def get_allowed_multi_paste_fields(self):
617
        """Returns a list of fields that allow multi paste
618
        """
619
        key = "sample_add_form_allow_multi_paste"
620
        record = get_registry_record(key)
621
        if not record:
622
            return []
623
        # convert to plain list to avoid persistent references
624
        return list(record)
625
626
    def get_skip_analyses_states(self):
627
        """Returns a list of analyses WF states to skip on copy
628
        """
629
        key = "sample_add_form_skip_analyses_in_states"
630
        record = get_registry_record(key)
631
        if not record:
632
            return []
633
        # convert to plain list to avoid persistent references
634
        return list(record)
635
636
    def get_skip_partition_analyses(self):
637
        """Returns whether to skip partition analyses on copy
638
        """
639
        key = "sample_add_form_skip_partition_analyses"
640
        record = get_registry_record(key)
641
        return bool(record)
642
643
    def filter_objs_with_states(self, objs, filter_states=None):
644
        """Filter out objects that are in the given workflow states
645
646
        :param objs: List of objects to filter
647
        :param filter_states: List of workflow state IDs to exclude
648
        :return: List of objects not in the filter_states
649
        """
650
        if not filter_states:
651
            return objs
652
        if not isinstance(filter_states, (list, tuple)):
653
            return objs
654
        filtered = []
655
        for obj in objs:
656
            status = api.get_review_status(obj)
657
            if status not in filter_states:
658
                filtered.append(obj)
659
        return filtered
660
661
    def filter_partition_analyses(self, analyses, source):
662
        """Filter out analyses that belong to partitions
663
664
        Only keeps analyses that directly belong to the source sample.
665
        Analyses from partitions are identified by checking if they are
666
        direct children of the source sample.
667
668
        :param analyses: List of analysis brains/objects to filter
669
        :param source: The source sample object
670
        :return: List of analyses that belong directly to the source sample
671
        """
672
        if not analyses or not source:
673
            return analyses
674
675
        # Get the physical paths of analyses that directly belong to the source
676
        source_analysis_paths = set()
677
        for analysis in source.objectValues("Analysis"):
678
            source_analysis_paths.add(api.get_path(analysis))
679
680
        # Filter the analyses to keep only those in the source
681
        filtered = []
682
        for analysis in analyses:
683
            # Get the object if it's a brain
684
            analysis_path = api.get_path(analysis)
685
            if analysis_path in source_analysis_paths:
686
                filtered.append(analysis)
687
688
        return filtered
689
690
691
class AnalysisRequestManageView(BrowserView):
692
    """AR Manage View
693
    """
694
    template = ViewPageTemplateFile("templates/ar_add_manage.pt")
695
696
    def __init__(self, context, request):
697
        # disable CSRF protection
698
        alsoProvides(request, IDisableCSRFProtection)
699
        self.context = context
700
        self.request = request
701
        self.tmp_ar = None
702
703
    def __call__(self):
704
        protect.CheckAuthenticator(self.request.form)
705
        form = self.request.form
706
        if form.get("submitted", False) and form.get("save", False):
707
            order = form.get("order")
708
            self.set_field_order(order)
709
            visibility = form.get("visibility")
710
            self.set_field_visibility(visibility)
711
        if form.get("submitted", False) and form.get("reset", False):
712
            self.flush()
713
        return self.template()
714
715
    def get_ar(self):
716
        if not self.tmp_ar:
717
            self.tmp_ar = self.context.restrictedTraverse(
718
                "portal_factory/AnalysisRequest/Request new analyses")
719
        return self.tmp_ar
720
721
    def get_annotation(self):
722
        setup = api.get_setup()
723
        return IAnnotations(setup)
724
725
    @property
726
    def storage(self):
727
        annotation = self.get_annotation()
728
        if annotation.get(AR_CONFIGURATION_STORAGE) is None:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable AR_CONFIGURATION_STORAGE does not seem to be defined.
Loading history...
729
            annotation[AR_CONFIGURATION_STORAGE] = OOBTree()
730
        return annotation[AR_CONFIGURATION_STORAGE]
731
732
    def flush(self):
733
        annotation = self.get_annotation()
734
        if annotation.get(AR_CONFIGURATION_STORAGE) is not None:
735
            del annotation[AR_CONFIGURATION_STORAGE]
736
737
    def set_field_order(self, order):
738
        self.storage.update({"order": order})
739
740
    def get_field_order(self):
741
        order = self.storage.get("order")
742
        if order is None:
743
            return map(lambda f: f.getName(), self.get_fields())
744
        return order
745
746
    def set_field_visibility(self, visibility):
747
        self.storage.update({"visibility": visibility})
748
749
    def get_field_visibility(self):
750
        return self.storage.get("visibility")
751
752
    def is_field_visible(self, field):
753
        if field.required:
754
            return True
755
        visibility = self.get_field_visibility()
756
        if visibility is None:
757
            return True
758
        return visibility.get(field.getName(), True)
759
760
    def get_field(self, name):
761
        """Get AR field by name
762
        """
763
        ar = self.get_ar()
764
        return ar.getField(name)
765
766
    def get_fields(self):
767
        """Return all AR fields
768
        """
769
        ar = self.get_ar()
770
        return ar.Schema().fields()
771
772
    def get_sorted_fields(self):
773
        """Return the sorted fields
774
        """
775
        inf = float("inf")
776
        order = self.get_field_order()
777
778
        def field_cmp(field1, field2):
779
            _n1 = field1.getName()
780
            _n2 = field2.getName()
781
            _i1 = _n1 in order and order.index(_n1) + 1 or inf
782
            _i2 = _n2 in order and order.index(_n2) + 1 or inf
783
            return cmp(_i1, _i2)
784
785
        return sorted(self.get_fields(), cmp=field_cmp)
786
787
    def get_fields_with_visibility(self, visibility="edit", mode="add"):
788
        """Return the fields with visibility
789
        """
790
        fields = self.get_sorted_fields()
791
792
        out = []
793
794
        for field in fields:
795
            v = field.widget.isVisible(
796
                self.context, mode, default='invisible', field=field)
797
798
            if self.is_field_visible(field) is False:
799
                v = "hidden"
800
801
            visibility_guard = True
802
            # visibility_guard is a widget field defined in the schema in order
803
            # to know the visibility of the widget when the field is related to
804
            # a dynamically changing content such as workflows. For instance
805
            # those fields related to the workflow will be displayed only if
806
            # the workflow is enabled, otherwise they should not be shown.
807
            if 'visibility_guard' in dir(field.widget):
808
                visibility_guard = eval(field.widget.visibility_guard)
809
            if v == visibility and visibility_guard:
810
                out.append(field)
811
812
        return out
813
814
815
class ajaxAnalysisRequestAddView(AnalysisRequestAddView):
816
    """Ajax helpers for the analysis request add form
817
    """
818
    implements(IPublishTraverse)
819
820
    def __init__(self, context, request):
821
        super(ajaxAnalysisRequestAddView, self).__init__(context, request)
822
        self.context = context
823
        self.request = request
824
        self.traverse_subpath = []
825
        # Errors are aggregated here, and returned together to the browser
826
        self.errors = {}
827
828
    def publishTraverse(self, request, name):
829
        """ get's called before __call__ for each path name
830
        """
831
        self.traverse_subpath.append(name)
832
        return self
833
834
    @returns_json
835
    def __call__(self):
836
        """Dispatch the path to a method and return JSON.
837
        """
838
        protect.CheckAuthenticator(self.request.form)
839
        protect.PostOnly(self.request.form)
840
841
        if len(self.traverse_subpath) != 1:
842
            return self.error("Not found", status=404)
843
        func_name = "ajax_{}".format(self.traverse_subpath[0])
844
        func = getattr(self, func_name, None)
845
        if func is None:
846
            return self.error("Invalid function", status=400)
847
        return func()
848
849
    def error(self, message, status=500, **kw):
850
        """Set a JSON error object and a status to the response
851
        """
852
        self.request.response.setStatus(status)
853
        result = {"success": False, "errors": message}
854
        result.update(kw)
855
        return result
856
857
    def to_iso_date(self, dt):
858
        """Return the ISO representation of a date object
859
        """
860
        if dt is None:
861
            return ""
862
        if isinstance(dt, DateTime):
863
            return dt.ISO8601()
864
        if isinstance(dt, datetime):
865
            return dt.isoformat()
866
        raise TypeError("{} is neiter an instance of DateTime nor datetime"
867
                        .format(repr(dt)))
868
869
    @viewcache.memoize
870
    def is_uid_reference_field(self, fieldname):
871
        """Checks if the field is a UID reference field
872
        """
873
        schema = self.get_ar_schema()
874
        field = schema.get(fieldname)
875
        if field is None:
876
            return False
877
        return IUIDReferenceField.providedBy(field)
878
879
    @viewcache.memoize
880
    def is_multi_reference_field(self, fieldname):
881
        """Checks if the field is a multi UID reference field
882
        """
883
        if not self.is_uid_reference_field(fieldname):
884
            return False
885
        schema = self.get_ar_schema()
886
        field = schema.get(fieldname)
887
        return getattr(field, "multiValued", False)
888
889
    def get_records(self):
890
        """Returns a list of AR records
891
892
        Fields coming from `request.form` have a number prefix, e.g. Contact-0.
893
        Fields with the same suffix number are grouped together in a record.
894
        Each record represents the data for one column in the AR Add form and
895
        contains a mapping of the fieldName (w/o prefix) -> value.
896
897
        Example:
898
        [{"Contact": "Rita Mohale", ...}, {Contact: "Neil Standard"} ...]
899
        """
900
        form = self.request.form
901
        ar_count = self.get_ar_count()
902
903
        records = []
904
        # Group belonging AR fields together
905
        for arnum in range(ar_count):
906
            record = {}
907
            s1 = "-{}".format(arnum)
908
            keys = filter(lambda key: s1 in key, form.keys())
0 ignored issues
show
introduced by
The variable s1 does not seem to be defined in case the for loop on line 905 is not entered. Are you sure this can never be the case?
Loading history...
909
            for key in keys:
910
                new_key = key.replace(s1, "")
911
                value = form.get(key)
912
                if self.is_uid_reference_field(new_key):
913
                    # handle new UID reference fields that store references in
914
                    # a textarea (one UID per line)
915
                    uids = value.split("\r\n")
916
                    # remove empties
917
                    uids = list(filter(None, uids))
918
                    if self.is_multi_reference_field(new_key):
919
                        value = uids
920
                    else:
921
                        value = uids[0] if len(uids) > 0 else ""
922
                record[new_key] = value
923
            records.append(record)
924
        return records
925
926
    def get_uids_from_record(self, record, key):
927
        """Returns a list of parsed UIDs from a single form field identified by
928
        the given key.
929
930
        A form field of an UID reference can contain an empty value, a single
931
        UID or multiple UIDs separated by a \r\n.
932
933
        This method parses the UID value and returns a list of non-empty UIDs.
934
        """
935
        if not self.is_uid_reference_field(key):
936
            return []
937
        value = record.get(key, None)
938
        if not value:
939
            return []
940
        if api.is_string(value):
941
            value = value.split("\r\n")
942
        return list(filter(None, value))
943
944
    @cache(cache_key)
945
    def get_base_info(self, obj):
946
        """Returns the base info of an object
947
        """
948
        if obj is None:
949
            return {}
950
951
        info = {
952
            "id": api.get_id(obj),
953
            "uid": api.get_uid(obj),
954
            "url": api.get_url(obj),
955
            "title": api.get_title(obj),
956
            "field_values": {},
957
            "filter_queries": {},
958
        }
959
960
        return info
961
962
    @cache(cache_key)
963
    def get_client_info(self, obj):
964
        """Returns the client info of an object
965
        """
966
        info = self.get_base_info(obj)
967
968
        # Set the default contact, but only if empty. The Contact field is
969
        # flushed each time the Client changes, so we can assume that if there
970
        # is a selected contact, it belongs to current client already
971
        default_contact = self.get_default_contact(client=obj)
972
        if default_contact:
973
            contact_info = self.get_contact_info(default_contact)
974
            contact_info.update({"if_empty": True})
975
            info["field_values"].update({
976
                "Contact": contact_info
977
            })
978
979
        # Set default CC Email field
980
        info["field_values"].update({
981
            "CCEmails": {"value": obj.getCCEmails(), "if_empty": True}
982
        })
983
984
        return info
985
986
    @cache(cache_key)
987
    def get_contact_info(self, obj):
988
        """Returns the client info of an object
989
        """
990
991
        info = self.get_base_info(obj)
992
        fullname = obj.getFullname()
993
        email = obj.getEmailAddress()
994
995
        # Note: It might get a circular dependency when calling:
996
        #       map(self.get_contact_info, obj.getCCContact())
997
        cccontacts = []
998
        for contact in obj.getCCContact():
999
            uid = api.get_uid(contact)
1000
            fullname = contact.getFullname()
1001
            email = contact.getEmailAddress()
1002
            cccontacts.append({
1003
                "uid": uid,
1004
                "title": fullname,
1005
                "fullname": fullname,
1006
                "email": email
1007
            })
1008
1009
        info.update({
1010
            "fullname": fullname,
1011
            "email": email,
1012
            "field_values": {
1013
                "CCContact": cccontacts
1014
            },
1015
        })
1016
1017
        return info
1018
1019
    @cache(cache_key)
1020
    def get_service_info(self, obj):
1021
        """Returns the info for a Service
1022
        """
1023
        info = self.get_base_info(obj)
1024
1025
        info.update({
1026
            "short_title": obj.getShortTitle(),
1027
            "scientific_name": obj.getScientificName(),
1028
            "unit": obj.getUnit(),
1029
            "keyword": obj.getKeyword(),
1030
            "methods": map(self.get_method_info, obj.getMethods()),
1031
            "calculation": self.get_calculation_info(obj.getCalculation()),
1032
            "price": obj.getPrice(),
1033
            "currency_symbol": self.get_currency().symbol,
1034
            "accredited": obj.getAccredited(),
1035
            "category": obj.getCategoryTitle(),
1036
            "poc": obj.getPointOfCapture(),
1037
            "conditions": self.get_conditions_info(obj),
1038
            "max_holding_time": obj.getMaxHoldingTime(),
1039
        })
1040
1041
        dependencies = get_calculation_dependencies_for(obj).values()
1042
        info["dependencies"] = map(self.get_base_info, dependencies)
1043
        return info
1044
1045
    @cache(cache_key)
1046
    def get_template_info(self, obj):
1047
        """Returns the info for a Template
1048
        """
1049
        client = self.get_client()
1050
        client_uid = api.get_uid(client) if client else ""
1051
1052
        sample_type = obj.getSampleType()
1053
        sample_type_uid = api.get_uid(sample_type) if sample_type else ""
1054
        sample_type_title = sample_type.Title() if sample_type else ""
1055
1056
        sample_point = obj.getSamplePoint()
1057
        sample_point_uid = api.get_uid(sample_point) if sample_point else ""
1058
        sample_point_title = sample_point.Title() if sample_point else ""
1059
1060
        service_uids = []
1061
        analyses_partitions = {}
1062
        services = obj.getRawServices()
1063
1064
        for record in services:
1065
            service_uid = record.get("uid")
1066
            service_uids.append(service_uid)
1067
            analyses_partitions[service_uid] = record.get("part_id")
1068
1069
        info = self.get_base_info(obj)
1070
        info.update({
1071
            "analyses_partitions": analyses_partitions,
1072
            "client_uid": client_uid,
1073
            "composite": obj.getComposite(),
1074
            "partitions": obj.getPartitions(),
1075
            "sample_point_title": sample_point_title,
1076
            "sample_point_uid": sample_point_uid,
1077
            "sample_type_title": sample_type_title,
1078
            "sample_type_uid": sample_type_uid,
1079
            "service_uids": service_uids,
1080
        })
1081
        return info
1082
1083
    @cache(cache_key)
1084
    def get_profile_info(self, obj):
1085
        """Returns the info for a Profile
1086
        """
1087
        info = self.get_base_info(obj)
1088
        info.update({})
1089
        return info
1090
1091
    @cache(cache_key)
1092
    def get_method_info(self, obj):
1093
        """Returns the info for a Method
1094
        """
1095
        info = self.get_base_info(obj)
1096
        info.update({})
1097
        return info
1098
1099
    @cache(cache_key)
1100
    def get_calculation_info(self, obj):
1101
        """Returns the info for a Calculation
1102
        """
1103
        info = self.get_base_info(obj)
1104
        info.update({})
1105
        return info
1106
1107
    @cache(cache_key)
1108
    def get_sampletype_info(self, obj):
1109
        """Returns the info for a Sample Type
1110
        """
1111
        info = self.get_base_info(obj)
1112
1113
        info.update({
1114
            "prefix": obj.getPrefix(),
1115
            "minimum_volume": obj.getMinimumVolume(),
1116
            "hazardous": obj.getHazardous(),
1117
            "retention_period": obj.getRetentionPeriod(),
1118
        })
1119
1120
        return info
1121
1122
    @cache(cache_key)
1123
    def get_primaryanalysisrequest_info(self, obj):
1124
        """Returns the info for a Primary Sample
1125
        """
1126
        info = self.get_base_info(obj)
1127
1128
        batch = obj.getBatch()
1129
        client = obj.getClient()
1130
        sample_type = obj.getSampleType()
1131
        sample_condition = obj.getSampleCondition()
1132
        storage_location = obj.getStorageLocation()
1133
        sample_point = obj.getSamplePoint()
1134
        container = obj.getContainer()
1135
        deviation = obj.getSamplingDeviation()
1136
        cccontacts = obj.getCCContact() or []
1137
        contact = obj.getContact()
1138
1139
        info.update({
1140
            "composite": obj.getComposite(),
1141
        })
1142
1143
        # Set the fields for which we want the value to be set automatically
1144
        # when the primary sample is selected
1145
        info["field_values"].update({
1146
            "Client": self.to_field_value(client),
1147
            "Contact": self.to_field_value(contact),
1148
            "CCContact": map(self.to_field_value, cccontacts),
1149
            "CCEmails": obj.getCCEmails(),
1150
            "Batch": self.to_field_value(batch),
1151
            "DateSampled": {"value": self.to_iso_date(obj.getDateSampled())},
1152
            "SamplingDate": {"value": self.to_iso_date(obj.getSamplingDate())},
1153
            "SampleType": self.to_field_value(sample_type),
1154
            "EnvironmentalConditions": {
1155
                "value": obj.getEnvironmentalConditions(),
1156
            },
1157
            "ClientSampleID": {"value": obj.getClientSampleID()},
1158
            "ClientReference": {"value": obj.getClientReference()},
1159
            "ClientOrderNumber": {"value": obj.getClientOrderNumber()},
1160
            "SampleCondition": self.to_field_value(sample_condition),
1161
            "SamplePoint": self.to_field_value(sample_point),
1162
            "StorageLocation": self.to_field_value(storage_location),
1163
            "Container": self.to_field_value(container),
1164
            "SamplingDeviation": self.to_field_value(deviation),
1165
            "Composite": {"value": obj.getComposite()}
1166
        })
1167
1168
        return info
1169
1170
    @cache(cache_key)
1171
    def get_conditions_info(self, obj):
1172
        conditions = obj.getConditions()
1173
        for condition in conditions:
1174
            choices = condition.get("choices", "")
1175
            options = filter(None, choices.split('|'))
1176
            if options:
1177
                condition.update({"options": options})
1178
        return conditions
1179
1180
    @cache(cache_key)
1181
    def to_field_value(self, obj):
1182
        return {
1183
            "uid": obj and api.get_uid(obj) or "",
1184
            "title": obj and api.get_title(obj) or ""
1185
        }
1186
1187
    def to_attachment_record(self, fileupload):
1188
        """Returns a dict-like structure with suitable information for the
1189
        proper creation of Attachment objects
1190
        """
1191
        if not fileupload.filename:
1192
            # ZPublisher.HTTPRequest.FileUpload is empty
1193
            return None
1194
        return {
1195
            "AttachmentFile": fileupload,
1196
            "AttachmentType": "",
1197
            "RenderInReport": False,
1198
            "AttachmentKeys": "",
1199
            "Service": "",
1200
        }
1201
1202
    def create_attachment(self, sample, attachment_record):
1203
        """Creates an attachment for the given sample with the information
1204
        provided in attachment_record
1205
        """
1206
        # create the attachment object
1207
        client = sample.getClient()
1208
        attachment = api.create(client, "Attachment", **attachment_record)
1209
        uid = attachment_record.get("Service")
1210
        if not uid:
1211
            # Link the attachment to the sample
1212
            sample.addAttachment(attachment)
1213
            return attachment
1214
1215
        # Link the attachment to analyses with this service uid
1216
        ans = sample.objectValues(spec="Analysis")
1217
        ans = filter(lambda an: an.getRawAnalysisService() == uid, ans)
1218
        for analysis in ans:
1219
            attachments = analysis.getRawAttachment()
1220
            analysis.setAttachment(attachments + [attachment])
1221
1222
        # Assign the attachment to the given condition
1223
        condition_title = attachment_record.get("Condition")
1224
        if not condition_title:
1225
            return attachment
1226
1227
        conditions = sample.getServiceConditions()
1228
        for condition in conditions:
1229
            is_uid = condition.get("uid") == uid
1230
            is_title = condition.get("title") == condition_title
1231
            is_file = condition.get("type") == "file"
1232
            if all([is_uid, is_title, is_file]):
1233
                condition["value"] = api.get_uid(attachment)
1234
        sample.setServiceConditions(conditions)
1235
        return attachment
1236
1237
    def ajax_get_global_settings(self):
1238
        """Returns the global Bika settings
1239
        """
1240
        setup = api.get_setup()
1241
        settings = {
1242
            "show_prices": setup.getShowPrices(),
1243
        }
1244
        return settings
1245
1246
    def ajax_is_reference_value_allowed(self):
1247
        """Checks if the current reference value is allowed for the query
1248
        """
1249
        payload = self.get_json()
1250
1251
        catalog = payload.get("catalog", "")
1252
        query = payload.get("query", {})
1253
        uids = payload.get("uids", [])
1254
        name = payload.get("name", "")
1255
        label = payload.get("label", "")
1256
        field = label or name
1257
1258
        # Remove sort_limit to prevent false negatives
1259
        query.pop("sort_limit", None)
1260
1261
        # Skip the catalog search if we can assume to be allowed
1262
        white_keys = ["portal_type", "sort_on", "sort_order", "is_active"]
1263
        if set(query.keys()).issubset(white_keys):
1264
            return {"allowed": True}
1265
1266
        if all([catalog, query, uids]):
1267
            # check if the current value is allowed for the new query
1268
            brains = api.search(query, catalog=catalog)
1269
            allowed_uids = list(map(api.get_uid, brains))
1270
            if set(uids).issubset(allowed_uids):
1271
                return {"allowed": True}
1272
1273
        message = {
1274
            "title": _("Field flushed"),
1275
            "text": _(u"The value of field '%s' was emptied. "
1276
                      u"Please select a new value." % api.safe_unicode(field)),
1277
        }
1278
1279
        return {
1280
            "allowed": False,
1281
            "message": message,
1282
        }
1283
1284
    def ajax_get_flush_settings(self):
1285
        """Returns the settings for fields flush
1286
1287
        NOTE: We automatically flush fields if the current value of a dependent
1288
              reference field is *not* allowed by the set new query.
1289
              -> see self.ajax_is_reference_value_allowed()
1290
              Therefore, it makes only sense for non-reference fields!
1291
        """
1292
        flush_settings = {
1293
            "Client": [
1294
            ],
1295
            "Contact": [
1296
            ],
1297
            "SampleType": [
1298
            ],
1299
            "PrimarySample": [
1300
                "EnvironmentalConditions",
1301
            ]
1302
        }
1303
1304
        # Maybe other add-ons have additional fields that require flushing
1305
        for name, ad in getAdapters((self.context,), IAddSampleFieldsFlush):
1306
            logger.info("Additional flush settings from {}".format(name))
1307
            additional_settings = ad.get_flush_settings()
1308
            for key, values in additional_settings.items():
1309
                new_values = flush_settings.get(key, []) + values
1310
                flush_settings[key] = list(set(new_values))
1311
1312
        return flush_settings
1313
1314
    def ajax_get_service(self):
1315
        """Returns the services information
1316
        """
1317
        uid = self.request.form.get("uid", None)
1318
1319
        if uid is None:
1320
            return self.error("Invalid UID", status=400)
1321
1322
        service = self.get_object_by_uid(uid)
1323
        if not service:
1324
            return self.error("Service not found", status=404)
1325
1326
        info = self.get_service_info(service)
1327
        return info
1328
1329
    def ajax_recalculate_records(self):
1330
        out = {}
1331
        records = self.get_records()
1332
        for num_sample, record in enumerate(records):
1333
            # Get reference fields metadata
1334
            metadata = self.get_record_metadata(record)
1335
1336
            # service_to_templates, template_to_services
1337
            templates_additional = self.get_template_additional_info(metadata)
1338
            metadata.update(templates_additional)
1339
1340
            # service_to_profiles, profiles_to_services
1341
            profiles_additional = self.get_profiles_additional_info(metadata)
1342
            metadata.update(profiles_additional)
1343
1344
            # services conducted beyond the holding time limit
1345
            beyond = self.get_services_beyond_holding_time(record)
1346
            metadata["beyond_holding_time"] = beyond
1347
1348
            # Set the metadata for current sample number (column)
1349
            out[num_sample] = metadata
1350
1351
        return out
1352
1353
    @viewcache.memoize
1354
    def get_services_max_holding_time(self):
1355
        """Returns a dict where the key is the uid of active services and the
1356
        value is a dict representing the maximum holding time in days, hours
1357
        and minutes. The dictionary only contains uids for services that have
1358
        a valid maximum holding time set
1359
        """
1360
        services = {}
1361
        query = {
1362
            "portal_type": "AnalysisService",
1363
            "point_of_capture": "lab",
1364
            "is_active": True,
1365
        }
1366
        brains = api.search(query, SETUP_CATALOG)
1367
        for brain in brains:
1368
            obj = api.get_object(brain)
1369
            max_holding_time = obj.getMaxHoldingTime()
1370
            if max_holding_time:
1371
                uid = api.get_uid(brain)
1372
                services[uid] = max_holding_time.copy()
1373
1374
        return services
1375
1376
    def get_services_beyond_holding_time(self, record):
1377
        """Return a list with the uids of the services that cannot be selected
1378
        because would be conducted past the holding time limit
1379
        """
1380
        # get the date to start count from
1381
        start_date = self.get_start_holding_date(record)
1382
        if not start_date:
1383
            return []
1384
1385
        # get the timezone of the start date for correct comparisons
1386
        tz = dtime.get_timezone(start_date)
1387
1388
        uids = []
1389
1390
        # get the max holding times grouped by service uid
1391
        services = self.get_services_max_holding_time()
1392
        for uid, max_holding_time in services.items():
1393
1394
            # calculate the maximum holding date
1395
            delta = timedelta(minutes=api.to_minutes(**max_holding_time))
1396
            max_holding_date = start_date + delta
1397
1398
            # TypeError: can't compare offset-naive and offset-aware datetimes
1399
            max_date = dtime.to_ansi(max_holding_date)
1400
            now = dtime.to_ansi(dtime.now(), timezone=tz)
1401
            if now > max_date:
1402
                uids.append(uid)
1403
1404
        return uids
1405
1406
    def get_start_holding_date(self, record):
1407
        """Returns the datetime used to calculate the holding time limit,
1408
        typically the sample collection date.
1409
        """
1410
        sampled = record.get("DateSampled")
1411
        return dtime.to_dt(sampled)
1412
1413
    def get_record_metadata(self, record):
1414
        """Returns the metadata for the record passed in
1415
        """
1416
        metadata = {}
1417
        extra_fields = {}
1418
        for key, value in record.items():
1419
            metadata_key = "{}_metadata".format(key.lower())
1420
            metadata[metadata_key] = {}
1421
1422
            if not value:
1423
                continue
1424
1425
            # Get objects information (metadata)
1426
            objs_info = self.get_objects_info(record, key)
1427
            objs_uids = map(lambda obj: obj["uid"], objs_info)
1428
            metadata[metadata_key] = dict(zip(objs_uids, objs_info))
1429
1430
            # Grab 'field_values' fields to be recalculated too
1431
            for obj_info in objs_info:
1432
                field_values = obj_info.get("field_values", {})
1433
                for field_name, field_value in field_values.items():
1434
                    if not isinstance(field_value, dict):
1435
                        # this is probably a list, e.g. "Profiles" field
1436
                        continue
1437
                    uids = self.get_uids_from_record(field_value, "uid")
1438
                    if len(uids) == 1:
1439
                        extra_fields[field_name] = uids[0]
1440
1441
        # Populate metadata with object info from extra fields (hidden fields)
1442
        for field_name, uid in extra_fields.items():
1443
            key = "{}_metadata".format(field_name.lower())
1444
            if metadata.get(key):
1445
                # This object has been processed already, skip
1446
                continue
1447
            obj = self.get_object_by_uid(uid)
1448
            if not obj:
1449
                continue
1450
            obj_info = self.get_object_info(
1451
                obj, field_name, record=extra_fields)
1452
            if not obj_info or "uid" not in obj_info:
1453
                continue
1454
            metadata[key] = {obj_info["uid"]: obj_info}
1455
1456
        return metadata
1457
1458
    def get_template_additional_info(self, metadata):
1459
        template_to_services = {}
1460
        service_to_templates = {}
1461
        service_metadata = metadata.get("service_metadata", {})
1462
        template = metadata.get("template_metadata", {})
1463
        # We don't expect more than one template, but who knows about future?
1464
        for uid, obj_info in template.items():
1465
            obj = self.get_object_by_uid(uid)
1466
            # get the template services
1467
            # [{'part_id': 'part-1', 'uid': '...'},
1468
            # {'part_id': 'part-1', 'uid': '...'}]
1469
            services = obj.getRawServices() or []
1470
            # get all UIDs of the template records
1471
            service_uids = map(lambda rec: rec.get("uid"), services)
1472
            # remember a mapping of template uid -> service
1473
            template_to_services[uid] = service_uids
1474
            # remember a mapping of service uid -> templates
1475
            for service_uid in service_uids:
1476
                # remember the template of all services
1477
                if service_uid in service_to_templates:
1478
                    service_to_templates[service_uid].append(uid)
1479
                else:
1480
                    service_to_templates[service_uid] = [uid]
1481
                # remember the service metadata
1482
                if service_uid not in service_metadata:
1483
                    service = self.get_object_by_uid(service_uid)
1484
                    service_info = self.get_service_info(service)
1485
                    service_metadata[service_uid] = service_info
1486
1487
        return {
1488
            "service_to_templates": service_to_templates,
1489
            "template_to_services": template_to_services,
1490
            "service_metadata": service_metadata,
1491
        }
1492
1493
    def get_profiles_additional_info(self, metadata):
1494
        profile_to_services = {}
1495
        service_to_profiles = metadata.get("service_to_profiles", {})
1496
        service_metadata = metadata.get("service_metadata", {})
1497
        profiles = metadata.get("profiles_metadata", {})
1498
        for uid, obj_info in profiles.items():
1499
            obj = self.get_object_by_uid(uid)
1500
            # get all services of this profile
1501
            services = obj.getServices()
1502
            # get all UIDs of the profile services
1503
            service_uids = map(api.get_uid, services)
1504
            # remember all services of this profile
1505
            profile_to_services[uid] = service_uids
1506
            # remember a mapping of service uid -> profiles
1507
            for service in services:
1508
                # get the UID of this service
1509
                service_uid = api.get_uid(service)
1510
                # remember the profiles of this service
1511
                if service_uid in service_to_profiles:
1512
                    service_to_profiles[service_uid].append(uid)
1513
                else:
1514
                    service_to_profiles[service_uid] = [uid]
1515
                # remember the service metadata
1516
                if service_uid not in service_metadata:
1517
                    service_info = self.get_service_info(service)
1518
                    service_metadata[service_uid] = service_info
1519
1520
        return {
1521
            "profile_to_services": profile_to_services,
1522
            "service_to_profiles": service_to_profiles,
1523
            "service_metadata": service_metadata,
1524
        }
1525
1526
    def get_objects_info(self, record, key):
1527
        """
1528
        Returns a list with the metadata for the objects the field with
1529
        field_name passed in refers to. Returns empty list if the field is not
1530
        a reference field or the record for this key cannot be handled
1531
        :param record: a record for a single sample (column)
1532
        :param key: The key of the field from the record (e.g. Client_uid)
1533
        :return: list of info objects
1534
        """
1535
        # Get the objects from this record. Returns a list because the field
1536
        # can be multivalued
1537
        uids = self.get_uids_from_record(record, key)
1538
        objects = map(self.get_object_by_uid, uids)
1539
        objects = map(lambda obj: self.get_object_info(
1540
            obj, key, record=record), objects)
1541
        return filter(None, objects)
1542
1543
    def object_info_cache_key(method, self, obj, key, **kw):
1544
        if obj is None or not key:
1545
            raise DontCache
1546
        field_name = key.lower()
1547
        obj_key = api.get_cache_key(obj)
1548
        return "-".join([field_name, obj_key] + kw.keys())
1549
1550
    @cache(object_info_cache_key)
1551
    def get_object_info(self, obj, key, record=None):
1552
        """Returns the object info metadata for the passed in object and key
1553
        :param obj: the object from which extract the info from
1554
        :param key: The key of the field from the record (e.g. Client_uid)
1555
        :return: dict that represents the object
1556
        """
1557
        # Check if there is a function to handle objects for this field
1558
        field_name = key
1559
        func_name = "get_{}_info".format(field_name.lower())
1560
        func = getattr(self, func_name, None)
1561
1562
        # always ensure we have a record
1563
        if record is None:
1564
            record = {}
1565
1566
        # Get the info for each object
1567
        info = callable(func) and func(obj) or self.get_base_info(obj)
1568
1569
        # update query filters based on record values
1570
        func_name = "get_{}_queries".format(field_name.lower())
1571
        func = getattr(self, func_name, None)
1572
        if callable(func):
1573
            info["filter_queries"] = func(obj, record)
1574
1575
        # Check if there is any adapter to handle objects for this field
1576
        for name, adapter in getAdapters((obj, ), IAddSampleObjectInfo):
1577
            logger.info("adapter for '{}': {}".format(field_name, name))
1578
            ad_info = adapter.get_object_info_with_record(record)
1579
            self.update_object_info(info, ad_info)
1580
1581
        return info
1582
1583
    def get_client_queries(self, obj, record=None):
1584
        """Returns the filter queries to be applied to other fields based on
1585
        both the Client object and record
1586
        """
1587
        # UID of the client
1588
        uid = api.get_uid(obj)
1589
1590
        # catalog queries for UI field filtering
1591
        queries = {
1592
            "Contact": {
1593
                "getParentUID": [uid]
1594
            },
1595
            "CCContact": {
1596
                "getParentUID": [uid]
1597
            },
1598
            "SamplePoint": {
1599
                "getClientUID": [uid, ""],
1600
            },
1601
            "Template": {
1602
                "getClientUID": [uid, ""],
1603
            },
1604
            "Profiles": {
1605
                "getClientUID": [uid, ""],
1606
            },
1607
            "Specification": {
1608
                "getClientUID": [uid, ""],
1609
            },
1610
            "Sample": {
1611
                "getClientUID": [uid],
1612
            },
1613
            "Batch": {
1614
                "getClientUID": [uid, ""],
1615
            },
1616
            "PrimaryAnalysisRequest": {
1617
                "getClientUID": [uid, ""],
1618
            }
1619
        }
1620
1621
        # additional filtering by sample type
1622
        record = record if record else {}
1623
        sample_type_uid = record.get("SampleType")
1624
        if api.is_uid(sample_type_uid):
1625
            fields = ["Template", "Specification", "Profiles", "SamplePoint"]
1626
            for field in fields:
1627
                queries[field]["sampletype_uid"] = [sample_type_uid, ""]
1628
1629
        return queries
1630
1631
    def get_sampletype_queries(self, obj, record=None):
1632
        """Returns the filter queries to apply to other fields based on both
1633
        the SampleType object and record
1634
        """
1635
        uid = api.get_uid(obj)
1636
        queries = {
1637
            # Display Sample Points that have this sample type assigned plus
1638
            # those that do not have a sample type assigned
1639
            "SamplePoint": {
1640
                "sampletype_uid": [uid, ""],
1641
            },
1642
            # Display Analysis Profiles that have this sample type assigned
1643
            # in addition to those that do not have a sample profile assigned
1644
            "Profiles": {
1645
                "sampletype_uid": [uid, ""],
1646
            },
1647
            # Display Specifications that have this sample type assigned only
1648
            "Specification": {
1649
                "sampletype_uid": uid,
1650
            },
1651
            # Display Sample Templates that have this sample type assigned plus
1652
            # those that do not have a sample type assigned
1653
            "Template": {
1654
                "sampletype_uid": [uid, ""],
1655
            }
1656
        }
1657
1658
        # additional filters by client
1659
        record = record if record else {}
1660
        client = record.get("Client") or self.get_client()
1661
        client_uid = api.get_uid(client) if client else None
1662
        if client_uid:
1663
            fields = ["Template", "Specification", "Profiles", "SamplePoint"]
1664
            for field in fields:
1665
                queries[field]["getClientUID"] = [client_uid, ""]
1666
1667
        return queries
1668
1669
    def update_object_info(self, base_info, additional_info):
1670
        """Updates the dictionaries for keys 'field_values' and 'filter_queries'
1671
        from base_info with those defined in additional_info. If base_info is
1672
        empty or None, updates the whole base_info dict with additional_info
1673
        """
1674
        if not base_info:
1675
            base_info.update(additional_info)
1676
            return
1677
1678
        # Merge field_values info
1679
        field_values = base_info.get("field_values", {})
1680
        field_values.update(additional_info.get("field_values", {}))
1681
        base_info["field_values"] = field_values
1682
1683
        # Merge filter_queries info
1684
        filter_queries = base_info.get("filter_queries", {})
1685
        filter_queries.update(additional_info.get("filter_queries", {}))
1686
        base_info["filter_queries"] = filter_queries
1687
1688
    def show_recalculate_prices(self):
1689
        setup = api.get_setup()
1690
        return setup.getShowPrices()
1691
1692
    def ajax_recalculate_prices(self):
1693
        """Recalculate prices for all ARs
1694
        """
1695
        # When the option "Include and display pricing information" in
1696
        # Bika Setup Accounting tab is not selected
1697
        if not self.show_recalculate_prices():
1698
            return {}
1699
1700
        # The sorted records from the request
1701
        records = self.get_records()
1702
1703
        client = self.get_client()
1704
        setup = api.get_setup()
1705
1706
        member_discount = float(setup.getMemberDiscount())
1707
        member_discount_applies = False
1708
        if client:
1709
            member_discount_applies = client.getMemberDiscountApplies()
1710
1711
        prices = {}
1712
        for n, record in enumerate(records):
1713
            ardiscount_amount = 0.00
1714
            arservices_price = 0.00
1715
            arprofiles_price = 0.00
1716
            arprofiles_vat_amount = 0.00
1717
            arservice_vat_amount = 0.00
1718
            services_from_priced_profile = []
1719
1720
            profile_uids = record.get("Profiles", [])
1721
            profiles = map(self.get_object_by_uid, profile_uids)
1722
            services = map(self.get_object_by_uid, record.get("Analyses", []))
1723
1724
            # ANALYSIS PROFILES PRICE
1725
            for profile in profiles:
1726
                use_profile_price = profile.getUseAnalysisProfilePrice()
1727
                if not use_profile_price:
1728
                    continue
1729
1730
                profile_price = float(profile.getAnalysisProfilePrice())
1731
                arprofiles_price += profile_price
1732
                arprofiles_vat_amount += profile.getVATAmount()
1733
                profile_services = profile.getServices()
1734
                services_from_priced_profile.extend(profile_services)
1735
1736
            # ANALYSIS SERVICES PRICE
1737
            for service in services:
1738
                # skip services that are part of a priced profile
1739
                if service in services_from_priced_profile:
1740
                    continue
1741
                service_price = float(service.getPrice())
1742
                # service_vat = float(service.getVAT())
1743
                service_vat_amount = float(service.getVATAmount())
1744
                arservice_vat_amount += service_vat_amount
1745
                arservices_price += service_price
1746
1747
            base_price = arservices_price + arprofiles_price
1748
1749
            # Calculate the member discount if it applies
1750
            if member_discount and member_discount_applies:
1751
                logger.info("Member discount applies with {}%".format(
1752
                    member_discount))
1753
                ardiscount_amount = base_price * member_discount / 100
1754
1755
            subtotal = base_price - ardiscount_amount
1756
            vat_amount = arprofiles_vat_amount + arservice_vat_amount
1757
            total = subtotal + vat_amount
1758
1759
            prices[n] = {
1760
                "discount": "{0:.2f}".format(ardiscount_amount),
1761
                "subtotal": "{0:.2f}".format(subtotal),
1762
                "vat": "{0:.2f}".format(vat_amount),
1763
                "total": "{0:.2f}".format(total),
1764
            }
1765
            logger.info("Prices for AR {}: Discount={discount} "
1766
                        "VAT={vat} Subtotal={subtotal} total={total}"
1767
                        .format(n, **prices[n]))
1768
1769
        return prices
1770
1771
    def get_field(self, field_name):
1772
        """Returns the field from the temporary sample with the given name
1773
        """
1774
        if IField.providedBy(field_name):
1775
            return field_name
1776
1777
        for field in self.get_ar_fields():
1778
            if field.getName() == field_name:
1779
                return field
1780
        return None
1781
1782
    def get_field_label(self, field):
1783
        """Returns the translated label of the given field
1784
        """
1785
        field = self.get_field(field)
1786
        if not field:
1787
            return ""
1788
1789
        instance = self.get_ar()
1790
        label = field.widget.Label(instance)
1791
        if isinstance(label, Message):
1792
            return self.context.translate(label)
1793
        return label
1794
1795
    def check_confirmation(self):
1796
        """Returns a dict when user confirmation is required for the creation of
1797
        samples. Returns None otherwise
1798
        """
1799
        if self.request.form.get("confirmed") == "1":
1800
            # User pressed the "yes" button in the confirmation pane already
1801
            return None
1802
1803
        # Find out if there is a confirmation adapter available
1804
        adapter = queryAdapter(self.request, IAddSampleConfirmation)
1805
        if not adapter:
1806
            return None
1807
1808
        # Extract records from the request and call the adapter
1809
        records = self.get_records()
1810
        return adapter.check_confirmation(records)
1811
1812
    def ajax_cancel(self):
1813
        """Cancel and redirect to configured actions
1814
        """
1815
        message = _("Sample creation cancelled")
1816
        self.context.plone_utils.addPortalMessage(message, "info")
1817
        return self.handle_redirect([], message)
1818
1819
    def ajax_submit(self):
1820
        """Create samples and redirect to configured actions
1821
        """
1822
        # Check if there is the need to display a confirmation pane
1823
        confirmation = self.check_confirmation()
1824
        if confirmation:
1825
            return {"confirmation": confirmation}
1826
1827
        # Get the maximum number of samples to create per record
1828
        max_samples_record = self.get_max_samples_per_record()
1829
1830
        # Get AR required fields (including extended fields)
1831
        fields = self.get_ar_fields()
1832
        required_keys = [field.getName() for field in fields if field.required]
1833
1834
        # extract records from request
1835
        records = self.get_records()
1836
1837
        fielderrors = {}
1838
        errors = {"message": "", "fielderrors": {}}
1839
1840
        valid_records = []
1841
1842
        # Validate required fields
1843
        for num, record in enumerate(records):
1844
1845
            # Extract file uploads (fields ending with _file)
1846
            # These files will be added later as attachments
1847
            file_fields = filter(lambda f: f.endswith("_file"), record)
1848
            uploads = map(lambda f: record.pop(f), file_fields)
0 ignored issues
show
introduced by
The variable record does not seem to be defined for all execution paths.
Loading history...
1849
            attachments = [self.to_attachment_record(f) for f in uploads]
1850
1851
            # Required fields and their values
1852
            required_values = [record.get(key) for key in required_keys]
1853
            required_fields = dict(zip(required_keys, required_values))
1854
1855
            # Client field is required but hidden in the AR Add form. We remove
1856
            # it therefore from the list of required fields to let empty
1857
            # columns pass the required check below.
1858
            if record.get("Client", False):
1859
                required_fields.pop("Client", None)
1860
1861
            # Check if analyses are required for sample registration
1862
            if not self.analyses_required():
1863
                required_fields.pop("Analyses", None)
1864
1865
            # Contacts get pre-filled out if only one contact exists.
1866
            # We won't force those columns with only the Contact filled out to
1867
            # be required.
1868
            contact = required_fields.pop("Contact", None)
1869
1870
            # None of the required fields are filled, skip this record
1871
            if not any(required_fields.values()):
1872
                continue
1873
1874
            # Re-add the Contact
1875
            required_fields["Contact"] = contact
1876
1877
            # Check if the contact belongs to the selected client
1878
            contact_obj = api.get_object(contact, None)
1879
            if not contact_obj:
1880
                fielderrors["Contact"] = _("No valid contact")
1881
            else:
1882
                parent_uid = api.get_uid(api.get_parent(contact_obj))
1883
                if parent_uid != record.get("Client"):
1884
                    msg = _("Contact does not belong to the selected client")
1885
                    fielderrors["Contact"] = msg
1886
1887
            # Check if the number of samples per record is permitted
1888
            num_samples = self.get_num_samples(record)
1889
            if num_samples > max_samples_record:
1890
                msg = _(u"error_analyssirequest_numsamples_above_max",
1891
                        u"The number of samples to create for the record "
1892
                        u"'Sample ${record_index}' (${num_samples}) is above "
1893
                        u"${max_num_samples}",
1894
                        mapping={
1895
                            "record_index": num+1,
1896
                            "num_samples": num_samples,
1897
                            "max_num_samples": max_samples_record,
1898
                        })
1899
                fielderrors["NumSamples"] = self.context.translate(msg)
1900
1901
            # Missing required fields
1902
            missing = [f for f in required_fields if not record.get(f, None)]
1903
1904
            # Handle fields from Service conditions
1905
            for condition in record.get("ServiceConditions", []):
1906
                if condition.get("type") == "file":
1907
                    # Add the file as an attachment
1908
                    file_upload = condition.get("value")
1909
                    att = self.to_attachment_record(file_upload)
1910
                    if att:
1911
                        # Add the file as an attachment
1912
                        att.update({
1913
                            "Service": condition.get("uid"),
1914
                            "Condition": condition.get("title"),
1915
                        })
1916
                        attachments.append(att)
1917
                    # Reset the condition value
1918
                    filename = file_upload and file_upload.filename or ""
1919
                    condition.value = filename
1920
1921
                if condition.get("required") == "on":
1922
                    if not condition.get("value"):
1923
                        title = condition.get("title")
1924
                        if title not in missing:
1925
                            missing.append(title)
1926
1927
            # If there are required fields missing, flag an error
1928
            for field in missing:
1929
                fieldname = "{}-{}".format(field, num)
1930
                label = self.get_field_label(field) or field
1931
                msg = self.context.translate(_("Field '{}' is required"))
1932
                fielderrors[fieldname] = msg.format(label)
1933
1934
            # Process and validate field values
1935
            valid_record = dict()
1936
            tmp_sample = self.get_ar()
1937
            for field in fields:
1938
                field_name = field.getName()
1939
                field_value = record.get(field_name)
1940
                if field_value in ['', None]:
1941
                    continue
1942
1943
                # process the value as the widget would usually do
1944
                process_value = field.widget.process_form
1945
                value, msgs = process_value(tmp_sample, field, record)
1946
                if not value:
1947
                    continue
1948
1949
                # store the processed value as the valid record
1950
                valid_record[field_name] = value
1951
1952
                # validate the value
1953
                error = field.validate(value, tmp_sample)
1954
                if error:
1955
                    field_name = "{}-{}".format(field_name, num)
1956
                    fielderrors[field_name] = error
1957
1958
            # add the attachments to the record
1959
            valid_record["attachments"] = filter(None, attachments)
1960
1961
            # keep the `_source_uid` in the record for the create process
1962
            valid_record["_source_uid"] = record.get("_source_uid")
1963
1964
            # append the valid record to the list of valid records
1965
            valid_records.append(valid_record)
1966
1967
        # return immediately with an error response if some field checks failed
1968
        if fielderrors:
1969
            errors["fielderrors"] = fielderrors
1970
            return {'errors': errors}
1971
1972
        # do a custom validation of records. For instance, we may want to rise
1973
        # an error if a value set to a given field is not consistent with a
1974
        # value set to another field
1975
        validators = getAdapters((self.request, ), IAddSampleRecordsValidator)
1976
        for name, validator in validators:
1977
            validation_err = validator.validate(valid_records)
1978
            if validation_err:
1979
                # Not valid, return immediately with an error response
1980
                return {"errors": validation_err}
1981
1982
        # create the samples
1983
        try:
1984
            samples = self.create_samples(valid_records)
1985
        except Exception as e:
1986
            errors["message"] = str(e)
1987
            logger.error(e, exc_info=True)
1988
            return {"errors": errors}
1989
1990
        # We keep the title to check if AR is newly created
1991
        # and UID to print stickers
1992
        ARs = OrderedDict()
1993
        for sample in samples:
1994
            ARs[sample.Title()] = sample.UID()
1995
1996
        level = "info"
1997
        if len(ARs) == 0:
1998
            message = _('No Samples could be created.')
1999
            level = "error"
2000
        elif len(ARs) > 1:
2001
            message = _('Samples ${ARs} were successfully created.',
2002
                        mapping={'ARs': safe_unicode(', '.join(ARs.keys()))})
2003
        else:
2004
            message = _('Sample ${AR} was successfully created.',
2005
                        mapping={'AR': safe_unicode(ARs.keys()[0])})
2006
2007
        # Display a portal message
2008
        self.context.plone_utils.addPortalMessage(message, level)
2009
2010
        return self.handle_redirect(ARs.values(), message)
2011
2012
    def create_samples(self, records):
2013
        """Creates samples for the given records
2014
        """
2015
        samples = []
2016
        request = self.request
2017
        for record in records:
2018
            client_uid = record.get("Client")
2019
            client = self.get_object_by_uid(client_uid)
2020
            if not client:
2021
                raise ValueError("No client found")
2022
2023
            # Pop the attachments
2024
            attachments = record.pop("attachments", [])
2025
2026
            # Pop the source UID
2027
            source_uid = record.pop("_source_uid", None)
2028
2029
            # Fetch the source object
2030
            source = None
2031
            if source_uid:
2032
                source = api.get_object(source_uid)
2033
2034
            # Create as many samples as required
2035
            num_samples = self.get_num_samples(record)
2036
            for idx in range(num_samples):
2037
                sample = crar(client, self.request, record)
2038
2039
                # Create the attachments
2040
                for attachment_record in attachments:
2041
                    self.create_attachment(sample, attachment_record)
2042
2043
                # Pass the new sample to all subscription hooks
2044
                hooks = subscribers((sample, request), IAfterCreateSampleHook)
2045
                for hook in hooks:
2046
                    hook.update(sample, source=source)
2047
2048
                transaction.savepoint(optimistic=True)
2049
                samples.append(sample)
2050
2051
        return samples
2052
2053
    def get_num_samples(self, record):
2054
        """Return the number of samples to create for the given record
2055
        """
2056
        num_samples = record.get("NumSamples", 1)
2057
        num_samples = api.to_int(num_samples, default=1)
2058
        return num_samples if num_samples > 0 else 1
2059
2060
    @viewcache.memoize
2061
    def get_max_samples_per_record(self):
2062
        """Returns the maximum number of samples that can be created for each
2063
        record/column from the sample add form
2064
        """
2065
        setup = api.get_senaite_setup()
2066
        return setup.getMaxNumberOfSamplesAdd()
2067
2068
    def is_automatic_label_printing_enabled(self):
2069
        """Returns whether the automatic printing of barcode labels is active
2070
        """
2071
        setup = api.get_setup()
2072
        auto_print = setup.getAutoPrintStickers()
2073
        auto_receive = setup.getAutoreceiveSamples()
2074
        action = "receive" if auto_receive else "register"
2075
        return action in auto_print
2076
2077
    def handle_redirect(self, uids, message):
2078
        """Handle redirect after sample creation or cancel
2079
        """
2080
        # Automatic label printing
2081
        setup = api.get_setup()
2082
        auto_print = self.is_automatic_label_printing_enabled()
2083
        # Check if immediate results entry is enabled in setup and the current
2084
        # user has enough privileges to do so
2085
        multi_results = setup.getImmediateResultsEntry() and check_permission(
2086
            TransitionMultiResults, self.context)
2087
        redirect_to = self.context.absolute_url()
2088
2089
        # UIDs of the new created samples
2090
        sample_uids = ",".join(uids)
2091
        # UIDs of previous created samples when save&copy was selected
2092
        prev_sample_uids = self.request.get("sample_uids")
2093
        if prev_sample_uids:
2094
            sample_uids = ",".join([prev_sample_uids, sample_uids])
2095
        # Get the submit action (either "Save" or "Save and Copy")
2096
        submit_action = self.request.form.get("submit_action", "save")
2097
        if submit_action == "save_and_copy":
2098
            # redirect to the sample add form, but keep track of
2099
            # previous created sample UIDs
2100
            redirect_to = "{}/ar_add?copy_from={}&ar_count={}&sample_uids={}" \
2101
                .format(self.context.absolute_url(),
2102
                        ",".join(uids),  # copy_from
2103
                        len(uids),  # ar_count
2104
                        sample_uids)  # sample_uids
2105
        elif auto_print and sample_uids:
2106
            redirect_to = "{}/sticker?autoprint=1&items={}".format(
2107
                self.context.absolute_url(), sample_uids)
2108
        elif multi_results and sample_uids:
2109
            redirect_to = "{}/multi_results?uids={}".format(
2110
                self.context.absolute_url(),
2111
                sample_uids)
2112
        return {
2113
            "success": message,
2114
            "redirect_to": redirect_to,
2115
        }
2116
2117
    def get_json(self, encoding="utf8"):
2118
        """Extracts the JSON from the request
2119
        """
2120
        body = self.request.get("BODY", "{}")
2121
2122
        def encode_hook(pairs):
2123
            """This hook is called for dicitionaries on JSON deserialization
2124
2125
            It is used to encode unicode strings with the given encoding,
2126
            because ZCatalogs have sometimes issues with unicode queries.
2127
            """
2128
            new_pairs = []
2129
            for key, value in pairs.iteritems():
2130
                # Encode the key
2131
                if isinstance(key, six.string_types):
2132
                    key = key.encode(encoding)
2133
                # Encode the value
2134
                if isinstance(value, six.string_types):
2135
                    value = value.encode(encoding)
2136
                new_pairs.append((key, value))
2137
            return dict(new_pairs)
2138
2139
        return json.loads(body, object_hook=encode_hook)
2140