Skip to content

views

main.views ¤

Views for the main app.

Classes¤

CapacitiesListView ¤

Bases: LoginRequiredMixin, PermissionRequiredMixin, SingleTableMixin, FilterView

View to display the list of capacities.

CapacityPlanningView ¤

Bases: LoginRequiredMixin, PermissionRequiredMixin, TemplateView

View that renders the Capacity Planning page.

Methods:¤
get_context_data(**kwargs) ¤

Add HTML components and Bokeh version to the context.

Source code in main/views.py
 96
 97
 98
 99
100
101
102
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:  # type: ignore
    """Add HTML components and Bokeh version to the context."""
    context = super().get_context_data(**kwargs)
    layout = plots.create_capacity_planning_layout()
    context.update(plots.html_components_from_plot(layout))
    context["bokeh_version"] = bokeh.__version__
    return context

CostRecoveryView ¤

Bases: LoginRequiredMixin, PermissionRequiredMixin, FormView

View that renders the Cost Recovery page.

Methods:¤
form_valid(form) ¤

Generate csv using the dates provided in the form.

Source code in main/views.py
112
113
114
115
116
117
def form_valid(self, form: Form) -> HttpResponse:
    """Generate csv using the dates provided in the form."""
    month = form.cleaned_data["month"]
    year = form.cleaned_data["year"]
    response = report.create_charges_report_for_download(month, year)
    return response
get_context_data(**kwargs) ¤

Add HTML components and Bokeh version to the context.

Source code in main/views.py
119
120
121
122
123
124
125
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:  # type: ignore
    """Add HTML components and Bokeh version to the context."""
    context = super().get_context_data(**kwargs)
    layout = plots.create_cost_recovery_layout()
    context.update(plots.html_components_from_plot(layout))
    context["bokeh_version"] = bokeh.__version__
    return context

ProjectCreateInlineView ¤

Bases: PermissionRequiredMixin, View

Create a Project together with its Funding and Phases in one page.

This view lets a new project be created with its funding and phases all filled in together on a single page, via the FundingInlineFormSet and ProjectPhaseInlineFormSet formsets, rather than requiring the project to be created first and its funding/phases added separately afterwards.

Methods:¤
get(request) ¤

Render an empty Project form with empty Funding/Phase formsets.

Source code in main/views.py
145
146
147
148
149
150
151
152
def get(self, request: HttpRequest) -> HttpResponse:
    """Render an empty Project form with empty Funding/Phase formsets."""
    context = {
        "project_form": forms.ProjectForm(),
        "funding_formset": forms.FundingInlineFormSet(prefix="funding"),
        "phase_formset": forms.ProjectPhaseInlineFormSet(prefix="phase"),
    }
    return render(request, self.template_name, context)
post(request) ¤

Validate and save the Project, then its Funding and Phase rows.

The project is saved first, so that the funding and phase rows (which reference it via a foreign key) can be validated and saved against a real primary key. Everything happens inside one atomic transaction: if the funding or phase formsets turn out to be invalid, the project creation is rolled back too, and the whole page is redisplayed with every error shown together.

Source code in main/views.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def post(self, request: HttpRequest) -> HttpResponse:
    """Validate and save the Project, then its Funding and Phase rows.

    The project is saved first, so that the funding and phase rows (which
    reference it via a foreign key) can be validated and saved against a
    real primary key. Everything happens inside one atomic transaction:
    if the funding or phase formsets turn out to be invalid, the project
    creation is rolled back too, and the whole page is redisplayed with
    every error shown together.
    """
    project_form = forms.ProjectForm(request.POST)

    if not project_form.is_valid():
        context = {
            "project_form": project_form,
            "funding_formset": forms.FundingInlineFormSet(
                request.POST, prefix="funding"
            ),
            "phase_formset": forms.ProjectPhaseInlineFormSet(
                request.POST, prefix="phase"
            ),
        }
        return render(request, self.template_name, context)

    with transaction.atomic():
        project = project_form.save()
        funding_formset = forms.FundingInlineFormSet(
            request.POST, instance=project, prefix="funding"
        )
        phase_formset = forms.ProjectPhaseInlineFormSet(
            request.POST, instance=project, prefix="phase"
        )
        # Evaluate both explicitly (rather than `and`-chaining) so that,
        # if one of them is invalid, the other's errors are still
        # available to show alongside it.
        funding_valid = funding_formset.is_valid()
        phase_valid = phase_formset.is_valid()

        if funding_valid and phase_valid:
            funding_formset.save()
            phase_formset.save()
            return redirect("main:project_detail", pk=project.pk)

        transaction.set_rollback(True)

    return render(
        request,
        self.template_name,
        {
            "project_form": project_form,
            "funding_formset": funding_formset,
            "phase_formset": phase_formset,
        },
    )

ProjectDetailInlineView ¤

Bases: PermissionRequiredMixin, View

Display a Project together with its Funding and Phases in one page.

This is the read-only counterpart of ProjectCreateInlineView and ProjectUpdateInlineView: it shows the same Project/Funding/Phases layout, but with every field disabled and without any of the add-row/save affordances, since nothing here can be edited.

Methods:¤
get(request, pk) ¤

Render the Project, Funding and Phase details, all read-only.

Unlike the create/update views, no formsets are used here: each existing Funding/Phase row is rendered as its own disabled ModelForm, since there is no need for the extra blank rows, management form, or delete checkboxes that a formset would add.

Source code in main/views.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
def get(self, request: HttpRequest, pk: int) -> HttpResponse:
    """Render the Project, Funding and Phase details, all read-only.

    Unlike the create/update views, no formsets are used here: each
    existing Funding/Phase row is rendered as its own disabled
    `ModelForm`, since there is no need for the extra blank rows,
    management form, or delete checkboxes that a formset would add.
    """
    project = get_object_or_404(models.Project, pk=pk)

    context = {
        "project": project,
        "total_funding_badge": style_plain_badge(
            project.total_funding_left, format_currency, size_class="fs-4"
        ),
        "total_days_badge": style_plain_badge(
            project.total_effort, lambda value: f"{value:.2f}", size_class="fs-4"
        ),
        "weeks_to_deadline_badge": style_fraction_badge(
            project.weeks_to_deadline, size_class="fs-4"
        ),
        "days_left_badge": style_fraction_badge(
            project.days_left, size_class="fs-4"
        ),
        "project_form": self._disable(forms.ProjectForm(instance=project)),
        "funding_header_form": forms.FundingInlineForm(),
        "funding_forms": [
            self._disable(forms.FundingInlineForm(instance=funding))
            for funding in project.funding_source.all()
        ],
        "phase_header_form": forms.ProjectPhaseInlineForm(),
        "phase_forms": [
            self._disable(forms.ProjectPhaseInlineForm(instance=phase))
            for phase in project.phases.all()
        ],
    }
    return render(request, self.template_name, context)

ProjectUpdateInlineView ¤

Bases: PermissionRequiredMixin, View

Update a Project together with its Funding and Phases in one page.

Methods:¤
get(request, pk) ¤

Render the Project form with its Funding/Phase formsets pre-filled.

Source code in main/views.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def get(self, request: HttpRequest, pk: int) -> HttpResponse:
    """Render the Project form with its Funding/Phase formsets pre-filled."""
    project = get_object_or_404(models.Project, pk=pk)
    context = {
        "project": project,
        "project_form": forms.ProjectForm(instance=project),
        "funding_formset": forms.FundingInlineFormSet(
            instance=project, prefix="funding"
        ),
        "phase_formset": forms.ProjectPhaseInlineFormSet(
            instance=project, prefix="phase"
        ),
    }
    return render(request, self.template_name, context)
post(request, pk) ¤

Validate and save the Project together with its Funding/Phase rows.

Unlike the create view, the project already has a primary key, so all three forms/formsets can be validated together up front.

Source code in main/views.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def post(self, request: HttpRequest, pk: int) -> HttpResponse:
    """Validate and save the Project together with its Funding/Phase rows.

    Unlike the create view, the project already has a primary key, so all
    three forms/formsets can be validated together up front.
    """
    project = get_object_or_404(models.Project, pk=pk)
    project_form = forms.ProjectForm(request.POST, instance=project)
    funding_formset = forms.FundingInlineFormSet(
        request.POST, instance=project, prefix="funding"
    )
    phase_formset = forms.ProjectPhaseInlineFormSet(
        request.POST, instance=project, prefix="phase"
    )

    # Evaluate all three explicitly so every error can be shown together.
    project_valid = project_form.is_valid()
    funding_valid = funding_formset.is_valid()
    phase_valid = phase_formset.is_valid()

    if project_valid and funding_valid and phase_valid:
        with transaction.atomic():
            project = project_form.save()
            funding_formset.save()
            phase_formset.save()
        return redirect("main:project_detail", pk=project.pk)

    return render(
        request,
        self.template_name,
        {
            "project": project,
            "project_form": project_form,
            "funding_formset": funding_formset,
            "phase_formset": phase_formset,
        },
    )

ProjectsListView ¤

Bases: LoginRequiredMixin, PermissionRequiredMixin, FilterView

View to display the list of projects split in five pre-filtered tables.

Methods:¤
get_context_data(**kwargs) ¤

Add multiple pre-filtered tables to the context.

Source code in main/views.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:  # type: ignore
    """Add multiple pre-filtered tables to the context."""
    context = super().get_context_data(**kwargs)

    base_qs = self.get_queryset()

    buckets = [
        ("Active", {"status": "Active"}, "active-"),
        ("Maintenance", {"status": "Maintenance"}, "maintenance-"),
        ("Confirmed", {"status": "Confirmed"}, "confirmed-"),
        ("Tentative", {"status": "Tentative"}, "tentative-"),
        ("Finished", {"status": "Finished"}, "finished-"),
        ("Not done", {"status": "Not done"}, "not-done-"),
    ]

    created_tables: list[tuple[str, tables.ProjectTable]] = []
    for title, filt, prefix in buckets:
        qs = base_qs.filter(**filt)
        tbl = tables.ProjectTable(qs, prefix=prefix)
        RequestConfig(self.request).configure(tbl)
        created_tables.append((title, tbl))

    context["tables"] = created_tables
    return context

RegistrationView ¤

Bases: CreateView

View to register new users.

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

Functions:¤

index(request) ¤

View for the index page, using the home template.

Source code in main/views.py
25
26
27
def index(request: HttpRequest) -> HttpResponse:
    """View for the index page, using the home template."""
    return render(request, "main/home.html")