Add first attempt at locker rental management
This commit is contained in:
parent
1577cb7654
commit
0d34d4832a
@ -23,6 +23,7 @@ ALLOWED_HOSTS = []
|
|||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'tasks.apps.TasksConfig',
|
'tasks.apps.TasksConfig',
|
||||||
|
'rentals.apps.RentalsConfig',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'markdownx',
|
'markdownx',
|
||||||
'markdownify',
|
'markdownify',
|
||||||
|
@ -23,6 +23,7 @@ from django.contrib.auth.views import LoginView, LogoutView
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', lambda request: redirect('/tasks/')),
|
path('', lambda request: redirect('/tasks/')),
|
||||||
path('tasks/', include('tasks.urls')),
|
path('tasks/', include('tasks.urls')),
|
||||||
|
path('rentals/', include('rentals.urls')),
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('auth/', include([
|
path('auth/', include([
|
||||||
path('login/', LoginView.as_view(
|
path('login/', LoginView.as_view(
|
||||||
|
0
rentals/__init__.py
Normal file
0
rentals/__init__.py
Normal file
14
rentals/admin.py
Normal file
14
rentals/admin.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import LockerBank, LockerRental
|
||||||
|
|
||||||
|
class LockerRentalInline(admin.TabularInline):
|
||||||
|
model = LockerRental
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
@admin.register(LockerBank)
|
||||||
|
class LockerBankAdmin(admin.ModelAdmin):
|
||||||
|
inlines = [LockerRentalInline]
|
||||||
|
prepopulated_fields = {"slug": ("name",)}
|
||||||
|
|
||||||
|
admin.site.register(LockerRental)
|
6
rentals/apps.py
Normal file
6
rentals/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class RentalsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'rentals'
|
38
rentals/migrations/0001_initial.py
Normal file
38
rentals/migrations/0001_initial.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 3.2.11 on 2022-01-24 04:08
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LockerBank',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=200)),
|
||||||
|
('location', models.CharField(max_length=200)),
|
||||||
|
('slug', models.SlugField(unique=True)),
|
||||||
|
('rows', models.PositiveIntegerField()),
|
||||||
|
('columns', models.PositiveIntegerField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='LockerRental',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('row', models.PositiveIntegerField()),
|
||||||
|
('column', models.PositiveIntegerField()),
|
||||||
|
('locker_bank', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rentals.lockerbank')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
rentals/migrations/__init__.py
Normal file
0
rentals/migrations/__init__.py
Normal file
41
rentals/models.py
Normal file
41
rentals/models.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class LockerBank(models.Model):
|
||||||
|
name = models.CharField(max_length=200)
|
||||||
|
location = models.CharField(max_length=200)
|
||||||
|
slug = models.SlugField(unique=True)
|
||||||
|
rows = models.PositiveIntegerField()
|
||||||
|
columns = models.PositiveIntegerField()
|
||||||
|
# TODO: add constraint for unique first letter?
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.columns}x{self.rows})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def initial(self):
|
||||||
|
return self.name[0].upper()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def iter_doors(self):
|
||||||
|
for row in range(self.rows):
|
||||||
|
for column in range(self.columns):
|
||||||
|
# TODO: filter could be optimized
|
||||||
|
yield chr(row + ord('A')), column + 1, self.rentals.filter(row=row, column=column)
|
||||||
|
|
||||||
|
|
||||||
|
class LockerRental(models.Model):
|
||||||
|
locker_bank = models.ForeignKey(LockerBank, on_delete=models.CASCADE, related_name="rentals")
|
||||||
|
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
|
||||||
|
row = models.PositiveIntegerField()
|
||||||
|
column = models.PositiveIntegerField()
|
||||||
|
# TODO: add check constraint to ensure that row and column are within locker_bank bounds
|
||||||
|
# TODO: add unique constraint on (bank, row, column)?
|
||||||
|
|
||||||
|
@property
|
||||||
|
def address(self) -> str:
|
||||||
|
return f"{self.locker_bank.name}-{self.locker_bank.initial}{chr(self.row + ord('A'))}{self.column}"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.user}: {self.address}"
|
82
rentals/templates/rentals/lockers.dj.html
Normal file
82
rentals/templates/rentals/lockers.dj.html
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{% extends "base.dj.html" %}
|
||||||
|
|
||||||
|
{% block title %}Lockers Index{% endblock %}
|
||||||
|
{% block admin_link %}/admin/rentals/{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.locker-bank {
|
||||||
|
width: min-content;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locker {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #333333;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locker[data-columns="2"] {
|
||||||
|
grid-template-columns: repeat(2, min-content);
|
||||||
|
}
|
||||||
|
.locker[data-columns="4"] {
|
||||||
|
grid-template-columns: repeat(4, min-content);
|
||||||
|
}
|
||||||
|
.locker[data-columns="6"] {
|
||||||
|
grid-template-columns: repeat(6, min-content);
|
||||||
|
}
|
||||||
|
.locker[data-columns="8"] {
|
||||||
|
grid-template-columns: repeat(8, min-content);
|
||||||
|
}
|
||||||
|
.locker[data-columns="10"] {
|
||||||
|
grid-template-columns: repeat(10, min-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.locker .door {
|
||||||
|
background-color: hsl(24, 100%, 25%);
|
||||||
|
border: 0.2rem solid black;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 7rem;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: lightgray;
|
||||||
|
}
|
||||||
|
.locker .door:not([data-rentals="0"]) {
|
||||||
|
background-color: hsl(24, 15%, 25%);;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% for bank in locker_banks %}
|
||||||
|
<div class="locker-bank">
|
||||||
|
<h2>{{ bank.name }}</h2>
|
||||||
|
<div>{{ bank.location }}</div>
|
||||||
|
<div class="locker"
|
||||||
|
data-columns="{{ bank.columns }}"
|
||||||
|
data-bank="{{ bank.slug }}">
|
||||||
|
{% for row, column, rentals in bank.iter_doors %}
|
||||||
|
<div class="door"
|
||||||
|
data-rentals="{{ rentals|length }}"
|
||||||
|
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 %}
|
||||||
|
{% else %}
|
||||||
|
Occupied
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
Empty
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
3
rentals/tests.py
Normal file
3
rentals/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
8
rentals/urls.py
Normal file
8
rentals/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.lockerIndex, name="index"),
|
||||||
|
]
|
10
rentals/views.py
Normal file
10
rentals/views.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from .models import LockerBank, LockerRental
|
||||||
|
|
||||||
|
|
||||||
|
def lockerIndex(request):
|
||||||
|
context = {
|
||||||
|
"locker_banks": LockerBank.objects.all(),
|
||||||
|
}
|
||||||
|
return render(request, "rentals/lockers.dj.html", context)
|
Loading…
Reference in New Issue
Block a user