Skip to content

forms

main.forms ¤

Forms needed by ProCAT.

Classes¤

CostRecoveryForm ¤

Bases: Form

Form to use in the Cost Recovery view.

CustomUserCreationForm ¤

Bases: UserCreationForm

Form to create users with the custom User model.

TODO: This is a placeholder for development. When SSO is implemented, this won't be needed since available users will be retrieved automatically.

Classes¤
Meta ¤

Meta class for the form.

FundingInlineForm ¤

Bases: ModelForm

Form to create and edit a Funding row inline, as part of a Project form.

The project field is left out, since it is supplied automatically by the enclosing formset (see FundingInlineFormSet).

Classes¤
Meta ¤

Meta class for the form.

ProjectForm ¤

Bases: ModelForm

Form to create and edit Project instances.

Classes¤
Meta ¤

Meta class for the form.

ProjectPhaseForm(*args, **kwargs) ¤

Bases: ModelForm

Form to create and edit Project Phase instances.

Override init to populate 'days', if available.

Source code in main/forms.py
88
89
90
91
92
def __init__(self, *args: Any, **kwargs: Any) -> None:  # type: ignore[explicit-any]
    """Override init to populate 'days', if available."""
    super().__init__(*args, **kwargs)
    if self.instance.start_date:
        self.initial["days"] = self.instance.days
Classes¤
Meta ¤

Meta class for the form.

Methods:¤
clean() ¤

Convert the 'days' field into the model's 'value' (FTE) field.

Source code in main/forms.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
def clean(self) -> dict[str, Any] | None:  # type: ignore[explicit-any]
    """Convert the 'days' field into the model's 'value' (FTE) field."""
    cleaned_data = super().clean() or {}

    days = cleaned_data.get("days")
    start_date = cleaned_data.get("start_date")
    end_date = cleaned_data.get("end_date")

    if days is not None and start_date and end_date and end_date > start_date:
        # `end_date` is an inclusive calendar date, hence the `+ timedelta(days=1)`.
        self.instance.value = days_to_fte(
            start_date, end_date + timedelta(days=1), days
        )

    return cleaned_data

ProjectPhaseInlineForm(*args, **kwargs) ¤

Bases: ProjectPhaseForm

Form to create and edit a Project Phase row inline, as part of a Project form.

Identical to ProjectPhaseForm, except that:

  • the project field is left out, since it is supplied automatically by the enclosing formset (see ProjectPhaseInlineFormSet);
  • the database-backed sibling checks (phase overlap, alignment, and single maintenance phase - see models.ProjectPhase.clean()) are skipped during the normal per-row validation. Instead, ProjectPhaseInlineFormSetBase.clean() performs them once, together, against the complete, in-memory set of phases being submitted, once all rows have passed their individual field-level validation.

Override init to flag the instance as validated by the formset.

Source code in main/forms.py
162
163
164
165
def __init__(self, *args: Any, **kwargs: Any) -> None:  # type: ignore[explicit-any]
    """Override init to flag the instance as validated by the formset."""
    super().__init__(*args, **kwargs)
    self.instance._validated_by_formset = True
Classes¤
Meta ¤

Meta class for the form.

Methods:¤

ProjectPhaseInlineFormSetBase ¤

Bases: BaseInlineFormSet

Inline formset to create/edit all Project Phases of a Project together.

Validates the phases as a group (overlap, alignment, single maintenance phase) against their complete, in-memory candidate set - i.e. every surviving (not deleted, not blank) row, whether new or pre-existing - rather than one row at a time against the database. This allows several new and/or edited phases to be submitted together in a single request.

Methods:¤
clean() ¤

Validate the whole set of (surviving) phases together.

Source code in main/forms.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def clean(self) -> None:
    """Validate the whole set of (surviving) phases together."""
    super().clean()

    if any(self.errors):
        # Individual rows already have errors; the group-level checks
        # below need every row's data to already be well-formed.
        return

    candidates = [
        form.instance
        for form in self.forms
        if form not in self.deleted_forms and form.cleaned_data
    ]

    errors: list[str] = []
    for candidate in candidates:
        others = [c for c in candidates if c is not candidate]
        try:
            candidate.check_overlapping_phases(siblings=others)
            candidate.check_phase_alignment(siblings=others)
            candidate.check_only_one_maintenance_phase(siblings=others)
        except ValidationError as e:
            errors.extend(e.messages)

    if errors:
        # De-duplicate while preserving order: the same message can be
        # raised more than once, e.g. the alignment error is identically
        # worded for every phase that fails it.
        raise forms.ValidationError(list(dict.fromkeys(errors)))

Functions:¤