diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
new file mode 100644
index 0000000..d03c05a
--- /dev/null
+++ b/.github/workflows/build-and-test.yml
@@ -0,0 +1,30 @@
+name: build and test senaite.ast
+on:
+ - push
+ - pull_request
+env:
+ PLONE_VERSION: "5.2"
+jobs:
+ build-and-test:
+ runs-on: 'ubuntu-20.04'
+ container:
+ image: python:2.7.18-buster
+ steps:
+ - uses: actions/checkout@v3
+ - name: cache eggs
+ uses: actions/cache@v3
+ with:
+ key: eggs-cache-${{ hashFiles('buildout.cfg', 'requirements.txt') }}
+ path: |
+ eggs/
+ - name: install
+ run: |
+ pip install virtualenv
+ virtualenv -p `which python` .
+ bin/pip install --upgrade pip
+ bin/pip install -r requirements.txt
+ bin/buildout -N -t 3 annotate
+ bin/buildout -N -t 3
+ - name: test
+ run: |
+ bin/test -s senaite.ast.tests
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..080b0a6
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,15 @@
+# Read the Docs configuration file for Sphinx projects
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the OS, Python version and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.12"
+
+# Build documentation in the "docs/" directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f97fdaa..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-version: ~> 1.0
-language: python
-os: linux
-dist: xenial
-cache:
- pip: true
- directories:
- - eggs
-matrix:
- fast_finish: true
- include:
- - python: "2.7"
- env: PLONE_VERSION="5.2"
-before_install:
- - virtualenv -p `which python` .
- - bin/pip install -r requirements.txt
- - bin/buildout -N -t 3 annotate
-install:
- - bin/buildout -N -t 3
-script:
- - bin/test -s senaite.ast
diff --git a/README.rst b/README.rst
index 1e8c076..192ab42 100644
--- a/README.rst
+++ b/README.rst
@@ -4,8 +4,8 @@ Antibiotic Sensitivity Testing (AST) for SENAITE
.. image:: https://img.shields.io/pypi/v/senaite.ast.svg?style=flat-square
:target: https://pypi.python.org/pypi/senaite.ast
-.. image:: https://img.shields.io/travis/com/senaite/senaite.ast/master.svg?style=flat-square
- :target: https://app.travis-ci.com/github/senaite/senaite.ast
+.. image:: https://img.shields.io/github/actions/workflow/status/senaite/senaite.ast/build-and-test.yml?branch=2.x
+ :target: https://github.com/senaite/senaite.ast/actions/workflows/build-and-test.yml?query=branch:2.x
.. image:: https://readthedocs.org/projects/pip/badge/
:target: https://senaiteast.readthedocs.org
diff --git a/buildout.cfg b/buildout.cfg
index 3f7ef16..754c187 100644
--- a/buildout.cfg
+++ b/buildout.cfg
@@ -1,105 +1,7 @@
[buildout]
-index = https://pypi.org/simple/
-extends = https://dist.plone.org/release/5.2-latest/versions.cfg
-find-links =
- https://dist.plone.org/release/5.2-latest/
- https://dist.plone.org/thirdparty/
-
-parts =
- instance
- test
- omelette
- i18ndude
- zopepy
- update_translations
- write_code_headers
-
-eggs =
- senaite.core
- senaite.app.listing
- senaite.app.spotlight
- senaite.app.supermodel
- senaite.impress
- senaite.jsonapi
- senaite.lims
- senaite.abx
- senaite.microorganism
- senaite.ast
- plone.reload
- Products.PrintingMailHost
-
-extensions = mr.developer
-
+extends = https://raw.githubusercontent.com/senaite/senaite.core/2.x/buildout.base.cfg
package-name = senaite.ast
-versions = versions
-show-picked-versions = true
-
-plone-user = admin:admin
-
-develop = .
-sources = sources
-auto-checkout = *
-
[sources]
-senaite.core = git https://github.com/senaite/senaite.core.git branch=2.x
-senaite.app.listing = git https://github.com/senaite/senaite.app.listing.git branch=2.x
-senaite.app.spotlight = git https://github.com/senaite/senaite.app.spotlight.git branch=2.x
-senaite.app.supermodel = git https://github.com/senaite/senaite.app.supermodel.git branch=2.x
-senaite.impress = git https://github.com/senaite/senaite.impress.git branch=2.x
-senaite.jsonapi = git https://github.com/senaite/senaite.jsonapi.git branch=2.x
-senaite.lims = git https://github.com/senaite/senaite.lims.git branch=2.x
senaite.abx = git https://github.com/senaite/senaite.abx.git
senaite.microorganism = git https://github.com/senaite/senaite.microorganism.git
-
-[instance]
-recipe = plone.recipe.zope2instance
-http-address = 127.0.0.1:8080
-user = ${buildout:plone-user}
-wsgi = on
-eggs =
- Plone
- plone.app.upgrade
- ${buildout:package-name}
- ${buildout:eggs}
-deprecation-warnings = on
-environment-vars =
- zope_i18n_compile_mo_files true
-zcml =
-
-[i18ndude]
-unzip = true
-recipe = zc.recipe.egg
-eggs = i18ndude
-
-[update_translations]
-recipe = collective.recipe.template
-output = ${buildout:directory}/bin/update_translations
-input = ${buildout:directory}/templates/update_translations.in
-mode = 755
-
-[write_code_headers]
-recipe = collective.recipe.template
-output = ${buildout:directory}/bin/write_code_headers
-input = ${buildout:directory}/templates/write_code_headers.py.in
-mode = 755
-
-[test]
-recipe = zc.recipe.testrunner
-defaults = ['--auto-color', '--auto-progress']
-eggs =
- senaite.ast [test]
-
-[omelette]
-recipe = collective.recipe.omelette
-eggs = ${buildout:eggs}
-
-[zopepy]
-recipe = zc.recipe.egg
-eggs = ${instance:eggs}
-interpreter = zopepy
-scripts = zopepy
-
-[versions]
-setuptools =
-zc.buildout =
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 9626b19..b341443 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,14 @@ Changelog
1.2.0 (unreleased)
------------------
+- #37 Compatibility with core#2567 (AnalysisCategory to DX)
+- #37 Compatibility with core#2471 (Department to DX)
+- #36 Fix user can edit ast built-in services after upgrades
+- #35 Compatibility with core#2521 - AT2DX ARTemplate/SampleTemplate
+- #34 Add transition "Reject antibiotics"
+- #33 Display the Antibiotic Sensitivity section only when necessary
+- #32 Compatibility with senaite.core#2492 (AnalysisProfile to DX)
+
1.1.0 (2024-01-04)
------------------
@@ -17,6 +25,7 @@ Changelog
- #21 Fix AST entry is empty when analyses categorization for sample is checked
- #20 Compatibility with senaite.app.listing#87
+
1.0.0 (2022-06-18)
------------------
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 2e10e14..1187c24 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -175,7 +175,30 @@ From this view, user can choose the tuples Microorganism-Antibiotic to be
reported in results. Once the button "Save" is pressed, the value for analyses
with name "Report" for all microorganisms are updated accordingly.
-.. Links
+
+.. _RejectAntibiotics:
+
+Flag antibiotics as Not Tested
+------------------------------
+
+Quite often, laboratory manager will want to report "Not tested" for some
+antibiotics and microorganisms, while keeping them in the AST panel. This can
+be easily achieved with the transition "Reject antibiotics". This transition is
+available for analyses that are part of a sensitivity testing panel. Once
+clicked, a modal view is displayed, where user can choose the antibiotics to be
+flagged as "Not Tested":
+
+.. image:: static/reject_antibiotics.png
+ :width: 640
+ :alt: Reject antibiotics
+
+Once done, "NT" is displayed as a result for the selected antibiotics, both in
+results entry and in results report:
+
+.. image:: static/not_tested.png
+ :width: 640
+ :alt: Antibiotics flagged as Not Tested
+
.. _SENAITE LIMS: https://www.senaite.com
.. _senaite.ast: https://pypi.python.org/pypi/senaite.ast
diff --git a/docs/static/not_tested.png b/docs/static/not_tested.png
new file mode 100644
index 0000000..99c3b0d
Binary files /dev/null and b/docs/static/not_tested.png differ
diff --git a/docs/static/reject_antibiotics.png b/docs/static/reject_antibiotics.png
new file mode 100644
index 0000000..e56fd8f
Binary files /dev/null and b/docs/static/reject_antibiotics.png differ
diff --git a/requirements.txt b/requirements.txt
index b65a7a0..1e1b3ab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-setuptools==42.0.2
+setuptools==44.1.1
zc.buildout==2.13.3
wheel
diff --git a/src/senaite/ast/adapters/configure.zcml b/src/senaite/ast/adapters/configure.zcml
index e5870ca..e342915 100644
--- a/src/senaite/ast/adapters/configure.zcml
+++ b/src/senaite/ast/adapters/configure.zcml
@@ -23,15 +23,15 @@
AST-like analyses are always added manually afterwards, based on the result
oif the "Microorganism Identification" analysis -->
-
+
diff --git a/src/senaite/ast/adapters/guards.py b/src/senaite/ast/adapters/guards.py
index 74e7a6b..7e34a5c 100644
--- a/src/senaite/ast/adapters/guards.py
+++ b/src/senaite/ast/adapters/guards.py
@@ -26,6 +26,7 @@
from senaite.ast.config import DISK_CONTENT_KEY
from senaite.ast.config import MIC_KEY
from senaite.ast.config import ZONE_SIZE_KEY
+from senaite.ast.utils import is_ast_analysis
from zope.interface import implementer
OPERATORS = ["<=", ">=", "<", ">"]
@@ -103,6 +104,10 @@ def guard_submit(self):
# Skip extrapolated antibiotics
continue
+ if utils.is_rejected_interim(antibiotic):
+ # Skip rejected antibiotics
+ continue
+
if utils.is_interim_empty(antibiotic):
# Cannot submit if no result
return False
@@ -135,3 +140,9 @@ def guard_submit(self):
return False
return True
+
+ def guard_reject_antibiotics(self):
+ """Rejection of antibiotics is only possible for sensitivity testing
+ (AST) analyses
+ """
+ return is_ast_analysis(self.context)
diff --git a/src/senaite/ast/browser/configure.zcml b/src/senaite/ast/browser/configure.zcml
index 7b2b524..7c010df 100644
--- a/src/senaite/ast/browser/configure.zcml
+++ b/src/senaite/ast/browser/configure.zcml
@@ -6,6 +6,7 @@
+
diff --git a/src/senaite/ast/browser/modals/__init__.py b/src/senaite/ast/browser/modals/__init__.py
new file mode 100644
index 0000000..6899a65
--- /dev/null
+++ b/src/senaite/ast/browser/modals/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of SENAITE.AST.
+#
+# SENAITE.AST 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 2020-2024 by it's authors.
+# Some rights reserved, see README and LICENSE.
diff --git a/src/senaite/ast/browser/modals/configure.zcml b/src/senaite/ast/browser/modals/configure.zcml
new file mode 100644
index 0000000..4420d95
--- /dev/null
+++ b/src/senaite/ast/browser/modals/configure.zcml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/src/senaite/ast/browser/modals/rejectantibiotics.py b/src/senaite/ast/browser/modals/rejectantibiotics.py
new file mode 100644
index 0000000..9b78334
--- /dev/null
+++ b/src/senaite/ast/browser/modals/rejectantibiotics.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of SENAITE.AST.
+#
+# SENAITE.AST 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 2020-2024 by it's authors.
+# Some rights reserved, see README and LICENSE.
+
+import itertools
+from datetime import datetime
+
+from bika.lims import api
+from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
+from senaite.ast import logger
+from senaite.ast import utils
+from senaite.ast.calc import update_sensitivity_result
+from senaite.ast.config import NOT_TESTED
+from senaite.ast.config import REPORT_EXTRAPOLATED_KEY
+from senaite.ast.config import REPORT_KEY
+from senaite.ast.config import RESISTANCE_KEY
+from senaite.core.api import dtime as dt
+from senaite.core.browser.modals import Modal
+
+
+class RejectAntibioticsModal(Modal):
+ """Modal that allows to reject antibiotics (flag them as Not Tested)
+ """
+
+ template = ViewPageTemplateFile("templates/reject_antibiotics.pt")
+
+ def __call__(self):
+ if self.request.form.get("submitted", False):
+ self.handle_submit()
+ return self.template()
+
+ @property
+ def analyses(self):
+ """Returns the analyses passed-in as UIDs through the request
+ """
+ analyses = map(api.get_object_by_uid, self.uids)
+ # be sure we rely on AST analyses only
+ analyses = filter(utils.is_ast_analysis, analyses)
+ # extend with siblings (AST-like analyses from same microorganism)
+ siblings = map(utils.get_ast_siblings, analyses)
+ siblings = list(itertools.chain.from_iterable(siblings))
+ analyses.extend(siblings)
+ # remove duplicates
+ analyses = set(analyses)
+ # exclude analyses meant for selective reporting
+ exclude = [REPORT_KEY, REPORT_EXTRAPOLATED_KEY]
+ analyses = filter(lambda an: an.getKeyword() not in exclude, analyses)
+ return analyses
+
+ @property
+ def antibiotics(self):
+ """Returns a list of antibiotics with the antibiotics assigned to
+ the analyses passed-in as UIDs through the request, sorted by name
+ ascending
+ """
+ kwargs = dict(filter_criteria=self.is_valid_antibiotic)
+ antibiotics = utils.get_antibiotics(self.analyses, **kwargs)
+ return sorted(antibiotics, key=lambda ab: api.get_title(ab))
+
+ def is_valid_antibiotic(self, interim_field):
+ """Returns whether the interim field corresponds to an antibiotic that
+ has not been rejected (flagged as not tested)
+ """
+ if interim_field.get("status_rejected", False):
+ return False
+ return True
+
+ def handle_submit(self):
+ """Handles the form submit. Flag the antibiotics selected in the form
+ as Not Tested for all analyses passed-in as UIDs through the request
+ """
+ # get the uids that have been selected for rejection
+ rejected_uids = self.request.get("antibiotics")
+
+ # flag the antibiotic as Not Tested (NT) for each analysis
+ for analysis in self.analyses:
+ self.reject_antibiotics(analysis, rejected_uids)
+
+ def reject_antibiotics(self, analysis, antibiotics):
+ """Flags the antibiotics passed-in for the given analysis as Not tested
+ """
+ interims = []
+ keyword = analysis.getKeyword()
+ to_reject = map(api.get_uid, antibiotics)
+ for interim in analysis.getInterimFields():
+ abx_uid = interim.get("uid")
+ if abx_uid in to_reject:
+
+ # set rejected status
+ user_id = api.get_current_user().id
+ timestamp = dt.to_iso_format(datetime.now())
+ interim["status_rejected"] = timestamp
+ interim["status_rejected_by"] = user_id
+
+ # set the result value
+ self.set_not_tested_result(interim)
+
+ interims.append(interim)
+
+ # update the interims/antibiotics
+ analysis.setInterimFields(interims)
+
+ if keyword == RESISTANCE_KEY:
+ # Compute all combinations of interim/antibiotic and possible
+ # result and generate the result options for this analysis (the
+ # "Result" field is never displayed and is only used for reporting)
+ result_options = utils.get_result_options(analysis)
+ analysis.setResultOptions(result_options)
+
+ # Update the final result to be reported
+ update_sensitivity_result(analysis)
+
+ def set_not_tested_result(self, interim):
+ """Sets the 'Not tested' result to the interim field. If the interim
+ has choices, it uses '0' as the value for the Not Tested. Otherwise,
+ sets NT as the textual result
+ """
+ choices = utils.get_choices(interim)
+ if not choices:
+ # no choices, set string result
+ interim["value"] = NOT_TESTED
+ interim["string_result"] = True
+ return
+
+ # insert a new choice ("-1", "NT")
+ choice = ("-1", NOT_TESTED)
+ if choice not in choices:
+ choices.insert(0, choice)
+ choices = ["{}:{}".format(ch[0], ch[1]) for ch in choices]
+ interim["choices"] = "|".join(choices)
+
+ # assign the result value
+ interim["value"] = "-1"
+ interim["string_result"] = True
diff --git a/src/senaite/ast/browser/modals/templates/reject_antibiotics.pt b/src/senaite/ast/browser/modals/templates/reject_antibiotics.pt
new file mode 100644
index 0000000..061a2ce
--- /dev/null
+++ b/src/senaite/ast/browser/modals/templates/reject_antibiotics.pt
@@ -0,0 +1,54 @@
+
diff --git a/src/senaite/ast/browser/results.py b/src/senaite/ast/browser/results.py
index 29002f5..493c1d6 100644
--- a/src/senaite/ast/browser/results.py
+++ b/src/senaite/ast/browser/results.py
@@ -24,6 +24,7 @@
from bika.lims import api
from bika.lims.browser.analyses import AnalysesView
from bika.lims.interfaces import IVerified
+from bika.lims.utils import get_image
from bika.lims.utils import get_link
from plone.memoize import view
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
@@ -31,6 +32,9 @@
from senaite.ast import messageFactory as _
from senaite.ast import utils
from senaite.ast.config import AST_POINT_OF_CAPTURE
+from senaite.ast.config import IDENTIFICATION_KEY
+from senaite.ast.utils import get_ast_analyses
+from senaite.ast.i18n import translate as t
from senaite.core.browser.viewlets.sampleanalyses import LabAnalysesViewlet
@@ -42,9 +46,24 @@ class ASTAnalysesViewlet(LabAnalysesViewlet):
capture = AST_POINT_OF_CAPTURE
def available(self):
- """Returns true if senaite.ast is installed
+ """Returns true if senaite.ast is installed and the sample contains
+ at least one sensitivity testing analysis or the microorganism
+ identification analysis is present
"""
- return is_installed()
+ if not is_installed():
+ return False
+
+ # does this sample has the identification analysis?
+ analyses = self.context.getAnalyses(getKeyword=IDENTIFICATION_KEY)
+ if analyses:
+ return True
+
+ # does this have sensitivity testing analyses?
+ ast_analyses = get_ast_analyses(self.context)
+ if ast_analyses:
+ return True
+
+ return False
class ManageResultsView(AnalysesView):
@@ -166,6 +185,9 @@ def folder_interim_fields(self, obj, item):
# This interim will be displayed as readonly mode, display text
text = utils.get_interim_text(interim_field, default="")
+ if interim_field.get("status_rejected", False):
+ icon = get_image('warning.png', title=t(_("Not tested")))
+ text = "{}{}".format(text, icon)
item["replace"][keyword] = text or " "
def folderitems(self):
diff --git a/src/senaite/ast/calc.py b/src/senaite/ast/calc.py
index eb236f6..04fcfa9 100644
--- a/src/senaite/ast/calc.py
+++ b/src/senaite/ast/calc.py
@@ -97,7 +97,7 @@ def calc_sensitivity_categories(analysis):
# The result for each antibiotic is stored as an interim field
breakpoints = breakpoints_analysis.getInterimFields()
values = target_analysis.getInterimFields()
- categories = sensitivity.getInterimFields()
+ antibiotics = sensitivity.getInterimFields()
# Get the mapping of Antibiotic -> BreakpointsTable
breakpoints = dict(map(lambda b: (b['uid'], b['value']), breakpoints))
@@ -108,12 +108,16 @@ def calc_sensitivity_categories(analysis):
# Get the microorganism this analysis is associated to
microorganism = get_microorganism(analysis)
- # Update sensitivity categories
- for category in categories:
+ # Update sensitivity category for each antibiotic
+ for antibiotic in antibiotics:
+ # Skip non-editable antibiotics
+ if not utils.is_interim_editable(antibiotic):
+ continue
+
# If extrapolated, assume same zone size as representative
- abx_uid = category.get("primary")
+ abx_uid = antibiotic.get("primary")
if not api.is_uid(abx_uid):
- abx_uid = category["uid"]
+ abx_uid = antibiotic["uid"]
# Get the zone size / MIC value
value = values.get(abx_uid)
@@ -121,7 +125,7 @@ def calc_sensitivity_categories(analysis):
# No value entered yet or not floatable
continue
- # Get the selected Breakpoints Table for this category
+ # Get the selected Breakpoints Table for this antibiotic
breakpoints_uid = breakpoints.get(abx_uid)
# Get the breakpoint for this microorganism and antibiotic
@@ -133,10 +137,10 @@ def calc_sensitivity_categories(analysis):
cat = get_sensitivity_category_value(key, default="")
# Update the sensitivity category
- category.update({"value": cat})
+ antibiotic.update({"value": cat})
- # Assign the updated categories to the sensitivity category analysis
- sensitivity.setInterimFields(categories)
+ # Assign the antibiotics with the updated sensitivity categories
+ sensitivity.setInterimFields(antibiotics)
def calc_disk_dosages(analysis):
@@ -171,12 +175,16 @@ def calc_disk_dosages(analysis):
breakpoints = dict(map(lambda b: (b['uid'], b['value']), breakpoints))
# Dosages are stored as interim fields
- disk_dosages = disk_dosages_analysis.getInterimFields()
- for dosage in disk_dosages:
+ antibiotics = disk_dosages_analysis.getInterimFields()
+ for antibiotic in antibiotics:
+ # Skip non-editable antibiotics
+ if not utils.is_interim_editable(antibiotic):
+ continue
+
# If extrapolated, assume same zone size as representative
- abx_uid = dosage.get("primary")
+ abx_uid = antibiotic.get("primary")
if not api.is_uid(abx_uid):
- abx_uid = dosage["uid"]
+ abx_uid = antibiotic["uid"]
# Get the selected Breakpoints Table for this category
breakpoints_uid = breakpoints.get(abx_uid)
@@ -189,10 +197,10 @@ def calc_disk_dosages(analysis):
# Update the dosage
breakpoint_dosage = breakpoint.get("disk_content")
if api.to_float(breakpoint_dosage, default=0) > 0:
- dosage.update({"value": breakpoint_dosage})
+ antibiotic.update({"value": breakpoint_dosage})
# Assign the inferred disk dosages
- disk_dosages_analysis.setInterimFields(disk_dosages)
+ disk_dosages_analysis.setInterimFields(antibiotics)
def update_extrapolated_antibiotics(analysis):
diff --git a/src/senaite/ast/config.py b/src/senaite/ast/config.py
index 10143ab..9c9fa6a 100644
--- a/src/senaite/ast/config.py
+++ b/src/senaite/ast/config.py
@@ -79,6 +79,9 @@
# Description for autogenerated contents
AUTOGENERATED = _(u"Autogenerated by senaite.ast")
+# Abbreviation for "Not tested"
+NOT_TESTED = "NT"
+
# Id of the Diffusion Disk method
METHOD_DIFFUSION_DISK_ID = "diffusion_disk"
diff --git a/src/senaite/ast/configure.zcml b/src/senaite/ast/configure.zcml
index 39e4414..9c1ca38 100644
--- a/src/senaite/ast/configure.zcml
+++ b/src/senaite/ast/configure.zcml
@@ -13,11 +13,15 @@
+
+
+
+
diff --git a/src/senaite/ast/datamanagers.py b/src/senaite/ast/datamanagers.py
index 3bdee16..3571c64 100644
--- a/src/senaite/ast/datamanagers.py
+++ b/src/senaite/ast/datamanagers.py
@@ -18,12 +18,15 @@
# Copyright 2020-2024 by it's authors.
# Some rights reserved, see README and LICENSE.
+from senaite.ast import logger
from senaite.ast.config import BREAKPOINTS_TABLE_KEY
from senaite.ast.config import DISK_CONTENT_KEY
from senaite.ast.config import RESISTANCE_KEY
from senaite.ast.config import ZONE_SIZE_KEY
from senaite.ast.interfaces import IASTAnalysis
from senaite.ast.utils import get_ast_siblings
+from senaite.ast.utils import is_ast_analysis
+from senaite.ast.utils import is_interim_editable
from senaite.core.datamanagers.analysis import RoutineAnalysisDataManager
from zope.component import adapter
@@ -33,6 +36,32 @@ class ASTAnalysisDataManager(RoutineAnalysisDataManager):
"""Data Manager for AST-like analyses
"""
+ def get_antibiotic_interim(self, keyword, default=None):
+ """Returns the interim that represents the antibiotic with the given
+ keyword, but only if the context is an AST-like analysis
+ """
+ if not is_ast_analysis(self.context):
+ return default
+
+ for interim in self.context.getInterimFields():
+ if interim.get("keyword") == keyword:
+ return interim
+
+ return default
+
+ def set(self, name, value):
+ """Set analysis field/interim value
+ """
+ # Check if an antibiotic/interim of an AST analysis
+ antibiotic = self.get_antibiotic_interim(name)
+ if antibiotic and not is_interim_editable(antibiotic):
+ logger.error("Interim field '{}' not writeable!".format(name))
+ return []
+
+ # rely on the base class
+ base = super(ASTAnalysisDataManager, self)
+ return base.set(name, value)
+
def recalculate_results(self, obj, recalculated=None):
recalculated = super(ASTAnalysisDataManager, self).\
recalculate_results(obj, recalculated=recalculated)
diff --git a/src/senaite/ast/i18n.py b/src/senaite/ast/i18n.py
new file mode 100644
index 0000000..a035e56
--- /dev/null
+++ b/src/senaite/ast/i18n.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of SENAITE.AST.
+#
+# SENAITE.AST 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 2020-2024 by it's authors.
+# Some rights reserved, see README and LICENSE.
+
+from senaite.core.i18n import translate as core_translate
+
+
+def translate(msgid, **kwargs):
+ """Translate any zope i18n msgid returned from MessageFactory
+ """
+ domain = kwargs.pop("domain", "senaite.ast")
+ return core_translate(msgid, domain=domain, **kwargs)
diff --git a/src/senaite/ast/permissions.py b/src/senaite/ast/permissions.py
new file mode 100644
index 0000000..292858c
--- /dev/null
+++ b/src/senaite/ast/permissions.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of SENAITE.AST.
+#
+# SENAITE.AST 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 2020-2024 by it's authors.
+# Some rights reserved, see README and LICENSE.
+
+TransitionRejectAntibiotics = "senaite.ast: Transition: Reject Antibiotics"
diff --git a/src/senaite/ast/permissions.zcml b/src/senaite/ast/permissions.zcml
new file mode 100644
index 0000000..a3dd245
--- /dev/null
+++ b/src/senaite/ast/permissions.zcml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/senaite/ast/profiles/default/metadata.xml b/src/senaite/ast/profiles/default/metadata.xml
index f785b9b..2bc3e64 100644
--- a/src/senaite/ast/profiles/default/metadata.xml
+++ b/src/senaite/ast/profiles/default/metadata.xml
@@ -6,7 +6,7 @@
dependencies before installing this add-on own profile.
-->
- 1200
+ 1202
diff --git a/src/senaite/ast/profiles/default/rolemap.xml b/src/senaite/ast/profiles/default/rolemap.xml
new file mode 100644
index 0000000..ccfb4a9
--- /dev/null
+++ b/src/senaite/ast/profiles/default/rolemap.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/senaite/ast/setuphandlers.py b/src/senaite/ast/setuphandlers.py
index 1aebce9..ab4f224 100644
--- a/src/senaite/ast/setuphandlers.py
+++ b/src/senaite/ast/setuphandlers.py
@@ -29,9 +29,12 @@
from senaite.ast.config import AST_CALCULATION_TITLE
from senaite.ast.config import AST_POINT_OF_CAPTURE
from senaite.ast.config import AUTOGENERATED
-from senaite.ast.config import IDENTIFICATION_KEY
from senaite.ast.config import SERVICE_CATEGORY
from senaite.ast.config import SERVICES_SETTINGS
+from senaite.ast.permissions import TransitionRejectAntibiotics
+from senaite.core.api.workflow import update_workflow
+from senaite.core.catalog import SETUP_CATALOG
+from senaite.core.workflow import ANALYSIS_WORKFLOW
from zope.component import getUtility
# Tuples of (folder_id, folder_name, type)
@@ -47,6 +50,33 @@
])
]
+WORKFLOWS_TO_UPDATE = {
+ ANALYSIS_WORKFLOW: {
+ "states": {
+ "assigned": {
+ "transitions": ["modal_reject_antibiotics"],
+ },
+ "unassigned": {
+ "transitions": ["modal_reject_antibiotics"],
+ },
+ },
+ "transitions": {
+ "modal_reject_antibiotics": {
+ "title": "Reject antibiotics",
+ "new_state": "",
+ "action": "Reject antibiotics",
+ "action_url": "%(content_url)s/reject_antibiotics",
+ "guard": {
+ "guard_permissions": TransitionRejectAntibiotics,
+ "guard_roles": "",
+ "guard_expr":
+ "python:here.guard_handler('reject_antibiotics')",
+ }
+ }
+ }
+ }
+}
+
def setup_handler(context):
"""Generic setup handler
@@ -71,6 +101,12 @@ def setup_handler(context):
# Add behaviors
setup_behaviors(portal)
+ # setup workflows
+ setup_workflows(portal)
+
+ # Revoke edit permissions for ast setup objects
+ revoke_edition_permissions(portal)
+
logger.info("{} setup handler [DONE]".format(PRODUCT_NAME.upper()))
@@ -117,9 +153,10 @@ def setup_ast_category(portal):
"""
name = SERVICE_CATEGORY
logger.info("Setup category '{}' ...".format(name))
- folder = api.get_setup().bika_analysiscategories
- exists = filter(lambda c: api.get_title(c) == name, folder.objectValues())
- if exists:
+ setup = api.get_senaite_setup()
+ folder = setup.analysiscategories
+ categories = search_by_title(folder, name)
+ if categories:
logger.info("Category '{}' exists already [SKIP]".format(name))
return
@@ -163,15 +200,15 @@ def setup_ast_services(portal, update_existing=True):
selective reporting
"""
logger.info("Setup AST services ...")
+
+ # Get the category to apply to all AST services
+ cats_folder = api.get_senaite_setup().analysiscategories
+ category = search_by_title(cats_folder, SERVICE_CATEGORY)[0]
+
setup = api.get_setup()
for key, settings in SERVICES_SETTINGS.items():
logger.info("Setup template service '{}' ...".format(key))
- # Get the category
- cat_name = SERVICE_CATEGORY
- categories = setup.bika_analysiscategories.objectValues()
- category = filter(lambda c: api.get_title(c) == cat_name, categories)[0]
-
title = settings["title"]
if "{}" in title:
title = title.format(_("Antibiotic Sensitivity"))
@@ -247,6 +284,33 @@ def remove_behaviors(portal):
logger.info("Removing Behaviors [DONE]")
+def revoke_edition_permissions(portal):
+ """Revoke the 'Modify portal content' permission to 'ast' services
+ """
+ logger.info("Revoking edition permissions to AST setup objects ...")
+
+ def revoke_permission(obj):
+ obj = api.get_object(obj)
+ roles = security.get_valid_roles_for(obj)
+ security.revoke_permission_for(obj, ModifyPortalContent, roles)
+ obj.reindexObject()
+
+ # analysis services
+ query = {
+ "portal_type": "AnalysisService",
+ "point_of_capture": AST_POINT_OF_CAPTURE
+ }
+ brains = api.search(query, SETUP_CATALOG)
+ map(revoke_permission, brains)
+
+ # calculation
+ query = {"portal_type": "Calculation", "title": AST_CALCULATION_TITLE}
+ brains = api.search(query, SETUP_CATALOG)
+ map(revoke_permission, brains)
+
+ logger.info("Revoking edition permissions to AST setup objects [DONE]")
+
+
def search_by_title(container, title):
"""Returns the items from the container that match with the title passed-in
"""
@@ -254,6 +318,15 @@ def search_by_title(container, title):
return filter(lambda obj: api.get_title(obj) == title, objs)
+def setup_workflows(portal):
+ """Setup workflow changes (status, transitions, permissions, etc.)
+ """
+ logger.info("Setup workflows ...")
+ for wf_id, settings in WORKFLOWS_TO_UPDATE.items():
+ update_workflow(wf_id, **settings)
+ logger.info("Setup workflows [DONE]")
+
+
def pre_install(portal_setup):
"""Runs before the first import step of the *default* profile
This handler is registered as a *pre_handler* in the generic setup profile
diff --git a/src/senaite/ast/subscribers/__init__.py b/src/senaite/ast/subscribers/__init__.py
new file mode 100644
index 0000000..6899a65
--- /dev/null
+++ b/src/senaite/ast/subscribers/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of SENAITE.AST.
+#
+# SENAITE.AST 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 2020-2024 by it's authors.
+# Some rights reserved, see README and LICENSE.
diff --git a/src/senaite/ast/subscribers/configure.zcml b/src/senaite/ast/subscribers/configure.zcml
new file mode 100644
index 0000000..6925721
--- /dev/null
+++ b/src/senaite/ast/subscribers/configure.zcml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/senaite/ast/subscribers/upgrade.py b/src/senaite/ast/subscribers/upgrade.py
new file mode 100644
index 0000000..3ad8b4e
--- /dev/null
+++ b/src/senaite/ast/subscribers/upgrade.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of SENAITE.AST.
+#
+# SENAITE.AST 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 2020-2024 by it's authors.
+# Some rights reserved, see README and LICENSE.
+
+
+from bika.lims.api import get_portal
+from senaite.ast import is_installed
+from senaite.ast import logger
+from senaite.ast import PRODUCT_NAME
+from senaite.ast.setuphandlers import revoke_edition_permissions
+from senaite.ast.setuphandlers import setup_behaviors
+from senaite.ast.setuphandlers import setup_navigation_types
+from senaite.ast.setuphandlers import setup_workflows
+
+
+def afterUpgradeStepHandler(event):
+ """Event handler executed after running an upgrade step of senaite.core
+ """
+ if not is_installed():
+ return
+
+ logger.info("Run {}.afterUpgradeStepHandler ...".format(PRODUCT_NAME))
+ portal = get_portal()
+ setup = portal.portal_setup # noqa
+
+ profile = "profile-{0}:default".format(PRODUCT_NAME)
+ setup.runImportStepFromProfile(profile, "typeinfo")
+ setup.runImportStepFromProfile(profile, "rolemap")
+ setup.runImportStepFromProfile(profile, "workflow")
+
+ # Configure visible navigation items
+ setup_navigation_types(portal)
+
+ # Setup additional behaviors
+ setup_behaviors(portal)
+
+ # Setup workflows
+ setup_workflows(portal)
+
+ # Revoke edit permissions for ast setup objects
+ revoke_edition_permissions(portal)
+
+ logger.info("Run {}.afterUpgradeStepHandler [DONE]".format(PRODUCT_NAME))
diff --git a/src/senaite/ast/tests/base.py b/src/senaite/ast/tests/base.py
index ad490c7..07ea9e4 100644
--- a/src/senaite/ast/tests/base.py
+++ b/src/senaite/ast/tests/base.py
@@ -18,31 +18,45 @@
# Copyright 2020-2024 by it's authors.
# Some rights reserved, see README and LICENSE.
-from plone.app.testing import TEST_USER_ID
-from plone.app.testing import setRoles
-from plone.app.testing.bbb_at import PloneTestCase
-from plone.protect.authenticator import createToken
-from senaite.ast.tests.layers import BASE_TESTING
+import transaction
+from plone.app.testing import applyProfile
+from plone.app.testing import FunctionalTesting
+from plone.testing import zope
+from senaite.core.tests.base import BaseTestCase
+from senaite.core.tests.layers import BaseLayer
-class BaseTestCase(PloneTestCase):
- """Use for test cases which do not rely on the demo data
+class SimpleTestLayer(BaseLayer):
+
+ def setUpZope(self, app, configurationContext):
+ super(SimpleTestLayer, self).setUpZope(app, configurationContext)
+
+ # Load ZCML
+ import senaite.abx
+ import senaite.microorganism
+ self.loadZCML(package=senaite.abx)
+ self.loadZCML(package=senaite.microorganism)
+ self.loadZCML(package=senaite.ast)
+
+ # Install product and call its initialize() function
+ zope.installProduct(app, "senaite.abx")
+ zope.installProduct(app, "senaite.microorganism")
+ zope.installProduct(app, "senaite.ast")
+
+ def setUpPloneSite(self, portal):
+ super(SimpleTestLayer, self).setUpPloneSite(portal)
+ applyProfile(portal, "senaite.ast:default")
+ transaction.commit()
+
+
+SIMPLE_TEST_LAYER_FIXTURE = SimpleTestLayer()
+SIMPLE_TESTING = FunctionalTesting(
+ bases=(SIMPLE_TEST_LAYER_FIXTURE, ),
+ name="senaite.ast:SimpleTesting"
+)
+
+
+class SimpleTestCase(BaseTestCase):
+ """Use for test cases which do not rely on demo data
"""
- layer = BASE_TESTING
-
- def setUp(self):
- super(BaseTestCase, self).setUp()
- # Fixing CSRF protection
- # https://github.com/plone/plone.protect/#fixing-csrf-protection-failures-in-tests
- self.request = self.layer["request"]
- # Disable plone.protect for these tests
- self.request.form["_authenticator"] = createToken()
- # Eventuelly you find this also useful
- self.request.environ["REQUEST_METHOD"] = "POST"
-
- setRoles(self.portal, TEST_USER_ID, ["LabManager", "Manager"])
-
- # Default skin is set to "Sunburst Theme"!
- # => This causes an `AttributeError` when we want to access
- # e.g. 'guard_handler' FSPythonScript
- self.portal.changeSkin("Plone Default")
+ layer = SIMPLE_TESTING
diff --git a/src/senaite/ast/tests/doctests/AST.rst b/src/senaite/ast/tests/doctests/AST.rst
index 33e69e3..e4206c9 100644
--- a/src/senaite/ast/tests/doctests/AST.rst
+++ b/src/senaite/ast/tests/doctests/AST.rst
@@ -44,8 +44,8 @@ We need to create some basic objects for the test:
>>> contact = api.create(client, "Contact", Firstname="Rita", Lastname="Mohale")
>>> sampletype = api.create(setup.bika_sampletypes, "SampleType", title="Blood", Prefix="B")
>>> labcontact = api.create(setup.bika_labcontacts, "LabContact", Firstname="Lab", Lastname="Manager")
- >>> department = api.create(setup.bika_departments, "Department", title="Microbiology", Manager=labcontact)
- >>> category = api.create(setup.bika_analysiscategories, "AnalysisCategory", title="Microbiology", Department=department)
+ >>> department = api.create(portal.setup.departments, "Department", title="Microbiology", Manager=labcontact)
+ >>> category = api.create(portal.setup.analysiscategories, "AnalysisCategory", title="Microbiology", Department=department)
>>> g = api.create(setup.bika_analysisservices, "AnalysisService", title="GRAM Test", Keyword="G", Price="15", Category=category.UID(), Accredited=True)
diff --git a/src/senaite/ast/tests/layers.py b/src/senaite/ast/tests/layers.py
deleted file mode 100644
index dafc302..0000000
--- a/src/senaite/ast/tests/layers.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of SENAITE.AST.
-#
-# SENAITE.AST 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 2020-2024 by it's authors.
-# Some rights reserved, see README and LICENSE.
-
-import transaction
-from plone.app.testing import applyProfile
-from plone.app.testing import FunctionalTesting
-from plone.app.testing import PLONE_FIXTURE
-from plone.app.testing import PloneSandboxLayer
-from plone.testing import zope
-
-
-class BaseLayer(PloneSandboxLayer):
- defaultBases = (PLONE_FIXTURE,)
-
- def setUpZope(self, app, configurationContext):
- super(BaseLayer, self).setUpZope(app, configurationContext)
-
- # Load ZCML
- import bika.lims
- import senaite.app.listing
- import senaite.app.spotlight
- import senaite.core
- import senaite.abx
- import senaite.ast
- import senaite.impress
- import senaite.microorganism
-
- self.loadZCML(package=bika.lims)
- self.loadZCML(package=senaite.core)
- self.loadZCML(package=senaite.app.listing)
- self.loadZCML(package=senaite.app.spotlight)
- self.loadZCML(package=senaite.impress)
- self.loadZCML(package=senaite.lims)
- self.loadZCML(package=senaite.abx)
- self.loadZCML(package=senaite.microorganism)
- self.loadZCML(package=senaite.ast)
-
- # Install product and call its initialize() function
- zope.installProduct(app, "bika.lims")
- zope.installProduct(app, "senaite.core")
- zope.installProduct(app, "senaite.app.listing")
- zope.installProduct(app, "senaite.app.spotlight")
- zope.installProduct(app, "senaite.impress")
- zope.installProduct(app, "senaite.lims")
- zope.installProduct(app, "senaite.abx")
- zope.installProduct(app, "senaite.microorganism")
- zope.installProduct(app, "senaite.ast")
-
- def setUpPloneSite(self, portal):
- # Install into Plone site using portal_setup
- applyProfile(portal, "senaite.core:default")
- applyProfile(portal, "senaite.ast:default")
- transaction.commit()
-
-
-BASE_LAYER_FIXTURE = BaseLayer()
-BASE_TESTING = FunctionalTesting(
- bases=(BASE_LAYER_FIXTURE,), name="SENAITE.AST:BaseTesting")
diff --git a/src/senaite/ast/tests/test_doctests.py b/src/senaite/ast/tests/test_doctests.py
index 2cb7b28..a472506 100644
--- a/src/senaite/ast/tests/test_doctests.py
+++ b/src/senaite/ast/tests/test_doctests.py
@@ -25,7 +25,7 @@
import unittest2 as unittest
from senaite.ast import PRODUCT_NAME
-from senaite.ast.tests.base import BaseTestCase
+from senaite.ast.tests.base import SimpleTestCase
from Testing import ZopeTestCase as ztc
# Option flags for doctests
@@ -38,7 +38,7 @@ def test_suite():
suite.addTests([
ztc.ZopeDocFileSuite(
doctestfile,
- test_class=BaseTestCase,
+ test_class=SimpleTestCase,
optionflags=flags
)
])
diff --git a/src/senaite/ast/upgrade/v01_00_000.py b/src/senaite/ast/upgrade/v01_00_000.py
index 76ef321..7550581 100644
--- a/src/senaite/ast/upgrade/v01_00_000.py
+++ b/src/senaite/ast/upgrade/v01_00_000.py
@@ -33,6 +33,7 @@
from senaite.ast.setuphandlers import setup_navigation_types
from senaite.ast.utils import get_result_options
from senaite.core.catalog import SETUP_CATALOG
+from senaite.core.interfaces import ISampleTemplate
from senaite.core.upgrade import upgradestep
from senaite.core.upgrade.utils import UpgradeUtils
@@ -146,15 +147,11 @@ def remove_ast_from_profiles(portal):
"""
logger.info("Removing AST-like analyses from profiles ...")
ast_uids = get_ast_services_uids(portal)
- query = {
- "portal_type": "AnalysisProfile"
- }
- brains = api.search(query, SETUP_CATALOG)
- for brain in brains:
- obj = api.get_object(brain)
- services = obj.getRawService() or []
- services = filter(lambda s: s not in ast_uids, services)
- obj.setService(services)
+ profiles = portal.setup.analysisprofiles.objectValues()
+ for obj in profiles:
+ services = obj.getRawServices() or []
+ services = filter(lambda s: s.get("uid") not in ast_uids, services)
+ obj.setServices(services)
logger.info("Removing AST-like analyses from profiles [DONE]")
@@ -164,11 +161,18 @@ def remove_ast_from_templates(portal):
logger.info("Removing AST-like analyses from templates ...")
ast_uids = get_ast_services_uids(portal)
query = {
- "portal_type": "ARTemplate"
+ "portal_type": ["ARTemplate", "SampleTemplate"]
}
brains = api.search(query, SETUP_CATALOG)
for brain in brains:
obj = api.get_object(brain)
+ if ISampleTemplate.providedBy(obj):
+ ans = obj.getRawServices()
+ ans = filter(lambda an: an.get("uid") not in ast_uids, ans)
+ obj.setServices(list(ans))
+ continue
+
+ # Old AT object (https://github.com/senaite/senaite.core/pull/2521)
ans = obj.getAnalyses()
ans = filter(lambda an: an.get("service_uid") not in ast_uids, ans)
obj.setAnalyses(ans)
diff --git a/src/senaite/ast/upgrade/v01_02_000.py b/src/senaite/ast/upgrade/v01_02_000.py
index 8c0ba24..547470f 100644
--- a/src/senaite/ast/upgrade/v01_02_000.py
+++ b/src/senaite/ast/upgrade/v01_02_000.py
@@ -18,12 +18,18 @@
# Copyright 2020-2024 by it's authors.
# Some rights reserved, see README and LICENSE.
+from bika.lims import api
from senaite.ast import logger
from senaite.ast import PRODUCT_NAME
+from senaite.ast.config import AST_POINT_OF_CAPTURE
+from senaite.ast.setuphandlers import revoke_edition_permissions
+from senaite.ast.setuphandlers import setup_workflows
+from senaite.core.catalog import ANALYSIS_CATALOG
from senaite.core.upgrade import upgradestep
from senaite.core.upgrade.utils import UpgradeUtils
version = "1.1.0"
+profile = "profile-{0}:default".format(PRODUCT_NAME)
@upgradestep(PRODUCT_NAME, version)
@@ -45,3 +51,43 @@ def upgrade(tool):
logger.info("{0} upgraded to version {1}".format(PRODUCT_NAME, version))
return True
+
+
+def setup_reject_antibiotics(tool):
+ logger.info("Setup reject antibiotics transition ...")
+ portal = tool.aq_inner.aq_parent
+
+ # import rolemap and workflow
+ setup = portal.portal_setup
+ setup.runImportStepFromProfile(profile, "rolemap")
+ setup.runImportStepFromProfile(profile, "workflow")
+
+ # setup custom workflow modifs
+ setup_workflows(portal)
+
+ # update role mappings
+ statuses = ["assigned", "unassigned"]
+ cat = api.get_tool(ANALYSIS_CATALOG)
+ brains = cat(portal_type="Analysis", review_state=statuses,
+ getPointOfCapture=AST_POINT_OF_CAPTURE)
+ map(update_role_mappings_for, brains)
+
+ logger.info("Setup reject antibiotics transition [DONE]")
+
+
+def update_role_mappings_for(object_or_brain):
+ """Update role mappings for the specified object
+ """
+ obj = api.get_object(object_or_brain)
+ path = api.get_path(obj)
+ logger.info("Updating workflow role mappings for {} ...".format(path))
+ tool = api.get_tool("portal_workflow")
+ for wf_id in api.get_workflows_for(obj):
+ wf = tool.getWorkflowById(wf_id)
+ wf.updateRoleMappingsFor(obj)
+ obj.reindexObject(idxs=["allowedRolesAndUsers"])
+
+
+def revoke_setup_permissions(tool):
+ portal = tool.aq_inner.aq_parent
+ revoke_edition_permissions(portal)
diff --git a/src/senaite/ast/upgrade/v01_02_000.zcml b/src/senaite/ast/upgrade/v01_02_000.zcml
index ef1cb7d..b63486f 100644
--- a/src/senaite/ast/upgrade/v01_02_000.zcml
+++ b/src/senaite/ast/upgrade/v01_02_000.zcml
@@ -1,7 +1,22 @@
+ xmlns:genericsetup="http://namespaces.zope.org/genericsetup">
+
+
+
+