[rentals] Store more info on lockers and allow editing via front end
This commit is contained in:
parent
83c97197a7
commit
24e8bf819b
@ -1,6 +1,11 @@
|
||||
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):
|
||||
@ -14,16 +19,14 @@ class LockerBankAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {"slug": ("name",)}
|
||||
|
||||
|
||||
class LockerRentalInline(admin.TabularInline):
|
||||
model = LockerRental
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(LockerUnit)
|
||||
class LockerUnitAdmin(admin.ModelAdmin):
|
||||
inlines = [LockerRentalInline]
|
||||
inlines = [LockerInfoInline]
|
||||
|
||||
|
||||
@admin.register(LockerRental)
|
||||
class LockerRentalAdmin(admin.ModelAdmin):
|
||||
search_fields = ["user__username", "locker_unit__bank__name"]
|
||||
@admin.register(LockerInfo)
|
||||
class LockerInfoAdmin(admin.ModelAdmin):
|
||||
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"]
|
||||
|
@ -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
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
@ -11,7 +10,7 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("membershipworks", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@ -71,7 +70,7 @@ class Migration(migrations.Migration):
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LockerRental",
|
||||
name="LockerInfo",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
@ -84,19 +83,38 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
("column", 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",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="rentals",
|
||||
related_name="lockers",
|
||||
to="rentals.lockerunit",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
"renter",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
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"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lockerinfo",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("locker_unit", "column", "row"), name="unique_locker_info"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -1,7 +1,8 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
|
||||
from membershipworks.models import Member
|
||||
|
||||
|
||||
class LockerBank(models.Model):
|
||||
"""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:
|
||||
return row + self.first_number + (self.columns - column - 1) * self.rows
|
||||
|
||||
@property
|
||||
def iter_doors(self):
|
||||
for row in range(self.rows):
|
||||
for column in range(self.columns):
|
||||
# TODO: filter could be optimized
|
||||
(locker, _) = self.lockers.get_or_create(
|
||||
locker_unit=self, row=row, column=column
|
||||
)
|
||||
|
||||
yield (
|
||||
self.letter_for_column(column),
|
||||
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 unique constraint on (unit, row, column)?
|
||||
class LockerRental(models.Model):
|
||||
"""A rental of a single locker"""
|
||||
class LockerInfo(models.Model):
|
||||
"""Information about a single locker"""
|
||||
|
||||
locker_unit = models.ForeignKey(
|
||||
LockerUnit, on_delete=models.CASCADE, related_name="rentals"
|
||||
LockerUnit, on_delete=models.CASCADE, related_name="lockers"
|
||||
)
|
||||
column = 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
|
||||
def address(self) -> str:
|
||||
@ -77,4 +99,4 @@ class LockerRental(models.Model):
|
||||
return f"{letter}{number}"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user}: {self.locker_unit}-{self.address}"
|
||||
return f"{self.locker_unit}-{self.address} [{self.renter}]"
|
||||
|
@ -7,20 +7,21 @@
|
||||
{% block content %}
|
||||
<style>
|
||||
.locker-bank {
|
||||
width: min-content;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lockers {
|
||||
padding: 1rem;
|
||||
background-color: #333333;
|
||||
border-radius: 0.4rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.3rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.locker {
|
||||
display: grid;
|
||||
background-color: #333333;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.7rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@ -40,7 +41,7 @@
|
||||
grid-template-columns: repeat(10, min-content);
|
||||
}
|
||||
|
||||
.locker .door {
|
||||
.locker .door {
|
||||
background-color: hsl(24, 100%, 25%);
|
||||
border: 0.2rem solid black;
|
||||
border-radius: 0.5rem;
|
||||
@ -51,29 +52,32 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: lightgray;
|
||||
}
|
||||
.locker .door:not([data-rentals="0"]) {
|
||||
}
|
||||
.locker .door:not([data-rented="false"]) {
|
||||
background-color: hsl(24, 15%, 25%);;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% for bank in locker_banks %}
|
||||
{% for bank, units in locker_banks.items %}
|
||||
<div class="locker-bank" data-bank="{{ bank.slug }}">
|
||||
<h2>{{ bank.name }}</h2>
|
||||
<div>{{ bank.location }}</div>
|
||||
<div class="lockers">
|
||||
{% for unit in bank.units.all %}
|
||||
{% for unit, lockers in units.items %}
|
||||
<div class="locker" data-columns="{{ unit.columns }}">
|
||||
{% for row, column, rentals in unit.iter_doors %}
|
||||
<div class="door"
|
||||
data-rentals="{{ rentals|length }}"
|
||||
{% for row, column, locker, form in lockers %}
|
||||
<div class="dropdown">
|
||||
<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-column="{{ column }}">
|
||||
<h3 class="locker-name">{{ bank.initial }}{{ row }}{{ column }}</h3>
|
||||
<div class="locker-status">
|
||||
{% if rentals|length > 0 %}
|
||||
{# TODO: Should check for more specific permission #}
|
||||
{% if user.is_staff %}
|
||||
{% for rental in rentals %}{{ rental.user }}{% endfor %}
|
||||
{% if locker.renter is not None %}
|
||||
{% if perms.rentals.view_lockerinfo %}
|
||||
{{ locker.renter }}
|
||||
{% else %}
|
||||
Occupied
|
||||
{% endif %}
|
||||
@ -81,6 +85,35 @@
|
||||
Empty
|
||||
{% endif %}
|
||||
</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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -6,4 +6,5 @@ app_name = "rentals"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.lockerIndex, name="index"),
|
||||
path("locker/<int:locker_id>", views.lockerUpdate, name="locker"),
|
||||
]
|
||||
|
@ -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):
|
||||
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 = {
|
||||
"locker_banks": LockerBank.objects.all(),
|
||||
"locker_banks": locker_banks,
|
||||
}
|
||||
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"))
|
||||
|
Loading…
Reference in New Issue
Block a user