138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
from django.contrib import admin
|
|
from django.core import validators
|
|
from django.core.exceptions import ValidationError
|
|
from django.db import models, transaction
|
|
from django.db.models import F, Q
|
|
from django.db.models.functions import Chr, Concat, Ord
|
|
|
|
from membershipworks.models import Member
|
|
|
|
|
|
class LockerBank(models.Model):
|
|
"""A set of locker units, placed together"""
|
|
|
|
name = models.CharField(max_length=200)
|
|
location = models.CharField(max_length=200)
|
|
slug = models.SlugField(unique=True)
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.units.count()} units)"
|
|
|
|
|
|
class LockerUnit(models.Model):
|
|
"""A standalone set of lockers"""
|
|
|
|
bank = models.ForeignKey(
|
|
LockerBank, on_delete=models.SET_NULL, related_name="units", null=True
|
|
)
|
|
index = models.PositiveIntegerField()
|
|
first_letter = models.CharField(
|
|
max_length=1, validators=[validators.RegexValidator("[A-Z]")], unique=True
|
|
)
|
|
first_number = models.PositiveIntegerField()
|
|
rows = models.PositiveIntegerField(default=5)
|
|
columns = models.PositiveIntegerField(default=2)
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self._state.adding:
|
|
# Create LockerInfo for each locker
|
|
with transaction.atomic():
|
|
super().save(self, *args, **kwargs)
|
|
for column in range(self.columns):
|
|
for row in range(self.rows):
|
|
self.lockers.create(column=column + 1, row=row + 1)
|
|
else:
|
|
super().save(self, *args, **kwargs)
|
|
|
|
class Meta:
|
|
# TODO: add constraint to check for letter overlaps
|
|
constraints = [
|
|
models.UniqueConstraint(fields=["bank", "index"], name="unique_bank_index")
|
|
]
|
|
ordering = ["index"]
|
|
|
|
def __str__(self):
|
|
last_letter = chr(ord(self.first_letter) + self.columns - 1)
|
|
last_number = self.first_number + self.columns * self.rows
|
|
return f"{self.bank.name} (Unit {last_letter}{self.first_number}-{self.first_letter}{last_number})"
|
|
|
|
def letter_for_column(self, column: int) -> str:
|
|
return chr(column + ord(self.first_letter))
|
|
|
|
def number_for_locker(self, column: int, row: int) -> int:
|
|
return row + self.first_number + (self.columns - column - 1) * self.rows
|
|
|
|
|
|
# TODO: add check constraint to ensure that column and number are within locker_unit bounds
|
|
class LockerInfo(models.Model):
|
|
"""Information about a single locker"""
|
|
|
|
locker_unit = models.ForeignKey(
|
|
LockerUnit, on_delete=models.CASCADE, related_name="lockers"
|
|
)
|
|
column = models.PositiveIntegerField()
|
|
row = models.PositiveIntegerField()
|
|
blind_code = models.CharField(
|
|
max_length=5,
|
|
help_text="Stamped on some keys. Usually D###A.",
|
|
blank=True,
|
|
)
|
|
bitting_code = models.CharField(
|
|
max_length=5,
|
|
help_text="National Disc Tumbler, depths 1-4. Read bow-to-tip.",
|
|
blank=True,
|
|
)
|
|
renter = models.ForeignKey(
|
|
Member, on_delete=models.CASCADE, null=True, blank=True, db_constraint=False
|
|
)
|
|
reserved = models.BooleanField(
|
|
default=False,
|
|
help_text="Locker is reserved for MakerSpace use, and cannot be rented.",
|
|
)
|
|
notes = models.TextField(blank=True)
|
|
|
|
def clean(self):
|
|
if self.reserved and self.renter is not None:
|
|
raise ValidationError("Locker cannot both be reserved and rented!")
|
|
|
|
class Meta:
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["locker_unit", "column", "row"], name="unique_locker_info"
|
|
),
|
|
models.CheckConstraint(
|
|
check=Q(reserved=False) | Q(renter__isnull=True),
|
|
name="locker_not_reserved_and_rented",
|
|
),
|
|
]
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self.renter is None and not self.reserved
|
|
|
|
@property
|
|
def letter(self) -> str:
|
|
return self.locker_unit.letter_for_column(self.column)
|
|
|
|
@property
|
|
def number(self) -> int:
|
|
return self.locker_unit.number_for_locker(self.column, self.row)
|
|
|
|
@property
|
|
@admin.display(
|
|
description="Address",
|
|
ordering=Concat(
|
|
Chr(F("column") + Ord("locker_unit__first_letter")),
|
|
(
|
|
F("row")
|
|
+ F("locker_unit__first_number")
|
|
+ (F("locker_unit__columns") - F("column") - 1) * F("locker_unit__rows")
|
|
),
|
|
),
|
|
)
|
|
def address(self) -> str:
|
|
return f"{self.letter}{self.number}"
|
|
|
|
def __str__(self):
|
|
return f"{self.locker_unit}-{self.address} [{self.renter}]"
|