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") )