Skip to content

Commit d73da51

Browse files
committed
test: room and slots creation/edition
1 parent f868432 commit d73da51

10 files changed

Lines changed: 249 additions & 74 deletions

File tree

club/templates/club/club_tools.jinja

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
{%- endfor -%}
4949
</ul>
5050
{%- else -%}
51-
{% trans %}This club manages no reservable room{% endtrans %}
51+
<p>
52+
{% trans %}This club manages no reservable room{% endtrans %}
53+
</p>
5254
{%- endif -%}
5355
<h4>{% trans %}Counters:{% endtrans %}</h4>
5456
<ul>

com/templates/com/news_list.jinja

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,12 @@
211211
<i class="fa-solid fa-magnifying-glass fa-xl"></i>
212212
<a href="{{ url("matmat:search_clear") }}">{% trans %}Matmatronch{% endtrans %}</a>
213213
</li>
214-
<li>
215-
<i class="fa-solid fa-thumbtack fa-xl"></i>
216-
<a href="{{ url("reservation:main") }}">{% trans %}Room reservation{% endtrans %}</a>
217-
</li>
214+
{% if user.has_perm("reservation.view_reservationslot") %}
215+
<li>
216+
<i class="fa-solid fa-thumbtack fa-xl"></i>
217+
<a href="{{ url("reservation:main") }}">{% trans %}Room reservation{% endtrans %}</a>
218+
</li>
219+
{% endif %}
218220
<li>
219221
<i class="fa-solid fa-check-to-slot fa-xl"></i>
220222
<a href="{{ url("election:list") }}">{% trans %}Elections{% endtrans %}</a>

reservation/api.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
from datetime import timedelta
21
from typing import Any, Literal
32

43
from django.core.exceptions import ValidationError
54
from ninja import Query
65
from ninja_extra import ControllerBase, api_controller, paginate, route
76
from ninja_extra.pagination import PageNumberPaginationExtra
87
from ninja_extra.schemas import PaginatedResponseSchema
9-
from pydantic import FutureDatetime
108

119
from api.permissions import HasPerm
1210
from reservation.models import ReservationSlot, Room
@@ -15,6 +13,7 @@
1513
RoomSchema,
1614
SlotFilterSchema,
1715
SlotSchema,
16+
UpdateReservationSlotSchema,
1817
)
1918

2019

@@ -52,11 +51,12 @@ def fetch_slots(self, filters: Query[SlotFilterSchema]):
5251
409: dict[Literal["detail"], dict[str, list[str]]],
5352
422: dict[Literal["detail"], list[dict[str, Any]]],
5453
},
54+
url_name="change_reservation_slot",
5555
)
56-
def update_slot(self, start: FutureDatetime, duration: timedelta, slot_id: int):
56+
def update_slot(self, slot_id: int, params: UpdateReservationSlotSchema):
5757
slot = self.get_object_or_exception(ReservationSlot, id=slot_id)
58-
slot.start_at = start
59-
slot.end_at = start + duration
58+
slot.start_at = params.start_at
59+
slot.end_at = params.end_at
6060
try:
6161
slot.full_clean()
6262
slot.save()

reservation/models.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from django.core.exceptions import ValidationError
66
from django.db import models
77
from django.db.models import F, Q
8-
from django.urls import reverse
98
from django.utils.translation import gettext_lazy as _
109

1110
from club.models import Club
@@ -39,9 +38,6 @@ class Meta:
3938
def __str__(self):
4039
return self.name
4140

42-
def get_absolute_url(self):
43-
return reverse("reservation:room_detail", kwargs={"room_id": self.id})
44-
4541
def can_be_edited_by(self, user: User) -> bool:
4642
# a user may edit a room if it has the global perm
4743
# or is in the owner club board
@@ -95,9 +91,10 @@ def clean(self):
9591
# so in this case, don't do the overlap check and let
9692
# Django manage the non-null constraint error.
9793
return
98-
if (
99-
ReservationSlot.objects.overlapping_with(self)
100-
.filter(room_id=self.room_id)
101-
.exists()
102-
):
94+
overlapping = ReservationSlot.objects.overlapping_with(self).filter(
95+
room_id=self.room_id
96+
)
97+
if self.id is not None:
98+
overlapping = overlapping.exclude(id=self.id)
99+
if overlapping.exists():
103100
raise ValidationError(_("There is already a reservation on this slot."))

reservation/schemas.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from datetime import datetime
22

3-
from ninja import FilterSchema, ModelSchema
4-
from pydantic import Field
3+
from ninja import FilterSchema, ModelSchema, Schema
4+
from pydantic import Field, FutureDatetime
55

66
from club.schemas import SimpleClubSchema
77
from core.schemas import SimpleUserSchema
@@ -39,3 +39,8 @@ class Meta:
3939
start: datetime = Field(alias="start_at")
4040
end: datetime = Field(alias="end_at")
4141
author: SimpleUserSchema
42+
43+
44+
class UpdateReservationSlotSchema(Schema):
45+
start_at: FutureDatetime
46+
end_at: FutureDatetime

reservation/static/bundled/reservation/components/room-scheduler-index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ export class RoomScheduler extends inheritHtmlElement("div") {
8080
const response = await reservationslotUpdateSlot({
8181
// biome-ignore lint/style/useNamingConvention: api is snake_case
8282
path: { slot_id: Number.parseInt(args.event.id) },
83-
query: {
84-
start: args.event.startStr,
85-
duration: `PT${duration.getUTCHours()}H${duration.getUTCMinutes()}M${duration.getUTCSeconds()}S`,
83+
body: {
84+
start_at: args.event.startStr,
85+
end_at: args.event.endStr,
8686
},
8787
});
8888
if (response.response.ok) {

reservation/tests/test_room.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import pytest
2+
from django.contrib.auth.models import Permission
3+
from django.test import Client
4+
from django.urls import reverse
5+
from model_bakery import baker
6+
from pytest_django.asserts import assertNumQueries, assertRedirects
7+
8+
from club.models import Club
9+
from core.models import User
10+
from reservation.forms import RoomUpdateForm
11+
from reservation.models import Room
12+
13+
14+
@pytest.mark.django_db
15+
class TestFetchRoom:
16+
@pytest.fixture
17+
def user(self):
18+
return baker.make(
19+
User,
20+
user_permissions=[Permission.objects.get(codename="view_room")],
21+
)
22+
23+
def test_fetch_simple(self, client: Client, user: User):
24+
rooms = baker.make(Room, _quantity=3, _bulk_create=True)
25+
client.force_login(user)
26+
response = client.get(reverse("api:fetch_reservable_rooms"))
27+
assert response.status_code == 200
28+
assert response.json() == [
29+
{
30+
"id": room.id,
31+
"name": room.name,
32+
"description": room.description,
33+
"location": room.location,
34+
"club": {"id": room.club.id, "name": room.club.name},
35+
}
36+
for room in rooms
37+
]
38+
39+
def test_nb_queries(self, client: Client, user: User):
40+
client.force_login(user)
41+
with assertNumQueries(5):
42+
# 4 for authentication
43+
# 1 to fetch the actual data
44+
client.get(reverse("api:fetch_reservable_rooms"))
45+
46+
47+
@pytest.mark.django_db
48+
class TestCreateRoom:
49+
def test_ok(self, client: Client):
50+
perm = Permission.objects.get(codename="add_room")
51+
club = baker.make(Club)
52+
client.force_login(
53+
baker.make(User, user_permissions=[perm], groups=[club.board_group])
54+
)
55+
response = client.post(
56+
reverse("reservation:room_create"),
57+
data={"club": club.id, "name": "test", "location": "BELFORT"},
58+
)
59+
assertRedirects(response, reverse("club:tools", kwargs={"club_id": club.id}))
60+
room = Room.objects.last()
61+
assert room is not None
62+
assert room.club == club
63+
assert room.name == "test"
64+
assert room.location == "BELFORT"
65+
66+
def test_permission_denied(self, client: Client):
67+
club = baker.make(Club)
68+
client.force_login(baker.make(User))
69+
response = client.get(reverse("reservation:room_create"))
70+
assert response.status_code == 403
71+
response = client.post(
72+
reverse("reservation:room_create"),
73+
data={"club": club.id, "name": "test", "location": "BELFORT"},
74+
)
75+
assert response.status_code == 403
76+
77+
78+
@pytest.mark.django_db
79+
class TestUpdateRoom:
80+
def test_ok(self, client: Client):
81+
club = baker.make(Club)
82+
room = baker.make(Room, club=club)
83+
client.force_login(baker.make(User, groups=[club.board_group]))
84+
url = reverse("reservation:room_edit", kwargs={"room_id": room.id})
85+
response = client.post(url, data={"name": "test", "location": "BELFORT"})
86+
assertRedirects(response, url)
87+
room.refresh_from_db()
88+
assert room.club == club
89+
assert room.name == "test"
90+
assert room.location == "BELFORT"
91+
92+
def test_permission_denied(self, client: Client):
93+
club = baker.make(Club)
94+
room = baker.make(Room, club=club)
95+
client.force_login(baker.make(User))
96+
url = reverse("reservation:room_edit", kwargs={"room_id": room.id})
97+
response = client.get(url)
98+
assert response.status_code == 403
99+
response = client.post(url, data={"name": "test", "location": "BELFORT"})
100+
assert response.status_code == 403
101+
102+
103+
@pytest.mark.django_db
104+
class TestUpdateRoomForm:
105+
def test_form_club_edition_rights(self):
106+
"""The club field should appear only if the request user can edit it."""
107+
room = baker.make(Room)
108+
perm = Permission.objects.get(codename="change_room")
109+
user_authorized = baker.make(User, user_permissions=[perm])
110+
assert "club" in RoomUpdateForm(request_user=user_authorized).fields
111+
112+
user_forbidden = baker.make(User, groups=[room.club.board_group])
113+
assert "club" not in RoomUpdateForm(request_user=user_forbidden).fields

reservation/tests/test_room_api.py

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)