create reservation models

This commit is contained in:
Thomas Girod 2025-04-13 15:09:42 +02:00 committed by imperosol
parent 4f5a69c353
commit 691d956c0e
10 changed files with 286 additions and 1 deletions

View File

@ -812,7 +812,16 @@ Welcome to the wiki page!
subscribers = Group.objects.create(name="Subscribers") subscribers = Group.objects.create(name="Subscribers")
subscribers.permissions.add( subscribers.permissions.add(
*list(perms.filter(codename__in=["add_news", "add_uvcomment"])) *list(
perms.filter(
codename__in=[
"add_news",
"add_uvcomment",
"add_reservationslot",
"view_reservationslot",
]
)
)
) )
old_subscribers = Group.objects.create(name="Old subscribers") old_subscribers = Group.objects.create(name="Old subscribers")
old_subscribers.permissions.add( old_subscribers.permissions.add(

0
reservation/__init__.py Normal file
View File

19
reservation/admin.py Normal file
View File

@ -0,0 +1,19 @@
from django.contrib import admin
from reservation.models import ReservationSlot, Room
@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
list_display = ("name", "club")
list_filter = (("club", admin.RelatedOnlyFieldListFilter),)
autocomplete_fields = ("club",)
search_fields = ("name",)
@admin.register(ReservationSlot)
class ReservationSlotAdmin(admin.ModelAdmin):
list_display = ("room", "start_at", "duration", "author")
autocomplete_fields = ("author",)
list_filter = ("room",)
date_hierarchy = "start_at"

6
reservation/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ReservationConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "reservation"

View File

@ -0,0 +1,141 @@
# Generated by Django 5.2 on 2025-04-21 14:19
import django.core.validators
import django.db.models.deletion
import django.db.models.expressions
from django.conf import settings
from django.db import migrations, models
import core.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
("club", "0014_alter_club_options_rename_unix_name_club_slug_name_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Room",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100, verbose_name="room name")),
(
"description",
models.TextField(
blank=True, default="", verbose_name="description"
),
),
(
"logo",
core.fields.ResizedImageField(
blank=True,
force_format="WEBP",
height=100,
upload_to="rooms",
verbose_name="logo",
width=100,
),
),
("address", models.CharField(max_length=255, verbose_name="address")),
(
"club",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="reservable_rooms",
to="club.club",
verbose_name="room owner",
),
),
(
"occupancy",
models.PositiveSmallIntegerField(
default=1,
help_text="The maximum number of people this room can host at once",
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="maximum occupancy",
),
),
],
options={
"verbose_name": "reservable room",
"verbose_name_plural": "reservable rooms",
},
),
migrations.CreateModel(
name="ReservationSlot",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"nb_people",
models.PositiveSmallIntegerField(
default=1,
help_text="How many people will attend this reservation slot",
validators=[django.core.validators.MinValueValidator(1)],
verbose_name="number of people",
),
),
(
"comment",
models.TextField(blank=True, default="", verbose_name="comment"),
),
(
"start_at",
models.DateTimeField(db_index=True, verbose_name="slot start"),
),
("duration", models.DurationField(verbose_name="duration")),
(
"end_at",
models.GeneratedField(
db_persist=False,
expression=django.db.models.expressions.CombinedExpression(
models.F("start_at"), "+", models.F("duration")
),
output_field=models.DateTimeField(),
verbose_name="slot end",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="author",
),
),
(
"room",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="slots",
to="reservation.room",
verbose_name="reserved room",
),
),
],
options={
"verbose_name": "reservation slot",
"verbose_name_plural": "reservation slots",
},
),
]

View File

107
reservation/models.py Normal file
View File

@ -0,0 +1,107 @@
from __future__ import annotations
from typing import Self
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import F
from django.db.models import F, Q
from django.utils.translation import gettext_lazy as _
from club.models import Club
from core.fields import ResizedImageField
from core.models import User
class Room(models.Model):
name = models.CharField(_("room name"), max_length=100)
description = models.TextField(_("description"), blank=True, default="")
logo = ResizedImageField(
width=100,
height=100,
force_format="WEBP",
upload_to="rooms",
verbose_name=_("logo"),
blank=True,
)
club = models.ForeignKey(
Club,
on_delete=models.CASCADE,
related_name="reservable_rooms",
verbose_name=_("room owner"),
)
address = models.CharField(_("address"), max_length=255)
occupancy = models.PositiveSmallIntegerField(
verbose_name=_("maximum occupancy"),
help_text=_("The maximum number of people this room can host at once"),
validators=[MinValueValidator(1)],
)
class Meta:
verbose_name = _("reservable room")
verbose_name_plural = _("reservable rooms")
def __str__(self):
return self.name
class ReservationSlotQuerySet(models.QuerySet):
def overlapping_with(self, other: ReservationSlot) -> Self:
return self.filter(
Q(end_at__gt=other.start_at, end_ad__lt=other.end_at)
| Q(start_at__lt=other.start_at, start_ad__gt=other.start_at)
)
class ReservationSlot(models.Model):
room = models.ForeignKey(
Room,
on_delete=models.CASCADE,
related_name="slots",
verbose_name=_("reserved room"),
)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("author"))
nb_people = models.PositiveSmallIntegerField(
verbose_name=_("number of people"),
help_text=_("How many people will attend this reservation slot"),
default=1,
validators=[MinValueValidator(1)],
)
comment = models.TextField(_("comment"), blank=True, default="")
start_at = models.DateTimeField(_("slot start"), db_index=True)
duration = models.DurationField(_("duration"))
end_at = models.GeneratedField(
verbose_name=_("slot end"),
expression=F("start_at") + F("duration"),
output_field=models.DateTimeField(),
db_persist=False,
)
created_at = models.DateTimeField(auto_now_add=True)
objects = ReservationSlotQuerySet.as_manager()
class Meta:
verbose_name = _("reservation slot")
verbose_name_plural = _("reservation slots")
def __str__(self):
return f"{self.room.name} : {self.start_at} - {self.end_at}"
def clean(self):
super().clean()
if self.nb_people > self.room.occupancy:
raise ValidationError(
_(
"You declared an attendance of %(nb_people)d, "
"but this room can only host %(occupancy)d people."
)
% {"nb_people": self.nb_people, "occupancy": self.room.occupancy}
)
if (
ReservationSlot.objects.overlapping_with(self)
.filter(room_id=self.room_id)
.exists()
):
raise ValidationError(_("There is already a reservation on this slot."))

1
reservation/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

1
reservation/views.py Normal file
View File

@ -0,0 +1 @@
# Create your views here.

View File

@ -121,6 +121,7 @@ INSTALLED_APPS = (
"trombi", "trombi",
"matmat", "matmat",
"pedagogy", "pedagogy",
"reservation",
"galaxy", "galaxy",
"antispam", "antispam",
) )