Compare commits
No commits in common. "ba58d90bf7cfa9ae06a49eb30d201efcaa2d1ad6" and "70691ff9724e0a8d009a312e7c93f5597d95b902" have entirely different histories.
ba58d90bf7
...
70691ff972
@ -3,18 +3,9 @@ from django.db.models.signals import post_migrate
|
|||||||
|
|
||||||
|
|
||||||
def post_migrate_callback(sender, **kwargs):
|
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
|
schedule_tasks()
|
||||||
|
|
||||||
from .tasks.scrapehidevents import q_getMessagesAllDoors
|
|
||||||
|
|
||||||
ensure_scheduled(
|
|
||||||
"Fetch HID Events",
|
|
||||||
q_getMessagesAllDoors,
|
|
||||||
schedule_type=Schedule.MINUTES,
|
|
||||||
minutes=15,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DoorControlConfig(AppConfig):
|
class DoorControlConfig(AppConfig):
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
import dashboard
|
import dashboard
|
||||||
from .views import REPORTS
|
|
||||||
|
|
||||||
|
|
||||||
@dashboard.register
|
@dashboard.register
|
||||||
@ -12,7 +13,16 @@ class DoorControlDashboardFragment(dashboard.DashboardFragment):
|
|||||||
@property
|
@property
|
||||||
def context(self) -> Any:
|
def context(self) -> Any:
|
||||||
return {
|
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
|
@property
|
||||||
|
@ -4,7 +4,9 @@ from django.db import transaction
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from django_q.tasks import async_task
|
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
|
from doorcontrol.models import Door, HIDEvent
|
||||||
|
|
||||||
|
|
||||||
@ -43,3 +45,12 @@ def q_getMessagesAllDoors():
|
|||||||
cluster="internal",
|
cluster="internal",
|
||||||
group=f"Fetch HID Events - {door.name}",
|
group=f"Fetch HID Events - {door.name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_tasks():
|
||||||
|
ensure_scheduled(
|
||||||
|
"Fetch HID Events",
|
||||||
|
q_getMessagesAllDoors,
|
||||||
|
schedule_type=Schedule.MINUTES,
|
||||||
|
minutes=15,
|
||||||
|
)
|
||||||
|
@ -1,5 +1,23 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = "doorcontrol"
|
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",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import calendar
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
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.core.exceptions import BadRequest
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.db.models.functions import Trunc
|
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 import dateparse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
from django.views.generic.list import ListView
|
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
|
from .models import HIDEvent
|
||||||
|
|
||||||
|
|
||||||
REPORTS = []
|
REPORT_TYPES = []
|
||||||
|
|
||||||
|
|
||||||
def register_report(cls: "BaseAccessReport"):
|
def register_report(cls: "BaseAccessReport"):
|
||||||
REPORTS.append(cls)
|
REPORT_TYPES.extend(cls._report_types())
|
||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
@ -43,11 +39,6 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
|
|||||||
reverse_lazy("doorcontrol:" + slugify(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)
|
|
||||||
|
|
||||||
def _selected_report(self):
|
def _selected_report(self):
|
||||||
return self._report_name
|
return self._report_name
|
||||||
|
|
||||||
@ -78,9 +69,7 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["report_types"] = [
|
context["report_types"] = REPORT_TYPES
|
||||||
rt for report in REPORTS for rt in report._report_types()
|
|
||||||
]
|
|
||||||
|
|
||||||
page: Page = context["page_obj"]
|
page: Page = context["page_obj"]
|
||||||
context["paginator_range"] = page.paginator.get_elided_page_range(page.number)
|
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]),
|
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:
|
def _selected_report(self) -> str:
|
||||||
return "Access per " + self.kwargs["unit_time"].title()
|
return "Access per " + self.kwargs["unit_time"].title()
|
||||||
|
|
||||||
@ -152,45 +133,16 @@ class AccessPerUnitTime(BaseAccessReport):
|
|||||||
.values(unit_time=Trunc("timestamp", unit_time))
|
.values(unit_time=Trunc("timestamp", unit_time))
|
||||||
.annotate(
|
.annotate(
|
||||||
members=Count("cardholder_id", distinct=True),
|
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=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")
|
.order_by("-unit_time")
|
||||||
|
.values("unit_time", "members", "access_count")
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
unit_time: self._format_date(event["unit_time"]),
|
unit_time: self._format_date(event["unit_time"]),
|
||||||
"members": event["members"],
|
"members": event["members"],
|
||||||
"Δ members": (
|
|
||||||
f'{event["members_delta"]:.2f}%'
|
|
||||||
if event["members_delta"] is not None
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"access count": event["access_count"],
|
"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
|
for event in events
|
||||||
]
|
]
|
||||||
@ -252,35 +204,3 @@ class MostActiveMembers(BaseAccessReport):
|
|||||||
}
|
}
|
||||||
for count in counts
|
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"))
|
|
||||||
]
|
|
||||||
|
@ -4,8 +4,7 @@ from typing import TypedDict, TYPE_CHECKING, Optional
|
|||||||
|
|
||||||
from semver import VersionInfo
|
from semver import VersionInfo
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, Window
|
from django.db.models import OuterRef, Q, ExpressionWrapper, Subquery
|
||||||
from django.db.models.functions import FirstValue
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django_stubs_ext import WithAnnotations
|
from django_stubs_ext import WithAnnotations
|
||||||
@ -140,23 +139,18 @@ class CertificationVersionAnnotations(TypedDict):
|
|||||||
|
|
||||||
class CertificationVersionManager(models.Manager["CertificationVersion"]):
|
class CertificationVersionManager(models.Manager["CertificationVersion"]):
|
||||||
def get_queryset(self) -> models.QuerySet["CertificationVersion"]:
|
def get_queryset(self) -> models.QuerySet["CertificationVersion"]:
|
||||||
window = {
|
qs = super().get_queryset()
|
||||||
"partition_by": "definition",
|
latest = qs.filter(definition__pk=OuterRef("definition__pk")).reverse()
|
||||||
"order_by": [
|
return qs.annotate(
|
||||||
"-major",
|
is_latest=ExpressionWrapper(
|
||||||
"-minor",
|
Q(pk=Subquery(latest.values("pk")[:1])),
|
||||||
"-patch",
|
output_field=models.BooleanField(),
|
||||||
"-prerelease",
|
),
|
||||||
],
|
# TODO: should do a more correct comparison than just major version
|
||||||
}
|
is_current=ExpressionWrapper(
|
||||||
return (
|
Q(major=Subquery(latest.values("major")[:1])),
|
||||||
super()
|
output_field=models.BooleanField(),
|
||||||
.get_queryset()
|
),
|
||||||
.annotate(
|
|
||||||
is_latest=Q(pk=Window(FirstValue("pk"), **window)),
|
|
||||||
# TODO: should do a more correct comparison than just major version
|
|
||||||
is_current=Q(major=Window(FirstValue("major"), **window)),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user