cmsmanage/doorcontrol/views.py

306 lines
8.8 KiB
Python

import calendar
import datetime
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import BadRequest
from django.core.paginator import Page
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.text import slugify
from django.views.generic.list import ListView
import django_filters
import django_tables2 as tables
from django_filters.views import BaseFilterView
from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin
from .models import HIDEvent
REPORTS = []
def register_report(cls: "BaseAccessReport"):
REPORTS.append(cls)
return cls
class DateTimeFilters(django_filters.FilterSet):
timestamp = django_filters.DateFromToRangeFilter()
class BaseAccessReport(
BaseFilterView, 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")
filterset_class = DateTimeFilters
_report_name = None
@classmethod
def _report_types(cls):
yield [
cls._report_name,
reverse_lazy("doorcontrol:" + slugify(cls._report_name)),
]
@classmethod
def _urlpattern(cls):
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
def get_paginate_by(self, queryset) -> int:
if "items_per_page" in self.request.GET:
return int(self.request.GET.get("items_per_page"))
return super().get_paginate_by(queryset)
def get_queryset(self):
return super().get_queryset().select_related("door")
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["report_types"] = [
rt for report in REPORTS for rt in report._report_types()
]
page: Page = context["page_obj"]
context["paginator_range"] = page.paginator.get_elided_page_range(page.number)
context["selected_report"] = self._selected_report()
context["items_per_page"] = self.get_paginate_by(None)
query_params = self.request.GET.copy()
if "page" in query_params:
query_params.pop("page")
context["query_params"] = query_params.urlencode()
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
def _report_types(cls):
for unit_time in cls.UNIT_TIMES:
yield (
"Access per " + unit_time.title(),
reverse_lazy("doorcontrol:access-per-unit-time", args=[unit_time]),
)
@classmethod
def _urlpattern(cls):
return path(
"reports/access-per-<unit_time>",
cls.as_view(),
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 get_table_kwargs(self):
unit_time = self.kwargs["unit_time"]
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)},
)
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"]
if unit_time not in self.UNIT_TIMES:
raise BadRequest("unit time must be one of day, week, month, or year")
granted_event_types = [
t for t in HIDEvent.EventType if t.name.startswith("GRANTED_ACCESS")
]
return (
super()
.get_queryset()
.filter(event_type__in=granted_event_types)
.values(unit_time=Trunc("timestamp", unit_time))
.annotate(
members=Count("cardholder_id", distinct=True),
members_delta=(
F("members")
/ Window(
Lead("members"),
order_by="-unit_time",
output_field=FloatField(),
)
* 100
- 100
),
access_count=Count("cardholder_id"),
access_count_delta=(
F("access_count")
/ Window(
Lead("access_count"),
order_by="-unit_time",
output_field=FloatField(),
)
* 100
- 100
),
)
.order_by("-unit_time")
)
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")
]
return (
super()
.get_queryset()
.filter(event_type__in=denied_event_types)
.with_decoded_card_number()
)
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):
return (
super()
.get_queryset()
.values("cardholder_id", "forename", "surname")
.order_by()
.annotate(access_count=Count("cardholder_id"))
.order_by("-access_count")
)
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 (
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"
table_pagination = False
table_class = BusiestTimeOfDayTable
def get_queryset(self):
return (
super()
.get_queryset()
.values("timestamp__hour")
.annotate(events=Count("timestamp"))
)