diff --git a/consumption/migrations/0004_alter_record_options.py b/consumption/migrations/0004_alter_record_options.py new file mode 100644 index 0000000..cd904e4 --- /dev/null +++ b/consumption/migrations/0004_alter_record_options.py @@ -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'}, + ), + ] diff --git a/consumption/models/record.py b/consumption/models/record.py index 132dc2c..c40fe3e 100644 --- a/consumption/models/record.py +++ b/consumption/models/record.py @@ -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( diff --git a/consumption/templates/consumption/resource_detail.html b/consumption/templates/consumption/resource_detail.html index b77d1d2..70e31cd 100644 --- a/consumption/templates/consumption/resource_detail.html +++ b/consumption/templates/consumption/resource_detail.html @@ -14,4 +14,29 @@

{{ resource_instance.name }}

This resource is tracked for {{ resource_instance.subject.name }}.

The unit of this resource is {{ resource_instance.unit }}

+ +
+ + + + {% if records %} + + + + + + + {% for record in records %} + + + + + + {% endfor %} +
Date/TimeValueActions
{{ record.timestamp }}{{ record.reading }} {{ resource_instance.unit }} + update | + delete +
+ {% endif %} +
{% endblock main %} diff --git a/consumption/urls.py b/consumption/urls.py index 386195d..72b9326 100644 --- a/consumption/urls.py +++ b/consumption/urls.py @@ -66,6 +66,11 @@ ), # Record-related URLs path("record/create/", RecordCreateView.as_view(), name="record-create"), + path( + "record/create//", + RecordCreateView.as_view(), + name="record-create", + ), path( "record//", RecordDetailView.as_view(), diff --git a/consumption/views/record.py b/consumption/views/record.py index f4c704c..85ec132 100644 --- a/consumption/views/record.py +++ b/consumption/views/record.py @@ -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 @@ -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 @@ -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. @@ -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 diff --git a/consumption/views/resource.py b/consumption/views/resource.py index 3d3e15b..b20a44a 100644 --- a/consumption/views/resource.py +++ b/consumption/views/resource.py @@ -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 @@ -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 `) + 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. @@ -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