Passed
Push — 2.x ( 8bc3ab...07088f )
by Ramon
06:25
created

ajaxAnalysisRequestAddView.get_record_metadata()   F

Complexity

Conditions 14

Size

Total Lines 50
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 34
dl 0
loc 50
rs 3.6
c 0
b 0
f 0
cc 14
nop 2

How to fix   Complexity   

Complexity

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

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

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