Source code for crate_anon.crateweb.research.forms
#!/usr/bin/env python
# crate_anon/crateweb/research/forms.py
"""
===============================================================================
Copyright (C) 2015-2018 Rudolf Cardinal (rudolf@pobox.com).
This file is part of CRATE.
CRATE 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, either version 3 of the License, or
(at your option) any later version.
CRATE 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 CRATE. If not, see <http://www.gnu.org/licenses/>.
===============================================================================
"""
import datetime
import logging
from typing import Any, Dict, List, Optional, Type
from cardinal_pythonlib.django.forms import (
MultipleIntAreaField,
MultipleWordAreaField,
)
from django import forms
from django.forms import (
BooleanField,
CharField,
ChoiceField,
DateField,
FileField,
FloatField,
IntegerField,
ModelForm,
)
from crate_anon.crateweb.research.models import Highlight, Query
from crate_anon.crateweb.research.research_db_info import SingleResearchDatabase # noqa
from crate_anon.common.sql import (
SQL_OPS_MULTIPLE_VALUES,
SQL_OPS_VALUE_UNNECESSARY,
QB_DATATYPE_DATE,
QB_DATATYPE_FLOAT,
QB_DATATYPE_INTEGER,
QB_DATATYPE_UNKNOWN,
QB_STRING_TYPES,
)
log = logging.getLogger(__name__)
[docs]class AddQueryForm(ModelForm):
class Meta:
model = Query
fields = ['sql']
widgets = {
'sql': forms.Textarea(attrs={'rows': 20, 'cols': 80}),
}
[docs]class DatabasePickerForm(forms.Form):
database = ChoiceField(label="Database", required=True)
def __init__(self,
*args,
dbinfolist: List[SingleResearchDatabase],
**kwargs) -> None:
super().__init__(*args, **kwargs)
f = self.fields['database'] # type: ChoiceField
f.choices = [(d.name, d.description) for d in dbinfolist]
[docs]class PidLookupForm(forms.Form):
rids = MultipleWordAreaField(required=False)
mrids = MultipleWordAreaField(required=False)
trids = MultipleIntAreaField(required=False)
def __init__(self,
*args,
dbinfo: SingleResearchDatabase,
**kwargs) -> None:
super().__init__(*args, **kwargs)
rids = self.fields['rids'] # type: MultipleIntAreaField
mrids = self.fields['mrids'] # type: MultipleIntAreaField
trids = self.fields['trids'] # type: MultipleIntAreaField
rids.label = "{}: {} (RID)".format(dbinfo.rid_field,
dbinfo.rid_description)
mrids.label = "{}: {} (MRID)".format(dbinfo.mrid_field,
dbinfo.mrid_description)
trids.label = "{}: {} (TRID)".format(dbinfo.trid_field,
dbinfo.trid_description)
[docs]class RidLookupForm(forms.Form):
pids = MultipleWordAreaField(required=False)
mpids = MultipleWordAreaField(required=False)
def __init__(self,
*args,
dbinfo: SingleResearchDatabase,
**kwargs) -> None:
super().__init__(*args, **kwargs)
pids = self.fields['pids'] # type: MultipleIntAreaField
mpids = self.fields['mpids'] # type: MultipleIntAreaField
pids.label = "{} (PID)".format(dbinfo.pid_description)
mpids.label = "{} (MPID)".format(dbinfo.mpid_description)
DEFAULT_MIN_TEXT_FIELD_LENGTH = 100
class FieldPickerInfo(object):
def __init__(self, value: str, description: str, type_: Type,
permits_empty_id: bool):
self.value = value
self.description = description
self.type_ = type_
self.permits_empty_id = permits_empty_id
[docs]class SQLHelperTextAnywhereForm(forms.Form):
fkname = ChoiceField(required=True)
patient_id = CharField(label="ID value (to restrict to a single patient)",
required=False)
fragment = CharField(label="String fragment to find", required=True)
use_fulltext_index = BooleanField(
label="Use full-text indexing where available "
"(faster, but requires whole words)",
required=False)
min_length = IntegerField(
label="Minimum 'width' of textual field to include (e.g. {})".format(
DEFAULT_MIN_TEXT_FIELD_LENGTH
),
min_value=1, required=True)
include_content = BooleanField(
label="Include content from fields where found (slower)",
required=False)
include_datetime = BooleanField(
label="Include date/time from where known",
required=False)
def __init__(
self,
*args,
fk_options: List[FieldPickerInfo],
fk_label: str = "Field name containing patient research ID",
**kwargs) -> None:
super().__init__(*args, **kwargs)
self.fk_options = fk_options
# Set the choices available for fkname
f = self.fields['fkname'] # type: ChoiceField
f.choices = [(opt.value, opt.description) for opt in fk_options]
f.label = fk_label
[docs] def clean(self) -> Dict[str, Any]:
cleaned_data = super().clean()
fieldname = cleaned_data.get("fkname")
pidvalue = cleaned_data.get("patient_id")
if fieldname:
opt = next(o for o in self.fk_options if o.value == fieldname)
if pidvalue:
try:
_ = opt.type_(pidvalue)
except (TypeError, ValueError):
raise forms.ValidationError(
"For field {!r}, the ID value must be of "
"type {}".format(opt.description, opt.type_))
else:
self._check_permits_empty_id_for_blank_id(opt)
return cleaned_data
def _check_permits_empty_id_for_blank_id(self,
opt: FieldPickerInfo) -> None:
# Exists as a function so ClinicianAllTextFromPidForm can override it.
if not opt.permits_empty_id:
raise forms.ValidationError(
"For this ID type ({}), you must specify an ID "
"value".format(opt.value))
[docs]class ClinicianAllTextFromPidForm(SQLHelperTextAnywhereForm):
patient_id = CharField(label="ID value", required=True)
# ... the clinician view always requires an ID (no "patient browsing";
# that's in the domain of research as it might yield patients that aren't
# being cared for by this clinician)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args,
fk_label="Field name containing patient ID",
**kwargs)
inccontent = self.fields['include_content'] # type: BooleanField
incdate = self.fields['include_datetime'] # type: BooleanField
# Hide include_content/include_datetime (always true here)
# inccontent.widget = inccontent.hidden_widget # ... nope!
inccontent.widget = forms.HiddenInput() # yes, this works
incdate.widget = forms.HiddenInput()
def _check_permits_empty_id_for_blank_id(self,
opt: FieldPickerInfo) -> None:
return
def html_form_date_to_python(text: str) -> datetime.datetime:
return datetime.datetime.strptime(text, "%Y-%m-%d")
def int_validator(text: str) -> str:
return str(int(text)) # may raise ValueError, TypeError
def float_validator(text: str) -> str:
return str(float(text)) # may raise ValueError, TypeError
[docs]class QueryBuilderForm(forms.Form):
# See also querybuilder.js
database = CharField(label="Schema", required=False)
schema = CharField(label="Schema", required=True)
table = CharField(label="Table", required=True)
column = CharField(label="Column", required=True)
datatype = CharField(label="Data type", required=True)
offer_where = BooleanField(label="Offer WHERE?", required=False)
# BooleanField generally needs "required=False", or you can't have False!
where_op = CharField(label="WHERE comparison", required=False)
date_value = DateField(label="Date value (e.g. 1900-01-31)",
required=False)
int_value = IntegerField(label="Integer value", required=False)
float_value = FloatField(label="Float value", required=False)
string_value = CharField(label="String value", required=False)
file = FileField(label="File (for IN)", required=False)
def __init__(self, *args, **kwargs) -> None:
self.file_values_list = []
super().__init__(*args, **kwargs)
def get_datatype(self) -> Optional[str]:
return self.data.get('datatype', None)
def is_datatype_unknown(self) -> bool:
return self.get_datatype() == QB_DATATYPE_UNKNOWN
def offering_where(self) -> bool:
if self.is_datatype_unknown():
return False
return self.data.get('offer_where', False)
def get_value_fieldname(self) -> str:
datatype = self.get_datatype()
if datatype == QB_DATATYPE_INTEGER:
return "int_value"
if datatype == QB_DATATYPE_FLOAT:
return "float_value"
if datatype == QB_DATATYPE_DATE:
return "date_value"
if datatype in QB_STRING_TYPES:
return "string_value"
if datatype == QB_DATATYPE_UNKNOWN:
return ""
raise ValueError("Invalid field type")
def get_cleaned_where_value(self) -> Any:
# Only call this if you've already cleaned/validated the form!
return self.cleaned_data[self.get_value_fieldname()]
[docs] def clean(self) -> None:
# Check the WHERE information is sufficient.
if 'submit_select' in self.data or 'submit_select_star' in self.data:
# Form submitted via the "Add" method, so no checks required.
# http://stackoverflow.com/questions/866272/how-can-i-build-multiple-submit-buttons-django-form # noqa
return
if not self.offering_where():
return
cleaned_data = super().clean()
if not cleaned_data['where_op']:
self.add_error('where_op',
forms.ValidationError("Must specify comparison"))
# No need for a value for NULL-related comparisons. But otherwise:
where_op = cleaned_data['where_op']
if where_op not in SQL_OPS_VALUE_UNNECESSARY + SQL_OPS_MULTIPLE_VALUES:
# Can't take 0 or many parameters, so need the standard single
# value:
value_fieldname = self.get_value_fieldname()
value = cleaned_data.get(value_fieldname)
if not value:
self.add_error(
value_fieldname,
forms.ValidationError("Must specify WHERE condition"))
# ---------------------------------------------------------------------
# Special processing for file upload operations
# ---------------------------------------------------------------------
if where_op not in SQL_OPS_MULTIPLE_VALUES:
return
fileobj = cleaned_data['file']
# ... is an instance of InMemoryUploadedFile
if not fileobj:
self.add_error('file', forms.ValidationError("Must specify file"))
return
datatype = self.get_datatype()
if datatype in QB_STRING_TYPES:
form_to_python_fn = str
elif datatype == QB_DATATYPE_DATE:
form_to_python_fn = html_form_date_to_python
elif datatype == QB_DATATYPE_INTEGER:
form_to_python_fn = int_validator
elif datatype == QB_DATATYPE_FLOAT:
form_to_python_fn = float_validator
else:
# Safe defaults
form_to_python_fn = str
# Or: http://www.dabeaz.com/generators/Generators.pdf
self.file_values_list = []
for line in fileobj.read().decode("utf8").splitlines():
raw_item = line.strip()
if not raw_item or raw_item.startswith('#'):
continue
try:
value = form_to_python_fn(raw_item)
except (TypeError, ValueError):
self.add_error('file', forms.ValidationError(
"File contains bad value: {}".format(repr(raw_item))))
return
self.file_values_list.append(value)
if not self.file_values_list:
self.add_error('file', forms.ValidationError(
"No values found in file"))
[docs]class ManualPeQueryForm(forms.Form):
sql = CharField(
required=False,
widget=forms.Textarea(attrs={'rows': 20, 'cols': 80})
)