diff --git a/dashboard/models.py b/dashboard/models.py index 1aa11ffa9..11fc65430 100644 --- a/dashboard/models.py +++ b/dashboard/models.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import connections, models +from django.db.models.functions import JSONObject from django.utils.translation import gettext_lazy as _ from django_hosts.resolvers import reverse @@ -33,6 +34,35 @@ def __str__(self): return self.name +class MetricQuerySet(models.QuerySet): + def with_latest(self): + """ + Annotate the queryset with a `latest` JSON object containing two keys: + * `measurement` (int): the value of the most recent datum for that metric + * `timestamp` (str): the timestamp of the most recent datum + """ + data = Datum.objects.filter( + content_type=self.model.content_type(), + object_id=models.OuterRef("pk"), + ) + jsonobj = JSONObject( + measurement=models.F("measurement"), + timestamp=models.F("timestamp"), + ) + latest = models.Subquery(data.values_list(jsonobj).order_by("-timestamp")[:1]) + + return self.annotate(latest=latest) + + def for_dashboard(self): + """ + Return a queryset optimized for being displayed on the dashboard index + page. + """ + return ( + self.filter(show_on_dashboard=True).select_related("category").with_latest() + ) + + class Metric(models.Model): name = models.CharField(max_length=300) slug = models.SlugField() @@ -49,6 +79,8 @@ class Metric(models.Model): unit = models.CharField(max_length=100) unit_plural = models.CharField(max_length=100) + objects = MetricQuerySet.as_manager() + class Meta: abstract = True @@ -116,13 +148,13 @@ def _gather_data_periodic(self, since, period): AND object_id = %s AND timestamp >= %s GROUP BY 1;""", - [period, OFFSET, self.content_type.id, self.id, since], + [period, OFFSET, self.content_type().id, self.id, since], ) return [(calendar.timegm(t.timetuple()), float(m)) for (t, m) in c.fetchall()] - @property - def content_type(self): - return ContentType.objects.get_for_model(self) + @classmethod + def content_type(cls): + return ContentType.objects.get_for_model(cls) class TracTicketMetric(Metric): diff --git a/dashboard/templates/dashboard/index.html b/dashboard/templates/dashboard/index.html index e20ccb1d3..7aed3f1c1 100644 --- a/dashboard/templates/dashboard/index.html +++ b/dashboard/templates/dashboard/index.html @@ -3,23 +3,25 @@ {% block content %}
- {% for report in data %} - {% ifchanged report.metric.category %} - {% if report.metric.category %}

{{ report.metric.category }}

{% endif %} + {% for metric in data %} + {% ifchanged metric.category %} + {% if metric.category %}

{{ metric.category }}

{% endif %} {% endifchanged %} -
-

{{ report.metric.name }}

+ {% endfor %} -

- {% blocktranslate with timestamp=data.0.latest.timestamp|timesince %}Updated {{ timestamp }} ago.{% endblocktranslate %} -

+ {% if last_updated %} +

+ {% blocktranslate with timestamp=last_updated|timesince %}Updated {{ timestamp }} ago.{% endblocktranslate %} +

+ {% endif %}
{% endblock %} diff --git a/dashboard/tests.py b/dashboard/tests.py index c01ddb24c..102312766 100644 --- a/dashboard/tests.py +++ b/dashboard/tests.py @@ -16,6 +16,7 @@ from .models import ( METRIC_PERIOD_DAILY, METRIC_PERIOD_WEEKLY, + Category, GithubItemCountMetric, GitHubSearchCountMetric, Metric, @@ -33,12 +34,10 @@ def setUp(self): def test_index(self): for MC in Metric.__subclasses__(): for metric in MC.objects.filter(show_on_dashboard=True): - metric.data.create(measurement=44) metric.data.create(measurement=42) request = self.factory.get(reverse("dashboard-index", host="dashboard")) - with self.assertNumQueries(7): - response = index(request) + response = index(request) self.assertContains(response, "Development dashboard") self.assertEqual(response.content.count(b'