Skip to content

Commit fd28fb1

Browse files
committed
create reservation models
1 parent b1eae5b commit fd28fb1

10 files changed

Lines changed: 283 additions & 1 deletion

File tree

core/management/commands/populate.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,16 @@ def _create_groups(self) -> PopulatedGroups:
788788

789789
subscribers = Group.objects.create(name="Subscribers")
790790
subscribers.permissions.add(
791-
*list(perms.filter(codename__in=["add_news", "add_uvcomment"]))
791+
*list(
792+
perms.filter(
793+
codename__in=[
794+
"add_news",
795+
"add_uvcomment",
796+
"add_reservationslot",
797+
"view_reservationslot",
798+
]
799+
)
800+
)
792801
)
793802
old_subscribers = Group.objects.create(name="Old subscribers")
794803
old_subscribers.permissions.add(

reservation/__init__.py

Whitespace-only changes.

reservation/admin.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from django.contrib import admin
2+
3+
from reservation.models import ReservationSlot, Room
4+
5+
6+
@admin.register(Room)
7+
class RoomAdmin(admin.ModelAdmin):
8+
list_display = ("name", "club")
9+
list_filter = (("club", admin.RelatedOnlyFieldListFilter), "location")
10+
autocomplete_fields = ("club",)
11+
search_fields = ("name",)
12+
13+
14+
@admin.register(ReservationSlot)
15+
class ReservationSlotAdmin(admin.ModelAdmin):
16+
list_display = ("room", "start_at", "duration", "author")
17+
autocomplete_fields = ("author",)
18+
list_filter = ("room",)
19+
date_hierarchy = "start_at"

reservation/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ReservationConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "reservation"
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Generated by Django 5.2.1 on 2025-06-05 10:44
2+
3+
import django.core.validators
4+
import django.db.models.deletion
5+
import django.db.models.expressions
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
9+
import core.fields
10+
11+
12+
class Migration(migrations.Migration):
13+
initial = True
14+
15+
dependencies = [
16+
("club", "0014_alter_club_options_rename_unix_name_club_slug_name_and_more"),
17+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
18+
]
19+
20+
operations = [
21+
migrations.CreateModel(
22+
name="Room",
23+
fields=[
24+
(
25+
"id",
26+
models.BigAutoField(
27+
auto_created=True,
28+
primary_key=True,
29+
serialize=False,
30+
verbose_name="ID",
31+
),
32+
),
33+
("name", models.CharField(max_length=100, verbose_name="room name")),
34+
(
35+
"description",
36+
models.TextField(
37+
blank=True, default="", verbose_name="description"
38+
),
39+
),
40+
(
41+
"logo",
42+
core.fields.ResizedImageField(
43+
blank=True,
44+
force_format="WEBP",
45+
height=100,
46+
upload_to="rooms",
47+
verbose_name="logo",
48+
width=100,
49+
),
50+
),
51+
(
52+
"location",
53+
models.CharField(
54+
blank=True,
55+
choices=[
56+
("BELFORT", "Belfort"),
57+
("SEVENANS", "Sévenans"),
58+
("MONTBELIARD", "Montbéliard"),
59+
],
60+
verbose_name="site",
61+
),
62+
),
63+
(
64+
"club",
65+
models.ForeignKey(
66+
help_text="The club which manages this room",
67+
on_delete=django.db.models.deletion.CASCADE,
68+
related_name="reservable_rooms",
69+
to="club.club",
70+
verbose_name="room owner",
71+
),
72+
),
73+
],
74+
options={
75+
"verbose_name": "reservable room",
76+
"verbose_name_plural": "reservable rooms",
77+
},
78+
),
79+
migrations.CreateModel(
80+
name="ReservationSlot",
81+
fields=[
82+
(
83+
"id",
84+
models.BigAutoField(
85+
auto_created=True,
86+
primary_key=True,
87+
serialize=False,
88+
verbose_name="ID",
89+
),
90+
),
91+
(
92+
"nb_people",
93+
models.PositiveSmallIntegerField(
94+
default=1,
95+
help_text="How many people will attend this reservation slot",
96+
validators=[django.core.validators.MinValueValidator(1)],
97+
verbose_name="number of people",
98+
),
99+
),
100+
(
101+
"comment",
102+
models.TextField(blank=True, default="", verbose_name="comment"),
103+
),
104+
(
105+
"start_at",
106+
models.DateTimeField(db_index=True, verbose_name="slot start"),
107+
),
108+
("duration", models.DurationField(verbose_name="duration")),
109+
(
110+
"end_at",
111+
models.GeneratedField(
112+
db_persist=False,
113+
expression=django.db.models.expressions.CombinedExpression(
114+
models.F("start_at"), "+", models.F("duration")
115+
),
116+
output_field=models.DateTimeField(),
117+
verbose_name="slot end",
118+
),
119+
),
120+
("created_at", models.DateTimeField(auto_now_add=True)),
121+
(
122+
"author",
123+
models.ForeignKey(
124+
on_delete=django.db.models.deletion.CASCADE,
125+
to=settings.AUTH_USER_MODEL,
126+
verbose_name="author",
127+
),
128+
),
129+
(
130+
"room",
131+
models.ForeignKey(
132+
on_delete=django.db.models.deletion.CASCADE,
133+
related_name="slots",
134+
to="reservation.room",
135+
verbose_name="reserved room",
136+
),
137+
),
138+
],
139+
options={
140+
"verbose_name": "reservation slot",
141+
"verbose_name_plural": "reservation slots",
142+
},
143+
),
144+
]

reservation/migrations/__init__.py

Whitespace-only changes.

reservation/models.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from __future__ import annotations
2+
3+
from typing import Self
4+
5+
from django.core.exceptions import ValidationError
6+
from django.core.validators import MinValueValidator
7+
from django.db import models
8+
from django.db.models import F, Q
9+
from django.utils.translation import gettext_lazy as _
10+
11+
from club.models import Club
12+
from core.fields import ResizedImageField
13+
from core.models import User
14+
15+
16+
class Room(models.Model):
17+
name = models.CharField(_("room name"), max_length=100)
18+
description = models.TextField(_("description"), blank=True, default="")
19+
logo = ResizedImageField(
20+
width=100,
21+
height=100,
22+
force_format="WEBP",
23+
upload_to="rooms",
24+
verbose_name=_("logo"),
25+
blank=True,
26+
)
27+
club = models.ForeignKey(
28+
Club,
29+
on_delete=models.CASCADE,
30+
related_name="reservable_rooms",
31+
verbose_name=_("room owner"),
32+
help_text=_("The club which manages this room"),
33+
)
34+
location = models.CharField(
35+
_("site"),
36+
blank=True,
37+
choices=[
38+
("BELFORT", "Belfort"),
39+
("SEVENANS", "Sévenans"),
40+
("MONTBELIARD", "Montbéliard"),
41+
],
42+
)
43+
44+
class Meta:
45+
verbose_name = _("reservable room")
46+
verbose_name_plural = _("reservable rooms")
47+
48+
def __str__(self):
49+
return self.name
50+
51+
52+
class ReservationSlotQuerySet(models.QuerySet):
53+
def overlapping_with(self, other: ReservationSlot) -> Self:
54+
return self.filter(
55+
Q(end_at__gt=other.start_at, end_ad__lt=other.end_at)
56+
| Q(start_at__lt=other.start_at, start_ad__gt=other.start_at)
57+
)
58+
59+
60+
class ReservationSlot(models.Model):
61+
room = models.ForeignKey(
62+
Room,
63+
on_delete=models.CASCADE,
64+
related_name="slots",
65+
verbose_name=_("reserved room"),
66+
)
67+
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("author"))
68+
nb_people = models.PositiveSmallIntegerField(
69+
verbose_name=_("number of people"),
70+
help_text=_("How many people will attend this reservation slot"),
71+
default=1,
72+
validators=[MinValueValidator(1)],
73+
)
74+
comment = models.TextField(_("comment"), blank=True, default="")
75+
start_at = models.DateTimeField(_("slot start"), db_index=True)
76+
duration = models.DurationField(_("duration"))
77+
end_at = models.GeneratedField(
78+
verbose_name=_("slot end"),
79+
expression=F("start_at") + F("duration"),
80+
output_field=models.DateTimeField(),
81+
db_persist=False,
82+
)
83+
created_at = models.DateTimeField(auto_now_add=True)
84+
85+
objects = ReservationSlotQuerySet.as_manager()
86+
87+
class Meta:
88+
verbose_name = _("reservation slot")
89+
verbose_name_plural = _("reservation slots")
90+
91+
def __str__(self):
92+
return f"{self.room.name} : {self.start_at} - {self.end_at}"
93+
94+
def clean(self):
95+
super().clean()
96+
if (
97+
ReservationSlot.objects.overlapping_with(self)
98+
.filter(room_id=self.room_id)
99+
.exists()
100+
):
101+
raise ValidationError(_("There is already a reservation on this slot."))

reservation/tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your tests here.

reservation/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your views here.

sith/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def optional_file_parser(value: str) -> Path | None:
123123
"trombi",
124124
"matmat",
125125
"pedagogy",
126+
"reservation",
126127
"galaxy",
127128
"antispam",
128129
)

0 commit comments

Comments
 (0)