cmsmanage/rentals/models.py

138 lines
4.5 KiB
Python
Raw Permalink Normal View History

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)
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 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)
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"
2022-01-24 23:37:04 -05:00
)
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)
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",
),
]
def __str__(self):
return f"{self.locker_unit}-{self.address} [{self.renter}]"
def clean(self):
if self.reserved and self.renter is not None:
raise ValidationError("Locker cannot both be 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}"