+
-
-
+
-
-
Reset
+
+ {% include "membershipworks/components/download_table.dj.html" %}
-
-
-
-
- {% for column in object_list.0.keys %}{{ column|title }} | {% endfor %}
-
-
-
- {% for object in object_list %}
-
- {% for field in object.values %}{{ field }} | {% endfor %}
-
- {% endfor %}
-
-
-
-
+ {% 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"))
- ]
+ )