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) |
|
|
|
|
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: |
|
|
|
|
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()) |
|
|
|
|
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) |
|
|
|
|
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© 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
|
|
|
|