Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selective reporting for extrapolated antibiotics #17

Merged
merged 7 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
1.0.0 (unreleased)
------------------

- #17 Selective reporting for extrapolated antibiotics
- #15 Support for extrapolated antibiotics
- #13 Allow addition of new antibiotics to submitted/verified AST analyses
- #12 Negative values for diameter and zone size tests not permitted
Expand Down
6 changes: 4 additions & 2 deletions src/senaite/ast/adapters/guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@ def guard_submit(self):
# Check that all interim fields have non-empty values
keyword = self.context.getKeyword()
for interim in self.context.getInterimFields():
if utils.is_interim_empty(interim):
return False

if utils.is_extrapolated_interim(interim):
# Skip extrapolated interims
continue

if keyword in [ZONE_SIZE_KEY, DISK_CONTENT_KEY]:
# Negative values are not permitted
Expand Down
19 changes: 14 additions & 5 deletions src/senaite/ast/browser/addpanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
from senaite.ast import utils
from senaite.ast.config import BREAKPOINTS_TABLE_KEY
from senaite.ast.config import DISK_CONTENT_KEY
from senaite.ast.config import REPORT_EXTRAPOLATED_KEY
from senaite.ast.config import REPORT_KEY
from senaite.ast.config import RESISTANCE_KEY
from senaite.ast.config import ZONE_SIZE_KEY
from senaite.ast.config import REPORT_KEY
from senaite.ast.utils import get_extrapolated_antibiotics
from senaite.ast.utils import update_breakpoint_tables_choices


Expand All @@ -49,6 +51,7 @@ def __call__(self):
microorganisms = filter(lambda m: m in identified, microorganisms)

# Create an analysis for each microorganism
add = self.add_ast_analysis
for microorganism in microorganisms:

# Create/Update the breakpoints table analysis
Expand All @@ -57,18 +60,24 @@ def __call__(self):

# Create/Update the disk content (potency) analysis
if panel.disk_content:
self.add_ast_analysis(DISK_CONTENT_KEY, microorganism, antibiotics)
add(DISK_CONTENT_KEY, microorganism, antibiotics)

# Create/Update the zone size analysis
if panel.zone_size:
self.add_ast_analysis(ZONE_SIZE_KEY, microorganism, antibiotics)
add(ZONE_SIZE_KEY, microorganism, antibiotics)

# Create/Update the sensitivity result analysis
self.add_ast_analysis(RESISTANCE_KEY, microorganism, antibiotics)

# Create/Update the selective reporting analysis
# Create/Update the selective reporting analyses
if panel.selective_reporting:
self.add_ast_analysis(REPORT_KEY, microorganism, antibiotics)
add(REPORT_KEY, microorganism, antibiotics)

# If there are extrapolated antibiotics defined, add the
# analysis for selective reporting of extrapolated
extra = get_extrapolated_antibiotics(antibiotics, uids=True)
if extra:
add(REPORT_EXTRAPOLATED_KEY, microorganism, antibiotics)

return "{} objects affected".format(len(panel.microorganisms))

Expand Down
8 changes: 8 additions & 0 deletions src/senaite/ast/browser/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from senaite.ast import messageFactory as _
from senaite.ast import utils
from senaite.ast.browser.panel import ASTPanelView
from senaite.ast.config import REPORT_EXTRAPOLATED_KEY
from senaite.ast.config import REPORT_KEY


Expand Down Expand Up @@ -138,6 +139,13 @@ def update_analyses(self, microorganism, antibiotics):
rep_analyses = [utils.create_ast_analysis(self.context, REPORT_KEY,
microorganism, all_abx)]

# Create the selective reporting for extrapolated if required
extra = utils.get_extrapolated_antibiotics(all_abx, uids=True)
if extra:
utils.create_ast_analysis(self.context,
REPORT_EXTRAPOLATED_KEY,
microorganism, all_abx)

# Reporting is true for the given antibiotics
selected = map(lambda o: o.abbreviation, antibiotics)
for analysis in rep_analyses:
Expand Down
57 changes: 46 additions & 11 deletions src/senaite/ast/calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
# Copyright 2020-2021 by it's authors.
# Some rights reserved, see README and LICENSE.

import json
from bika.lims import api
from bika.lims.interfaces import IAuditable
from bika.lims.interfaces import ISubmitted
from senaite.ast import utils
from senaite.ast.config import BREAKPOINTS_TABLE_KEY
from senaite.ast.config import DISK_CONTENT_KEY
from senaite.ast.config import REPORT_EXTRAPOLATED_KEY
from senaite.ast.config import REPORT_KEY
from senaite.ast.config import RESISTANCE_KEY
from senaite.ast.config import ZONE_SIZE_KEY
Expand Down Expand Up @@ -102,7 +104,10 @@ def calc_sensitivity_categories(analysis):

# Update sensitivity categories
for category in categories:
abx_uid = category["uid"]
# If extrapolated, assume same zone size as representative
abx_uid = category.get("primary")
if not api.is_uid(abx_uid):
abx_uid = category["uid"]

# Get the zone size
zone_size = zone_sizes.get(abx_uid)
Expand Down Expand Up @@ -161,7 +166,10 @@ def calc_disk_dosages(analysis):
# Dosages are stored as interim fields
disk_dosages = disk_dosages_analysis.getInterimFields()
for dosage in disk_dosages:
abx_uid = dosage["uid"]
# If extrapolated, assume same zone size as representative
abx_uid = dosage.get("primary")
if not api.is_uid(abx_uid):
abx_uid = dosage["uid"]

# Get the selected Breakpoints Table for this category
breakpoints_uid = breakpoints.get(abx_uid)
Expand All @@ -181,14 +189,14 @@ def calc_disk_dosages(analysis):


def update_extrapolated_antibiotics(analysis):
"""Updates the sensitivity categories (R/I/S) and reporting option (Y/N) of
extrapolated antibiotics assigned to the analysis passed-in.
"""Updates the sensitivity categories (R/I/S) of extrapolated antibiotics
assigned to the analysis passed-in.

The sensitivity result (category) obtained for a particular antibiotic is
extrapolated to extrapolated antibiotics
"""
keyword = analysis.getKeyword()
if keyword not in [BREAKPOINTS_TABLE_KEY, RESISTANCE_KEY, REPORT_KEY]:
keys = [BREAKPOINTS_TABLE_KEY, ZONE_SIZE_KEY, RESISTANCE_KEY, REPORT_KEY]
if analysis.getKeyword() not in keys:
return

def update_extrapolated(target):
Expand All @@ -210,6 +218,11 @@ def update_extrapolated(target):
# Get the analysis (keyword: analysis) from same sample and microorganism
analyses = utils.get_ast_group(analysis)

# Update the analysis that stores the zone diameter category
zone_diameter = analyses.get(ZONE_SIZE_KEY)
if zone_diameter:
update_extrapolated(zone_diameter)

# Update the analysis that stores the sensitivity category
sensitivity = analyses.get(RESISTANCE_KEY)
if sensitivity:
Expand Down Expand Up @@ -261,7 +274,7 @@ def to_report(option):

# Set the final result
capture_date = sensitivity.getResultCaptureDate()
sensitivity.setResult(result)
sensitivity.setResult(json.dumps(result))
sensitivity.setResultCaptureDate(capture_date)

# Re-enable the audit for this analysis
Expand All @@ -279,19 +292,41 @@ def get_reportable_antibiotics(analysis):
sensitivity = analyses.get(RESISTANCE_KEY)
results = sensitivity.getInterimFields()

# The analysis "Report Extrapolated" is used to identify the antibiotics
# their sensitivity category has been extrapolated from representative
# antibiotics. Build a mapping with keys as the UIDs of the representatives
# and values as the UIDs of the extrapolated antibiotics to report
reportable = {}
extrapolated = analyses.get(REPORT_EXTRAPOLATED_KEY)
if extrapolated:
for interim in extrapolated.getInterimFields():
try:
selected = json.loads(interim.get("value", "[]"))
except:
selected = []
reportable[interim.get("uid")] = selected

def is_reportable(interim):
primary = interim.get("primary", None)
if primary:
# Check if the primary is reportable
uid = interim.get("uid")
return uid in reportable.get(primary)
return interim.get("value") == "1"

# The analysis "Report" is used to identify the results from the sensitivity
# category analysis that need to be reported
report = analyses.get(REPORT_KEY)
if report:
# The results to be reported are defined by the Y/N values
# XXX senaite.app.listing has no support boolean type for interim fields
report_values = report.getInterimFields()
to_report = filter(lambda k: k.get("value") == "1", report_values)
to_report = filter(is_reportable, report_values)

# Get the abbreviation of microorganisms (keyword)
microorganisms = map(lambda k: k.get("keyword"), to_report)
# Get the abbreviation of antibiotics (keyword)
antibiotics = map(lambda k: k.get("keyword"), to_report)

# Bail out (Sensitivity) "Category" results to not report
results = filter(lambda r: r.get("keyword") in microorganisms, results)
results = filter(lambda r: r.get("keyword") in antibiotics, results)

return results
40 changes: 40 additions & 0 deletions src/senaite/ast/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
# is not present, the system assign all microorganisms from the panel
IDENTIFICATION_KEY = "senaite_ast_identification"

# Keyword of the Analysis Service used to choose the extrapolated antibiotics
# to be reported in results report
REPORT_EXTRAPOLATED_KEY = "senaite_ast_report_extrapolated"

# Title for the AST calculation object. This calculation allows AST machinery
# to assign a final result by its own, without prompting the user
AST_CALCULATION_TITLE = "senaite_ast_calc"
Expand All @@ -84,6 +88,9 @@
u"(Susceptible, increased exposure) and R (Resistant)"),
"choices": "0:|1:S|2:I|3:R",
"sort_key": 530,
"string_result": True,
"point_of_capture": AST_POINT_OF_CAPTURE,
"calculation": AST_CALCULATION_TITLE,
},

BREAKPOINTS_TABLE_KEY: {
Expand All @@ -96,6 +103,9 @@
# XXX This is a choices field, but choices are populated on creation
"choices": "",
"sort_key": 505,
"string_result": True,
"point_of_capture": AST_POINT_OF_CAPTURE,
"calculation": AST_CALCULATION_TITLE,
},

DISK_CONTENT_KEY: {
Expand All @@ -108,12 +118,18 @@
u"charge."),
"size": "1",
"sort_key": 510,
"string_result": True,
"point_of_capture": AST_POINT_OF_CAPTURE,
"calculation": AST_CALCULATION_TITLE,
},

ZONE_SIZE_KEY: {
"title": "{} - " + _(u"Zone diameter (mm)"),
"size": "1",
"sort_key": 520,
"string_result": True,
"point_of_capture": AST_POINT_OF_CAPTURE,
"calculation": AST_CALCULATION_TITLE,
},

REPORT_KEY: {
Expand All @@ -122,11 +138,35 @@
# XXX senaite.app.listing has no support for boolean types (interim)
"type": "boolean",
"sort_key": 540,
"string_result": True,
"point_of_capture": AST_POINT_OF_CAPTURE,
"calculation": AST_CALCULATION_TITLE,
},

REPORT_EXTRAPOLATED_KEY: {
"title": "{} - " + _(u"Report extrapolated"),
"description":
_(u"Selection of the antibiotics to be included in results report "
u"that their sensitivity is extrapolated from antibiotic "
u"representatives"),
# XXX This is a choices field, but choices are populated on creation
"choices": "",
"type": "multichoice",
"sort_key": 550,
"string_result": True,
"point_of_capture": AST_POINT_OF_CAPTURE,
"calculation": AST_CALCULATION_TITLE,
},

IDENTIFICATION_KEY: {
"title": _(u"Microorganism identification"),
"sort_key": 500,
# The options are the list of microorganisms and are automatically
# added when the corresponding analysis is initialized
"options_type": "multiselect",
"string_result": False,
"point_of_capture": "lab",
"calculation": None,
}

}
32 changes: 16 additions & 16 deletions src/senaite/ast/setuphandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,6 @@ def setup_ast_services(portal):
categories = setup.bika_analysiscategories.objectValues()
category = filter(lambda c: api.get_title(c) == cat_name, categories)[0]

# Get the calculation
calc_name = AST_CALCULATION_TITLE
calcs = setup.bika_calculations.objectValues()
calc = filter(lambda c: api.get_title(c) == calc_name, calcs)[0]

title = settings["title"]
if "{}" in title:
title = title.format(_("Antibiotic Sensitivity"))
Expand All @@ -190,20 +185,25 @@ def setup_ast_services(portal):
service = api.create(folder, "AnalysisService", Category=category,
title=title, Keyword=key)

description = settings.get("description", AUTOGENERATED)
sort_key = settings.get("sort_key", 1000)
options_type = settings.get("options_type", "select")
string_result = settings.get("string_result", False)
poc = settings.get("point_of_capture", AST_POINT_OF_CAPTURE)
calc_name = settings.get("calculation", AST_CALCULATION_TITLE)

service.setKeyword(key)
service.setTitle(title)
description = settings.get("description", AUTOGENERATED)
service.setDescription(description)
service.setSortKey(settings["sort_key"])
if key == IDENTIFICATION_KEY:
# This is the lab analysis for the identification of microorganisms
# The options are the list of microorganisms and are automatically
# added when the corresponding analysis is initialized
service.setResultOptionsType("multiselect")
else:
# These are "ast" analyses
service.setStringResult(True)
service.setPointOfCapture(AST_POINT_OF_CAPTURE)
service.setSortKey(sort_key)
service.setResultOptionsType(options_type)
service.setStringResult(string_result)
service.setPointOfCapture(poc)

# Get the calculation
if calc_name:
calcs = setup.bika_calculations.objectValues()
calc = filter(lambda c: api.get_title(c) == calc_name, calcs)[0]
service.setCalculation(calc)

# Do not allow the modification of this service
Expand Down
Loading