diff --git a/doorcontrol/admin.py b/doorcontrol/admin.py index 34e7196..0470e10 100644 --- a/doorcontrol/admin.py +++ b/doorcontrol/admin.py @@ -30,9 +30,10 @@ class HIDEventAdmin(admin.ModelAdmin): "event_type", IsRedFilter, ] + readonly_fields = ["decoded_card_number"] def get_queryset(self, request): - return super().get_queryset(request).with_is_red() + return super().get_queryset(request).with_is_red().with_decoded_card_number() @admin.display(boolean=True) def is_red(self, obj): diff --git a/doorcontrol/models.py b/doorcontrol/models.py index 345c858..a615e88 100644 --- a/doorcontrol/models.py +++ b/doorcontrol/models.py @@ -1,5 +1,6 @@ from django.db import models -from django.db.models import ExpressionWrapper, Q +from django.db.models import ExpressionWrapper, F, Func, Q +from django.db.models.functions import Mod class HIDEventQuerySet(models.QuerySet): @@ -28,6 +29,42 @@ class HIDEventQuerySet(models.QuerySet): ) ) + def with_decoded_card_number(self): + # TODO: CONV and BIT_COUNT are MySQL/MariaDB specific + class Conv(Func): + function = "CONV" + arity = 3 + # This is technically not true, but fine for my purposes + output_field = models.IntegerField() + + class BitCount(Func): + function = "BIT_COUNT" + arity = 1 + + return ( + self.alias(card_number=Conv(F("raw_card_number"), 16, 10)) + .alias(more_than_26_bits=F("card_number").bitrightshift(26)) + .annotate(card_is_26_bit=Q(more_than_26_bits=0)) + .alias( + parity_a=Mod( + BitCount(F("card_number").bitrightshift(1).bitand(0xFFF)), 2 + ), + parity_b=Mod( + BitCount(F("card_number").bitrightshift(13).bitand(0xFFF)), 2 + ), + ) + .annotate( + card_is_valid_26_bit=~Q(parity_a=F("card_number").bitand(1)) + & Q(parity_b=F("card_number").bitrightshift(25).bitand(1)) + ) + .annotate( + card_number_26_bit=F("card_number").bitrightshift(1).bitand(0xFFFF), + card_facility_code_26_bit=F("card_number") + .bitrightshift(17) + .bitand(0xFF), + ) + ) + class HIDEvent(models.Model): objects = HIDEventQuerySet.as_manager() @@ -121,6 +158,18 @@ class HIDEvent(models.Model): def __str__(self): return f"{self.door_name} {self.timestamp} - {self.description}" + def decoded_card_number(self) -> str: + """Requires annotations from `with_decoded_card_number`""" + if self.raw_card_number is None: + return None + elif self.card_is_26_bit: + if self.card_is_valid_26_bit: + return f"{self.card_facility_code_26_bit} - {self.card_number_26_bit}" + else: + return "Invalid" + else: + return "Not 26 bit card" + class Meta: constraints = [ models.UniqueConstraint(