rentals: Change numbering scheme to match reality

This commit is contained in:
Adam Goldsmith 2022-01-27 14:55:27 -05:00
parent edc66865e0
commit 91255cb06c
4 changed files with 133 additions and 66 deletions

View File

@ -1,6 +1,17 @@
from django.contrib import admin from django.contrib import admin
from .models import LockerBank, LockerRental from .models import LockerBank, LockerRental, LockerUnit
class LockerUnitInline(admin.TabularInline):
model = LockerUnit
extra = 0
@admin.register(LockerBank)
class LockerBankAdmin(admin.ModelAdmin):
inlines = [LockerUnitInline]
prepopulated_fields = {"slug": ("name",)}
class LockerRentalInline(admin.TabularInline): class LockerRentalInline(admin.TabularInline):
@ -8,10 +19,9 @@ class LockerRentalInline(admin.TabularInline):
extra = 0 extra = 0
@admin.register(LockerBank) @admin.register(LockerUnit)
class LockerBankAdmin(admin.ModelAdmin): class LockerUnitAdmin(admin.ModelAdmin):
inlines = [LockerRentalInline] inlines = [LockerRentalInline]
prepopulated_fields = {"slug": ("name",)}
admin.site.register(LockerRental) admin.site.register(LockerRental)

View File

@ -1,6 +1,7 @@
# Generated by Django 3.2.11 on 2022-01-24 04:08 # Generated by Django 3.2.11 on 2022-01-27 22:14
from django.conf import settings from django.conf import settings
import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
@ -21,18 +22,35 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=200)), ('name', models.CharField(max_length=200)),
('location', models.CharField(max_length=200)), ('location', models.CharField(max_length=200)),
('slug', models.SlugField(unique=True)), ('slug', models.SlugField(unique=True)),
('rows', models.PositiveIntegerField()),
('columns', models.PositiveIntegerField()),
], ],
), ),
migrations.CreateModel(
name='LockerUnit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('index', models.PositiveIntegerField()),
('first_letter', models.CharField(max_length=1, unique=True, validators=[django.core.validators.RegexValidator('[A-Z]')])),
('first_number', models.PositiveIntegerField()),
('rows', models.PositiveIntegerField(default=5)),
('columns', models.PositiveIntegerField(default=2)),
('bank', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='units', to='rentals.lockerbank')),
],
options={
'ordering': ['index'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='LockerRental', name='LockerRental',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('row', models.PositiveIntegerField()),
('column', models.PositiveIntegerField()), ('column', models.PositiveIntegerField()),
('locker_bank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rentals.lockerbank')), ('row', models.PositiveIntegerField()),
('locker_unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rentals', to='rentals.lockerunit')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), ),
migrations.AddConstraint(
model_name='lockerunit',
constraint=models.UniqueConstraint(fields=('bank', 'index'), name='unique_bank_index'),
),
] ]

View File

@ -1,45 +1,75 @@
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import validators
from django.db import models from django.db import models
class LockerBank(models.Model): class LockerBank(models.Model):
"""A set of locker units, placed together"""
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
location = models.CharField(max_length=200) location = models.CharField(max_length=200)
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)
rows = models.PositiveIntegerField()
columns = models.PositiveIntegerField()
# TODO: add constraint for unique first letter?
def __str__(self): def __str__(self):
return f"{self.name} ({self.columns}x{self.rows})" return f"{self.name} ({self.units.count()} units)"
@property
def initial(self): class LockerUnit(models.Model):
return self.name[0].upper() """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 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 + column * self.rows
@property @property
def iter_doors(self): def iter_doors(self):
for row in range(self.rows): for row in range(self.rows):
for column in range(self.columns): for column in range(self.columns):
# TODO: filter could be optimized # TODO: filter could be optimized
yield chr(row + ord("A")), column + 1, self.rentals.filter( yield (
row=row, column=column self.letter_for_column(column),
self.number_for_locker(column, row),
self.rentals.filter(row=row, column=column),
) )
# TODO: add check constraint to ensure that column and number are within locker_unit bounds
# TODO: add unique constraint on (unit, row, column)?
class LockerRental(models.Model): class LockerRental(models.Model):
locker_bank = models.ForeignKey( """A rental of a single locker"""
LockerBank, on_delete=models.CASCADE, related_name="rentals"
locker_unit = models.ForeignKey(
LockerUnit, on_delete=models.CASCADE, related_name="rentals"
) )
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
row = models.PositiveIntegerField()
column = models.PositiveIntegerField() column = models.PositiveIntegerField()
# TODO: add check constraint to ensure that row and column are within locker_bank bounds row = models.PositiveIntegerField()
# TODO: add unique constraint on (bank, row, column)? user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
@property @property
def address(self) -> str: def address(self) -> str:
return f"{self.locker_bank.name}-{self.locker_bank.initial}{chr(self.row + ord('A'))}{self.column}" letter = self.locker_unit.letter_for_column(self.column)
number = self.locker_unit.number_for_locker(self.column, self.row)
return f"{self.locker_unit}-{letter}{number}"
def __str__(self): def __str__(self):
return f"{self.user}: {self.address}" return f"{self.user}: {self.address}"

View File

@ -1,7 +1,9 @@
{% extends "base.dj.html" %} {% extends "base.dj.html" %}
{% block title %}Lockers Index{% endblock %} {% block title %}Lockers Index{% endblock %}
{% block admin_link %}{% url 'admin:app_list' 'rentals' %}{% endblock %} {% block admin_link %}
{% url 'admin:app_list' 'rentals' %}
{% endblock %}
{% block content %} {% block content %}
<style> <style>
.locker-bank { .locker-bank {
@ -9,12 +11,17 @@
text-align: center; text-align: center;
} }
.locker { .lockers {
display: grid;
gap: 0.5rem;
padding: 1rem; padding: 1rem;
background-color: #333333; background-color: #333333;
border-radius: 0.4rem; border-radius: 0.4rem;
display: flex;
gap: 0.5rem;
}
.locker {
display: grid;
gap: 0.5rem;
} }
.locker[data-columns="2"] { .locker[data-columns="2"] {
@ -33,47 +40,49 @@
grid-template-columns: repeat(10, min-content); grid-template-columns: repeat(10, min-content);
} }
.locker .door { .locker .door {
background-color: hsl(24, 100%, 25%); background-color: hsl(24, 100%, 25%);
border: 0.2rem solid black; border: 0.2rem solid black;
border-radius: 0.5rem; border-radius: 0.5rem;
width: 7rem; width: 7rem;
aspect-ratio: 1; aspect-ratio: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: lightgray; color: lightgray;
} }
.locker .door:not([data-rentals="0"]) { .locker .door:not([data-rentals="0"]) {
background-color: hsl(24, 15%, 25%);; background-color: hsl(24, 15%, 25%);;
} }
</style> </style>
{% for bank in locker_banks %} {% for bank in locker_banks %}
<div class="locker-bank"> <div class="locker-bank" data-bank="{{ bank.slug }}">
<h2>{{ bank.name }}</h2> <h2>{{ bank.name }}</h2>
<div>{{ bank.location }}</div> <div>{{ bank.location }}</div>
<div class="locker" <div class="lockers">
data-columns="{{ bank.columns }}" {% for unit in bank.units.all %}
data-bank="{{ bank.slug }}"> <div class="locker" data-columns="{{ unit.columns }}">
{% for row, column, rentals in bank.iter_doors %} {% for row, column, rentals in unit.iter_doors %}
<div class="door" <div class="door"
data-rentals="{{ rentals|length }}" data-rentals="{{ rentals|length }}"
data-row="{{ row }}" data-row="{{ row }}"
data-column="{{ column }}"> data-column="{{ column }}">
<h3 class="locker-name">{{ bank.initial }}{{ row }}{{ column }}</h3> <h3 class="locker-name">{{ bank.initial }}{{ row }}{{ column }}</h3>
<div class="locker-status"> <div class="locker-status">
{% if rentals|length > 0 %} {% if rentals|length > 0 %}
{# TODO: Should check for more specific permission #} {# TODO: Should check for more specific permission #}
{% if user.is_staff %} {% if user.is_staff %}
{% for rental in rentals %}{{ rental.user }}{% endfor %} {% for rental in rentals %}{{ rental.user }}{% endfor %}
{% else %} {% else %}
Occupied Occupied
{% endif %} {% endif %}
{% else %} {% else %}
Empty Empty
{% endif %} {% endif %}
</div> </div>
</div>
{% endfor %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>