Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions consumption/migrations/0004_alter_record_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.0.2 on 2022-02-25 08:29

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('consumption', '0003_record'),
]

operations = [
migrations.AlterModelOptions(
name='record',
options={'ordering': ['-timestamp'], 'verbose_name': 'Record', 'verbose_name_plural': 'Records'},
),
]
1 change: 1 addition & 0 deletions consumption/models/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Meta: # noqa: D106
app_label = "consumption"
verbose_name = _("Record")
verbose_name_plural = _("Records")
ordering = ["-timestamp"]

def __str__(self): # noqa: D105
return "{}: {} ({}, {})".format(
Expand Down
25 changes: 25 additions & 0 deletions consumption/templates/consumption/resource_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,29 @@ <h2>{{ resource_instance.name }}</h2>
<p>This resource is tracked for <a href="{% url "consumption:subject-detail" resource_instance.subject.id %}">{{ resource_instance.subject.name }}</a>.</p>
<p>The unit of this resource is <strong>{{ resource_instance.unit }}</strong></p>
</div>

<div class="resource-records">
<a href="{% url "consumption:record-create" resource_instance.id %}">
<button>add Record for this Resource</button>
</a>
{% if records %}
<table>
<tr>
<th>Date/Time</th>
<th>Value</th>
<th>Actions</th>
</tr>
{% for record in records %}
<tr>
<td>{{ record.timestamp }}</td>
<td>{{ record.reading }} {{ resource_instance.unit }}</td>
<td>
<a href="{% url "consumption:record-update" record.id %}">update</a> |
<a href="{% url "consumption:record-delete" record.id %}">delete</a>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
</div>
{% endblock main %}
5 changes: 5 additions & 0 deletions consumption/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@
),
# Record-related URLs
path("record/create/", RecordCreateView.as_view(), name="record-create"),
path(
"record/create/<int:resource_id>/",
RecordCreateView.as_view(),
name="record-create",
),
path(
"record/<int:record_id>/",
RecordDetailView.as_view(),
Expand Down
44 changes: 42 additions & 2 deletions consumption/views/record.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: MIT

"""Views related to the :class:`~consumption.models.subject.Subject` model."""
"""Views related to the :class:`~consumption.models.record.Record` model."""

# Django imports
from django.contrib.auth.mixins import LoginRequiredMixin
Expand All @@ -18,6 +18,15 @@ class RecordCreateView(LoginRequiredMixin, generic.CreateView):
(as of now), meaning: every (authenticated) user is able to create
:class:`~consumption.models.record.Record` objects.

The view supports two different operation modes:

- create a new
:class:`~consumption.models.record.Record` instance *from scratch*;
- create a new instance of
:class:`~consumption.models.record.Record` with a pre-defined
:class:`~consumption.models.resource.Resource` instance to associate
the new instance to.

After successfully creating a new instance of
:class:`~consumption.models.record.Record` the user will be redirected
to the URL as provided by
Expand All @@ -41,6 +50,37 @@ class RecordCreateView(LoginRequiredMixin, generic.CreateView):
template_name_suffix = "_create"
"""Uses the template ``templates/consumption/record_create.html``."""

def get_form_kwargs(self):
"""Provide *initial values* for the form.

This method implements the two operation modes.

If a ``resource_id`` is provided as an URL parameter, the referenced
:class:`~consumption.models.resource.Resource` instance is provided as
initial value while rendering the form (template).
"""
kwargs = super().get_form_kwargs()

try:
kwargs["initial"]["resource"] = self.kwargs["resource_id"]
except KeyError:
pass

return kwargs

def get_success_url(self): # pragma: nocover
"""Determine the URL for redirecting after successful deletion.

This has to be done dynamically with a method instead of statically
with the ``success_url`` attribute, because the user should be
redirected to the *parent*
:class:`~consumption.models.resource.Resource` instance.
"""
resource = self.object.resource
return reverse_lazy(
"consumption:resource-detail", kwargs={"resource_id": resource.id}
)


class RecordDetailView(generic.DetailView):
"""Provide the details of :class:`~consumption.models.record.Record` instances.
Expand Down Expand Up @@ -120,7 +160,7 @@ class RecordDeleteView(LoginRequiredMixin, generic.DeleteView):
pk_url_kwarg = "record_id"
"""The keyword argument as provided in :mod:`consumption.urls`."""

def get_success_url(self):
def get_success_url(self): # pragma: nocover
"""Determine the URL for redirecting after successful deletion.

This has to be done dynamically with a method instead of statically
Expand Down
51 changes: 49 additions & 2 deletions consumption/views/resource.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: MIT

"""Views related to the :class:`~consumption.models.subject.Subject` model."""
"""Views related to the :class:`~consumption.models.resource.Resource` model."""

# Django imports
from django.contrib.auth.mixins import LoginRequiredMixin
Expand Down Expand Up @@ -57,6 +57,53 @@ class ResourceDetailView(generic.DetailView):
pk_url_kwarg = "resource_id"
"""The keyword argument as provided in :mod:`consumption.urls`."""

def get_queryset(self):
"""Optimize database queries.

Override to the default implementation of ``get_queryset()`` to
select the referenced instance of
:class:`~consumption.models.subject.Subject` (referenced by
:attr:`Resource.subject <consumption.models.resource.Resource.subject>`)
and prefetch all associated instances of
:class:`~consumption.models.record.Record` (that is: all instances of
:class:`~consumption.models.record.Record` that reference this instance
of :class:`~consumption.models.resource.Resource` by their
:attr:`~consumption.models.record.Record.resource` attribute).

Warning
-------
This method does only modify / extend / prepare the actual database
queries, it does not provide the resulting objects in the rendering
context for the template (actually the
:class:`~consumption.models.subject.Subject` instance will be easily
accessible by using ``resource_instance.subject`` in the template).

See
:meth:`~consumption.views.resource.ResourceDetailView.get_context_data`
for that.
"""
return (
super()
.get_queryset()
.select_related("subject")
.prefetch_related("record_set")
)

def get_context_data(self, **kwargs):
"""Add a list of related ``Record`` instances to the context.

:meth:`~consumption.views.resource.ResourceDetailView.get_queryset`
will optimize the database access, but the list of
:class:`~consumption.models.record.Record` instances must still be
added to the rendering context.
"""
context = super().get_context_data(**kwargs)

if self.object:
context["records"] = self.object.record_set.all()

return context


class ResourceUpdateView(LoginRequiredMixin, generic.UpdateView):
"""Generic class-based view to update :class:`~consumption.models.resource.Resource` objects.
Expand Down Expand Up @@ -120,7 +167,7 @@ class ResourceDeleteView(LoginRequiredMixin, generic.DeleteView):
pk_url_kwarg = "resource_id"
"""The keyword argument as provided in :mod:`consumption.urls`."""

def get_success_url(self):
def get_success_url(self): # pragma: nocover
"""Determine the URL for redirecting after successful deletion.

This has to be done dynamically with a method instead of statically
Expand Down