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