Skip to content

Commit e24f862

Browse files
authored
Merge branch '2.x' into sort-cats-analysisspec
2 parents 38bc708 + ea8189a commit e24f862

File tree

29 files changed

+892
-384
lines changed

29 files changed

+892
-384
lines changed

.github/workflows/docker.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,33 @@ jobs:
1515
# This workflow contains a single job called "build"
1616
build:
1717
# The type of runner that the job will run on
18-
runs-on: ubuntu-latest
18+
runs-on: 'ubuntu-latest'
1919

2020
# Steps represent a sequence of tasks that will be executed as part of the job
2121
steps:
2222

2323
- name: Login to Docker Hub
24-
uses: docker/login-action@v1
24+
uses: docker/login-action@v2
2525
with:
2626
username: ${{ secrets.DOCKER_HUB_USERNAME }}
2727
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
2828

2929
- name: Set up Docker Buildx
3030
id: buildx
31-
uses: docker/setup-buildx-action@v1
31+
uses: docker/setup-buildx-action@v2
3232

3333
- name: Show workspace
3434
run: echo ${{ github.workspace }}
3535

3636
- name: Checkout senaite.docker
37-
uses: actions/checkout@v2
37+
uses: actions/checkout@v3
3838
with:
3939
repository: "senaite/senaite.docker"
4040
path: "senaite.docker"
4141

4242
- name: Build and push
4343
id: docker_build
44-
uses: docker/build-push-action@v2
44+
uses: docker/build-push-action@v3
4545
with:
4646
context: ${{ github.workspace }}/senaite.docker/latest
4747
file: ${{ github.workspace }}/senaite.docker/latest/Dockerfile

CHANGES.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ Changelog
55
------------------
66

77
- #2191 Apply category sort order on analysis specifications
8+
- #2219 Make `UIDReferenceField` to not keep back-references by default
9+
- #2209 Migrate AnalysisRequest's ReferenceField fields to UIDReferenceField
10+
- #2218 Improve performance of legacy AT `UIDReferenceField`'s setter
11+
- #2214 Remove `DefaultContainerType` field (stale) from AnalysisRequest
12+
- #2215 Fix ParseError when search term contains parenthesis in widget search
13+
- #2213 Purge ComputedField fields from AnalysisRequest related with Profiles
14+
- #2212 Improve performance of legacy AT `UIDReferenceField`'s getter
15+
- #2211 Remove `Profile` field (stale) from AnalysisRequest
16+
- #2207 Support for file upload on analysis (pre) conditions
17+
- #2208 Remove `default_method` from AnalysisRequest's Contact field
18+
- #2204 Fix traceback when retracting an analysis with a detection limit
19+
- #2202 Fix detection limit set manually is not displayed on result save
20+
- #2203 Fix empty date sampled in samples listing when sampling workflow is enabled
21+
- #2197 Use portal as relative path for sticker icons
22+
- #2196 Order sample analyses by sortable title on get per default
823
- #2193 Fix analyst cannot import results from instruments
924
- #2190 Fix sample actions without translation
1025
- #2189 Fix auto-print of barcode labels when auto-receive is enabled

src/bika/lims/adapters/referencewidgetvocabulary.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ def search_term(self):
6363
"""Returns the search term
6464
"""
6565
search_term = _c(self.request.get("searchTerm", ""))
66+
# Normalize the search term
67+
special = "*.!$%&/()=#+:'`´^"
68+
search_term = filter(lambda it: it not in special, search_term)
6669
return search_term.lower().strip()
6770

6871
@property

src/bika/lims/browser/analyses/view.py

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,14 @@ def _folder_item_result(self, analysis_brain, item):
892892
if self.is_result_edition_allowed(analysis_brain):
893893
item["allow_edit"].append("Result")
894894

895+
# Display the DL operand (< or >) in the results entry field if
896+
# the manual entry of DL is set, but DL selector is hidden
897+
allow_manual = obj.getAllowManualDetectionLimit()
898+
selector = obj.getDetectionLimitSelector()
899+
if allow_manual and not selector:
900+
operand = obj.getDetectionLimitOperand()
901+
item["Result"] = "{} {}".format(operand, result).strip()
902+
895903
# Prepare result options
896904
choices = obj.getResultOptions()
897905
if choices:
@@ -1138,12 +1146,12 @@ def _folder_item_attachments(self, obj, item):
11381146
attachments_names = []
11391147
attachments_html = []
11401148
analysis = self.get_object(obj)
1141-
for at in analysis.getAttachment():
1142-
at_file = at.getAttachmentFile()
1143-
url = "{}/at_download/AttachmentFile".format(api.get_url(at))
1144-
link = get_link(url, at_file.filename, tabindex="-1")
1149+
for attachment in analysis.getRawAttachment():
1150+
attachment = self.get_object(attachment)
1151+
link = self.get_attachment_link(attachment)
11451152
attachments_html.append(link)
1146-
attachments_names.append(at_file.filename)
1153+
filename = attachment.getFilename()
1154+
attachments_names.append(filename)
11471155

11481156
if attachments_html:
11491157
item["replace"]["Attachments"] = "<br/>".join(attachments_html)
@@ -1153,6 +1161,14 @@ def _folder_item_attachments(self, obj, item):
11531161
img = get_image("warning.png", title=_("Attachment required"))
11541162
item["replace"]["Attachments"] = img
11551163

1164+
def get_attachment_link(self, attachment):
1165+
"""Returns a well-formed link for the attachment passed in
1166+
"""
1167+
filename = attachment.getFilename()
1168+
att_url = api.get_url(attachment)
1169+
url = "{}/at_download/AttachmentFile".format(att_url)
1170+
return get_link(url, filename, tabindex="-1")
1171+
11561172
def _folder_item_uncertainty(self, analysis_brain, item):
11571173
"""Fills the analysis' uncertainty to the item passed in.
11581174
@@ -1500,12 +1516,21 @@ def _folder_item_conditions(self, analysis_brain, item):
15001516
return
15011517

15021518
conditions = analysis.getConditions()
1503-
if conditions:
1504-
conditions = map(lambda it: ": ".join([it["title"], it["value"]]),
1505-
conditions)
1506-
conditions = "<br/>".join(conditions)
1507-
service = item["replace"].get("Service") or item["Service"]
1508-
item["replace"]["Service"] = "{}<br/>{}".format(service, conditions)
1519+
if not conditions:
1520+
return
1521+
1522+
def to_str(condition):
1523+
title = condition.get("title")
1524+
value = condition.get("value", "")
1525+
if condition.get("type") == "file" and api.is_uid(value):
1526+
att = self.get_object(value)
1527+
value = self.get_attachment_link(att)
1528+
return ": ".join([title, str(value)])
1529+
1530+
# Display the conditions properly formatted
1531+
conditions = "<br/>".join([to_str(cond) for cond in conditions])
1532+
service = item["replace"].get("Service") or item["Service"]
1533+
item["replace"]["Service"] = "<br/>".join([service, conditions])
15091534

15101535
def is_method_required(self, analysis):
15111536
"""Returns whether the render of the selection list with methods is

src/bika/lims/browser/analysisrequest/add2.py

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
from bika.lims.interfaces import IAddSampleObjectInfo
3636
from bika.lims.interfaces import IAddSampleRecordsValidator
3737
from bika.lims.interfaces import IGetDefaultFieldValueARAddHook
38-
from bika.lims.utils import tmpID
3938
from bika.lims.utils.analysisrequest import create_analysisrequest as crar
4039
from bika.lims.workflow import ActionHandlerPool
4140
from BTrees.OOBTree import OOBTree
@@ -45,7 +44,6 @@
4544
from plone.memoize.volatile import DontCache
4645
from plone.memoize.volatile import cache
4746
from plone.protect.interfaces import IDisableCSRFProtection
48-
from Products.CMFPlone.utils import _createObjectByType
4947
from Products.CMFPlone.utils import safe_unicode
5048
from Products.Five.browser import BrowserView
5149
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
@@ -1086,6 +1084,56 @@ def to_field_value(self, obj):
10861084
"title": obj and api.get_title(obj) or ""
10871085
}
10881086

1087+
def to_attachment_record(self, fileupload):
1088+
"""Returns a dict-like structure with suitable information for the
1089+
proper creation of Attachment objects
1090+
"""
1091+
if not fileupload.filename:
1092+
# ZPublisher.HTTPRequest.FileUpload is empty
1093+
return None
1094+
return {
1095+
"AttachmentFile": fileupload,
1096+
"AttachmentType": "",
1097+
"ReportOption": "",
1098+
"AttachmentKeys": "",
1099+
"Service": "",
1100+
}
1101+
1102+
def create_attachment(self, sample, attachment_record):
1103+
"""Creates an attachment for the given sample with the information
1104+
provided in attachment_record
1105+
"""
1106+
# create the attachment object
1107+
client = sample.getClient()
1108+
attachment = api.create(client, "Attachment", **attachment_record)
1109+
uid = attachment_record.get("Service")
1110+
if not uid:
1111+
# Link the attachment to the sample
1112+
sample.addAttachment(attachment)
1113+
return attachment
1114+
1115+
# Link the attachment to analyses with this service uid
1116+
ans = sample.objectValues(spec="Analysis")
1117+
ans = filter(lambda an: an.getRawAnalysisService() == uid, ans)
1118+
for analysis in ans:
1119+
attachments = analysis.getRawAttachment()
1120+
analysis.setAttachment(attachments + [attachment])
1121+
1122+
# Assign the attachment to the given condition
1123+
condition_title = attachment_record.get("Condition")
1124+
if not condition_title:
1125+
return attachment
1126+
1127+
conditions = sample.getServiceConditions()
1128+
for condition in conditions:
1129+
is_uid = condition.get("uid") == uid
1130+
is_title = condition.get("title") == condition_title
1131+
is_file = condition.get("type") == "file"
1132+
if all([is_uid, is_title, is_file]):
1133+
condition["value"] = api.get_uid(attachment)
1134+
sample.setServiceConditions(conditions)
1135+
return attachment
1136+
10891137
def ajax_get_global_settings(self):
10901138
"""Returns the global Bika settings
10911139
"""
@@ -1563,7 +1611,8 @@ def ajax_submit(self):
15631611
# Extract file uploads (fields ending with _file)
15641612
# These files will be added later as attachments
15651613
file_fields = filter(lambda f: f.endswith("_file"), record)
1566-
attachments[n] = map(lambda f: record.pop(f), file_fields)
1614+
uploads = map(lambda f: record.pop(f), file_fields)
1615+
attachments[n] = [self.to_attachment_record(f) for f in uploads]
15671616

15681617
# Required fields and their values
15691618
required_keys = [field.getName() for field in fields
@@ -1606,8 +1655,23 @@ def ajax_submit(self):
16061655
# Missing required fields
16071656
missing = [f for f in required_fields if not record.get(f, None)]
16081657

1609-
# Handle required fields from Service conditions
1658+
# Handle fields from Service conditions
16101659
for condition in record.get("ServiceConditions", []):
1660+
if condition.get("type") == "file":
1661+
# Add the file as an attachment
1662+
file_upload = condition.get("value")
1663+
att = self.to_attachment_record(file_upload)
1664+
if att:
1665+
# Add the file as an attachment
1666+
att.update({
1667+
"Service": condition.get("uid"),
1668+
"Condition": condition.get("title"),
1669+
})
1670+
attachments[n].append(att)
1671+
# Reset the condition value
1672+
filename = file_upload and file_upload.filename or ""
1673+
condition.value = filename
1674+
16111675
if condition.get("required") == "on":
16121676
if not condition.get("value"):
16131677
title = condition.get("title")
@@ -1670,16 +1734,16 @@ def ajax_submit(self):
16701734
errors["message"] = str(e)
16711735
logger.error(e, exc_info=True)
16721736
return {"errors": errors}
1737+
16731738
# We keep the title to check if AR is newly created
16741739
# and UID to print stickers
16751740
ARs[ar.Title()] = ar.UID()
1676-
for attachment in attachments.get(n, []):
1677-
if not attachment.filename:
1678-
continue
1679-
att = _createObjectByType("Attachment", client, tmpID())
1680-
att.setAttachmentFile(attachment)
1681-
att.processForm()
1682-
ar.addAttachment(att)
1741+
1742+
# Create the attachments
1743+
ar_attachments = filter(None, attachments.get(n, []))
1744+
for attachment_record in ar_attachments:
1745+
self.create_attachment(ar, attachment_record)
1746+
16831747
actions.resume()
16841748

16851749
level = "info"

src/bika/lims/browser/fields/aranalysesfield.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,19 @@ def get(self, instance, **kwargs):
7575
:param kwargs: Keyword arguments to inject in the search query
7676
:returns: A list of Analysis Objects/Catalog Brains
7777
"""
78-
# Do we need to return objects or brains
79-
full_objects = kwargs.get("full_objects", False)
80-
81-
# Bail out parameters from kwargs that don't match with indexes
78+
# Filter out parameters from kwargs that don't match with indexes
8279
catalog = api.get_tool(ANALYSIS_CATALOG)
8380
indexes = catalog.indexes()
8481
query = dict([(k, v) for k, v in kwargs.items() if k in indexes])
8582

86-
# Do the search against the catalog
8783
query["portal_type"] = "Analysis"
8884
query["getAncestorsUIDs"] = api.get_uid(instance)
85+
query["sort_on"] = kwargs.get("sort_on", "sortable_title")
86+
query["sort_order"] = kwargs.get("sort_order", "ascending")
87+
88+
# Do the search against the catalog
8989
brains = catalog(query)
90-
if full_objects:
90+
if kwargs.get("full_objects", False):
9191
return map(api.get_object, brains)
9292
return brains
9393

0 commit comments

Comments
 (0)