Skip to content

Commit

Permalink
Merge branch '2.x' into sort-cats-analysisspec
Browse files Browse the repository at this point in the history
  • Loading branch information
ramonski authored Jan 27, 2023
2 parents 7995d7d + 5135edd commit 0e7e4f0
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 74 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Changelog
------------------

- #2191 Apply category sort order on analysis specifications
- #2239 Allow to create multiple samples for each sample record in add form
- #2241 Little improvement of getRaw function from legacy uidreference field
- #2238 Split the add sample's `ajax_form` function to make patching easier
- #2240 Explicitly set client on sample creation
- #2237 Fix default value of interim choices and allow empty selection
- #2234 Add interpretation template columns for assigned sampletypes and result text
- #2234 Change base class for interpretation templates from Item -> Container
Expand Down
135 changes: 89 additions & 46 deletions src/bika/lims/browser/analysisrequest/add2.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import six

import transaction
from bika.lims import POINTS_OF_CAPTURE
from bika.lims import api
from bika.lims import bikaMessageFactory as _
Expand All @@ -36,7 +37,6 @@
from bika.lims.interfaces import IAddSampleRecordsValidator
from bika.lims.interfaces import IGetDefaultFieldValueARAddHook
from bika.lims.utils.analysisrequest import create_analysisrequest as crar
from bika.lims.workflow import ActionHandlerPool
from BTrees.OOBTree import OOBTree
from DateTime import DateTime
from plone import protect
Expand All @@ -57,7 +57,8 @@
from zope.publisher.interfaces import IPublishTraverse

AR_CONFIGURATION_STORAGE = "bika.lims.browser.analysisrequest.manage.add"
SKIP_FIELD_ON_COPY = ["Sample", "PrimaryAnalysisRequest", "Remarks"]
SKIP_FIELD_ON_COPY = ["Sample", "PrimaryAnalysisRequest", "Remarks",
"NumSamples"]


def returns_json(func):
Expand Down Expand Up @@ -429,7 +430,8 @@ def get_default_contact(self, client=None):
elif client == api.get_current_client():
# Current user is a Client contact. Use current contact
current_user = api.get_current_user()
return api.get_user_contact(current_user, contact_types=["Contact"])
return api.get_user_contact(current_user,
contact_types=["Contact"])

return None

Expand Down Expand Up @@ -1053,7 +1055,9 @@ def get_primaryanalysisrequest_info(self, obj):
"DateSampled": {"value": self.to_iso_date(obj.getDateSampled())},
"SamplingDate": {"value": self.to_iso_date(obj.getSamplingDate())},
"SampleType": self.to_field_value(sample_type),
"EnvironmentalConditions": {"value": obj.getEnvironmentalConditions()},
"EnvironmentalConditions": {
"value": obj.getEnvironmentalConditions(),
},
"ClientSampleID": {"value": obj.getClientSampleID()},
"ClientReference": {"value": obj.getClientReference()},
"ClientOrderNumber": {"value": obj.getClientOrderNumber()},
Expand Down Expand Up @@ -1285,7 +1289,8 @@ def get_record_metadata(self, record):
obj = self.get_object_by_uid(uid)
if not obj:
continue
obj_info = self.get_object_info(obj, field_name, record=extra_fields)
obj_info = self.get_object_info(
obj, field_name, record=extra_fields)
if not obj_info or "uid" not in obj_info:
continue
metadata[key] = {obj_info["uid"]: obj_info}
Expand Down Expand Up @@ -1584,20 +1589,23 @@ def ajax_submit(self):
if confirmation:
return {"confirmation": confirmation}

# Get the maximum number of samples to create per record
max_samples_record = self.get_max_samples_per_record()

# Get AR required fields (including extended fields)
fields = self.get_ar_fields()
required_keys = [field.getName() for field in fields if field.required]

# extract records from request
records = self.get_records()

fielderrors = {}
errors = {"message": "", "fielderrors": {}}

attachments = {}
valid_records = []

# Validate required fields
for n, record in enumerate(records):
for num, record in enumerate(records):

# Process UID fields first and set their values to the linked field
uid_fields = filter(lambda f: f.endswith("_uid"), record)
Expand All @@ -1612,11 +1620,9 @@ def ajax_submit(self):
# These files will be added later as attachments
file_fields = filter(lambda f: f.endswith("_file"), record)
uploads = map(lambda f: record.pop(f), file_fields)
attachments[n] = [self.to_attachment_record(f) for f in uploads]
attachments = [self.to_attachment_record(f) for f in uploads]

# Required fields and their values
required_keys = [field.getName() for field in fields
if field.required]
required_values = [record.get(key) for key in required_keys]
required_fields = dict(zip(required_keys, required_values))

Expand Down Expand Up @@ -1652,6 +1658,20 @@ def ajax_submit(self):
msg = _("Contact does not belong to the selected client")
fielderrors["Contact"] = msg

# Check if the number of samples per record is permitted
num_samples = self.get_num_samples(record)
if num_samples > max_samples_record:
msg = _(u"error_analyssirequest_numsamples_above_max",
u"The number of samples to create for the record "
u"'Sample ${record_index}' (${num_samples}) is above "
u"${max_num_samples}",
mapping={
"record_index": num+1,
"num_samples": num_samples,
"max_num_samples": max_samples_record,
})
fielderrors["NumSamples"] = self.context.translate(msg)

# Missing required fields
missing = [f for f in required_fields if not record.get(f, None)]

Expand All @@ -1667,7 +1687,7 @@ def ajax_submit(self):
"Service": condition.get("uid"),
"Condition": condition.get("title"),
})
attachments[n].append(att)
attachments.append(att)
# Reset the condition value
filename = file_upload and file_upload.filename or ""
condition.value = filename
Expand All @@ -1680,7 +1700,7 @@ def ajax_submit(self):

# If there are required fields missing, flag an error
for field in missing:
fieldname = "{}-{}".format(field, n)
fieldname = "{}-{}".format(field, num)
msg = _("Field '{}' is required").format(safe_unicode(field))
fielderrors[fieldname] = msg

Expand All @@ -1692,6 +1712,9 @@ def ajax_submit(self):
continue
valid_record[fieldname] = fieldvalue

# add the attachments to the record
valid_record["attachments"] = filter(None, attachments)

# append the valid record to the list of valid records
valid_records.append(valid_record)

Expand All @@ -1710,41 +1733,19 @@ def ajax_submit(self):
# Not valid, return immediately with an error response
return {"errors": validation_err}

# Process Form
actions = ActionHandlerPool.get_instance()
actions.queue_pool()
# create the samples
try:
samples = self.create_samples(valid_records)
except Exception as e:
errors["message"] = str(e)
logger.error(e, exc_info=True)
return {"errors": errors}

# We keep the title to check if AR is newly created
# and UID to print stickers
ARs = OrderedDict()
for n, record in enumerate(valid_records):
client_uid = record.get("Client")
client = self.get_object_by_uid(client_uid)

if not client:
actions.resume()
raise RuntimeError("No client found")

# Create the Analysis Request
try:
ar = crar(
client,
self.request,
record,
)
except Exception as e:
actions.resume()
errors["message"] = str(e)
logger.error(e, exc_info=True)
return {"errors": errors}

# We keep the title to check if AR is newly created
# and UID to print stickers
ARs[ar.Title()] = ar.UID()

# Create the attachments
ar_attachments = filter(None, attachments.get(n, []))
for attachment_record in ar_attachments:
self.create_attachment(ar, attachment_record)

actions.resume()
for sample in samples:
ARs[sample.Title()] = sample.UID()

level = "info"
if len(ARs) == 0:
Expand All @@ -1762,6 +1763,48 @@ def ajax_submit(self):

return self.handle_redirect(ARs.values(), message)

def create_samples(self, records):
"""Creates samples for the given records
"""
samples = []
for record in records:
client_uid = record.get("Client")
client = self.get_object_by_uid(client_uid)
if not client:
raise ValueError("No client found")

# Pop the attachments
attachments = record.pop("attachments", [])

# Create as many samples as required
num_samples = self.get_num_samples(record)
for idx in range(num_samples):
sample = crar(client, self.request, record)

# Create the attachments
for attachment_record in attachments:
self.create_attachment(sample, attachment_record)

transaction.savepoint(optimistic=True)
samples.append(sample)

return samples

def get_num_samples(self, record):
"""Return the number of samples to create for the given record
"""
num_samples = record.get("NumSamples", 1)
num_samples = api.to_int(num_samples, default=1)
return num_samples if num_samples > 0 else 1

@viewcache.memoize
def get_max_samples_per_record(self):
"""Returns the maximum number of samples that can be created for each
record/column from the sample add form
"""
setup = api.get_senaite_setup()
return setup.getMaxNumberOfSamplesAdd()

def is_automatic_label_printing_enabled(self):
"""Returns whether the automatic printing of barcode labels is active
"""
Expand Down
44 changes: 17 additions & 27 deletions src/bika/lims/browser/fields/uidreferencefield.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import six

from AccessControl import ClassSecurityInfo
from bika.lims import APIError
from Products.Archetypes.Field import Field, StringField
from bika.lims import logger
from bika.lims import api
Expand Down Expand Up @@ -108,21 +109,16 @@ def unlink_reference(self, source, target):
return True

@security.public
def get_uid(self, context, value, default=""):
"""Takes a brain or object (or UID), and returns a UID.
:param context: context is the object who's schema contains this field.
:type context: BaseContent
:param value: Brain, object, or UID.
def get_uid(self, value):
"""Takes a brain or object (or UID), and returns a UID
:param value: Brain, object, or UID
:type value: Any
:return: resolved UID.
:rtype: string
:return: resolved UID
"""
if api.is_object(value):
value = api.get_uid(value)
elif not api.is_uid(value):
value = default
return value
try:
return api.get_uid(value)
except APIError:
return None

@security.public
def get(self, context, **kwargs):
Expand Down Expand Up @@ -158,28 +154,22 @@ def get(self, context, **kwargs):
return None

@security.public
def getRaw(self, context, aslist=False, **kwargs):
def getRaw(self, context, **kwargs):
"""Grab the stored value, and return it directly as UIDs.
:param context: context is the object who's schema contains this field.
:type context: BaseContent
:param aslist: Forces a single-valued field to return a list type.
:type aslist: bool
:param kwargs: kwargs are passed directly to the underlying get.
:type kwargs: dict
:return: UID or list of UIDs for multiValued fields.
:rtype: string | list[string]
"""
value = StringField.get(self, context, **kwargs)
if not value:
return [] if self.multiValued else None
uids = StringField.get(self, context, **kwargs)
if not isinstance(uids, list):
uids = [uids]
if self.multiValued:
ret = value
else:
ret = self.get_uid(context, value)
if aslist:
ret = [ret]
return ret
return filter(None, uids)
return uids[0]

def _set_backreferences(self, context, items, **kwargs):
"""Set the back references on the linked items
Expand Down Expand Up @@ -236,8 +226,8 @@ def set(self, context, value, **kwargs):
value = [value]

# Extract uids and remove empties
uids = [self.get_uid(context, item) for item in value]
uids = filter(api.is_uid, uids)
uids = [self.get_uid(item) for item in value]
uids = filter(None, uids)

# Back-reference current object to referenced objects
if self.keep_backreferences:
Expand Down
28 changes: 27 additions & 1 deletion src/bika/lims/content/analysisrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

from bika.lims.browser.fields.uidreferencefield import get_backreferences
from Products.Archetypes.config import UID_CATALOG
from Products.Archetypes.Field import IntegerField
from Products.Archetypes.Widget import IntegerWidget
from six.moves.urllib.parse import urljoin

from AccessControl import ClassSecurityInfo
Expand Down Expand Up @@ -1269,7 +1271,31 @@
RecordsField(
"ServiceConditions",
widget=ComputedWidget(visible=False)
)
),

# Number of samples to create on add form
IntegerField(
"NumSamples",
default=1,
widget=IntegerWidget(
label=_(
u"label_analysisrequest_numsamples",
default=u"Number of samples"
),
description=_(
u"description_analysisrequest_numsamples",
default=u"Number of samples to create with the information "
u"provided"),
# This field is only visible in add sample form
visible={
"add": "edit",
"view": "invisible",
"header_table": "invisible",
"secondary": "invisible",
},
render_own_label=True,
),
),
)
)

Expand Down
Loading

0 comments on commit 0e7e4f0

Please sign in to comment.