From 8317ae83c270b42f555e5a5e7b321dc83fb7b04a Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Mon, 22 Jan 2024 22:58:07 -0500 Subject: [PATCH] doorcontrol: Use django-tables2 for access reports --- .../doorcontrol/access_report.dj.html | 60 ++---- doorcontrol/views.py | 182 +++++++++++------- 2 files changed, 123 insertions(+), 119 deletions(-) diff --git a/doorcontrol/templates/doorcontrol/access_report.dj.html b/doorcontrol/templates/doorcontrol/access_report.dj.html index c0e6959..e5567ed 100644 --- a/doorcontrol/templates/doorcontrol/access_report.dj.html +++ b/doorcontrol/templates/doorcontrol/access_report.dj.html @@ -1,5 +1,7 @@ {% extends "base.dj.html" %} +{% load render_table from django_tables2 %} + {% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %} {% block content %}
@@ -11,9 +13,9 @@ {% endfor %} -
-
-
+ +
+
Start Date
-
+
End Date
-
+
Items Per Page
-
- +
+ + Reset
-
- Reset +
+ {% include "membershipworks/components/download_table.dj.html" %}
-
- - - - {% for column in object_list.0.keys %}{% endfor %} - - - - {% for object in object_list %} - - {% for field in object.values %}{% endfor %} - - {% endfor %} - -
{{ column|title }}
{{ field }}
-
- + {% render_table table %}
{% endblock %} diff --git a/doorcontrol/views.py b/doorcontrol/views.py index 918d055..ad46135 100644 --- a/doorcontrol/views.py +++ b/doorcontrol/views.py @@ -8,11 +8,14 @@ from django.db.models import Count, F, FloatField, Window from django.db.models.functions import Lead, Trunc from django.urls import path, reverse_lazy from django.utils import dateparse -from django.utils.formats import date_format from django.utils.text import slugify from django.utils.timezone import localtime from django.views.generic.list import ListView +import django_tables2 as tables +from django_tables2 import SingleTableMixin +from django_tables2.export.views import ExportMixin + from .models import HIDEvent REPORTS = [] @@ -23,13 +26,17 @@ def register_report(cls: "BaseAccessReport"): return cls -class BaseAccessReport(PermissionRequiredMixin, ListView): +class BaseAccessReport( + ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView +): model = HIDEvent permission_required = "doorcontrol.view_hidevent" paginate_by = 20 context_object_name = "object_list" template_name = "doorcontrol/access_report.dj.html" + export_formats = ("csv", "xlsx", "ods") + _report_name = None @classmethod @@ -44,6 +51,10 @@ class BaseAccessReport(PermissionRequiredMixin, ListView): slug = slugify(cls._report_name) return path(f"reports/{slug}", cls.as_view(), name=slug) + @property + def export_name(self): + return slugify(self._report_name) + def _selected_report(self): return self._report_name @@ -95,8 +106,23 @@ class BaseAccessReport(PermissionRequiredMixin, ListView): return context +class UnitTimeTable(tables.Table): + members = tables.columns.Column() + members_delta = tables.columns.TemplateColumn( + "{{ value|floatformat:2}}%", verbose_name="Δ Members" + ) + access_count = tables.columns.Column() + access_count_delta = tables.columns.TemplateColumn( + "{{ value|floatformat:2}}%", verbose_name="Δ Access Count" + ) + + class Meta: + fields = ("members", "members_delta", "access_count", "access_count_delta") + + @register_report class AccessPerUnitTime(BaseAccessReport): + table_class = UnitTimeTable UNIT_TIMES = ["day", "week", "month", "year"] @classmethod @@ -115,23 +141,41 @@ class AccessPerUnitTime(BaseAccessReport): name="access-per-unit-time", ) + @property + def _report_name(self): + unit_time = self.kwargs["unit_time"] + return "Access per " + unit_time.title() + def _selected_report(self) -> str: return "Access per " + self.kwargs["unit_time"].title() - def _format_date(self, date: datetime.datetime) -> str: + def get_table_kwargs(self): unit_time = self.kwargs["unit_time"] - if unit_time == "day": - return date_format(date, "DATE_FORMAT") - elif unit_time == "week": - return ( - date_format(date, "DATE_FORMAT") - + " - " - + date_format(date + datetime.timedelta(weeks=1), "DATE_FORMAT") + if unit_time == "week": + unit_time_column = tables.TemplateColumn( + verbose_name=unit_time.title(), + template_code=( + "{{ value|date|default:default }} - " + "{{ value|add:one_week|date|default:default }}" + ), + extra_context={"one_week": datetime.timedelta(weeks=1)}, ) - elif unit_time == "month": - return date_format(date, "N Y") - elif unit_time == "year": - return date_format(date, "Y") + else: + if unit_time == "day": + date_format = "DATE_FORMAT" + elif unit_time == "month": + date_format = "N Y" + elif unit_time == "year": + date_format = "Y" + + unit_time_column = tables.DateColumn( + date_format, verbose_name=unit_time.title() + ) + + return { + "sequence": ("unit_time", "..."), + "extra_columns": (("unit_time", unit_time_column),), + } def get_queryset(self): unit_time = self.kwargs["unit_time"] @@ -141,7 +185,7 @@ class AccessPerUnitTime(BaseAccessReport): granted_event_types = [ t for t in HIDEvent.EventType if t.name.startswith("GRANTED_ACCESS") ] - events = ( + return ( super() .get_queryset() .filter(event_type__in=granted_event_types) @@ -172,64 +216,58 @@ class AccessPerUnitTime(BaseAccessReport): ) .order_by("-unit_time") ) - return [ - { - unit_time: self._format_date(event["unit_time"]), - "members": event["members"], - "Δ members": ( - f'{event["members_delta"]:.2f}%' - if event["members_delta"] is not None - else "" - ), - "access count": event["access_count"], - "Δ access count": ( - f'{event["access_count_delta"]:.2f}%' - if event["access_count_delta"] is not None - else "" - ), - } - for event in events - ] + + +class DeniedAccessTable(tables.Table): + name = tables.TemplateColumn( + "{{ record.forename|default:'' }} {{ record.surname|default:'' }}" + ) + + class Meta: + model = HIDEvent + + fields = ( + "timestamp", + "door", + "event_type", + "name", + "raw_card_number", + "decoded_card_number", + ) @register_report class DeniedAccess(BaseAccessReport): _report_name = "Denied Access" + table_class = DeniedAccessTable def get_queryset(self): denied_event_types = [ t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS") ] - events = ( + return ( super() .get_queryset() .filter(event_type__in=denied_event_types) .with_decoded_card_number() ) - return [ - { - "timestamp": event.timestamp, - "door name": event.door.name, - "event type": HIDEvent.EventType(event.event_type).label, - "name": " ".join( - n for n in [event.forename, event.surname] if n is not None - ), - "raw card number": ( - event.raw_card_number if event.raw_card_number is not None else "" - ), - "card number": event.decoded_card_number() or "", - } - for event in events - ] + +class MostActiveMembersTable(tables.Table): + cardholder_id = tables.Column() + name = tables.TemplateColumn( + "{{ record.forename|default:'' }} {{ record.surname|default:'' }}" + ) + access_count = tables.Column() @register_report class MostActiveMembers(BaseAccessReport): _report_name = "Most Active Members" + table_class = MostActiveMembersTable def get_queryset(self): - counts = ( + return ( super() .get_queryset() .values("cardholder_id", "forename", "surname") @@ -238,45 +276,45 @@ class MostActiveMembers(BaseAccessReport): .order_by("-access_count") ) - return [ - { - "cardholder id": count["cardholder_id"], - "name": " ".join( - n for n in [count["forename"], count["surname"]] if n is not None - ), - "access count": count["access_count"], - } - for count in counts - ] + +class BusiestDayOfWeekTable(tables.Table): + timestamp__week_day = tables.Column("Week Day") + events = tables.Column() + + def render_timestamp__week_day(self, value): + return calendar.day_name[(value - 2) % 7] @register_report class BusiestDayOfWeek(BaseAccessReport): _report_name = "Busiest Day of the Week" + table_pagination = False + table_class = BusiestDayOfWeekTable def get_queryset(self): - return [ - { - "week day": calendar.day_name[(count["timestamp__week_day"] - 2) % 7], - "events": count["events"], - } - for count in super() + return ( + super() .get_queryset() .values("timestamp__week_day") .annotate(events=Count("timestamp")) - ] + ) + + +class BusiestTimeOfDayTable(tables.Table): + timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour") + events = tables.Column() @register_report class BusiestTimeOfDay(BaseAccessReport): _report_name = "Busiest Time of Day" - paginate_by = 24 + table_pagination = False + table_class = BusiestTimeOfDayTable def get_queryset(self): - return [ - {"hour": f'{count["timestamp__hour"]}:00', "events": count["events"]} - for count in super() + return ( + super() .get_queryset() .values("timestamp__hour") .annotate(events=Count("timestamp")) - ] + )