Compare commits

...

3 Commits

Author SHA1 Message Date
d4670a7d02 doorcontrol: Add unique member count to "busiest..." reports
All checks were successful
Ruff / ruff (push) Successful in 23s
2024-02-09 12:20:32 -05:00
747df72725 doorcontrol: Add "detail by day" access report 2024-02-09 12:17:09 -05:00
6f1a9c0436 doorcontrol: Store cardholder_id->member per door for correct stats 2024-02-09 12:17:09 -05:00
3 changed files with 106 additions and 12 deletions

View File

@ -2,10 +2,12 @@ from datetime import datetime
from django.conf import settings
from django.db import models
from django.db.models import F, Func, Q
from django.db.models import F, Func, OuterRef, Q, Subquery
from django.db.models.functions import Mod
from django.utils import timezone
from membershipworks.models import Member
from .hid.DoorController import DoorController
@ -25,6 +27,25 @@ class Door(models.Model):
return self.name
class DoorCardholderMember(models.Model):
door = models.ForeignKey(Door, on_delete=models.CASCADE)
cardholder_id = models.IntegerField()
member = models.ForeignKey(Member, on_delete=models.CASCADE, db_constraint=False)
class Meta:
constraints = (
models.UniqueConstraint(
fields=("door", "cardholder_id"), name="unique_door_cardholder_id"
),
models.UniqueConstraint(
fields=("door", "member"), name="unique_door_member"
),
)
def __str__(self):
return f"{self.door} [{self.cardholder_id}]: {self.member}"
class HIDEventQuerySet(models.QuerySet):
def with_decoded_card_number(self):
# TODO: CONV and BIT_COUNT are MySQL/MariaDB specific
@ -62,6 +83,15 @@ class HIDEventQuerySet(models.QuerySet):
)
)
def with_member_id(self):
return self.annotate(
member_id=Subquery(
DoorCardholderMember.objects.filter(
door=OuterRef("door"), cardholder_id=OuterRef("cardholder_id")
).values("member_id"),
)
)
class HIDEvent(models.Model):
objects = HIDEventQuerySet.as_manager()

View File

@ -6,11 +6,33 @@ from django.utils import timezone
from django_q.tasks import async_task
from cmsmanage.django_q2_helper import q_task_group
from doorcontrol.models import Door, HIDEvent
from doorcontrol.models import Door, DoorCardholderMember, HIDEvent
def get_cardholders(door: Door):
def make_ch_member(cardholder):
return DoorCardholderMember(
door=door,
cardholder_id=cardholder.attrib["cardholderID"],
member_id=cardholder.attrib.get("custom2"),
)
DoorCardholderMember.objects.bulk_create(
(
make_ch_member(cardholder)
for cardholder in door.controller.get_cardholders()
if "custom2" in cardholder.attrib
),
update_conflicts=True,
update_fields=("member",),
)
@transaction.atomic()
def getMessages(door: Door):
# TODO: this should in the cardholder syncing task
get_cardholders(door)
last_event = door.hidevent_set.order_by("timestamp").last()
if last_event is not None:
last_ts = timezone.make_naive(last_event.timestamp)

View File

@ -13,6 +13,8 @@ from django.views.generic.list import ListView
import django_filters
import django_tables2 as tables
from django_filters.views import BaseFilterView
from django_mysql.models import GroupConcat
from django_mysql.models.functions import ConcatWS
from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin
@ -177,9 +179,10 @@ class AccessPerUnitTime(BaseAccessReport):
super()
.get_table_data()
.filter(event_type__in=granted_event_types)
.with_member_id()
.values(unit_time=Trunc("timestamp", unit_time))
.annotate(
members=Count("cardholder_id", distinct=True),
members=Count("member_id", distinct=True),
members_delta=(
F("members")
/ Window(
@ -242,10 +245,7 @@ class DeniedAccess(BaseAccessReport):
class MostActiveMembersTable(tables.Table):
cardholder_id = tables.Column()
name = tables.TemplateColumn(
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
)
name = tables.Column()
access_count = tables.Column()
@ -258,16 +258,51 @@ class MostActiveMembers(BaseAccessReport):
return (
super()
.get_table_data()
.values("cardholder_id", "forename", "surname")
.order_by()
.annotate(access_count=Count("cardholder_id"))
.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
),
)
.order_by("-access_count")
)
class DetailByDayTable(tables.Table):
timestamp__date = tables.DateColumn(verbose_name="Date")
name = tables.Column()
access_count = tables.Column()
@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"),
name=GroupConcat(
ConcatWS("forename", "surname", separator=" "), distinct=True
),
)
.order_by("-timestamp__date")
)
class BusiestDayOfWeekTable(tables.Table):
timestamp__week_day = tables.Column("Week Day")
events = tables.Column()
members = tables.Column()
def render_timestamp__week_day(self, value):
return calendar.day_name[(value - 2) % 7]
@ -283,14 +318,18 @@ class BusiestDayOfWeek(BaseAccessReport):
return (
super()
.get_table_data()
.with_member_id()
.values("timestamp__week_day")
.annotate(events=Count("timestamp"))
.annotate(
events=Count("timestamp"), members=Count("member_id", distinct=True)
)
)
class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = tables.Column()
members = tables.Column()
@register_report
@ -303,6 +342,9 @@ class BusiestTimeOfDay(BaseAccessReport):
return (
super()
.get_table_data()
.with_member_id()
.values("timestamp__hour")
.annotate(events=Count("timestamp"))
.annotate(
events=Count("timestamp"), members=Count("member_id", distinct=True)
)
)