Compare commits

..

No commits in common. "ba58d90bf7cfa9ae06a49eb30d201efcaa2d1ad6" and "70691ff9724e0a8d009a312e7c93f5597d95b902" have entirely different histories.

6 changed files with 62 additions and 118 deletions

View File

@ -3,18 +3,9 @@ from django.db.models.signals import post_migrate
def post_migrate_callback(sender, **kwargs):
from django_q.models import Schedule
from .tasks.scrapehidevents import 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,
)
schedule_tasks()
class DoorControlConfig(AppConfig):

View File

@ -1,7 +1,8 @@
from typing import Any
from django.urls import reverse
import dashboard
from .views import REPORTS
@dashboard.register
@ -12,7 +13,16 @@ class DoorControlDashboardFragment(dashboard.DashboardFragment):
@property
def context(self) -> Any:
return {
"links": dict(rt for report in REPORTS for rt in report._report_types())
"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"),
}
}
@property

View File

@ -4,7 +4,9 @@ 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
@ -43,3 +45,12 @@ 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,5 +1,23 @@
from django.urls import path
from . import views
app_name = "doorcontrol"
urlpatterns = [report._urlpattern() for report in views.REPORTS]
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",
),
]

View File

@ -1,4 +1,3 @@
import calendar
import datetime
from django.contrib.auth.mixins import PermissionRequiredMixin
@ -6,24 +5,21 @@ 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, path
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 django.db.models import Window, F, FloatField
from django.db.models.functions import Lead
from .models import HIDEvent
REPORTS = []
REPORT_TYPES = []
def register_report(cls: "BaseAccessReport"):
REPORTS.append(cls)
REPORT_TYPES.extend(cls._report_types())
return cls
@ -43,11 +39,6 @@ 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
@ -78,9 +69,7 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
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()
]
context["report_types"] = REPORT_TYPES
page: Page = context["page_obj"]
context["paginator_range"] = page.paginator.get_elided_page_range(page.number)
@ -111,14 +100,6 @@ 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()
@ -152,45 +133,16 @@ 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
]
@ -252,35 +204,3 @@ 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,8 +4,7 @@ from typing import TypedDict, TYPE_CHECKING, Optional
from semver import VersionInfo
from django.db import models
from django.db.models import Q, Window
from django.db.models.functions import FirstValue
from django.db.models import OuterRef, Q, ExpressionWrapper, Subquery
from django.conf import settings
from django.core.validators import RegexValidator
from django_stubs_ext import WithAnnotations
@ -140,23 +139,18 @@ class CertificationVersionAnnotations(TypedDict):
class CertificationVersionManager(models.Manager["CertificationVersion"]):
def get_queryset(self) -> models.QuerySet["CertificationVersion"]:
window = {
"partition_by": "definition",
"order_by": [
"-major",
"-minor",
"-patch",
"-prerelease",
],
}
return (
super()
.get_queryset()
.annotate(
is_latest=Q(pk=Window(FirstValue("pk"), **window)),
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(),
),
# TODO: should do a more correct comparison than just major version
is_current=Q(major=Window(FirstValue("major"), **window)),
)
is_current=ExpressionWrapper(
Q(major=Subquery(latest.values("major")[:1])),
output_field=models.BooleanField(),
),
)