From 66b41e144849553f4badc24d0b7d4d38fe6abb21 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Fri, 9 Feb 2024 11:59:52 -0500 Subject: [PATCH] doorcontrol: Store cardholder_id->member per door for correct stats --- .../0005_doorcardholdermember_and_more.py | 56 +++++++++++++++++++ doorcontrol/models.py | 32 ++++++++++- doorcontrol/tasks/scrapehidevents.py | 24 +++++++- doorcontrol/views.py | 22 +++++--- 4 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 doorcontrol/migrations/0005_doorcardholdermember_and_more.py diff --git a/doorcontrol/migrations/0005_doorcardholdermember_and_more.py b/doorcontrol/migrations/0005_doorcardholdermember_and_more.py new file mode 100644 index 0000000..26eb69e --- /dev/null +++ b/doorcontrol/migrations/0005_doorcardholdermember_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 5.0.1 on 2024-02-09 16:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("doorcontrol", "0004_hidevent_is_red"), + ("membershipworks", "0014_remove_eventext_details_timestamp"), + ] + + operations = [ + migrations.CreateModel( + name="DoorCardholderMember", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("cardholder_id", models.IntegerField()), + ( + "door", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="doorcontrol.door", + ), + ), + ( + "member", + models.ForeignKey( + db_constraint=False, + on_delete=django.db.models.deletion.CASCADE, + to="membershipworks.member", + ), + ), + ], + ), + migrations.AddConstraint( + model_name="doorcardholdermember", + constraint=models.UniqueConstraint( + fields=("door", "cardholder_id"), name="unique_door_cardholder_id" + ), + ), + migrations.AddConstraint( + model_name="doorcardholdermember", + constraint=models.UniqueConstraint( + fields=("door", "member"), name="unique_door_member" + ), + ), + ] diff --git a/doorcontrol/models.py b/doorcontrol/models.py index 39031a0..63f68e5 100644 --- a/doorcontrol/models.py +++ b/doorcontrol/models.py @@ -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() diff --git a/doorcontrol/tasks/scrapehidevents.py b/doorcontrol/tasks/scrapehidevents.py index 04a66d3..287133d 100644 --- a/doorcontrol/tasks/scrapehidevents.py +++ b/doorcontrol/tasks/scrapehidevents.py @@ -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) diff --git a/doorcontrol/views.py b/doorcontrol/views.py index a6f7ec6..6b6e3c1 100644 --- a/doorcontrol/views.py +++ b/doorcontrol/views.py @@ -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,9 +258,15 @@ 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") )