[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 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"]
|
||||||
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -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}]"
|
||||||
|
@ -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>
|
||||||
|
@ -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"),
|
||||||
]
|
]
|
||||||
|
@ -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"))
|
||||||
|
Loading…
Reference in New Issue
Block a user