Passed
Push — master ( c96140...98a6fb )
by Ramon
05:44 queued 01:41
created

ajaxAnalysisRequestAddView.ajax_get_flush_settings()   B

Complexity

Conditions 3

Size

Total Lines 57
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 46
dl 0
loc 57
rs 8.7672
c 0
b 0
f 0
cc 3
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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