Compare commits

...

5 Commits

6 changed files with 118 additions and 62 deletions

View File

@ -3,9 +3,18 @@ from django.db.models.signals import post_migrate
def post_migrate_callback(sender, **kwargs):
from .tasks.scrapehidevents import schedule_tasks
from django_q.models import Schedule
schedule_tasks()
from cmsmanage.django_q2_helper import ensure_scheduled
from .tasks.scrapehidevents import q_getMessagesAllDoors
ensure_scheduled(
"Fetch HID Events",
q_getMessagesAllDoors,
schedule_type=Schedule.MINUTES,
minutes=15,
)
class DoorControlConfig(AppConfig):

View File

@ -1,8 +1,7 @@
from typing import Any
from django.urls import reverse
import dashboard
from .views import REPORTS
@dashboard.register
@ -13,16 +12,7 @@ class DoorControlDashboardFragment(dashboard.DashboardFragment):
@property
def context(self) -> Any:
return {
"links": {
"Access Per Day": reverse(
"doorcontrol:access-per-unit-time", kwargs={"unit_time": "day"}
),
"Access Per Month": reverse(
"doorcontrol:access-per-unit-time", kwargs={"unit_time": "month"}
),
"Access Failures": reverse("doorcontrol:denied-access"),
"Most Active Members": reverse("doorcontrol:most-active-members"),
}
"links": dict(rt for report in REPORTS for rt in report._report_types())
}
@property

View File

@ -4,9 +4,7 @@ from django.db import transaction
from django.utils import timezone
from django_q.tasks import async_task
from django_q.models import Schedule
from cmsmanage.django_q2_helper import ensure_scheduled
from doorcontrol.models import Door, HIDEvent
@ -45,12 +43,3 @@ def q_getMessagesAllDoors():
cluster="internal",
group=f"Fetch HID Events - {door.name}",
)
def schedule_tasks():
ensure_scheduled(
"Fetch HID Events",
q_getMessagesAllDoors,
schedule_type=Schedule.MINUTES,
minutes=15,
)

View File

@ -1,23 +1,5 @@
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",
),
]
urlpatterns = [report._urlpattern() for report in views.REPORTS]

View File

@ -1,3 +1,4 @@
import calendar
import datetime
from django.contrib.auth.mixins import PermissionRequiredMixin
@ -5,21 +6,24 @@ 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.urls import reverse_lazy, path
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 django.db.models import Window, F, FloatField
from django.db.models.functions import Lead
from .models import HIDEvent
REPORT_TYPES = []
REPORTS = []
def register_report(cls: "BaseAccessReport"):
REPORT_TYPES.extend(cls._report_types())
REPORTS.append(cls)
return cls
@ -39,6 +43,11 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
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)
def _selected_report(self):
return self._report_name
@ -69,7 +78,9 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["report_types"] = REPORT_TYPES
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)
@ -100,6 +111,14 @@ class AccessPerUnitTime(BaseAccessReport):
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",
)
def _selected_report(self) -> str:
return "Access per " + self.kwargs["unit_time"].title()
@ -133,16 +152,45 @@ class AccessPerUnitTime(BaseAccessReport):
.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")
.values("unit_time", "members", "access_count")
)
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
]
@ -204,3 +252,35 @@ class MostActiveMembers(BaseAccessReport):
}
for count in counts
]
@register_report
class BusiestDayOfWeek(BaseAccessReport):
_report_name = "Busiest Day of the Week"
def get_queryset(self):
return [
{
"week day": calendar.day_name[(count["timestamp__week_day"] - 2) % 7],
"events": count["events"],
}
for count in super()
.get_queryset()
.values("timestamp__week_day")
.annotate(events=Count("timestamp"))
]
@register_report
class BusiestTimeOfDay(BaseAccessReport):
_report_name = "Busiest Time of Day"
paginate_by = 24
def get_queryset(self):
return [
{"hour": f'{count["timestamp__hour"]}:00', "events": count["events"]}
for count in super()
.get_queryset()
.values("timestamp__hour")
.annotate(events=Count("timestamp"))
]

View File

@ -4,7 +4,8 @@ from typing import TypedDict, TYPE_CHECKING, Optional
from semver import VersionInfo
from django.db import models
from django.db.models import OuterRef, Q, ExpressionWrapper, Subquery
from django.db.models import Q, Window
from django.db.models.functions import FirstValue
from django.conf import settings
from django.core.validators import RegexValidator
from django_stubs_ext import WithAnnotations
@ -139,18 +140,23 @@ class CertificationVersionAnnotations(TypedDict):
class CertificationVersionManager(models.Manager["CertificationVersion"]):
def get_queryset(self) -> models.QuerySet["CertificationVersion"]:
qs = super().get_queryset()
latest = qs.filter(definition__pk=OuterRef("definition__pk")).reverse()
return qs.annotate(
is_latest=ExpressionWrapper(
Q(pk=Subquery(latest.values("pk")[:1])),
output_field=models.BooleanField(),
),
window = {
"partition_by": "definition",
"order_by": [
"-major",
"-minor",
"-patch",
"-prerelease",
],
}
return (
super()
.get_queryset()
.annotate(
is_latest=Q(pk=Window(FirstValue("pk"), **window)),
# TODO: should do a more correct comparison than just major version
is_current=ExpressionWrapper(
Q(major=Subquery(latest.values("major")[:1])),
output_field=models.BooleanField(),
),
is_current=Q(major=Window(FirstValue("major"), **window)),
)
)