bika.lims.browser.analysisrequest.add2   F
last analyzed

Complexity

Total Complexity 321

Size/Duplication

Total Lines 2045
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 321
eloc 1272
dl 0
loc 2045
rs 0.8
c 0
b 0
f 0

101 Methods

Rating   Name   Duplication   Size   Complexity  
A ajaxAnalysisRequestAddView.to_field_value() 0 5 1
B ajaxAnalysisRequestAddView.create_attachment() 0 34 7
A ajaxAnalysisRequestAddView.get_primaryanalysisrequest_info() 0 47 1
A ajaxAnalysisRequestAddView.get_conditions_info() 0 9 3
A ajaxAnalysisRequestAddView.ajax_get_global_settings() 0 8 1
A ajaxAnalysisRequestAddView.to_attachment_record() 0 13 2
A ajaxAnalysisRequestAddView.get_field_label() 0 12 3
A AnalysisRequestAddView.get_allowed_multi_paste_fields() 0 9 2
A AnalysisRequestManageView.get_field_visibility() 0 2 1
B ajaxAnalysisRequestAddView.get_records() 0 36 7
A AnalysisRequestAddView.get_ar() 0 8 2
A AnalysisRequestManageView.flush() 0 4 2
A AnalysisRequestAddView.analyses_required() 0 6 1
C AnalysisRequestAddView.get_default_value() 0 43 11
A AnalysisRequestAddView.get_currency() 0 7 1
A AnalysisRequestAddView.get_ar_schema() 0 6 1
A AnalysisRequestAddView.get_ar_fields() 0 6 1
A AnalysisRequestManageView.set_field_order() 0 2 1
A AnalysisRequestAddView.is_service_selected() 0 13 4
B AnalysisRequestManageView.get_fields_with_visibility() 0 26 6
A AnalysisRequestAddView.get_client() 0 14 5
A ajaxAnalysisRequestAddView.is_automatic_label_printing_enabled() 0 8 2
A AnalysisRequestAddView.get_input_widget() 0 53 1
A AnalysisRequestAddView.__call__() 0 13 1
A AnalysisRequestManageView.__call__() 0 11 5
A AnalysisRequestAddView.get_category_title() 0 9 2
C ajaxAnalysisRequestAddView.ajax_recalculate_prices() 0 78 10
A ajaxAnalysisRequestAddView.ajax_cancel() 0 6 1
A AnalysisRequestAddView.get_copy_from() 0 15 4
A ajaxAnalysisRequestAddView.is_multi_reference_field() 0 9 2
A ajaxAnalysisRequestAddView.ajax_recalculate_records() 0 23 2
A AnalysisRequestManageView.is_field_visible() 0 7 3
A AnalysisRequestAddView.get_default_contact() 0 34 4
A AnalysisRequestManageView.get_fields() 0 5 1
A ajaxAnalysisRequestAddView.is_uid_reference_field() 0 9 2
A ajaxAnalysisRequestAddView.ajax_is_reference_value_allowed() 0 36 4
A AnalysisRequestManageView.storage() 0 6 2
A ajaxAnalysisRequestAddView.get_uids_from_record() 0 17 4
A ajaxAnalysisRequestAddView.create_samples() 0 26 5
A AnalysisRequestAddView.is_field_visible() 0 15 5
A ajaxAnalysisRequestAddView.get_field() 0 10 4
A ajaxAnalysisRequestAddView.publishTraverse() 0 5 1
A AnalysisRequestAddView.get_ar_count() 0 9 2
B AnalysisRequestAddView.generate_fieldvalues() 0 35 6
A AnalysisRequestAddView.get_services() 0 27 4
D ajaxAnalysisRequestAddView.get_record_metadata() 0 44 13
A AnalysisRequestManageView.get_field() 0 5 1
A ajaxAnalysisRequestAddView.get_services_max_holding_time() 0 22 3
A ajaxAnalysisRequestAddView.get_profile_info() 0 7 1
A ajaxAnalysisRequestAddView.get_json() 0 23 4
A ajaxAnalysisRequestAddView.object_info_cache_key() 0 6 3
A AnalysisRequestManageView.__init__() 0 6 1
A AnalysisRequestAddView.get_fieldname() 0 8 1
A ajaxAnalysisRequestAddView.ajax_get_service() 0 14 3
A AnalysisRequestAddView.get_service_uid_from() 0 6 1
A ajaxAnalysisRequestAddView.get_services_beyond_holding_time() 0 29 4
B ajaxAnalysisRequestAddView.get_template_additional_info() 0 33 6
A AnalysisRequestAddView.__init__() 0 8 1
B AnalysisRequestAddView.get_service_categories() 0 25 6
A ajaxAnalysisRequestAddView.get_calculation_info() 0 7 1
A AnalysisRequestAddView.show_paste_button_for() 0 14 3
A ajaxAnalysisRequestAddView.get_max_samples_per_record() 0 7 1
A ajaxAnalysisRequestAddView.get_sampletype_info() 0 14 1
A AnalysisRequestAddView.get_parent_ar() 0 18 4
A AnalysisRequestAddView.get_view_url() 0 9 2
A AnalysisRequestAddView.get_field_value() 0 8 1
A ajaxAnalysisRequestAddView.show_recalculate_prices() 0 3 1
A ajaxAnalysisRequestAddView.error() 0 7 1
A ajaxAnalysisRequestAddView.get_object_info() 0 32 4
A ajaxAnalysisRequestAddView.get_client_info() 0 23 2
F ajaxAnalysisRequestAddView.ajax_submit() 0 189 29
A ajaxAnalysisRequestAddView.get_method_info() 0 7 1
A AnalysisRequestManageView.get_sorted_fields() 0 14 1
A AnalysisRequestAddView.getMemberDiscountApplies() 0 10 2
A ajaxAnalysisRequestAddView.get_contact_info() 0 32 2
A AnalysisRequestAddView.get_fields_with_visibility() 0 15 4
A AnalysisRequestAddView.get_object_by_uid() 0 9 2
B ajaxAnalysisRequestAddView.handle_redirect() 0 38 7
A ajaxAnalysisRequestAddView.to_iso_date() 0 11 4
B ajaxAnalysisRequestAddView.get_sampletype_queries() 0 37 5
A ajaxAnalysisRequestAddView.get_service_info() 0 25 1
A ajaxAnalysisRequestAddView.check_confirmation() 0 16 3
A ajaxAnalysisRequestAddView.__init__() 0 7 1
A AnalysisRequestAddView.get_points_of_capture() 0 3 1
A ajaxAnalysisRequestAddView.__call__() 0 14 3
A AnalysisRequestManageView.get_ar() 0 5 2
A ajaxAnalysisRequestAddView.get_num_samples() 0 6 2
A AnalysisRequestAddView.get_sample() 0 7 2
A AnalysisRequestAddView.show_copy_button_for() 0 8 4
A ajaxAnalysisRequestAddView.get_client_queries() 0 47 4
A ajaxAnalysisRequestAddView.get_objects_info() 0 16 2
A ajaxAnalysisRequestAddView.get_base_info() 0 17 2
A AnalysisRequestManageView.set_field_visibility() 0 2 1
A ajaxAnalysisRequestAddView.update_object_info() 0 18 2
A ajaxAnalysisRequestAddView.get_start_holding_date() 0 6 1
A AnalysisRequestAddView.get_batch() 0 10 3
A AnalysisRequestManageView.get_field_order() 0 5 3
A ajaxAnalysisRequestAddView.ajax_get_flush_settings() 0 29 3
B ajaxAnalysisRequestAddView.get_template_info() 0 37 7
B ajaxAnalysisRequestAddView.get_profiles_additional_info() 0 31 5
A AnalysisRequestManageView.get_annotation() 0 3 1

1 Function

Rating   Name   Duplication   Size   Complexity  
A cache_key() 0 4 2

How to fix   Complexity   

Complexity

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

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

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