Skip to content

Commit ecaeda3

Browse files
Allow setting room sizes from the room (#4594)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Marco Acierno <[email protected]>
1 parent 2073cea commit ecaeda3

File tree

8 files changed

+133
-20
lines changed

8 files changed

+133
-20
lines changed

backend/api/conferences/types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,11 +270,15 @@ def keynote(self, info: Info, slug: str) -> Keynote | None:
270270

271271
@strawberry.field
272272
def talks(self, info: Info) -> list[ScheduleItem]:
273-
return self.schedule_items.filter(type=ScheduleItemModel.TYPES.submission).all()
273+
return (
274+
self.schedule_items.filter(type=ScheduleItemModel.TYPES.submission)
275+
.prefetch_related("rooms")
276+
.all()
277+
)
274278

275279
@strawberry.field
276280
def talk(self, info: Info, slug: str) -> ScheduleItem | None:
277-
return self.schedule_items.filter(slug=slug).first()
281+
return self.schedule_items.filter(slug=slug).prefetch_related("rooms").first()
278282

279283
@strawberry.field
280284
def ranking(self, info: Info, topic: strawberry.ID) -> RankRequest | None:

backend/api/schedule/mutations/book_schedule_item.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ def book_schedule_item(info: Info, id: strawberry.ID) -> BookScheduleItemResult:
5656
):
5757
return UserNeedsConferenceTicket()
5858

59-
if schedule_item.attendees_total_capacity is None:
59+
if schedule_item.actual_attendees_total_capacity is None:
6060
return ScheduleItemNotBookable()
6161

6262
if schedule_item.attendees.filter(user_id=user_id).exists():
6363
return UserIsAlreadyBooked()
6464

65-
if schedule_item.attendees.count() >= schedule_item.attendees_total_capacity:
65+
if schedule_item.attendees.count() >= schedule_item.actual_attendees_total_capacity:
6666
return ScheduleItemIsFull()
6767

6868
ScheduleItemAttendee.objects.create(schedule_item=schedule_item, user_id=user_id)

backend/api/schedule/mutations/cancel_booking_schedule_item.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def cancel_booking_schedule_item(
3030
schedule_item = ScheduleItem.objects.get(id=id)
3131
user_id = info.context.request.user.id
3232

33-
if schedule_item.attendees_total_capacity is None:
33+
if schedule_item.actual_attendees_total_capacity is None:
3434
return ScheduleItemNotBookable()
3535

3636
if not schedule_item.attendees.filter(user_id=user_id).exists():

backend/api/schedule/tests/test_book_spot_schedule_item.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from schedule.tests.factories import (
44
DayFactory,
5+
RoomFactory,
56
ScheduleItemAttendeeFactory,
67
ScheduleItemFactory,
78
SlotFactory,
@@ -186,3 +187,69 @@ def test_user_cannot_book_any_event(graphql_client, user, simple_schedule_item,
186187
assert not ScheduleItemAttendee.objects.filter(
187188
schedule_item=schedule_item, user_id=user.id
188189
).exists()
190+
191+
192+
def test_user_book_with_limited_capacity_from_room(
193+
graphql_client, user, simple_schedule_item, mocker
194+
):
195+
mocker.patch(
196+
"api.schedule.mutations.book_schedule_item.user_has_admission_ticket",
197+
return_value=True,
198+
)
199+
200+
graphql_client.force_login(user)
201+
202+
schedule_item = simple_schedule_item
203+
schedule_item.rooms.add(RoomFactory(attendees_total_capacity=10))
204+
schedule_item.attendees_total_capacity = None
205+
schedule_item.save()
206+
207+
ScheduleItemAttendeeFactory.create_batch(2, schedule_item=schedule_item)
208+
209+
response = graphql_client.query(
210+
"""mutation($id: ID!) {
211+
bookScheduleItem(id: $id) {
212+
__typename
213+
}
214+
}""",
215+
variables={"id": schedule_item.id},
216+
)
217+
218+
assert response["data"]["bookScheduleItem"]["__typename"] == "ScheduleItem"
219+
220+
assert ScheduleItemAttendee.objects.filter(
221+
schedule_item=schedule_item, user_id=user.id
222+
).exists()
223+
224+
225+
def test_user_book_full_item_from_room(
226+
graphql_client, user, simple_schedule_item, mocker
227+
):
228+
mocker.patch(
229+
"api.schedule.mutations.book_schedule_item.user_has_admission_ticket",
230+
return_value=True,
231+
)
232+
233+
graphql_client.force_login(user)
234+
235+
schedule_item = simple_schedule_item
236+
schedule_item.rooms.add(RoomFactory(attendees_total_capacity=10))
237+
schedule_item.attendees_total_capacity = None
238+
schedule_item.save()
239+
240+
ScheduleItemAttendeeFactory.create_batch(11, schedule_item=schedule_item)
241+
242+
response = graphql_client.query(
243+
"""mutation($id: ID!) {
244+
bookScheduleItem(id: $id) {
245+
__typename
246+
}
247+
}""",
248+
variables={"id": schedule_item.id},
249+
)
250+
251+
assert response["data"]["bookScheduleItem"]["__typename"] == "ScheduleItemIsFull"
252+
253+
assert not ScheduleItemAttendee.objects.filter(
254+
schedule_item=schedule_item, user_id=user.id
255+
).exists()

backend/api/schedule/types/schedule_item.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ class ScheduleItem:
3030
duration: int | None
3131
highlight_color: str | None
3232
language: Language
33-
audience_level: Annotated[
34-
"AudienceLevel", strawberry.lazy("api.conferences.types")
35-
] | None
33+
audience_level: (
34+
Annotated["AudienceLevel", strawberry.lazy("api.conferences.types")] | None
35+
)
3636
youtube_video_id: str | None
3737
link_to: str
3838

@@ -45,21 +45,21 @@ class ScheduleItem:
4545

4646
@strawberry.field
4747
def has_limited_capacity(self) -> bool:
48-
return self.attendees_total_capacity is not None
48+
return self.actual_attendees_total_capacity is not None
4949

5050
@strawberry.field
5151
def has_spaces_left(self) -> bool:
52-
if self.attendees_total_capacity is None:
52+
if self.actual_attendees_total_capacity is None:
5353
return True
5454

55-
return self.attendees_total_capacity - self.attendees.count() > 0
55+
return self.actual_attendees_total_capacity - self.attendees.count() > 0
5656

5757
@strawberry.field
5858
def spaces_left(self) -> int:
59-
if self.attendees_total_capacity is None:
59+
if self.actual_attendees_total_capacity is None:
6060
return 0
6161

62-
return self.attendees_total_capacity - self.attendees.count()
62+
return self.actual_attendees_total_capacity - self.attendees.count()
6363

6464
@strawberry.field
6565
def user_has_spot(self, info) -> bool:
@@ -124,7 +124,7 @@ def image(self, info) -> str | None:
124124

125125
return info.context.request.build_absolute_uri(self.image.url)
126126

127-
@strawberry.field(name='slidoUrl')
127+
@strawberry.field(name="slidoUrl")
128128
def _slido_url(self, info) -> str:
129129
if self.slido_url:
130130
return self.slido_url

backend/schedule/admin.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ class ScheduleItemAdmin(ConferencePermissionMixin, admin.ModelAdmin):
341341
"link_to",
342342
"slido_url",
343343
"talk_manager",
344-
'livestreaming_room',
344+
"livestreaming_room",
345345
)
346346
},
347347
),
@@ -398,6 +398,9 @@ class ScheduleItemAdmin(ConferencePermissionMixin, admin.ModelAdmin):
398398
"invitation_link",
399399
)
400400

401+
def get_queryset(self, request):
402+
return super().get_queryset(request).prefetch_related("rooms")
403+
401404
def get_urls(self):
402405
return [
403406
path(
@@ -420,9 +423,9 @@ def export_attendees(self, request, object_id: int):
420423
export_data = csv_format.export_data(data)
421424
date_str = timezone.now().strftime("%Y-%m-%d")
422425
response = HttpResponse(export_data, content_type=csv_format.get_content_type())
423-
response[
424-
"Content-Disposition"
425-
] = f'attachment; filename="{schedule_item.slug}-attendees-{date_str}.csv"'
426+
response["Content-Disposition"] = (
427+
f'attachment; filename="{schedule_item.slug}-attendees-{date_str}.csv"'
428+
)
426429
return response
427430

428431
def email_speakers(self, request):
@@ -482,10 +485,10 @@ def email_speakers(self, request):
482485
return TemplateResponse(request, "email-speakers.html", context)
483486

484487
def spaces_left(self, obj):
485-
if obj.attendees_total_capacity is None:
488+
if obj.actual_attendees_total_capacity is None:
486489
return None
487490

488-
return obj.attendees_total_capacity - obj.attendees.count()
491+
return obj.actual_attendees_total_capacity - obj.attendees.count()
489492

490493
def save_form(self, request, form, change):
491494
if form.cleaned_data["new_slot"]:
@@ -672,8 +675,14 @@ class RoomAdmin(OrderedModelAdmin):
672675
list_display = (
673676
"name",
674677
"type",
678+
"attendees_total_capacity",
675679
)
676680
list_filter = ("type",)
681+
fields = (
682+
"name",
683+
"type",
684+
"attendees_total_capacity",
685+
)
677686

678687

679688
class DayRoomThroughModelInline(OrderedTabularInline):
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.8 on 2026-03-03 12:52
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('schedule', '0056_scheduleitem_livestreaming_room'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='room',
15+
name='attendees_total_capacity',
16+
field=models.PositiveIntegerField(blank=True, help_text='Maximum capacity for this room. Leave blank to not limit attendees.', null=True, verbose_name='default attendees total capacity'),
17+
),
18+
]

backend/schedule/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ class Room(models.Model):
3131

3232
name = models.CharField(_("name"), max_length=100)
3333
type = models.CharField(_("type"), choices=TYPES, max_length=10, default=TYPES.talk)
34+
attendees_total_capacity = models.PositiveIntegerField(
35+
_("default attendees total capacity"),
36+
null=True,
37+
blank=True,
38+
help_text=_(
39+
"Maximum capacity for this room. Leave blank to not limit attendees."
40+
),
41+
)
3442

3543
def __str__(self):
3644
return self.name
@@ -318,6 +326,13 @@ class ScheduleItem(TimeStampedModel):
318326

319327
objects = ScheduleItemQuerySet().as_manager()
320328

329+
@cached_property
330+
def actual_attendees_total_capacity(self):
331+
if self.attendees_total_capacity is not None:
332+
return self.attendees_total_capacity
333+
room = self.rooms.first()
334+
return room.attendees_total_capacity if room else None
335+
321336
@cached_property
322337
def speakers(self):
323338
speakers = []

0 commit comments

Comments
 (0)