From 8ce4fb9cf6c2042f59129a9493260264bd8695bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Fri, 27 Nov 2020 12:07:39 +0100 Subject: [PATCH 1/6] Allow to add new sections in Sample via adapters --- CHANGES.rst | 1 + .../browser/analysisrequest/configure.zcml | 18 ++++ .../lims/browser/analysisrequest/sections.py | 84 +++++++++++++++++++ .../templates/analysisrequest_view.pt | 53 +++--------- src/bika/lims/browser/analysisrequest/view.py | 41 ++------- src/senaite/core/interfaces/__init__.py | 13 +++ 6 files changed, 138 insertions(+), 72 deletions(-) create mode 100644 src/bika/lims/browser/analysisrequest/sections.py diff --git a/CHANGES.rst b/CHANGES.rst index fdc51388da..a27689429c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.0.0rc3 (unreleased) --------------------- +- #1690 Allow to add new sections in Sample via adapters - #1689 Display tabs in content edit view when more than one group - #1682 Fix `LocationError` when editing a entry in the configuration registry - #1685 Remove Supply Orders diff --git a/src/bika/lims/browser/analysisrequest/configure.zcml b/src/bika/lims/browser/analysisrequest/configure.zcml index 4f1d3aad22..baffbeabd5 100644 --- a/src/bika/lims/browser/analysisrequest/configure.zcml +++ b/src/bika/lims/browser/analysisrequest/configure.zcml @@ -95,6 +95,24 @@ layer="bika.lims.interfaces.IBikaLIMS" /> + + + + + + + + + 0 + + def get_listing_view(self): + request = api.get_request() + view_name = "table_{}_analyses".format(self.capture) + view = api.get_view(view_name, context=self.sample, request=request) + return view + + def render(self): + view = self.get_listing_view() + view.update() + view.before_render() + return view.ajax_contents_table() + + +class FieldAnalysesSection(LabAnalysesSection): + """Field analyses section adapter for Sample view + """ + order = 5 + title = _("Field Analyses") + capture = "field" + + +class QCAnalysesSection(LabAnalysesSection): + """QC analyses section adapter for Sample view + """ + order = 50 + title = _("QC Analyses") + capture = "qc" + + def is_visible(self): + """Returns true if this sample contains at least one qc analysis + """ + analyses = self.sample.getQCAnalyses() + return len(analyses) > 0 diff --git a/src/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt b/src/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt index c0b4906e98..2ffeba235a 100644 --- a/src/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt +++ b/src/bika/lims/browser/analysisrequest/templates/analysisrequest_view.pt @@ -70,47 +70,20 @@ - -
-
-

- - - - Field Analyses -

- -
-
-
- - -
-
-

- - - - Lab Analyses -

- -
-
-
- - -
-
-

- - - - QC Analyses -

- -
+ +
+
+

+ + +

+
- +
diff --git a/src/bika/lims/browser/analysisrequest/view.py b/src/bika/lims/browser/analysisrequest/view.py index 6eb4021201..85f1fd0fe9 100644 --- a/src/bika/lims/browser/analysisrequest/view.py +++ b/src/bika/lims/browser/analysisrequest/view.py @@ -22,6 +22,9 @@ from bika.lims.browser import BrowserView from bika.lims.browser.header_table import HeaderTableView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from senaite.core.interfaces import ISampleSection +from zope.component import subscribers + from resultsinterpretation import ARResultsInterpretationView @@ -47,39 +50,13 @@ def __call__(self): return self.template() - def render_analyses_table(self, table="lab"): - """Render Analyses Table - """ - if table not in ["lab", "field", "qc"]: - raise KeyError("Table '{}' does not exist".format(table)) - view_name = "table_{}_analyses".format(table) - view = api.get_view( - view_name, context=self.context, request=self.request) - # Call listing hooks - view.update() - view.before_render() - return view.ajax_contents_table() - - def has_lab_analyses(self): - """Check if the AR contains lab analyses - """ - # Negative performance impact - add a Metadata column - analyses = self.context.getAnalyses(getPointOfCapture="lab") - return len(analyses) > 0 - - def has_field_analyses(self): - """Check if the AR contains field analyses - """ - # Negative performance impact - add a Metadata column - analyses = self.context.getAnalyses(getPointOfCapture="field") - return len(analyses) > 0 - - def has_qc_analyses(self): - """Check if the AR contains field analyses + def get_sections(self): + """Returns a list with adapters that implement ISampleSection """ - # Negative performance impact - add a Metadata column - analyses = self.context.getQCAnalyses() - return len(analyses) > 0 + # We use subscriber adapters here because we need different add-ons to + # be able to add their own sections without dependencies amongst them + sections = subscribers((self.context, ), ISampleSection) + return sorted(sections, key=lambda s: getattr(s, "order", 100)) def is_hazardous(self): """Checks if the AR is hazardous diff --git a/src/senaite/core/interfaces/__init__.py b/src/senaite/core/interfaces/__init__.py index 5d8b175c78..6e2bb05809 100644 --- a/src/senaite/core/interfaces/__init__.py +++ b/src/senaite/core/interfaces/__init__.py @@ -48,3 +48,16 @@ class IHideActionsMenu(Interface): """Marker interface that can be applied for conttents that should not display the content actions menu """ + + +class ISampleSection(Interface): + """Marker for additional sections to be displayed in Sample view/edit form + """ + + def is_visible(self): + """Returns whether this section is visible or not + """ + + def render(self): + """Returns the html-like section + """ From 2ff648e63c293ede467702122ad293f513bef902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Mon, 30 Nov 2020 18:25:42 +0100 Subject: [PATCH 2/6] Ensure deepcopy interims --- src/bika/lims/browser/analyses/view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bika/lims/browser/analyses/view.py b/src/bika/lims/browser/analyses/view.py index 6045818975..072afe8be1 100644 --- a/src/bika/lims/browser/analyses/view.py +++ b/src/bika/lims/browser/analyses/view.py @@ -21,6 +21,7 @@ import json from collections import OrderedDict from copy import copy +from copy import deepcopy from bika.lims import api from bika.lims import bikaMessageFactory as _ @@ -824,7 +825,7 @@ def _folder_item_calculation(self, analysis_brain, item): interim_fields = analysis_obj.getInterimFields() or list() # Copy to prevent to avoid persistent changes - interim_fields = copy(interim_fields) + interim_fields = deepcopy(interim_fields) for interim_field in interim_fields: interim_keyword = interim_field.get('keyword', '') From 05fe0188d635bf214574000580b5f4eb99fdf78e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Puiggen=C3=A9?= Date: Wed, 2 Dec 2020 14:21:35 +0100 Subject: [PATCH 3/6] Use Viewlets approach instead of our own subscriber adapters --- .../browser/analysisrequest/configure.zcml | 37 ++++++++++++------- .../lims/browser/analysisrequest/sections.py | 27 +++++++------- .../lims/browser/analysisrequest/tables.py | 1 - .../templates/analysisrequest_view.pt | 14 +------ src/bika/lims/browser/analysisrequest/view.py | 11 ------ .../core/browser/viewlets/configure.zcml | 7 ++++ .../core/browser/viewlets/contentsections.py | 32 ++++++++++++++++ .../core/browser/viewlets/interfaces.py | 5 +++ .../viewlets/templates/contentsections.pt | 20 ++++++++++ src/senaite/core/interfaces/__init__.py | 13 ------- 10 files changed, 101 insertions(+), 66 deletions(-) create mode 100644 src/senaite/core/browser/viewlets/contentsections.py create mode 100644 src/senaite/core/browser/viewlets/templates/contentsections.pt diff --git a/src/bika/lims/browser/analysisrequest/configure.zcml b/src/bika/lims/browser/analysisrequest/configure.zcml index baffbeabd5..0c5550a149 100644 --- a/src/bika/lims/browser/analysisrequest/configure.zcml +++ b/src/bika/lims/browser/analysisrequest/configure.zcml @@ -95,23 +95,32 @@ layer="bika.lims.interfaces.IBikaLIMS" /> - - + - - - + + + - - - + + + + name="senaite.core.section.qc_analyses" + manager="senaite.core.browser.viewlets.interfaces.IContentSection" + class=".sections.QCAnalysesViewlet" + layer="senaite.core.interfaces.ISenaiteCore" + permission="zope2.View" /> -
-
-

- - -

- -
-
+
diff --git a/src/bika/lims/browser/analysisrequest/view.py b/src/bika/lims/browser/analysisrequest/view.py index 85f1fd0fe9..470fefb871 100644 --- a/src/bika/lims/browser/analysisrequest/view.py +++ b/src/bika/lims/browser/analysisrequest/view.py @@ -22,9 +22,6 @@ from bika.lims.browser import BrowserView from bika.lims.browser.header_table import HeaderTableView from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile -from senaite.core.interfaces import ISampleSection -from zope.component import subscribers - from resultsinterpretation import ARResultsInterpretationView @@ -50,14 +47,6 @@ def __call__(self): return self.template() - def get_sections(self): - """Returns a list with adapters that implement ISampleSection - """ - # We use subscriber adapters here because we need different add-ons to - # be able to add their own sections without dependencies amongst them - sections = subscribers((self.context, ), ISampleSection) - return sorted(sections, key=lambda s: getattr(s, "order", 100)) - def is_hazardous(self): """Checks if the AR is hazardous """ diff --git a/src/senaite/core/browser/viewlets/configure.zcml b/src/senaite/core/browser/viewlets/configure.zcml index 96d92aa02f..9cc42e6970 100644 --- a/src/senaite/core/browser/viewlets/configure.zcml +++ b/src/senaite/core/browser/viewlets/configure.zcml @@ -227,5 +227,12 @@ layer="senaite.core.interfaces.ISenaiteCore" /> + + diff --git a/src/senaite/core/browser/viewlets/contentsections.py b/src/senaite/core/browser/viewlets/contentsections.py new file mode 100644 index 0000000000..3b8699a8c1 --- /dev/null +++ b/src/senaite/core/browser/viewlets/contentsections.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.CORE. +# +# SENAITE.CORE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright 2018-2020 by it's authors. +# Some rights reserved, see README and LICENSE. + +from plone.app.viewletmanager.manager import OrderedViewletManager +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile + + +class ContentSectionsViewletManager(OrderedViewletManager): + custom_template = ViewPageTemplateFile("templates/contentsections.pt") + + def render(self): + return self.custom_template() + + def get_sections_viewlets(self): + return sorted(self.viewlets, key=lambda v: v.order) diff --git a/src/senaite/core/browser/viewlets/interfaces.py b/src/senaite/core/browser/viewlets/interfaces.py index cd8829529a..77a97f86a3 100644 --- a/src/senaite/core/browser/viewlets/interfaces.py +++ b/src/senaite/core/browser/viewlets/interfaces.py @@ -44,3 +44,8 @@ class IAboveListingTable(IViewletManager): class IBelowListingTable(IViewletManager): """A viewlet manager that sits below listing tables """ + + +class IContentSection(IViewletManager): + """A viewlet manager that render sections inside content + """ diff --git a/src/senaite/core/browser/viewlets/templates/contentsections.pt b/src/senaite/core/browser/viewlets/templates/contentsections.pt new file mode 100644 index 0000000000..177facb150 --- /dev/null +++ b/src/senaite/core/browser/viewlets/templates/contentsections.pt @@ -0,0 +1,20 @@ +
+ +
+
+ + +

+ + +

+ + + + +
+
+ +
diff --git a/src/senaite/core/interfaces/__init__.py b/src/senaite/core/interfaces/__init__.py index 6e2bb05809..5d8b175c78 100644 --- a/src/senaite/core/interfaces/__init__.py +++ b/src/senaite/core/interfaces/__init__.py @@ -48,16 +48,3 @@ class IHideActionsMenu(Interface): """Marker interface that can be applied for conttents that should not display the content actions menu """ - - -class ISampleSection(Interface): - """Marker for additional sections to be displayed in Sample view/edit form - """ - - def is_visible(self): - """Returns whether this section is visible or not - """ - - def render(self): - """Returns the html-like section - """ From d501248d6688f35cccb7f336b076ef7a12163ef4 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Thu, 3 Dec 2020 10:01:50 +0100 Subject: [PATCH 4/6] Refactored sample sections viewlets --- .../browser/analysisrequest/configure.zcml | 27 ------------ .../templates/analysisrequest_view.pt | 8 +++- .../core/browser/viewlets/configure.zcml | 41 ++++++++++++++++--- .../core/browser/viewlets/contentsections.py | 32 --------------- .../core/browser/viewlets/interfaces.py | 4 +- .../core/browser/viewlets/sampleanalyses.py} | 16 +++----- .../viewlets/templates/contentsections.pt | 20 --------- .../viewlets/templates/sampleanalyses.pt | 19 +++++++++ .../core/profiles/default/viewlets.xml | 7 ++++ src/senaite/core/upgrade/v02_00_000.py | 1 + 10 files changed, 75 insertions(+), 100 deletions(-) delete mode 100644 src/senaite/core/browser/viewlets/contentsections.py rename src/{bika/lims/browser/analysisrequest/sections.py => senaite/core/browser/viewlets/sampleanalyses.py} (91%) delete mode 100644 src/senaite/core/browser/viewlets/templates/contentsections.pt create mode 100644 src/senaite/core/browser/viewlets/templates/sampleanalyses.pt diff --git a/src/bika/lims/browser/analysisrequest/configure.zcml b/src/bika/lims/browser/analysisrequest/configure.zcml index 0c5550a149..4f1d3aad22 100644 --- a/src/bika/lims/browser/analysisrequest/configure.zcml +++ b/src/bika/lims/browser/analysisrequest/configure.zcml @@ -95,33 +95,6 @@ layer="bika.lims.interfaces.IBikaLIMS" /> - - - - - - - - - - -
+ +
+
+
+
+
diff --git a/src/senaite/core/browser/viewlets/configure.zcml b/src/senaite/core/browser/viewlets/configure.zcml index 9cc42e6970..4d08b893bf 100644 --- a/src/senaite/core/browser/viewlets/configure.zcml +++ b/src/senaite/core/browser/viewlets/configure.zcml @@ -227,12 +227,41 @@ layer="senaite.core.interfaces.ISenaiteCore" /> - + + name="senaite.samplesections" + provides=".interfaces.ISampleSection" + class="plone.app.viewletmanager.manager.OrderedViewletManager" + permission="zope2.View" + layer="senaite.core.interfaces.ISenaiteCore" + /> + + + + + + + + + + diff --git a/src/senaite/core/browser/viewlets/contentsections.py b/src/senaite/core/browser/viewlets/contentsections.py deleted file mode 100644 index 3b8699a8c1..0000000000 --- a/src/senaite/core/browser/viewlets/contentsections.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of SENAITE.CORE. -# -# SENAITE.CORE is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation, version 2. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., 51 -# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# -# Copyright 2018-2020 by it's authors. -# Some rights reserved, see README and LICENSE. - -from plone.app.viewletmanager.manager import OrderedViewletManager -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile - - -class ContentSectionsViewletManager(OrderedViewletManager): - custom_template = ViewPageTemplateFile("templates/contentsections.pt") - - def render(self): - return self.custom_template() - - def get_sections_viewlets(self): - return sorted(self.viewlets, key=lambda v: v.order) diff --git a/src/senaite/core/browser/viewlets/interfaces.py b/src/senaite/core/browser/viewlets/interfaces.py index 77a97f86a3..d31d9b9c7b 100644 --- a/src/senaite/core/browser/viewlets/interfaces.py +++ b/src/senaite/core/browser/viewlets/interfaces.py @@ -46,6 +46,6 @@ class IBelowListingTable(IViewletManager): """ -class IContentSection(IViewletManager): - """A viewlet manager that render sections inside content +class ISampleSection(IViewletManager): + """A viewlet manager responsible for sample sections below the header table """ diff --git a/src/bika/lims/browser/analysisrequest/sections.py b/src/senaite/core/browser/viewlets/sampleanalyses.py similarity index 91% rename from src/bika/lims/browser/analysisrequest/sections.py rename to src/senaite/core/browser/viewlets/sampleanalyses.py index ddbe3e7509..a8867ca5a4 100644 --- a/src/bika/lims/browser/analysisrequest/sections.py +++ b/src/senaite/core/browser/viewlets/sampleanalyses.py @@ -21,14 +21,13 @@ from bika.lims import api from bika.lims import senaiteMessageFactory as _ from plone.app.layout.viewlets import ViewletBase +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile class LabAnalysesViewlet(ViewletBase): """Laboratory analyses section viewlet for Sample view """ - - # Order index for this section - order = 10 + index = ViewPageTemplateFile("templates/sampleanalyses.pt") title = _("Analyses") icon_name = "analysisservice" @@ -38,7 +37,7 @@ class LabAnalysesViewlet(ViewletBase): def sample(self): return self.view.context - def is_visible(self): + def available(self): """Returns true if this sample contains at least one analysis for the point of capture (capture) """ @@ -51,10 +50,7 @@ def get_listing_view(self): view = api.get_view(view_name, context=self.sample, request=request) return view - def index(self): - return self.render() - - def render(self): + def contents_table(self): view = self.get_listing_view() view.update() view.before_render() @@ -64,7 +60,6 @@ def render(self): class FieldAnalysesViewlet(LabAnalysesViewlet): """Field analyses section viewlet for Sample view """ - order = 5 title = _("Field Analyses") capture = "field" @@ -72,11 +67,10 @@ class FieldAnalysesViewlet(LabAnalysesViewlet): class QCAnalysesViewlet(LabAnalysesViewlet): """QC analyses section viewlet for Sample view """ - order = 50 title = _("QC Analyses") capture = "qc" - def is_visible(self): + def available(self): """Returns true if this sample contains at least one qc analysis """ analyses = self.sample.getQCAnalyses() diff --git a/src/senaite/core/browser/viewlets/templates/contentsections.pt b/src/senaite/core/browser/viewlets/templates/contentsections.pt deleted file mode 100644 index 177facb150..0000000000 --- a/src/senaite/core/browser/viewlets/templates/contentsections.pt +++ /dev/null @@ -1,20 +0,0 @@ -
- -
-
- - -

- - -

- - - - -
-
- -
diff --git a/src/senaite/core/browser/viewlets/templates/sampleanalyses.pt b/src/senaite/core/browser/viewlets/templates/sampleanalyses.pt new file mode 100644 index 0000000000..1ebcba9eaf --- /dev/null +++ b/src/senaite/core/browser/viewlets/templates/sampleanalyses.pt @@ -0,0 +1,19 @@ +
+
+
+ + +

+ + +

+ + + + +
+
+
diff --git a/src/senaite/core/profiles/default/viewlets.xml b/src/senaite/core/profiles/default/viewlets.xml index 4fcd2913d3..5e2ea1ac2f 100644 --- a/src/senaite/core/profiles/default/viewlets.xml +++ b/src/senaite/core/profiles/default/viewlets.xml @@ -16,6 +16,13 @@ + + + + + + + diff --git a/src/senaite/core/upgrade/v02_00_000.py b/src/senaite/core/upgrade/v02_00_000.py index 071c8077da..8dbfc3e6e0 100644 --- a/src/senaite/core/upgrade/v02_00_000.py +++ b/src/senaite/core/upgrade/v02_00_000.py @@ -90,6 +90,7 @@ def upgrade(tool): setup.runImportStepFromProfile(profile, "typeinfo") setup.runImportStepFromProfile(profile, "workflow") setup.runImportStepFromProfile(profile, "browserlayer") + setup.runImportStepFromProfile(profile, "viewlets") # run import steps located in bika.lims profiles _run_import_step(portal, "typeinfo", profile="profile-bika.lims:default") _run_import_step(portal, "workflow", profile="profile-bika.lims:default") From d3a2b50e560be57451b46e115684499011465b90 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Thu, 3 Dec 2020 10:02:57 +0100 Subject: [PATCH 5/6] Added viewlet render condition --- src/senaite/core/browser/viewlets/templates/sampleanalyses.pt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/senaite/core/browser/viewlets/templates/sampleanalyses.pt b/src/senaite/core/browser/viewlets/templates/sampleanalyses.pt index 1ebcba9eaf..fe0ff32348 100644 --- a/src/senaite/core/browser/viewlets/templates/sampleanalyses.pt +++ b/src/senaite/core/browser/viewlets/templates/sampleanalyses.pt @@ -1,4 +1,5 @@
From 8a5c0963bcad68ab7d3703a6d57036a6d9f6d868 Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Thu, 3 Dec 2020 10:05:44 +0100 Subject: [PATCH 6/6] Sample is the current context --- src/senaite/core/browser/viewlets/sampleanalyses.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/senaite/core/browser/viewlets/sampleanalyses.py b/src/senaite/core/browser/viewlets/sampleanalyses.py index a8867ca5a4..1dd06ae287 100644 --- a/src/senaite/core/browser/viewlets/sampleanalyses.py +++ b/src/senaite/core/browser/viewlets/sampleanalyses.py @@ -35,7 +35,7 @@ class LabAnalysesViewlet(ViewletBase): @property def sample(self): - return self.view.context + return self.context def available(self): """Returns true if this sample contains at least one analysis for the