2023-04-08 10:28:44 -04:00
|
|
|
import datetime
|
2024-08-07 14:09:42 -04:00
|
|
|
from typing import TYPE_CHECKING
|
2023-01-24 21:21:26 -05:00
|
|
|
|
2023-04-08 10:28:44 -04:00
|
|
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
|
|
from django.core.exceptions import BadRequest
|
2024-05-13 00:38:01 -04:00
|
|
|
from django.db.models import Count, F, FloatField, Q, Window
|
2024-01-17 21:17:24 -05:00
|
|
|
from django.db.models.functions import Lead, Trunc
|
|
|
|
from django.urls import path, reverse_lazy
|
2023-04-08 10:28:44 -04:00
|
|
|
from django.utils.text import slugify
|
|
|
|
from django.views.generic.list import ListView
|
|
|
|
|
2024-01-26 13:58:14 -05:00
|
|
|
import django_filters
|
2024-01-22 22:58:07 -05:00
|
|
|
import django_tables2 as tables
|
2024-01-26 13:58:14 -05:00
|
|
|
from django_filters.views import BaseFilterView
|
2024-05-04 16:38:51 -04:00
|
|
|
from django_mysql.models.aggregates import GroupConcat
|
2024-02-09 11:59:52 -05:00
|
|
|
from django_mysql.models.functions import ConcatWS
|
2024-01-22 22:58:07 -05:00
|
|
|
from django_tables2 import SingleTableMixin
|
|
|
|
from django_tables2.export.views import ExportMixin
|
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
from .models import Door, HIDEvent
|
2024-04-18 11:30:18 -04:00
|
|
|
from .tables import (
|
|
|
|
BusiestDayOfWeekTable,
|
|
|
|
BusiestTimeOfDayTable,
|
|
|
|
DeniedAccessTable,
|
|
|
|
DetailByDayTable,
|
|
|
|
MostActiveMembersTable,
|
|
|
|
UnitTimeTable,
|
|
|
|
)
|
2023-04-08 10:28:44 -04:00
|
|
|
|
2024-08-07 14:09:42 -04:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from django.core.paginator import Page
|
|
|
|
|
2023-11-30 12:06:16 -05:00
|
|
|
REPORTS = []
|
2023-04-08 10:28:44 -04:00
|
|
|
|
|
|
|
|
2024-05-04 16:38:51 -04:00
|
|
|
def register_report(cls: "type[BaseAccessReport]"):
|
2023-11-30 12:06:16 -05:00
|
|
|
REPORTS.append(cls)
|
2023-04-08 10:28:44 -04:00
|
|
|
return cls
|
|
|
|
|
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
class AccessReportFilterSet(django_filters.FilterSet):
|
2024-01-26 13:58:14 -05:00
|
|
|
timestamp = django_filters.DateFromToRangeFilter()
|
2024-02-08 10:47:32 -05:00
|
|
|
door = django_filters.ModelMultipleChoiceFilter(
|
|
|
|
queryset=Door.objects.all(), distinct=False
|
|
|
|
)
|
2024-01-26 13:58:14 -05:00
|
|
|
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
class BaseAccessReport(
|
2024-01-26 13:58:14 -05:00
|
|
|
BaseFilterView, ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView
|
2024-01-22 22:58:07 -05:00
|
|
|
):
|
2023-04-08 10:28:44 -04:00
|
|
|
model = HIDEvent
|
|
|
|
permission_required = "doorcontrol.view_hidevent"
|
|
|
|
paginate_by = 20
|
|
|
|
context_object_name = "object_list"
|
|
|
|
template_name = "doorcontrol/access_report.dj.html"
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
export_formats = ("csv", "xlsx", "ods")
|
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
filterset_class = AccessReportFilterSet
|
2024-01-26 13:58:14 -05:00
|
|
|
|
2024-05-04 16:38:51 -04:00
|
|
|
_report_name: str
|
2023-04-08 10:28:44 -04:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _report_types(cls):
|
|
|
|
yield [
|
|
|
|
cls._report_name,
|
|
|
|
reverse_lazy("doorcontrol:" + slugify(cls._report_name)),
|
|
|
|
]
|
|
|
|
|
2023-11-30 12:06:16 -05:00
|
|
|
@classmethod
|
|
|
|
def _urlpattern(cls):
|
|
|
|
slug = slugify(cls._report_name)
|
|
|
|
return path(f"reports/{slug}", cls.as_view(), name=slug)
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
@property
|
|
|
|
def export_name(self):
|
|
|
|
return slugify(self._report_name)
|
|
|
|
|
2023-04-08 10:28:44 -04:00
|
|
|
def _selected_report(self):
|
|
|
|
return self._report_name
|
|
|
|
|
2024-05-04 16:38:51 -04:00
|
|
|
def get_paginate_by(self, queryset) -> int | None:
|
2023-04-08 10:28:44 -04:00
|
|
|
if "items_per_page" in self.request.GET:
|
2024-05-04 16:38:51 -04:00
|
|
|
return int(self.request.GET["items_per_page"])
|
2023-04-08 10:28:44 -04:00
|
|
|
return super().get_paginate_by(queryset)
|
|
|
|
|
|
|
|
def get_queryset(self):
|
2024-01-26 13:58:14 -05:00
|
|
|
return super().get_queryset().select_related("door")
|
2023-04-08 10:28:44 -04:00
|
|
|
|
|
|
|
def get_context_data(self, **kwargs):
|
|
|
|
context = super().get_context_data(**kwargs)
|
2023-11-30 12:06:16 -05:00
|
|
|
context["report_types"] = [
|
|
|
|
rt for report in REPORTS for rt in report._report_types()
|
|
|
|
]
|
2023-04-08 10:28:44 -04:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
@register_report
|
|
|
|
class AccessPerUnitTime(BaseAccessReport):
|
2024-01-22 22:58:07 -05:00
|
|
|
table_class = UnitTimeTable
|
2023-04-08 10:28:44 -04:00
|
|
|
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]),
|
|
|
|
)
|
|
|
|
|
2023-11-30 12:06:16 -05:00
|
|
|
@classmethod
|
|
|
|
def _urlpattern(cls):
|
|
|
|
return path(
|
|
|
|
"reports/access-per-<unit_time>",
|
|
|
|
cls.as_view(),
|
|
|
|
name="access-per-unit-time",
|
|
|
|
)
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
@property
|
|
|
|
def _report_name(self):
|
|
|
|
unit_time = self.kwargs["unit_time"]
|
|
|
|
return "Access per " + unit_time.title()
|
|
|
|
|
2023-04-08 10:28:44 -04:00
|
|
|
def _selected_report(self) -> str:
|
|
|
|
return "Access per " + self.kwargs["unit_time"].title()
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
def get_table_kwargs(self):
|
2023-04-08 10:28:44 -04:00
|
|
|
unit_time = self.kwargs["unit_time"]
|
2024-01-22 22:58:07 -05:00
|
|
|
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()
|
2023-04-08 10:28:44 -04:00
|
|
|
)
|
2024-01-22 22:58:07 -05:00
|
|
|
|
|
|
|
return {
|
|
|
|
"sequence": ("unit_time", "..."),
|
|
|
|
"extra_columns": (("unit_time", unit_time_column),),
|
|
|
|
}
|
2023-04-08 10:28:44 -04:00
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
def get_table_data(self):
|
2023-04-08 10:28:44 -04:00
|
|
|
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")
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
return (
|
2023-04-08 10:28:44 -04:00
|
|
|
super()
|
2024-02-08 10:47:32 -05:00
|
|
|
.get_table_data()
|
2024-05-13 00:38:01 -04:00
|
|
|
.filter(event_type__in=HIDEvent.EventType.any_granted_access())
|
2024-02-09 11:59:52 -05:00
|
|
|
.with_member_id()
|
2023-04-08 10:28:44 -04:00
|
|
|
.values(unit_time=Trunc("timestamp", unit_time))
|
|
|
|
.annotate(
|
2024-02-09 11:59:52 -05:00
|
|
|
members=Count("member_id", distinct=True),
|
2023-12-01 11:53:16 -05:00
|
|
|
members_delta=(
|
|
|
|
F("members")
|
|
|
|
/ Window(
|
|
|
|
Lead("members"),
|
|
|
|
order_by="-unit_time",
|
|
|
|
output_field=FloatField(),
|
|
|
|
)
|
|
|
|
* 100
|
|
|
|
- 100
|
|
|
|
),
|
2023-04-08 10:28:44 -04:00
|
|
|
access_count=Count("cardholder_id"),
|
2023-12-01 11:53:16 -05:00
|
|
|
access_count_delta=(
|
|
|
|
F("access_count")
|
|
|
|
/ Window(
|
|
|
|
Lead("access_count"),
|
|
|
|
order_by="-unit_time",
|
|
|
|
output_field=FloatField(),
|
|
|
|
)
|
|
|
|
* 100
|
|
|
|
- 100
|
|
|
|
),
|
2023-04-08 10:28:44 -04:00
|
|
|
)
|
|
|
|
.order_by("-unit_time")
|
|
|
|
)
|
2024-01-22 22:58:07 -05:00
|
|
|
|
|
|
|
|
2023-04-08 10:28:44 -04:00
|
|
|
@register_report
|
|
|
|
class DeniedAccess(BaseAccessReport):
|
|
|
|
_report_name = "Denied Access"
|
2024-01-22 22:58:07 -05:00
|
|
|
table_class = DeniedAccessTable
|
2023-04-08 10:28:44 -04:00
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
def get_table_data(self):
|
2023-04-08 10:28:44 -04:00
|
|
|
denied_event_types = [
|
|
|
|
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
|
|
|
|
]
|
2024-01-22 22:58:07 -05:00
|
|
|
return (
|
2023-04-08 10:28:44 -04:00
|
|
|
super()
|
2024-02-08 10:47:32 -05:00
|
|
|
.get_table_data()
|
2023-04-08 10:28:44 -04:00
|
|
|
.filter(event_type__in=denied_event_types)
|
|
|
|
.with_decoded_card_number()
|
|
|
|
)
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
|
2023-04-08 10:28:44 -04:00
|
|
|
@register_report
|
|
|
|
class MostActiveMembers(BaseAccessReport):
|
|
|
|
_report_name = "Most Active Members"
|
2024-01-22 22:58:07 -05:00
|
|
|
table_class = MostActiveMembersTable
|
2023-04-08 10:28:44 -04:00
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
def get_table_data(self):
|
2024-01-22 22:58:07 -05:00
|
|
|
return (
|
2023-04-08 10:28:44 -04:00
|
|
|
super()
|
2024-02-08 10:47:32 -05:00
|
|
|
.get_table_data()
|
2024-02-09 11:59:52 -05:00
|
|
|
.with_member_id()
|
|
|
|
.filter(member_id__isnull=False)
|
|
|
|
.values("member_id")
|
|
|
|
.annotate(
|
|
|
|
access_count=Count("member_id"),
|
|
|
|
name=GroupConcat(
|
|
|
|
ConcatWS("forename", "surname", separator=" "), distinct=True
|
|
|
|
),
|
|
|
|
)
|
2023-04-08 10:28:44 -04:00
|
|
|
.order_by("-access_count")
|
|
|
|
)
|
|
|
|
|
2024-01-22 22:58:07 -05:00
|
|
|
|
2024-02-09 12:16:30 -05:00
|
|
|
@register_report
|
|
|
|
class DetailByDay(BaseAccessReport):
|
|
|
|
_report_name = "Detail by Day"
|
|
|
|
table_class = DetailByDayTable
|
|
|
|
|
|
|
|
def get_table_data(self):
|
|
|
|
return (
|
|
|
|
super()
|
|
|
|
.get_table_data()
|
|
|
|
.with_member_id()
|
|
|
|
.values("timestamp__date", "member_id")
|
|
|
|
.filter(member_id__isnull=False)
|
|
|
|
.annotate(
|
|
|
|
access_count=Count("member_id"),
|
2024-05-13 00:38:01 -04:00
|
|
|
granted_access_count=Count(
|
|
|
|
"member_id",
|
|
|
|
filter=Q(event_type__in=HIDEvent.EventType.any_granted_access()),
|
|
|
|
),
|
2024-02-09 12:16:30 -05:00
|
|
|
name=GroupConcat(
|
|
|
|
ConcatWS("forename", "surname", separator=" "), distinct=True
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.order_by("-timestamp__date")
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-11-30 12:06:16 -05:00
|
|
|
@register_report
|
|
|
|
class BusiestDayOfWeek(BaseAccessReport):
|
|
|
|
_report_name = "Busiest Day of the Week"
|
2024-01-22 22:58:07 -05:00
|
|
|
table_pagination = False
|
|
|
|
table_class = BusiestDayOfWeekTable
|
2023-11-30 12:06:16 -05:00
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
def get_table_data(self):
|
2024-01-22 22:58:07 -05:00
|
|
|
return (
|
|
|
|
super()
|
2024-02-08 10:47:32 -05:00
|
|
|
.get_table_data()
|
2024-02-09 12:20:01 -05:00
|
|
|
.with_member_id()
|
2023-11-30 12:06:16 -05:00
|
|
|
.values("timestamp__week_day")
|
2024-02-09 12:20:01 -05:00
|
|
|
.annotate(
|
|
|
|
events=Count("timestamp"), members=Count("member_id", distinct=True)
|
|
|
|
)
|
2024-01-22 22:58:07 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-11-30 12:06:16 -05:00
|
|
|
@register_report
|
|
|
|
class BusiestTimeOfDay(BaseAccessReport):
|
|
|
|
_report_name = "Busiest Time of Day"
|
2024-01-22 22:58:07 -05:00
|
|
|
table_pagination = False
|
|
|
|
table_class = BusiestTimeOfDayTable
|
2023-11-30 12:06:16 -05:00
|
|
|
|
2024-02-08 10:47:32 -05:00
|
|
|
def get_table_data(self):
|
2024-01-22 22:58:07 -05:00
|
|
|
return (
|
|
|
|
super()
|
2024-02-08 10:47:32 -05:00
|
|
|
.get_table_data()
|
2024-02-09 12:20:01 -05:00
|
|
|
.with_member_id()
|
2023-11-30 12:06:16 -05:00
|
|
|
.values("timestamp__hour")
|
2024-02-09 12:20:01 -05:00
|
|
|
.annotate(
|
|
|
|
events=Count("timestamp"), members=Count("member_id", distinct=True)
|
|
|
|
)
|
2024-01-22 22:58:07 -05:00
|
|
|
)
|