Skip to content

Commit

Permalink
Merge pull request #120 from rmnldwg/release-0.4.6
Browse files Browse the repository at this point in the history
Release 0.4.6
  • Loading branch information
rmnldwg authored Sep 12, 2024
2 parents 84e1262 + 4e38ab5 commit aaf1084
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 85 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to this project will be documented in this file.

<a name="0.4.6"></a>

## [0.4.6] - 2024-09-12

### 🐛 Bug Fixes

- Add sub-/supraglottis to larynx selection (related to [#119])

### Change

- Add pCT to default selection

<a name="0.4.5"></a>

## [0.4.5] - 2024-08-07
Expand Down Expand Up @@ -314,3 +326,4 @@ Commits before the 2nd of March 2023 did not use conventional commits and no cha
[#110]: https://github.com/rmnldwg/lyprox/issues/110
[#111]: https://github.com/rmnldwg/lyprox/issues/111
[#115]: https://github.com/rmnldwg/lyprox/issues/115
[#119]: https://github.com/rmnldwg/lyprox/issues/119
155 changes: 74 additions & 81 deletions lyprox/dataexplorer/forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
The `dataexplorer.forms` module defines the relatively complex form that is
"""The `dataexplorer.forms` module defines the relatively complex form that is
used for querying the database later.
It also implements some custom form elements, like `ThreeWayToggle` and
Expand All @@ -11,9 +10,9 @@
is implemented here that allows us to select the institutions from which the
ptients should be included via check boxes with the institution logo on it.
"""

# pylint: disable=no-member
import logging
from typing import Tuple

from django import forms
from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -45,17 +44,13 @@ class ThreeWayToggleWidget(forms.RadioSelect):
attributes of the individual inputs (radio buttons) as `option_attrs` as
well as the attributes of the container as ``attrs``.
"""
template_name = 'widgets/three_way_toggle.html'
option_template_name = 'widgets/three_way_toggle_option.html'
option_attrs = {
"class": "radio is-hidden",
"onchange": "changeHandler();"
}

template_name = "widgets/three_way_toggle.html"
option_template_name = "widgets/three_way_toggle_option.html"
option_attrs = {"class": "radio is-hidden", "onchange": "changeHandler();"}

def __init__(
self,
attrs=None, choices=(),
option_attrs=None, label=None, tooltip=None
self, attrs=None, choices=(), option_attrs=None, label=None, tooltip=None
):
"""Store arguments and option attributes for later use."""
self.label = label
Expand All @@ -79,15 +74,21 @@ def create_option(
):
"""Pass the option attributes to the actual options"""
return super().create_option(
name, value, label, selected, index, subindex,
attrs=self.build_attrs(self.option_attrs, attrs)
name,
value,
label,
selected,
index,
subindex,
attrs=self.build_attrs(self.option_attrs, attrs),
)


class ThreeWayToggle(forms.ChoiceField):
"""A toggle switch than can be in three different states: Positive/True,
unkown/None and negative/False.
"""

def __init__(
self,
attrs=None,
Expand All @@ -97,10 +98,11 @@ def __init__(
choices=None,
initial=0,
required=False,
**kwargs
**kwargs,
):
"""Pass the arguments, like `label` and `tooltip` to the constructor
of the custom widget."""
of the custom widget.
"""
if choices is None:
choices = [(1, "plus"), (0, "ban"), (-1, "minus")]

Expand All @@ -109,15 +111,12 @@ def __init__(

super().__init__(
widget=ThreeWayToggleWidget(
attrs=attrs,
option_attrs=option_attrs,
label=label,
tooltip=tooltip
attrs=attrs, option_attrs=option_attrs, label=label, tooltip=tooltip
),
choices=choices,
initial=initial,
required=required,
**kwargs
**kwargs,
)

def to_python(self, value):
Expand All @@ -131,8 +130,7 @@ def to_python(self, value):


class DatasetModelChoiceIndexer:
"""
Custom class with which one can access additional information from
"""Custom class with which one can access additional information from
the model that is chosen by the `DatasetMultipleChoiceField`.
"""

Expand All @@ -144,21 +142,21 @@ def __getitem__(self, key):
obj = self.queryset[key]
return self.info(obj)

def info(self, obj: Dataset) -> Tuple[int, str]:
def info(self, obj: Dataset) -> tuple[int, str]:
"""Return the label and logo URL for the Dataset."""
return (
self.field.label_from_instance(obj),
self.field.logo_url_from_instance(obj)
self.field.logo_url_from_instance(obj),
)


class DatasetMultipleChoiceField(forms.ModelMultipleChoiceField):
"""
Customize label description and add method that returns the logo URL for
"""Customize label description and add method that returns the logo URL for
Datasets. The implementation is inspired by how the ``choices`` are
implemented. But since some other functionality depends on how those
choices are implemented, it cannot be changed easily.
"""

name_and_url_indexer = DatasetModelChoiceIndexer
"""Allows one to extract more info (name and logo) about the objects."""

Expand Down Expand Up @@ -188,36 +186,33 @@ class DashboardForm(FormLoggerMixin, forms.Form):
},
),
choices=Diagnose.Modalities.choices,
initial=["CT", "MRI", "PET", "FNA", "diagnostic_consensus", "pathology"]
initial=["CT", "MRI", "PET", "FNA", "diagnostic_consensus", "pathology", "pCT"],
)
modality_combine = forms.ChoiceField(
widget=forms.Select(attrs={"onchange": "changeHandler();"}),
choices=[
("AND" , "AND" ),
("OR" , "OR" ),
("AND", "AND"),
("OR", "OR"),
("maxLLH", "maxLLH"),
("RANK" , "RANK" ),
("RANK", "RANK"),
],
label="Combine",
initial="maxLLH",
)

# patient specific fields
nicotine_abuse = ThreeWayToggle(
label="smoking status",
tooltip="Select smokers or non-smokers"
label="smoking status", tooltip="Select smokers or non-smokers"
)
hpv_status = ThreeWayToggle(
label="HPV status",
tooltip="Select patients being HPV positive or negative"
label="HPV status", tooltip="Select patients being HPV positive or negative"
)
neck_dissection = ThreeWayToggle(
label="neck dissection",
tooltip="Include patients that have (or have not) received neck dissection"
tooltip="Include patients that have (or have not) received neck dissection",
)
n_status = ThreeWayToggle(
label="N+ vs N0",
tooltip="Select all N+ (or N0) patients"
label="N+ vs N0", tooltip="Select all N+ (or N0) patients"
)
dataset__in = DatasetMultipleChoiceField(
required=False,
Expand All @@ -229,7 +224,7 @@ class DashboardForm(FormLoggerMixin, forms.Form):
},
),
queryset=Dataset.objects.all().filter(is_public=True),
initial=Dataset.objects.all().filter(is_public=True)
initial=Dataset.objects.all().filter(is_public=True),
)

# tumor specific info
Expand All @@ -243,10 +238,10 @@ class DashboardForm(FormLoggerMixin, forms.Form):
),
choices=[
("base", "base of tongue"), # choices here must match entries
("tonsil", "tonsil"), # in the Tumor.SUBSITE_DICT keys
("rest_oro" , "other"),
("tonsil", "tonsil"), # in the Tumor.SUBSITE_DICT keys
("rest_oro", "other"),
],
initial=["base", "tonsil", "rest_oro"]
initial=["base", "tonsil", "rest_oro"],
)
subsite_hypopharynx = forms.MultipleChoiceField(
required=False,
Expand All @@ -256,8 +251,8 @@ class DashboardForm(FormLoggerMixin, forms.Form):
"onchange": "changeHandler();",
},
),
choices=[("rest_hypo" , "all")], # choices here must match entries in
initial=["rest_hypo"] # the Tumor.SUBSITE_DICT keys
choices=[("rest_hypo", "all")], # choices here must match entries in
initial=["rest_hypo"], # the Tumor.SUBSITE_DICT keys
)
subsite_larynx = forms.MultipleChoiceField(
required=False,
Expand All @@ -268,10 +263,12 @@ class DashboardForm(FormLoggerMixin, forms.Form):
},
),
choices=[
("glottis", "glottis"), # choices here must match entries
("rest_larynx" , "other"), # in the Tumor.SUBSITE_DICT keys
("glottis", "glottis"), # choices here must match entries
("supraglottis", "supraglottis"), # in the Tumor.SUBSITE_DICT keys
("subglottis", "subglottis"),
("rest_larynx", "other"),
],
initial=["glottis", "rest_larynx"]
initial=["glottis", "supraglottis", "subglottis", "rest_larynx"],
)
subsite_oral_cavity = forms.MultipleChoiceField(
required=False,
Expand All @@ -282,13 +279,13 @@ class DashboardForm(FormLoggerMixin, forms.Form):
},
),
choices=[
("tongue", "tongue"), # choices here must match entries
("gum_cheek", "gums and cheek"), # in the Tumor.SUBSITE_DICT keys
("tongue", "tongue"), # choices here must match entries
("gum_cheek", "gums and cheek"), # in the Tumor.SUBSITE_DICT keys
("mouth_floor", "floor of mouth"),
("palate", "palate"),
("glands", "salivary glands"),
],
initial=["tongue", "gum_cheek", "mouth_floor", "palate", "glands"]
initial=["tongue", "gum_cheek", "mouth_floor", "palate", "glands"],
)

t_stage__in = forms.MultipleChoiceField(
Expand All @@ -300,27 +297,27 @@ class DashboardForm(FormLoggerMixin, forms.Form):
},
),
choices=Patient.T_stages.choices,
initial=[0,1,2,3,4]
initial=[0, 1, 2, 3, 4],
)
central = ThreeWayToggle(
label="central",
tooltip="Choose to in- or exclude patients with central tumors"
label="central", tooltip="Choose to in- or exclude patients with central tumors"
)
extension = ThreeWayToggle(
label="midline extension",
tooltip=(
"Investigate patients with tumors that do (or do not) "
"cross the mid-sagittal line"
)
),
)

# checkbutton for switching to percent
show_percent = forms.BooleanField(
required=False, initial=False,
required=False,
initial=False,
widget=forms.widgets.RadioSelect(
attrs={"class": "radio is-hidden", "onchange": "changeHandler();"},
choices=[(True, "percent"), (False, "absolute")]
)
choices=[(True, "percent"), (False, "absolute")],
),
)

def __init__(self, *args, user, **kwargs):
Expand All @@ -337,35 +334,29 @@ def __init__(self, *args, user, **kwargs):
# add all LNL ToggleButtons so I don't have to write a myriad of them
for side in ["ipsi", "contra"]:
for lnl in Diagnose.LNLs:
if lnl in ['I', 'II', 'V']:
if lnl in ["I", "II", "V"]:
self.fields[f"{side}_{lnl}"] = ThreeWayToggle(
option_attrs={
"onclick": "bothClickHandler(this)"
}
option_attrs={"onclick": "bothClickHandler(this)"}
)
elif lnl in ['Ia', 'Ib', 'IIa', 'IIb', 'Va', 'Vb']:
elif lnl in ["Ia", "Ib", "IIa", "IIb", "Va", "Vb"]:
self.fields[f"{side}_{lnl}"] = ThreeWayToggle(
option_attrs={
"onclick": "subClickHandler(this)"
}
option_attrs={"onclick": "subClickHandler(this)"}
)
else:
self.fields[f"{side}_{lnl}"] = ThreeWayToggle()


def clean(self):
"""Make sure LNLs I & II have correct values corresponding to their
sublevels a & b. Also convert tstages from list of str to list of int."""
sublevels a & b. Also convert tstages from list of str to list of int.
"""
cleaned_data = super(DashboardForm, self).clean()

# necessary to prevent errors from processing invalid data
if len(self.errors) != 0:
return {}

# map all -1,0,1 fields to False,None,True
cleaned_data = {
key: trio_to_bool(value) for key,value in cleaned_data.items()
}
cleaned_data = {key: trio_to_bool(value) for key, value in cleaned_data.items()}

# make sure LNLs I & II aren't in conflict with their sublevels
for side in ["ipsi", "contra"]:
Expand All @@ -377,21 +368,23 @@ def clean(self):
if a is True or b is True:
cleaned_data[f"{side}_{lnl}"] = True
if a is False and b is False:
cleaned_data[f'{side}_{lnl}'] = False
cleaned_data[f"{side}_{lnl}"] = False

# map `central` from False,None,True to the respective list of sides
if cleaned_data['central'] is True:
cleaned_data['side__in'] = ['central']
elif cleaned_data['central'] is False:
cleaned_data["side__in"] = ['left', 'right']
if cleaned_data["central"] is True:
cleaned_data["side__in"] = ["central"]
elif cleaned_data["central"] is False:
cleaned_data["side__in"] = ["left", "right"]
else:
cleaned_data["side__in"] = ['left', 'right', 'central']
cleaned_data["side__in"] = ["left", "right", "central"]

# map subsites 'base','tonsil','rest' to list of ICD codes.
subsites = (cleaned_data["subsite_oropharynx"]
+ cleaned_data["subsite_hypopharynx"]
+ cleaned_data["subsite_larynx"]
+ cleaned_data["subsite_oral_cavity"])
subsites = (
cleaned_data["subsite_oropharynx"]
+ cleaned_data["subsite_hypopharynx"]
+ cleaned_data["subsite_larynx"]
+ cleaned_data["subsite_oral_cavity"]
)

icd_codes = []
for sub in subsites:
Expand All @@ -402,5 +395,5 @@ def clean(self):
str_list = cleaned_data["t_stage__in"]
cleaned_data["t_stage__in"] = [int(s) for s in str_list]

self.logger.debug(f'cleaned data: {cleaned_data}')
self.logger.debug(f"cleaned data: {cleaned_data}")
return cleaned_data
Loading

0 comments on commit aaf1084

Please sign in to comment.