Passed
Push — 2.x ( 992984...ac9ec3 )
by Jordi
06:43
created

AnalysisRequestAddView.get_ar_fields()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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