Passed
Push — 2.x ( 7be1e3...9538a4 )
by Jordi
07:43
created

ajaxAnalysisRequestAddView.get_method_info()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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