Passed
Push — 2.x ( aac90c...992984 )
by Jordi
06:59
created

AnalysisRequestAddView.get_parent_ar()   A

Complexity

Conditions 4

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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