diff --git a/backend/api/conferences/schema.py b/backend/api/conferences/schema.py index 881028656e..5be3d825b4 100644 --- a/backend/api/conferences/schema.py +++ b/backend/api/conferences/schema.py @@ -1,12 +1,12 @@ -from api.context import Info from api.conferences.types import Conference import strawberry +import strawberry_django -from conferences.models import Conference as ConferenceModel +from conferences import models @strawberry.type class ConferenceQuery: - @strawberry.field - def conference(self, info: Info, code: str) -> Conference: - return ConferenceModel.objects.prefetch_related("durations").get(code=code) + @strawberry_django.field + def conference(self, code: str) -> Conference: + return models.Conference.objects.filter(code=code) diff --git a/backend/api/conferences/tests/test_query_talk.py b/backend/api/conferences/tests/test_query_talk.py index 26d6b7bbfe..2c36e9924b 100644 --- a/backend/api/conferences/tests/test_query_talk.py +++ b/backend/api/conferences/tests/test_query_talk.py @@ -144,23 +144,29 @@ def test_abstract_shows_keynote_description(graphql_client, user): } -def test_fetch_schedule_talk(simple_schedule_item, graphql_client, user): +def test_fetch_schedule_talk( + simple_schedule_item, graphql_client, user, django_assert_num_queries +): graphql_client.force_login(user) schedule_item = simple_schedule_item - response = graphql_client.query( - """query($slug: String!, $code: String!) { - conference(code: $code) { - talk(slug: $slug) { - youtubeVideoId - userHasSpot - hasSpacesLeft - spacesLeft + with django_assert_num_queries(4): + response = graphql_client.query( + """query($slug: String!, $code: String!) { + conference(code: $code) { + talk(slug: $slug) { + youtubeVideoId + userHasSpot + hasSpacesLeft + spacesLeft + } } - } - }""", - variables={"slug": schedule_item.slug, "code": schedule_item.conference.code}, - ) + }""", + variables={ + "slug": schedule_item.slug, + "code": schedule_item.conference.code, + }, + ) assert response["data"]["conference"]["talk"] == { "youtubeVideoId": "AbCdEfGhIjK", diff --git a/backend/api/conferences/types.py b/backend/api/conferences/types.py index c7fc6e6588..622cc2eebf 100644 --- a/backend/api/conferences/types.py +++ b/backend/api/conferences/types.py @@ -1,6 +1,4 @@ from api.context import Info -from django.db.models import Case, When, Value, IntegerField -from django.db.models import Prefetch from participants.models import Participant as ParticipantModel from datetime import datetime from api.participants.types import Participant @@ -139,27 +137,31 @@ def youtube_video_id(self) -> str | None: return schedule_item.youtube_video_id if schedule_item else None -@strawberry.type +@strawberry_django.type(conference_models.Conference) class Conference: - id: strawberry.ID + id: strawberry.auto - name: str = strawberry.field(resolver=make_localized_resolver("name")) - introduction: str = strawberry.field( - resolver=make_localized_resolver("introduction") + name: str = strawberry_django.field( + resolver=make_localized_resolver("name"), only=["name"] + ) + introduction: str = strawberry_django.field( + resolver=make_localized_resolver("introduction"), only=["introduction"] ) - code: str - hostname: str + code: strawberry.auto + hostname: strawberry.auto start: datetime end: datetime map: Map | None = strawberry.field(resolver=resolve_map) - pretix_event_url: str + pretix_event_url: strawberry.auto - @strawberry.field + @strawberry_django.field( + only=["pretix_organizer_id", "pretix_event_id"], + ) def voucher(self, info: Info, code: str) -> Voucher | None: return get_voucher(self, code) - @strawberry.field + @strawberry_django.field(only=["timezone"]) def timezone(self, info: Info) -> str: return str(self.timezone) @@ -207,9 +209,7 @@ def topics(self, info: Info) -> list[Topic]: def languages(self, info: Info) -> list[Language]: return self.languages.all() - @strawberry.field - def durations(self, info: Info) -> list["Duration"]: - return self.durations.all() + durations: list["Duration"] @strawberry.field def submission_types(self, info: Info) -> list[SubmissionType]: @@ -272,9 +272,9 @@ def talks(self, info: Info) -> list[ScheduleItem]: .all() ) - @strawberry.field + @strawberry_django.field def talk(self, info: Info, slug: str) -> ScheduleItem | None: - return self.schedule_items.filter(slug=slug).prefetch_related("rooms").first() + return self.schedule_items.filter(slug=slug) @strawberry.field def ranking(self, info: Info, topic: strawberry.ID) -> RankRequest | None: @@ -296,74 +296,9 @@ def ranking(self, info: Info, topic: strawberry.ID) -> RankRequest | None: stats=rank_request.stats.all(), ) - @strawberry.field + @strawberry_django.field def days(self, info: Info) -> list[Day]: - days = list( - self.days.order_by("day") - .prefetch_related( - "slots", - "slots__day", - "slots__day__added_rooms", - "slots__day__added_rooms__room", - Prefetch( - "slots__items", - queryset=( - ScheduleItemModel.objects.for_conference(self.id) - .annotate( - order=Case( - When(type="custom", then=Value(1)), - When(type="break", then=Value(1)), - When(type="talk", then=Value(2)), - When(type="panel", then=Value(3)), - default=Value(4), - output_field=IntegerField(), - ) - ) - .order_by("order") - .prefetch_related( - "audience_level", - "language", - "rooms", - "additional_speakers", - "additional_speakers__user", - "language", - "submission", - "submission__type", - "submission__tags", - "submission__duration", - "submission__audience_level", - "submission__speaker", - "submission__languages", - "submission__schedule_items", - "keynote", - "keynote__schedule_items", - "keynote__schedule_items__rooms", - "keynote__schedule_items__slot", - "keynote__schedule_items__slot__day", - "keynote__speakers", - "keynote__speakers__user", - ) - ), - ), - ) - .all() - ) - all_speakers = [ - speaker.id - for day in days - for slot in day.slots.all() - for item in slot.items.all() - for speaker in item.speakers - ] - info.context._participants_data = { - participant.user_id: participant - for participant in ParticipantModel.objects.for_conference(self.id) - .filter(user_id__in=all_speakers) - .prefetch_related("user") - .all() - } - - return days + return self.days.all() @strawberry.field def current_day(self, info: Info) -> Day | None: diff --git a/backend/api/schedule/types/day.py b/backend/api/schedule/types/day.py index ec54ec83dd..ca44e6bcd9 100644 --- a/backend/api/schedule/types/day.py +++ b/backend/api/schedule/types/day.py @@ -2,16 +2,18 @@ from schedule.models import ScheduleItem as ScheduleItemModel from api.schedule.types.schedule_item import ScheduleItem from django.utils import timezone -from datetime import date, timedelta +from datetime import timedelta from api.schedule.types.day_room import DayRoom from api.schedule.types.slot import ScheduleSlot import strawberry +import strawberry_django +from schedule import models -@strawberry.type +@strawberry_django.type(models.Day) class Day: - id: strawberry.ID - day: date + id: strawberry.auto + day: strawberry.auto @strawberry.field def random_events(self, limit: int = 4) -> list[ScheduleItem]: @@ -31,13 +33,13 @@ def random_events(self, limit: int = 4) -> list[ScheduleItem]: .order_by("?")[:limit] ) - @strawberry.field + @strawberry_django.field def slots( self, info: Info, room: strawberry.ID | None = None ) -> list[ScheduleSlot]: if room: - return list(self.slots.filter(items__rooms__id=room)) - return list(self.slots.all()) + return self.slots.filter(items__rooms__id=room) + return self.slots.all() @strawberry.field def running_events(self, info: Info) -> list[ScheduleItem]: @@ -59,7 +61,7 @@ def running_events(self, info: Info) -> list[ScheduleItem]: return [item for item in current_slot.items.all()] - @strawberry.field + @strawberry_django.field(prefetch_related=["added_rooms__room"]) def rooms(self) -> list[DayRoom]: added_rooms = self.added_rooms.all() return [ diff --git a/backend/api/schedule/types/room.py b/backend/api/schedule/types/room.py index bbbab9a2e2..4454dc8a20 100644 --- a/backend/api/schedule/types/room.py +++ b/backend/api/schedule/types/room.py @@ -1,8 +1,10 @@ import strawberry +import strawberry_django +from schedule import models -@strawberry.type +@strawberry_django.type(models.Room) class Room: - id: strawberry.ID - name: str + id: strawberry.auto + name: strawberry.auto type: str diff --git a/backend/api/schedule/types/schedule_item.py b/backend/api/schedule/types/schedule_item.py index 1ddf847dae..40909dedaf 100644 --- a/backend/api/schedule/types/schedule_item.py +++ b/backend/api/schedule/types/schedule_item.py @@ -9,32 +9,85 @@ from api.schedule.types.schedule_item_user import ScheduleItemUser from api.submissions.types import Submission import strawberry +import strawberry_django +from django.db import models as django_models +from django.db.models.functions import Coalesce +from schedule import models from api.schedule.types.room import Room if TYPE_CHECKING: # pragma: no cover from api.conferences.types import AudienceLevel, Conference, Keynote -@strawberry.type +ATTENDEES_COUNT_ANNOTATION = "graphql_attendees_count" +CAPACITY_ANNOTATION = "graphql_attendees_total_capacity" +USER_HAS_SPOT_ANNOTATION = "graphql_user_has_spot" + + +def attendees_count_annotation(_info: Info): + return django_models.Count("attendees", distinct=True) + + +def capacity_annotation(_info: Info): + room_capacity = ( + models.Room.objects.filter(talks=django_models.OuterRef("pk")) + .order_by("pk") + .values("attendees_total_capacity")[:1] + ) + return Coalesce( + "attendees_total_capacity", + django_models.Subquery(room_capacity), + output_field=django_models.PositiveIntegerField(), + ) + + +def user_has_spot_annotation(info: Info): + user_id = info.context.request.user.id + if not user_id: + return django_models.Value(False, output_field=django_models.BooleanField()) + + return django_models.Exists( + models.ScheduleItemAttendee.objects.filter( + schedule_item_id=django_models.OuterRef("pk"), + user_id=user_id, + ) + ) + + +def _capacity(schedule_item) -> int | None: + try: + return schedule_item.graphql_attendees_total_capacity + except AttributeError: + return schedule_item.actual_attendees_total_capacity + + +def _attendees_count(schedule_item) -> int: + try: + return schedule_item.graphql_attendees_count + except AttributeError: + return schedule_item.attendees.count() + + +@strawberry_django.type(models.ScheduleItem) class ScheduleItem: - id: strawberry.ID + id: strawberry.auto conference: Annotated["Conference", strawberry.lazy("api.conferences.types")] - title: str + title: strawberry.auto start: datetime end: datetime - status: str + status: strawberry.auto submission: Submission | None slug: str - description: str + description: strawberry.auto type: str - duration: int | None + duration: strawberry.auto highlight_color: str | None language: Language audience_level: ( Annotated["AudienceLevel", strawberry.lazy("api.conferences.types")] | None ) youtube_video_id: str | None - link_to: str + link_to: strawberry.auto abstract: str elevator_pitch: str @@ -43,28 +96,60 @@ class ScheduleItem: ) livestreaming_room: Room | None - @strawberry.field + @classmethod + def get_queryset(cls, queryset, info: Info): + return queryset.annotate( + graphql_order=django_models.Case( + django_models.When(type="custom", then=django_models.Value(1)), + django_models.When(type="break", then=django_models.Value(1)), + django_models.When(type="talk", then=django_models.Value(2)), + django_models.When(type="panel", then=django_models.Value(3)), + default=django_models.Value(4), + output_field=django_models.IntegerField(), + ) + ).order_by("graphql_order") + + @strawberry_django.field( + annotate={CAPACITY_ANNOTATION: capacity_annotation}, + ) def has_limited_capacity(self) -> bool: - return self.actual_attendees_total_capacity is not None + return _capacity(self) is not None - @strawberry.field + @strawberry_django.field( + annotate={ + ATTENDEES_COUNT_ANNOTATION: attendees_count_annotation, + CAPACITY_ANNOTATION: capacity_annotation, + }, + ) def has_spaces_left(self) -> bool: - if self.actual_attendees_total_capacity is None: + capacity = _capacity(self) + if capacity is None: return True - return self.actual_attendees_total_capacity - self.attendees.count() > 0 + return capacity - _attendees_count(self) > 0 - @strawberry.field + @strawberry_django.field( + annotate={ + ATTENDEES_COUNT_ANNOTATION: attendees_count_annotation, + CAPACITY_ANNOTATION: capacity_annotation, + }, + ) def spaces_left(self) -> int: - if self.actual_attendees_total_capacity is None: + capacity = _capacity(self) + if capacity is None: return 0 - return self.actual_attendees_total_capacity - self.attendees.count() + return capacity - _attendees_count(self) - @strawberry.field + @strawberry_django.field( + annotate={USER_HAS_SPOT_ANNOTATION: user_has_spot_annotation}, + ) def user_has_spot(self, info: Info) -> bool: - user_id = info.context.request.user.id - return self.attendees.filter(user_id=user_id).exists() + try: + return self.graphql_user_has_spot + except AttributeError: + user_id = info.context.request.user.id + return self.attendees.filter(user_id=user_id).exists() @strawberry.field def user_is_talk_manager(self, info: Info) -> bool: @@ -73,22 +158,56 @@ def user_is_talk_manager(self, info: Info) -> bool: return self.talk_manager_id == user_id - @strawberry.field + @strawberry_django.field( + only=["conference_id"], + select_related=["submission__speaker"], + prefetch_related=[ + "keynote__speakers__user", + "additional_speakers__user", + ], + ) def speakers(self, info: Info) -> list[ScheduleItemUser]: speakers = [] - # TODO: Find a better solution participants_data = info.context._participants_data - if not participants_data: + if participants_data is None: + schedule_items = models.ScheduleItem.objects.filter( + conference_id=self.conference_id + ) + submission_speakers = schedule_items.values("submission__speaker_id") + keynote_speakers = schedule_items.values("keynote__speakers__user_id") + additional_speakers = schedule_items.values("additional_speakers__user_id") participants_data = { participant.user_id: participant for participant in ParticipantModel.objects.filter( - user_id__in=[speaker.id for speaker in self.speakers], - conference_id=self.conference_id, + conference_id=self.conference_id + ) + .filter( + django_models.Q(user_id__in=submission_speakers) + | django_models.Q(user_id__in=keynote_speakers) + | django_models.Q(user_id__in=additional_speakers) ) + .select_related("user") } + info.context._participants_data = participants_data + + schedule_item_speakers = [] + if self.submission_id: + schedule_item_speakers.append(self.submission.speaker) + + if self.keynote_id: + schedule_item_speakers.extend( + speaker.user for speaker in self.keynote.speakers.all() + ) + + schedule_item_speakers.extend( + speaker.user for speaker in self.additional_speakers.all() + ) + + for speaker in schedule_item_speakers: + if speaker is None: + continue - for speaker in self.speakers: speakers.append( ScheduleItemUser( id=speaker.id, @@ -102,18 +221,13 @@ def speakers(self, info: Info) -> list[ScheduleItemUser]: return speakers - @strawberry.field + @strawberry_django.field(select_related=["keynote"]) def keynote( self, info: Info ) -> Annotated["Keynote", strawberry.lazy("api.conferences.types")] | None: - if not self.keynote_id: - return None + return self.keynote if self.keynote_id else None - return self.keynote - - @strawberry.field - def rooms(self, info: Info) -> list[Room]: - return self.rooms.all() + rooms: list[Room] @strawberry.field def image(self, info: Info) -> str | None: diff --git a/backend/api/schedule/types/slot.py b/backend/api/schedule/types/slot.py index e0073ae0ec..131cf7fed9 100644 --- a/backend/api/schedule/types/slot.py +++ b/backend/api/schedule/types/slot.py @@ -3,11 +3,12 @@ from django.utils import timezone -from api.context import Info from api.schedule.types.schedule_item import ScheduleItem import strawberry +import strawberry_django +from schedule import models @strawberry.enum @@ -17,11 +18,11 @@ class ScheduleSlotType(Enum): BREAK = "break" -@strawberry.type +@strawberry_django.type(models.Slot) class ScheduleSlot: - id: strawberry.ID - hour: time - duration: int + id: strawberry.auto + hour: strawberry.auto + duration: strawberry.auto type: ScheduleSlotType @strawberry.field @@ -40,6 +41,4 @@ def end_hour(self) -> time: + timedelta(minutes=self.duration) ).time() - @strawberry.field - def items(self, info: Info) -> list[ScheduleItem]: - return self.items.all() + items: list[ScheduleItem] diff --git a/backend/api/tests/schema/conference/test_conference.py b/backend/api/tests/schema/conference/test_conference.py index d22c0f5368..b4db1fa746 100644 --- a/backend/api/tests/schema/conference/test_conference.py +++ b/backend/api/tests/schema/conference/test_conference.py @@ -23,21 +23,22 @@ pytestmark = mark.django_db -def test_get_conference_info(graphql_client): +def test_get_conference_info(graphql_client, django_assert_num_queries): conference = ConferenceFactory() - resp = graphql_client.query( - """ - query($code: String!) { - conference(code: $code) { - id - code - name - introduction + with django_assert_num_queries(1): + resp = graphql_client.query( + """ + query($code: String!) { + conference(code: $code) { + id + code + name + introduction + } } - } - """, - variables={"code": conference.code}, - ) + """, + variables={"code": conference.code}, + ) assert "errors" not in resp assert { @@ -345,6 +346,7 @@ def test_query_conference_languages(graphql_client, language): def test_get_conference_durations( graphql_client, + django_assert_num_queries, ): talk_type = SubmissionTypeFactory(name="talk") tutorial_type = SubmissionTypeFactory(name="tutorial") @@ -356,25 +358,26 @@ def test_get_conference_durations( conference = d1.conference - resp = graphql_client.query( - """ - query($code: String!) { - conference(code: $code) { - durations { - id - name - duration - notes - allowedSubmissionTypes { + with django_assert_num_queries(3): + resp = graphql_client.query( + """ + query($code: String!) { + conference(code: $code) { + durations { id name + duration + notes + allowedSubmissionTypes { + id + name + } } } } - } - """, - variables={"code": conference.code}, - ) + """, + variables={"code": conference.code}, + ) assert "errors" not in resp assert { @@ -654,23 +657,26 @@ def test_can_see_submissions_if_they_have_sent_one(graphql_client): assert len(response["data"]["conference"]["submissions"]) == 2 -def test_get_conference_voucher_with_invalid_code(graphql_client, requests_mock): +def test_get_conference_voucher_with_invalid_code( + graphql_client, requests_mock, django_assert_num_queries +): conference = ConferenceFactory() requests_mock.get( "https://pretix/api/organizers/base-pretix-organizer-id/events/base-pretix-event-id/extended-vouchers/test/", status_code=404, ) - response = graphql_client.query( - """query($code: String!, $voucherCode: String!) { - conference(code: $code) { - voucher(code: $voucherCode) { - id + with django_assert_num_queries(1): + response = graphql_client.query( + """query($code: String!, $voucherCode: String!) { + conference(code: $code) { + voucher(code: $voucherCode) { + id + } } - } - }""", - variables={"code": conference.code, "voucherCode": "test"}, - ) + }""", + variables={"code": conference.code, "voucherCode": "test"}, + ) assert response["data"]["conference"]["voucher"] is None diff --git a/backend/api/tests/schema/conference/test_days.py b/backend/api/tests/schema/conference/test_days.py index 49b05f2857..5a5173144e 100644 --- a/backend/api/tests/schema/conference/test_days.py +++ b/backend/api/tests/schema/conference/test_days.py @@ -1,12 +1,19 @@ from datetime import date, datetime, time -from conferences.tests.factories import ConferenceFactory +from conferences.tests.factories import ( + ConferenceFactory, + KeynoteFactory, + KeynoteSpeakerFactory, +) +from participants.tests.factories import ParticipantFactory from schedule.tests.factories import ( DayFactory, RoomFactory, + ScheduleItemAttendeeFactory, ScheduleItemFactory, SlotFactory, ) +from users.tests.factories import UserFactory from pytest import mark from pycon.constants import UTC @@ -91,9 +98,8 @@ def test_days_item_sorted(graphql_client): day = DayFactory(conference=conference, day=date(2020, 4, 2)) slot = SlotFactory(day=day, hour=time(8, 45), duration=60) - slot_2 = SlotFactory(day=day, hour=time(9, 45), duration=60) - ScheduleItemFactory(conference=conference, slot=slot, type="custom") - ScheduleItemFactory(conference=conference, slot=slot_2, image=None, type="talk") + ScheduleItemFactory(conference=conference, slot=slot, type="talk") + ScheduleItemFactory(conference=conference, slot=slot, image=None, type="custom") resp = graphql_client.query( """ @@ -116,8 +122,7 @@ def test_days_item_sorted(graphql_client): assert "errors" not in resp slots = resp["data"]["conference"]["days"][0]["slots"] - assert slots[0]["items"][0]["type"] == "custom" - assert slots[1]["items"][0]["type"] == "talk" + assert [item["type"] for item in slots[0]["items"]] == ["custom", "talk"] @mark.django_db @@ -197,3 +202,196 @@ def test_filter_days_by_room_not_found(graphql_client): assert "errors" not in resp assert len(resp["data"]["conference"]["days"][0]["slots"]) == 0 + + +@mark.django_db +def test_anonymous_user_does_not_have_schedule_spot(graphql_client): + conference = ConferenceFactory() + day = DayFactory(conference=conference) + slot = SlotFactory(day=day, hour=time(8, 45), duration=60) + ScheduleItemFactory( + conference=conference, + slot=slot, + submission=None, + type="custom", + ) + + resp = graphql_client.query( + """ + query($code: String!) { + conference(code: $code) { + days { + slots { + items { + userHasSpot + } + } + } + } + } + """, + variables={"code": conference.code}, + ) + + assert "errors" not in resp + assert resp["data"]["conference"]["days"][0]["slots"][0]["items"] == [ + {"userHasSpot": False} + ] + + +@mark.parametrize("item_count", [1, 4]) +@mark.parametrize("item_source", ["submission", "keynote"]) +@mark.django_db +def test_schedule_capacity_query_is_constant( + graphql_client, django_assert_num_queries, item_count, item_source +): + conference = ConferenceFactory( + start=datetime(2020, 4, 2, tzinfo=UTC), + end=datetime(2020, 4, 2, tzinfo=UTC), + ) + day = DayFactory(conference=conference, day=date(2020, 4, 2)) + slots = [ + SlotFactory(day=day, hour=time(8 + index, 45), duration=60) + for index in range(item_count) + ] + room = RoomFactory(attendees_total_capacity=20) + items = [] + speakers = [] + for index in range(item_count): + if item_source == "keynote": + keynote = KeynoteFactory(conference=conference) + speaker = KeynoteSpeakerFactory(keynote=keynote).user + item = ScheduleItemFactory( + conference=conference, + slot=slots[index], + type="keynote", + submission=None, + keynote=keynote, + rooms=[room], + ) + else: + item = ScheduleItemFactory( + conference=conference, + slot=slots[index], + type="submission", + rooms=[room], + ) + speaker = item.submission.speaker + + ParticipantFactory(conference=conference, user=speaker) + items.append(item) + speakers.append(speaker) + user = UserFactory() + ScheduleItemAttendeeFactory(schedule_item=items[0], user=user) + graphql_client.force_login(user) + + expected_queries = 12 if item_source == "keynote" else 10 + with django_assert_num_queries(expected_queries): + resp = graphql_client.query( + """ + query($code: String!, $language: String!) { + conference(code: $code) { + id + timezone + days { + day + rooms { + id + name + type + } + slots { + id + hour + endHour + duration + type + items { + id + title + slug + type + duration + hasLimitedCapacity + hasSpacesLeft + spacesLeft + userHasSpot + linkTo + audienceLevel { + id + name + } + language { + id + name + code + } + submission { + id + title(language: $language) + } + keynote { + id + title(language: "en") + } + speakers { + id + fullname + participant { + id + photo + } + } + rooms { + id + name + type + } + } + } + } + } + } + """, + variables={"code": conference.code, "language": "en"}, + ) + + assert "errors" not in resp + schedule_items = [ + item + for slot in resp["data"]["conference"]["days"][0]["slots"] + for item in slot["items"] + ] + capacity_fields = { + "id", + "hasLimitedCapacity", + "hasSpacesLeft", + "spacesLeft", + "userHasSpot", + } + assert {field: schedule_items[0][field] for field in capacity_fields} == { + "id": str(items[0].id), + "hasLimitedCapacity": True, + "hasSpacesLeft": True, + "spacesLeft": 19, + "userHasSpot": True, + } + assert [ + {field: schedule_item[field] for field in capacity_fields} + for schedule_item in schedule_items[1:] + ] == [ + { + "id": str(item.id), + "hasLimitedCapacity": True, + "hasSpacesLeft": True, + "spacesLeft": 20, + "userHasSpot": False, + } + for item in items[1:] + ] + assert [schedule_item["speakers"][0]["id"] for schedule_item in schedule_items] == [ + str(speaker.id) for speaker in speakers + ] + assert all( + schedule_item["speakers"][0]["participant"] for schedule_item in schedule_items + ) diff --git a/backend/schema.graphql b/backend/schema.graphql index 0c9cf4ebe6..6865a0c649 100644 --- a/backend/schema.graphql +++ b/backend/schema.graphql @@ -196,6 +196,7 @@ type Conference { end: DateTime! map: Map pretixEventUrl: String! + durations: [Duration!]! voucher(code: String!): Voucher timezone: String! tickets(language: String!, showUnavailableTickets: Boolean! = false): [TicketItem!]! @@ -208,7 +209,6 @@ type Conference { audienceLevels: [AudienceLevel!]! topics: [Topic!]! languages: [Language!]! - durations: [Duration!]! submissionTypes: [SubmissionType!]! proposalTags: [SubmissionTag!]! submissions: [Submission!] @@ -1140,6 +1140,7 @@ type ScheduleItem { elevatorPitch: String! talkManager: ScheduleItemUser livestreamingRoom: Room + rooms: [Room!]! hasLimitedCapacity: Boolean! hasSpacesLeft: Boolean! spacesLeft: Int! @@ -1147,7 +1148,6 @@ type ScheduleItem { userIsTalkManager: Boolean! speakers: [ScheduleItemUser!]! keynote: Keynote - rooms: [Room!]! image: String slidoUrl: String! } @@ -1179,9 +1179,9 @@ type ScheduleSlot { hour: Time! duration: Int! type: ScheduleSlotType! + items: [ScheduleItem!]! isLive: Boolean! endHour: Time! - items: [ScheduleItem!]! } enum ScheduleSlotType {