doorcontrol: Add some door access reports
This commit is contained in:
parent
906f662419
commit
b462e6b18f
@ -34,6 +34,7 @@ urlpatterns = [
|
|||||||
path("rentals/", include("rentals.urls")),
|
path("rentals/", include("rentals.urls")),
|
||||||
path("membershipworks/", include("membershipworks.urls")),
|
path("membershipworks/", include("membershipworks.urls")),
|
||||||
path("paperwork/", include("paperwork.urls")),
|
path("paperwork/", include("paperwork.urls")),
|
||||||
|
path("doorcontrol/", include("doorcontrol.urls")),
|
||||||
path("api/v1/", include((router.urls, "api"), namespace="v1")),
|
path("api/v1/", include((router.urls, "api"), namespace="v1")),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path(
|
path(
|
||||||
|
94
doorcontrol/templates/doorcontrol/access_report.dj.html
Normal file
94
doorcontrol/templates/doorcontrol/access_report.dj.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{% extends "base.dj.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
{% for report_name, report_url in report_types %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link{% if report_name == selected_report %} active{% endif %}"
|
||||||
|
href="{{ report_url }}?{{ query_params }}">
|
||||||
|
{{ report_name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<form method="get" class="form-floating">
|
||||||
|
<div class="row align-items-center row-cols-md-auto g-2 mb-2 mt-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="date"
|
||||||
|
class="form-control"
|
||||||
|
id="startDate"
|
||||||
|
name="timestamp__gte"
|
||||||
|
value="{{ timestamp__gte|date:'Y-m-d' }}">
|
||||||
|
<label for="startDate">Start Date</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="date"
|
||||||
|
class="form-control"
|
||||||
|
id="endDate"
|
||||||
|
name="timestamp__lte"
|
||||||
|
value="{{ timestamp__lte|date:'Y-m-d' }}">
|
||||||
|
<label for="endDate">End Date</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-2">
|
||||||
|
<div class="form-floating">
|
||||||
|
<input type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="itemsPerPage"
|
||||||
|
name="items_per_page"
|
||||||
|
value="{{ items_per_page }}"
|
||||||
|
min="10"
|
||||||
|
max="200"
|
||||||
|
step="10"
|
||||||
|
required>
|
||||||
|
<label for="itemsPerPage">Items Per Page</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
<a href="?" class="btn btn-warning">Reset</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<table class="table table-bordered table-striped table-hover mb-2">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for column in object_list.0.keys %}<th>{{ column|title }}</th>{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for object in object_list %}
|
||||||
|
<tr>
|
||||||
|
{% for field in object.values %}<td>{{ field }}</td>{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<div class="text-center mb-2">
|
||||||
|
Showing {{ page_obj.object_list|length }} of {{ paginator.count }} results.
|
||||||
|
</div>
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{{ query_params }}&page={{ page_obj.previous_page_number }}">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for page_num in paginator_range %}
|
||||||
|
<li class="page-item {% if page_num == page_obj.number %} active {% elif page_num == paginator.ELLIPSIS %} disabled {% endif %}">
|
||||||
|
<a class="page-link" href="?{{ query_params }}&page={{ page_num }}">{{ page_num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?{{ query_params }}&page={{ page_obj.next_page_number }}">Next</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
23
doorcontrol/urls.py
Normal file
23
doorcontrol/urls.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "doorcontrol"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"reports/access-per-<unit_time>",
|
||||||
|
views.AccessPerUnitTime.as_view(),
|
||||||
|
name="access-per-unit-time",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"reports/denied-access",
|
||||||
|
views.DeniedAccess.as_view(),
|
||||||
|
name="denied-access",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"reports/most-active-members",
|
||||||
|
views.MostActiveMembers.as_view(),
|
||||||
|
name="most-active-members",
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,203 @@
|
|||||||
from django.shortcuts import render
|
import datetime
|
||||||
|
|
||||||
# Create your views here.
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.core.paginator import Page
|
||||||
|
from django.core.exceptions import BadRequest
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.db.models.functions import Trunc
|
||||||
|
from django.urls import 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
|
||||||
|
|
||||||
|
from .models import HIDEvent
|
||||||
|
|
||||||
|
|
||||||
|
REPORT_TYPES = []
|
||||||
|
|
||||||
|
|
||||||
|
def register_report(cls: "BaseAccessReport"):
|
||||||
|
REPORT_TYPES.extend(cls._report_types())
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAccessReport(PermissionRequiredMixin, ListView):
|
||||||
|
model = HIDEvent
|
||||||
|
permission_required = "doorcontrol.view_hidevent"
|
||||||
|
paginate_by = 20
|
||||||
|
context_object_name = "object_list"
|
||||||
|
template_name = "doorcontrol/access_report.dj.html"
|
||||||
|
|
||||||
|
_report_name = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _report_types(cls):
|
||||||
|
yield [
|
||||||
|
cls._report_name,
|
||||||
|
reverse_lazy("doorcontrol:" + slugify(cls._report_name)),
|
||||||
|
]
|
||||||
|
|
||||||
|
def _selected_report(self):
|
||||||
|
return self._report_name
|
||||||
|
|
||||||
|
def _get_timestamp_range(self):
|
||||||
|
timestamp__gte = dateparse.parse_datetime(
|
||||||
|
self.request.GET.get("timestamp__gte") or "2019-01-01"
|
||||||
|
)
|
||||||
|
timestamp__lte = self.request.GET.get("timestamp__lte")
|
||||||
|
if timestamp__lte:
|
||||||
|
timestamp__lte = dateparse.parse_datetime(timestamp__lte)
|
||||||
|
else:
|
||||||
|
timestamp__lte = localtime()
|
||||||
|
|
||||||
|
return timestamp__gte, timestamp__lte
|
||||||
|
|
||||||
|
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().filter(timestamp__range=self._get_timestamp_range())
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["report_types"] = 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)
|
||||||
|
|
||||||
|
timestamp__gte, timestamp__lte = self._get_timestamp_range()
|
||||||
|
context["timestamp__gte"] = timestamp__gte
|
||||||
|
context["timestamp__lte"] = timestamp__lte
|
||||||
|
|
||||||
|
query_params = self.request.GET.copy()
|
||||||
|
if "page" in query_params:
|
||||||
|
query_params.pop("page")
|
||||||
|
context["query_params"] = query_params.urlencode()
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@register_report
|
||||||
|
class AccessPerUnitTime(BaseAccessReport):
|
||||||
|
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]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _selected_report(self) -> str:
|
||||||
|
return "Access per " + self.kwargs["unit_time"].title()
|
||||||
|
|
||||||
|
def _format_date(self, date: datetime.datetime) -> str:
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
elif unit_time == "month":
|
||||||
|
return date_format(date, "N Y")
|
||||||
|
elif unit_time == "year":
|
||||||
|
return date_format(date, "Y")
|
||||||
|
|
||||||
|
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")
|
||||||
|
]
|
||||||
|
events = (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(event_type__in=granted_event_types)
|
||||||
|
.values(unit_time=Trunc("timestamp", unit_time))
|
||||||
|
.annotate(
|
||||||
|
members=Count("cardholder_id", distinct=True),
|
||||||
|
access_count=Count("cardholder_id"),
|
||||||
|
)
|
||||||
|
.order_by("-unit_time")
|
||||||
|
.values("unit_time", "members", "access_count")
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
unit_time: self._format_date(event["unit_time"]),
|
||||||
|
"members": event["members"],
|
||||||
|
"access count": event["access_count"],
|
||||||
|
}
|
||||||
|
for event in events
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@register_report
|
||||||
|
class DeniedAccess(BaseAccessReport):
|
||||||
|
_report_name = "Denied Access"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
denied_event_types = [
|
||||||
|
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
|
||||||
|
]
|
||||||
|
events = (
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@register_report
|
||||||
|
class MostActiveMembers(BaseAccessReport):
|
||||||
|
_report_name = "Most Active Members"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
counts = (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.values("cardholder_id", "forename", "surname")
|
||||||
|
.order_by()
|
||||||
|
.annotate(access_count=Count("cardholder_id"))
|
||||||
|
.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
|
||||||
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user