diff --git a/CHANGES.rst b/CHANGES.rst index 83f82c4c0f..eefb9c4e9e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,6 +35,7 @@ Changelog **Fixed** +- #1512 QC Analyses listing appears empty in Sample view - #1510 Error when viewing a Sample w/o Batch as client contact - #1511 Links to partitions for Internal Use are displayed in partitions viewlet - #1505 Manage Analyses Form re-applies partitioned Analyses back to the Root diff --git a/bika/lims/browser/analyses/qc.py b/bika/lims/browser/analyses/qc.py index e9e76cdc3a..89e98cdc36 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,89 @@ 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) + + # 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"]) + + # Remove filters (Valid, Invalid, All) + self.review_states = [self.review_states[0]] - :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 + # Apply the columns to all review_states + 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": "sortable_title" + }) - 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 + 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 folderitems(self): - items = AnalysesView.folderitems(self) - # Sort items - items = sorted(items, key=itemgetter('sortcode')) - return items + def folderitem(self, obj, item, index): + item = super(QCAnalysesView, self).folderitem(obj, item, index) + + 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..8e4c82ba23 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,50 @@ 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()) + if not analyses_uids: + return [] - - '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()) + if not worksheet_uids: + return [] - 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 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( + 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