Source code for autowisp.browser_interface.diagnostics.forms
"""The form behind the diagnostic expression management page.
A ``ModelForm`` rather than hand-written POST handling, which is a new
pattern in this interface and a deliberate one. The name charset, the
uniqueness of the name, the create-versus-update branch and the per-field
error plumbing are all things Django already does correctly, and each one
written out by hand would be another thing to keep right.
What Django cannot know is whether the expression *means* anything. That
is :func:`~autowisp.diagnostics.expressions.check_expression`, which
returns its complaints as plain strings rather than raising so that it
stays usable with no Django at all -- import, and one day the command
line, reach it by the same path. Turning those strings into a
``ValidationError`` is this module's whole reason to exist, and the only
place that adaptation happens.
"""
from django import forms
from autowisp.diagnostics.expressions import (
check_expression,
get_bare_aggregates,
)
from .models import DiagnosticExpression
[docs]
class DiagnosticExpressionForm(forms.ModelForm):
"""
Validate one proposed expression against the library it would join.
No project is involved. An expression is valid or not in every project
alike -- see :mod:`autowisp.diagnostics.diagnostic_types` -- and
whether the open project has *recorded* what it needs is a separate
question, answered by counting rows elsewhere. So this form works with
no project open, which is part of what makes one global library
coherent.
"""
# A ModelForm is configuration plus one hook; the base class supplies
# the rest of the interface.
# pylint: disable=too-few-public-methods
[docs]
class Meta:
"""The three user-editable columns of the model."""
model = DiagnosticExpression
fields = ["name", "expression", "description"]
# Both are ``TextField`` because neither has a useful length limit,
# but both are written on one line, so a textarea would be a
# misleading amount of room.
widgets = {
"expression": forms.TextInput(),
"description": forms.TextInput(),
}
# pylint: enable=too-few-public-methods
[docs]
def __init__(self, *args, expressions=None, **kwargs):
"""
Args:
expressions(dict): The library, ``{name: expression}``, this
one would join. The view has it and the form does not, so
it arrives as a keyword argument. An entry of the same
name is treated as the one being replaced rather than as a
conflict, so an edit can pass the library unchanged.
"""
super().__init__(*args, **kwargs)
self.expressions = dict(expressions or {})
#: The NaN-propagating aggregates the accepted expression calls,
#: for the view to warn about. Not an error: a deliberate
#: ``median`` is a legitimate thing to write, it is merely almost
#: never what was meant.
self.bare_aggregates = set()
[docs]
def clean(self):
"""Report what ``check_expression`` says, against the field at fault."""
cleaned_data = super().clean()
name = cleaned_data.get("name")
expression = cleaned_data.get("expression")
if not name or not expression:
# A missing field has already failed; complaining about the
# pair as well would only repeat that.
return cleaned_data
# check_expression reports on the pair, but its complaints have to
# land on the field that caused them. The problems a name has on
# its own are exactly those it still has beside an expression that
# references nothing, which sorts them without matching on the
# message text.
name_problems = check_expression(name, "0", {})
for problem in check_expression(name, expression, self.expressions):
self.add_error(
"name" if problem in name_problems else "expression", problem
)
if not self.errors:
self.bare_aggregates = get_bare_aggregates(expression)
return cleaned_data