create reservation models

This commit is contained in:
Thomas Girod 2025-04-13 15:09:42 +02:00 committed by imperosol
parent 521e7b1254
commit da700a1c2c
10 changed files with 288 additions and 1 deletions

View File

@ -788,7 +788,16 @@ class Command(BaseCommand):
subscribers = Group.objects.create(name="Subscribers")
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.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), "location")
autocomplete_fields = ("club",)
search_fields = ("name",)
@admin.register(ReservationSlot)
class ReservationSlotAdmin(admin.ModelAdmin):
list_display = ("room", "start_at", "end_at", "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,138 @@
# Generated by Django 5.2.1 on 2025-06-05 10:44
import django.core.validators
import django.db.models.deletion
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,
),
),
(
"location",
models.CharField(
blank=True,
choices=[
("BELFORT", "Belfort"),
("SEVENANS", "Sévenans"),
("MONTBELIARD", "Montbéliard"),
],
verbose_name="site",
),
),
(
"club",
models.ForeignKey(
help_text="The club which manages this room",
on_delete=django.db.models.deletion.CASCADE,
related_name="reservable_rooms",
to="club.club",
verbose_name="room owner",
),
),
],
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"),
),
("end_at", 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",
"constraints": [
models.CheckConstraint(
condition=models.Q(("end_at__gt", models.F("start_at"))),
name="reservation_slot_end_after_start",
)
],
},
),
]

View File

112
reservation/models.py Normal file
View File

@ -0,0 +1,112 @@
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, Q
from django.urls import reverse
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"),
help_text=_("The club which manages this room"),
)
location = models.CharField(
_("site"),
blank=True,
choices=[
("BELFORT", "Belfort"),
("SEVENANS", "Sévenans"),
("MONTBELIARD", "Montbéliard"),
],
)
class Meta:
verbose_name = _("reservable room")
verbose_name_plural = _("reservable rooms")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse("reservation:room_detail", kwargs={"room_id": self.id})
def can_be_edited_by(self, user: User) -> bool:
# a user may edit a room if it has the global perm
# or is in the owner club board
return user.has_perm("reservation.change_room") or self.club.board_group_id in [
g.id for g in user.cached_groups
]
class ReservationSlotQuerySet(models.QuerySet):
def overlapping_with(self, other: ReservationSlot) -> Self:
return self.filter(
Q(end_at__gt=other.start_at, end_at__lt=other.end_at)
| Q(start_at__lt=other.start_at, start_at__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)
end_at = models.DateTimeField(verbose_name=_("slot end"))
created_at = models.DateTimeField(auto_now_add=True)
objects = ReservationSlotQuerySet.as_manager()
class Meta:
verbose_name = _("reservation slot")
verbose_name_plural = _("reservation slots")
constraints = [
models.CheckConstraint(
check=Q(end_at__gt=F("start_at")),
name="reservation_slot_end_after_start",
)
]
def __str__(self):
return f"{self.room.name} : {self.start_at} - {self.end_at}"
def clean(self):
super().clean()
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

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