Source code for fairdm.core.formsets
"""Shared formsets for the rows of related records a record carries."""
from django.core.exceptions import ValidationError
from django.forms import BaseInlineFormSet
from partial_date import PartialDate
from .dates import precedes
[docs]
def date_ordering_formset(start_type, end_type, message):
"""Return a formset class that refuses a backwards start/end date pair.
A formset validates every form before any of them saves, so a per-row
``clean()`` that looks its sibling up in the database (as
``ProjectDate.clean()`` and ``DatasetDate.clean()`` do) sees no unsaved
sibling when both dates are new rows in the same submission. The
returned formset reads the start and end values off the forms'
``cleaned_data`` instead, so the pair is checked whichever of the two
(or both) is unsaved.
The message is passed whole rather than assembled from a noun so that
each record type states its own date vocabulary and the sentence stays
translatable as one unit.
Args:
start_type: The address of the date type marking the start of the pair.
end_type: The address of the date type marking the end of the pair.
message: The error message, using ``%(start)s`` and ``%(end)s``
placeholders.
Returns:
A ``BaseInlineFormSet`` subclass whose ``clean()`` raises a
``ValidationError`` when the end precedes the start.
"""
class DateOrderingFormSet(BaseInlineFormSet):
def clean(self):
"""Reject a formset whose end date precedes its start date."""
super().clean()
start_value = None
end_value = None
for form in self.forms:
if not hasattr(form, "cleaned_data") or form.cleaned_data.get("DELETE"):
continue
value = form.cleaned_data.get("value")
if not value:
continue
# The form field holds the raw string. The model field only converts it
# in `full_clean()`, which runs after the formset's own `clean()`.
if not isinstance(value, PartialDate):
value = PartialDate(value)
# The type field cleans to the concept, and the pair is named by address.
chosen = getattr(form.cleaned_data.get("type"), "uri", None)
if chosen == start_type:
start_value = value
elif chosen == end_type:
end_value = value
if start_value is None or end_value is None:
return
if precedes(end_value, start_value):
raise ValidationError(
message % {"start": start_value, "end": end_value}
)
return DateOrderingFormSet