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.conf import settings
from django.db import models 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.db.models.functions import Mod
from django.utils import timezone from django.utils import timezone
from membershipworks.models import Member
from .hid.DoorController import DoorController from .hid.DoorController import DoorController
@ -25,6 +27,25 @@ class Door(models.Model):
return self.name 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): class HIDEventQuerySet(models.QuerySet):
def with_decoded_card_number(self): def with_decoded_card_number(self):
# TODO: CONV and BIT_COUNT are MySQL/MariaDB specific # 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): class HIDEvent(models.Model):
objects = HIDEventQuerySet.as_manager() objects = HIDEventQuerySet.as_manager()

View File

@ -6,11 +6,33 @@ from django.utils import timezone
from django_q.tasks import async_task from django_q.tasks import async_task
from cmsmanage.django_q2_helper import q_task_group 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() @transaction.atomic()
def getMessages(door: Door): def getMessages(door: Door):
# TODO: this should in the cardholder syncing task
get_cardholders(door)
last_event = door.hidevent_set.order_by("timestamp").last() last_event = door.hidevent_set.order_by("timestamp").last()
if last_event is not None: if last_event is not None:
last_ts = timezone.make_naive(last_event.timestamp) 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_filters
import django_tables2 as tables import django_tables2 as tables
from django_filters.views import BaseFilterView 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 import SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -177,9 +179,10 @@ class AccessPerUnitTime(BaseAccessReport):
super() super()
.get_table_data() .get_table_data()
.filter(event_type__in=granted_event_types) .filter(event_type__in=granted_event_types)
.with_member_id()
.values(unit_time=Trunc("timestamp", unit_time)) .values(unit_time=Trunc("timestamp", unit_time))
.annotate( .annotate(
members=Count("cardholder_id", distinct=True), members=Count("member_id", distinct=True),
members_delta=( members_delta=(
F("members") F("members")
/ Window( / Window(
@ -242,10 +245,7 @@ class DeniedAccess(BaseAccessReport):
class MostActiveMembersTable(tables.Table): class MostActiveMembersTable(tables.Table):
cardholder_id = tables.Column() name = tables.Column()
name = tables.TemplateColumn(
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
)
access_count = tables.Column() access_count = tables.Column()
@ -258,16 +258,51 @@ class MostActiveMembers(BaseAccessReport):
return ( return (
super() super()
.get_table_data() .get_table_data()
.values("cardholder_id", "forename", "surname") .with_member_id()
.order_by() .filter(member_id__isnull=False)
.annotate(access_count=Count("cardholder_id")) .values("member_id")
.annotate(
access_count=Count("member_id"),
name=GroupConcat(
ConcatWS("forename", "surname", separator=" "), distinct=True
),
)
.order_by("-access_count") .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): class BusiestDayOfWeekTable(tables.Table):
timestamp__week_day = tables.Column("Week Day") timestamp__week_day = tables.Column("Week Day")
events = tables.Column() events = tables.Column()
members = tables.Column()
def render_timestamp__week_day(self, value): def render_timestamp__week_day(self, value):
return calendar.day_name[(value - 2) % 7] return calendar.day_name[(value - 2) % 7]
@ -283,14 +318,18 @@ class BusiestDayOfWeek(BaseAccessReport):
return ( return (
super() super()
.get_table_data() .get_table_data()
.with_member_id()
.values("timestamp__week_day") .values("timestamp__week_day")
.annotate(events=Count("timestamp")) .annotate(
events=Count("timestamp"), members=Count("member_id", distinct=True)
)
) )
class BusiestTimeOfDayTable(tables.Table): class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour") timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = tables.Column() events = tables.Column()
members = tables.Column()
@register_report @register_report
@ -303,6 +342,9 @@ class BusiestTimeOfDay(BaseAccessReport):
return ( return (
super() super()
.get_table_data() .get_table_data()
.with_member_id()
.values("timestamp__hour") .values("timestamp__hour")
.annotate(events=Count("timestamp")) .annotate(
events=Count("timestamp"), members=Count("member_id", distinct=True)
)
) )