From 024a29ce829370515c94eaa3d06da76f819303d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 27 Jan 2020 22:47:32 +0100 Subject: [PATCH 1/8] QC Analyses listing appears empty in Sample view --- bika/lims/browser/analyses/qc.py | 153 ++++++++++++++------------- bika/lims/content/analysisrequest.py | 80 ++++++-------- 2 files changed, 109 insertions(+), 124 deletions(-) diff --git a/bika/lims/browser/analyses/qc.py b/bika/lims/browser/analyses/qc.py index e9e76cdc3a..f498b43209 100644 --- a/bika/lims/browser/analyses/qc.py +++ b/bika/lims/browser/analyses/qc.py @@ -18,11 +18,15 @@ # Copyright 2018-2019 by it's authors. # Some rights reserved, see README and LICENSE. -from operator import itemgetter +from collections import OrderedDict +from bika.lims import api from bika.lims import bikaMessageFactory as _ from bika.lims.browser.analyses.view import AnalysesView from bika.lims.config import QCANALYSIS_TYPES +from bika.lims.interfaces import IDuplicateAnalysis +from bika.lims.utils import get_image +from bika.lims.utils import get_link class QCAnalysesView(AnalysesView): @@ -36,81 +40,80 @@ class QCAnalysesView(AnalysesView): """ def __init__(self, context, request, **kwargs): - AnalysesView.__init__(self, context, request, **kwargs) - self.columns['getReferenceAnalysesGroupID'] = { - 'title': _('QC Sample ID'), - 'sortable': False} - self.columns['Worksheet'] = {'title': _('Worksheet'), - 'sortable': False} - self.review_states[0]['columns'] = ['Service', - 'Worksheet', - 'getReferenceAnalysesGroupID', - 'Partition', - 'Method', - 'Instrument', - 'Result', - 'Uncertainty', - 'CaptureDate', - 'DueDate', - 'state_title'] - - qcanalyses = context.getQCAnalyses() - asuids = [an.UID() for an in qcanalyses] - self.contentFilter = {'UID': asuids, - 'sort_on': 'getId'} - self.icon = self.portal_url + \ - "/++resource++bika.lims.images/referencesample.png" - - # TODO-performance: Do not use object. Using brain, use meta_type in - # order to get the object's type - def folderitem(self, obj, item, index): - """Prepare a data item for the listing. + super(QCAnalysesView, self).__init__(context, request, **kwargs) + + icon_path = "/++resource++bika.lims.images/referencesample.png" + self.icon = "{}{}".format(self.portal_url, icon_path) - :param obj: The catalog brain or content object - :param item: Listing item (dictionary) - :param index: Index of the listing item - :returns: Augmented listing data item + def update(self): + """Update hook """ + super(AnalysesView, self).update() - obj = obj.getObject() - # Group items by RefSample - Worksheet - Position - ws = obj.getWorksheet() - wsid = ws and ws.id or '' - wshref = ws.absolute_url() or None - if wshref: - item['replace']['Worksheet'] = "%s" % ( - wshref, wsid) - - imgtype = "" - if obj.portal_type == 'ReferenceAnalysis': - antype = QCANALYSIS_TYPES.getValue(obj.getReferenceType()) - if obj.getReferenceType() == 'c': - imgtype = " " % ( - antype, self.context.absolute_url()) - if obj.getReferenceType() == 'b': - imgtype = " " % ( - antype, self.context.absolute_url()) - item['replace']['Partition'] = "%s" % ( - obj.aq_parent.absolute_url(), obj.aq_parent.id) - elif obj.portal_type == 'DuplicateAnalysis': - antype = QCANALYSIS_TYPES.getValue('d') - imgtype = " " % ( - antype, self.context.absolute_url()) - item['sortcode'] = '%s_%s' % (obj.getRequestID(), obj.getKeyword()) - - item['before']['Service'] = imgtype - item['sortcode'] = '%s_%s' % (obj.getReferenceAnalysesGroupID(), - obj.getKeyword()) - return item + # Update the query with the Sample's worksheet UIDs + worksheet_uids = map(api.get_uid, self.context.getWorksheets()) + self.contentFilter.update({ + "portal_type": ["DuplicateAnalysis", "ReferenceAnalysis"], + "getWorksheetUID": worksheet_uids, + "sort_on": "getId" + }) + + # Add Worksheet and QC Sample ID columns + new_columns = OrderedDict(( + ("Worksheet", { + "title": _("Worksheet"), + "sortable": True, + }), + ("getReferenceAnalysesGroupID", { + "title": _('QC Sample ID'), + "sortable": False, + }), + ("Parent", { + "title": _("Source"), + "sortable": False, + }) + )) + self.columns.update(new_columns) + + # Remove unnecessary columns + if "Hidden" in self.columns: + del(self.columns["Hidden"]) + + # Apply the columns to all review_states + for review_state in self.review_states: + review_state.update({"columns": self.columns.keys()}) + + def folderitem(self, obj, item, index): + item = super(QCAnalysesView, self).folderitem(obj, item, index) - def folderitems(self): - items = AnalysesView.folderitems(self) - # Sort items - items = sorted(items, key=itemgetter('sortcode')) - return items + obj = self.get_object(obj) + + # Fill Worksheet cell + worksheet = obj.getWorksheet() + if not worksheet: + return item + + # Fill the Worksheet cell + ws_id = api.get_id(worksheet) + ws_url = api.get_url(worksheet) + item["replace"]["Worksheet"] = get_link(ws_url, value=ws_id) + + if IDuplicateAnalysis.providedBy(obj): + an_type = "d" + img_name = "duplicate.png" + parent = obj.getRequest() + else: + an_type = obj.getReferenceType() + img_name = an_type == "c" and "control.png" or "blank.png" + parent = obj.aq_parent + + # Render the image + an_type = QCANALYSIS_TYPES.getValue(an_type) + item['before']['Service'] = get_image(img_name, title=an_type) + + # Fill the Parent cell + parent_url = api.get_url(parent) + parent_id = api.get_id(parent) + item["replace"]["Parent"] = get_link(parent_url, value=parent_id) + + return item diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index 8f97cc101c..3025f9275d 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -45,6 +45,7 @@ from bika.lims.browser.widgets.durationwidget import DurationWidget from bika.lims.catalog import CATALOG_ANALYSIS_LISTING from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING +from bika.lims.catalog import CATALOG_WORKSHEET_LISTING from bika.lims.catalog.bika_catalog import BIKA_CATALOG from bika.lims.config import PRIORITIES from bika.lims.config import PROJECTNAME @@ -1804,59 +1805,40 @@ def current_date(self): # noinspection PyCallingNonCallable return DateTime() - def getQCAnalyses(self, qctype=None, review_state=None): - """return the QC analyses performed in the worksheet in which, at - least, one sample of this AR is present. - - Depending on qctype value, returns the analyses of: + def getWorksheets(self, full_objects=False): + """Returns the worksheets that contains analyses from this Sample + """ + # Get the Analyses UIDs of this Sample + analyses_uids = map(api.get_uid, self.getAnalyses()) - - 'b': all Blank Reference Samples used in related worksheet/s - - 'c': all Control Reference Samples used in related worksheet/s - - 'd': duplicates only for samples contained in this AR + # Get the worksheets that contain any of these analyses + query = dict(getAnalysesUIDs=analyses_uids) + worksheets = api.search(query, CATALOG_WORKSHEET_LISTING) + if full_objects: + worksheets = map(api.get_object, worksheets) + return worksheets - If qctype==None, returns all type of qc analyses mentioned above + def getQCAnalyses(self, review_state=None): + """Returns the Quality Control analyses assigned to worksheets that + contains analyses from this Sample """ - qcanalyses = [] - suids = [] - ans = self.getAnalyses() - wf = getToolByName(self, 'portal_workflow') - for an in ans: - an = an.getObject() - if an.getServiceUID() not in suids: - suids.append(an.getServiceUID()) + # Get the worksheet uids + worksheet_uids = map(api.get_uid, self.getWorksheets()) - def valid_dup(wan): - if wan.portal_type == 'ReferenceAnalysis': - return False - an_state = wf.getInfoFor(wan, 'review_state') - return \ - wan.portal_type == 'DuplicateAnalysis' \ - and wan.getRequestID() == self.id \ - and (review_state is None or an_state in review_state) - - def valid_ref(wan): - if wan.portal_type != 'ReferenceAnalysis': - return False - an_state = wf.getInfoFor(wan, 'review_state') - an_reftype = wan.getReferenceType() - return wan.getServiceUID() in suids \ - and wan not in qcanalyses \ - and (qctype is None or an_reftype == qctype) \ - and (review_state is None or an_state in review_state) - - for an in ans: - an = an.getObject() - ws = an.getWorksheet() - if not ws: - continue - was = ws.getAnalyses() - for wa in was: - if valid_dup(wa): - qcanalyses.append(wa) - elif valid_ref(wa): - qcanalyses.append(wa) - - return qcanalyses + # Get the qc analyses from these worksheets + portal_types = ["ReferenceAnalysis", "DuplicateAnalysis"] + query = dict(portal_type=portal_types, getWorksheetUID=worksheet_uids) + qc_analyses = api.search(query, CATALOG_ANALYSIS_LISTING) + + # Bail out analyses with a different review_state + if review_state: + qc_analyses = filter( + lambda an: api.get_review_status(an) in review_state, + qc_analyses + ) + + # Return the objects + return map(api.get_object, qc_analyses) def isInvalid(self): """return if the Analysis Request has been invalidated From 3d8680d8fbbb7bf68884d3f89bcd38ed81d078c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 27 Jan 2020 22:51:35 +0100 Subject: [PATCH 2/8] Changelog --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 22b7568dca..632bf605e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,6 +35,7 @@ Changelog **Fixed** +- #1512 QC Analyses listing appears empty in Sample view - #1511 Links to partitions for Internal Use are displayed in partitions viewlet - #1505 Manage Analyses Form re-applies partitioned Analyses back to the Root - #1503 Avoid duplicate CSS IDs in multi-column Add form From 44af3f6ccdae48b16c0e493ec6ac3e34c7906b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 27 Jan 2020 23:05:34 +0100 Subject: [PATCH 3/8] Return empty if no worksheets or no qc analyses are found --- bika/lims/content/analysisrequest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index 3025f9275d..d546b585ef 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -1810,6 +1810,8 @@ def getWorksheets(self, full_objects=False): """ # Get the Analyses UIDs of this Sample analyses_uids = map(api.get_uid, self.getAnalyses()) + if not analyses_uids: + return [] # Get the worksheets that contain any of these analyses query = dict(getAnalysesUIDs=analyses_uids) @@ -1824,6 +1826,8 @@ def getQCAnalyses(self, review_state=None): """ # Get the worksheet uids worksheet_uids = map(api.get_uid, self.getWorksheets()) + if not worksheet_uids: + return [] # Get the qc analyses from these worksheets portal_types = ["ReferenceAnalysis", "DuplicateAnalysis"] From 49fdf1ecbd9aec99c2292ffe6c70c55469952e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 27 Jan 2020 23:41:17 +0100 Subject: [PATCH 4/8] Hide duplicates in QC analyses listing that don't belong to us --- bika/lims/browser/analyses/qc.py | 6 +++--- bika/lims/content/analysisrequest.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bika/lims/browser/analyses/qc.py b/bika/lims/browser/analyses/qc.py index f498b43209..7491c53b8d 100644 --- a/bika/lims/browser/analyses/qc.py +++ b/bika/lims/browser/analyses/qc.py @@ -50,11 +50,11 @@ def update(self): """ super(AnalysesView, self).update() - # Update the query with the Sample's worksheet UIDs - worksheet_uids = map(api.get_uid, self.context.getWorksheets()) + # Update the query with the QC Analyses uids + qc_uids = map(api.get_uid, self.context.getQCAnalyses()) self.contentFilter.update({ + "UID": qc_uids, "portal_type": ["DuplicateAnalysis", "ReferenceAnalysis"], - "getWorksheetUID": worksheet_uids, "sort_on": "getId" }) diff --git a/bika/lims/content/analysisrequest.py b/bika/lims/content/analysisrequest.py index d546b585ef..8e4c82ba23 100644 --- a/bika/lims/content/analysisrequest.py +++ b/bika/lims/content/analysisrequest.py @@ -1829,11 +1829,17 @@ def getQCAnalyses(self, review_state=None): if not worksheet_uids: return [] - # Get the qc analyses from these worksheets - portal_types = ["ReferenceAnalysis", "DuplicateAnalysis"] - query = dict(portal_type=portal_types, getWorksheetUID=worksheet_uids) + # Get reference qc analyses from these worksheets + query = dict(portal_type="ReferenceAnalysis", + getWorksheetUID=worksheet_uids) qc_analyses = api.search(query, CATALOG_ANALYSIS_LISTING) + # Extend with duplicate qc analyses from these worksheets and Sample + query = dict(portal_type="DuplicateAnalysis", + getWorksheetUID=worksheet_uids, + getAncestorsUIDs=[api.get_uid(self)]) + qc_analyses += api.search(query, CATALOG_ANALYSIS_LISTING) + # Bail out analyses with a different review_state if review_state: qc_analyses = filter( From f95e729436b6615f280e2ca6ea9f50c867fc30c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 27 Jan 2020 23:46:52 +0100 Subject: [PATCH 5/8] Remove unnecessary filters (review_states) --- bika/lims/browser/analyses/qc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bika/lims/browser/analyses/qc.py b/bika/lims/browser/analyses/qc.py index 7491c53b8d..68168cebac 100644 --- a/bika/lims/browser/analyses/qc.py +++ b/bika/lims/browser/analyses/qc.py @@ -79,10 +79,14 @@ def update(self): if "Hidden" in self.columns: del(self.columns["Hidden"]) + # Remove filters (Valid, Invalid, All) + self.review_states = [self.review_states[0]] + # Apply the columns to all review_states for review_state in self.review_states: review_state.update({"columns": self.columns.keys()}) + def folderitem(self, obj, item, index): item = super(QCAnalysesView, self).folderitem(obj, item, index) From b8255b5ff616dfe470d5a9c1c92a14cf6d520a7e Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 28 Jan 2020 10:06:23 +0100 Subject: [PATCH 6/8] Only update the content filter in update method --- bika/lims/browser/analyses/qc.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/bika/lims/browser/analyses/qc.py b/bika/lims/browser/analyses/qc.py index 68168cebac..bdd2d18fe7 100644 --- a/bika/lims/browser/analyses/qc.py +++ b/bika/lims/browser/analyses/qc.py @@ -45,19 +45,6 @@ def __init__(self, context, request, **kwargs): icon_path = "/++resource++bika.lims.images/referencesample.png" self.icon = "{}{}".format(self.portal_url, icon_path) - def update(self): - """Update hook - """ - super(AnalysesView, self).update() - - # Update the query with the QC Analyses uids - qc_uids = map(api.get_uid, self.context.getQCAnalyses()) - self.contentFilter.update({ - "UID": qc_uids, - "portal_type": ["DuplicateAnalysis", "ReferenceAnalysis"], - "sort_on": "getId" - }) - # Add Worksheet and QC Sample ID columns new_columns = OrderedDict(( ("Worksheet", { @@ -86,6 +73,18 @@ def update(self): for review_state in self.review_states: review_state.update({"columns": self.columns.keys()}) + def update(self): + """Update hook + """ + super(AnalysesView, self).update() + + # Update the query with the QC Analyses uids + qc_uids = map(api.get_uid, self.context.getQCAnalyses()) + self.contentFilter.update({ + "UID": qc_uids, + "portal_type": ["DuplicateAnalysis", "ReferenceAnalysis"], + "sort_on": "getId" + }) def folderitem(self, obj, item, index): item = super(QCAnalysesView, self).folderitem(obj, item, index) From ff7efff85e84398b1367d3d80fa09c9900d13a7b Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 28 Jan 2020 10:53:21 +0100 Subject: [PATCH 7/8] Ensure read-only state for QC analyses --- bika/lims/browser/analyses/qc.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bika/lims/browser/analyses/qc.py b/bika/lims/browser/analyses/qc.py index bdd2d18fe7..b96ba10ac0 100644 --- a/bika/lims/browser/analyses/qc.py +++ b/bika/lims/browser/analyses/qc.py @@ -77,7 +77,6 @@ def update(self): """Update hook """ super(AnalysesView, self).update() - # Update the query with the QC Analyses uids qc_uids = map(api.get_uid, self.context.getQCAnalyses()) self.contentFilter.update({ @@ -86,6 +85,13 @@ def update(self): "sort_on": "getId" }) + def is_analysis_edition_allowed(self, analysis_brain): + """Overwrite this method to ensure the table is recognized as readonly + + XXX: why is the super method not recognizing `self.allow_edit`? + """ + return False + def folderitem(self, obj, item, index): item = super(QCAnalysesView, self).folderitem(obj, item, index) From c779201d4ffdcdccb7ed89d6958582582b794836 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 28 Jan 2020 10:54:23 +0100 Subject: [PATCH 8/8] Sort QC analyses on sortable_title --- bika/lims/browser/analyses/qc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bika/lims/browser/analyses/qc.py b/bika/lims/browser/analyses/qc.py index b96ba10ac0..89e98cdc36 100644 --- a/bika/lims/browser/analyses/qc.py +++ b/bika/lims/browser/analyses/qc.py @@ -82,7 +82,7 @@ def update(self): self.contentFilter.update({ "UID": qc_uids, "portal_type": ["DuplicateAnalysis", "ReferenceAnalysis"], - "sort_on": "getId" + "sort_on": "sortable_title" }) def is_analysis_edition_allowed(self, analysis_brain):