[rentals] Store more info on lockers and allow editing via front end

This commit is contained in:
Adam Goldsmith 2022-02-16 16:06:51 -05:00
parent 83c97197a7
commit 24e8bf819b
6 changed files with 184 additions and 67 deletions

View File

@ -1,6 +1,11 @@
from django.contrib import admin from django.contrib import admin
from .models import LockerBank, LockerRental, LockerUnit from .models import LockerBank, LockerInfo, LockerUnit
class LockerInfoInline(admin.TabularInline):
model = LockerInfo
extra = 0
class LockerUnitInline(admin.TabularInline): class LockerUnitInline(admin.TabularInline):
@ -14,16 +19,14 @@ class LockerBankAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)} prepopulated_fields = {"slug": ("name",)}
class LockerRentalInline(admin.TabularInline):
model = LockerRental
extra = 0
@admin.register(LockerUnit) @admin.register(LockerUnit)
class LockerUnitAdmin(admin.ModelAdmin): class LockerUnitAdmin(admin.ModelAdmin):
inlines = [LockerRentalInline] inlines = [LockerInfoInline]
@admin.register(LockerRental) @admin.register(LockerInfo)
class LockerRentalAdmin(admin.ModelAdmin): class LockerInfoAdmin(admin.ModelAdmin):
search_fields = ["user__username", "locker_unit__bank__name"] search_fields = ["renter__username", "locker_unit__bank__name"]
list_filter = ["locker_unit__bank", "locker_unit", "renter"]
list_display = ["locker_unit", "address", "blind_code", "bitting_code", "renter"]
list_display_links = ["locker_unit", "address"]

View File

@ -1,6 +1,5 @@
# Generated by Django 3.2.11 on 2022-01-27 22:14 # Generated by Django 4.0.2 on 2022-02-16 21:19
from django.conf import settings
import django.core.validators 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
@ -11,7 +10,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("membershipworks", "0001_initial"),
] ]
operations = [ operations = [
@ -71,7 +70,7 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="LockerRental", name="LockerInfo",
fields=[ fields=[
( (
"id", "id",
@ -84,19 +83,38 @@ class Migration(migrations.Migration):
), ),
("column", models.PositiveIntegerField()), ("column", models.PositiveIntegerField()),
("row", models.PositiveIntegerField()), ("row", models.PositiveIntegerField()),
(
"blind_code",
models.CharField(
blank=True,
help_text="Stamped on some keys. Usually D###A.",
max_length=5,
),
),
(
"bitting_code",
models.CharField(
blank=True,
help_text="National Disc Tumbler, depths 1-4. Read bow-to-tip.",
max_length=5,
),
),
( (
"locker_unit", "locker_unit",
models.ForeignKey( models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
related_name="rentals", related_name="lockers",
to="rentals.lockerunit", to="rentals.lockerunit",
), ),
), ),
( (
"user", "renter",
models.ForeignKey( models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.CASCADE, on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL, to="membershipworks.member",
), ),
), ),
], ],
@ -107,4 +125,10 @@ class Migration(migrations.Migration):
fields=("bank", "index"), name="unique_bank_index" fields=("bank", "index"), name="unique_bank_index"
), ),
), ),
migrations.AddConstraint(
model_name="lockerinfo",
constraint=models.UniqueConstraint(
fields=("locker_unit", "column", "row"), name="unique_locker_info"
),
),
] ]

View File

@ -1,7 +1,8 @@
from django.contrib.auth import get_user_model
from django.core import validators from django.core import validators
from django.db import models from django.db import models
from membershipworks.models import Member
class LockerBank(models.Model): class LockerBank(models.Model):
"""A set of locker units, placed together""" """A set of locker units, placed together"""
@ -46,29 +47,50 @@ class LockerUnit(models.Model):
def number_for_locker(self, column: int, row: int) -> int: def number_for_locker(self, column: int, row: int) -> int:
return row + self.first_number + (self.columns - column - 1) * self.rows return row + self.first_number + (self.columns - column - 1) * self.rows
@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
(locker, _) = self.lockers.get_or_create(
locker_unit=self, row=row, column=column
)
yield ( yield (
self.letter_for_column(column), self.letter_for_column(column),
self.number_for_locker(column, row), self.number_for_locker(column, row),
self.rentals.filter(row=row, column=column), locker,
) )
# TODO: add check constraint to ensure that column and number are within locker_unit bounds # TODO: add check constraint to ensure that column and number are within locker_unit bounds
# TODO: add unique constraint on (unit, row, column)? class LockerInfo(models.Model):
class LockerRental(models.Model): """Information about a single locker"""
"""A rental of a single locker"""
locker_unit = models.ForeignKey( locker_unit = models.ForeignKey(
LockerUnit, on_delete=models.CASCADE, related_name="rentals" LockerUnit, on_delete=models.CASCADE, related_name="lockers"
) )
column = models.PositiveIntegerField() column = models.PositiveIntegerField()
row = models.PositiveIntegerField() row = models.PositiveIntegerField()
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) 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
)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["locker_unit", "column", "row"], name="unique_locker_info"
)
]
@property @property
def address(self) -> str: def address(self) -> str:
@ -77,4 +99,4 @@ class LockerRental(models.Model):
return f"{letter}{number}" return f"{letter}{number}"
def __str__(self): def __str__(self):
return f"{self.user}: {self.locker_unit}-{self.address}" return f"{self.locker_unit}-{self.address} [{self.renter}]"

View File

@ -7,20 +7,21 @@
{% block content %} {% block content %}
<style> <style>
.locker-bank { .locker-bank {
width: min-content;
text-align: center; text-align: center;
} }
.lockers { .lockers {
padding: 1rem;
background-color: #333333;
border-radius: 0.4rem;
display: flex; display: flex;
gap: 0.5rem; gap: 0.3rem;
flex-wrap: wrap;
justify-content: center;
} }
.locker { .locker {
display: grid; display: grid;
background-color: #333333;
border-radius: 0.4rem;
padding: 0.7rem;
gap: 0.5rem; gap: 0.5rem;
} }
@ -40,7 +41,7 @@
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;
@ -51,29 +52,32 @@
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-rented="false"]) {
background-color: hsl(24, 15%, 25%);; background-color: hsl(24, 15%, 25%);;
} }
</style> </style>
{% for bank in locker_banks %} {% for bank, units in locker_banks.items %}
<div class="locker-bank" data-bank="{{ bank.slug }}"> <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="lockers"> <div class="lockers">
{% for unit in bank.units.all %} {% for unit, lockers in units.items %}
<div class="locker" data-columns="{{ unit.columns }}"> <div class="locker" data-columns="{{ unit.columns }}">
{% for row, column, rentals in unit.iter_doors %} {% for row, column, locker, form in lockers %}
<div class="door" <div class="dropdown">
data-rentals="{{ rentals|length }}" <span class="door {{ perms.rentals.view_lockerinfo|yesno:"btn dropdown-toggle," }}"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
data-rented="{{ locker.renter|yesno:"true,false" }}"
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 locker.renter is not None %}
{# TODO: Should check for more specific permission #} {% if perms.rentals.view_lockerinfo %}
{% if user.is_staff %} {{ locker.renter }}
{% for rental in rentals %}{{ rental.user }}{% endfor %}
{% else %} {% else %}
Occupied Occupied
{% endif %} {% endif %}
@ -81,6 +85,35 @@
Empty Empty
{% endif %} {% endif %}
</div> </div>
</span>
{% if perms.rentals.view_lockerinfo %}
<div class="dropdown-menu">
<div class="dropdown-header">{{ locker }}</div>
<form class="p-2"
action="{% url 'rentals:locker' locker.id %}"
method="post">
{% csrf_token %}
<fieldset {{ perms.rentals.change_lockerinfo|yesno:",disabled" }}>
{% for field in form.visible_fields %}
<div class="form-floating mb-3">
{{ field }}
{{ field.label_tag }}
{{ field.errors }}
{% if field.help_text %}
<p class="form-text">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>
{% endfor %}
</fieldset>
{% if perms.rentals.change_lockerinfo %}
{% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
<input class="btn btn-primary" type="submit" value="Submit">
{% endif %}
</form>
</div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -6,4 +6,5 @@ app_name = "rentals"
urlpatterns = [ urlpatterns = [
path("", views.lockerIndex, name="index"), path("", views.lockerIndex, name="index"),
path("locker/<int:locker_id>", views.lockerUpdate, name="locker"),
] ]

View File

@ -1,10 +1,44 @@
from django.shortcuts import render from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import render, reverse
from .models import LockerBank, LockerRental from .models import LockerBank, LockerInfo
from .forms import LockerInfoForm
def lockerIndex(request): def lockerIndex(request):
locker_banks = {
bank: {
unit: [
(col, num, locker, LockerInfoForm(instance=locker))
for col, num, locker in unit.iter_doors()
]
for unit in bank.units.all()
}
for bank in LockerBank.objects.all()
}
context = { context = {
"locker_banks": LockerBank.objects.all(), "locker_banks": locker_banks,
} }
return render(request, "rentals/lockers.dj.html", context) return render(request, "rentals/lockers.dj.html", context)
@login_required
@permission_required("rentals.change_lockerinfo", raise_exception=True)
def lockerUpdate(request, locker_id: int):
if request.method == "POST":
try:
instance = LockerInfo.objects.get(pk=locker_id)
except LockerInfo.DoesNotExist:
pass # TODO
form = LockerInfoForm(request.POST, instance=instance)
if form.is_valid():
form.save()
else:
messages.add_message(request, messages.ERROR, form.errors)
return HttpResponseRedirect(reverse("rentals:index"))