Skip to content

Commit 41cc7f5

Browse files
committed
Use reverse participant relation
1 parent 3d1005a commit 41cc7f5

8 files changed

Lines changed: 64 additions & 223 deletions

File tree

backend/api/conferences/types.py

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
from cms import models as cms_models
2828
from conferences import models as conference_models
2929
from conferences.models import deadline as deadline_models
30-
from participants import models as participant_models
3130
from schedule import models as schedule_models
3231
from submissions import models as submission_models
3332
from voting import models as voting_models
@@ -96,43 +95,25 @@ class Keynote:
9695
# cache. A narrower custom Prefetch is only worthwhile if profiling shows it.
9796
@strawberry_django.field(
9897
only=["conference_id"],
99-
prefetch_related=["speakers__user"],
98+
prefetch_related=["speakers__user__participants"],
10099
)
101-
def speakers(self, info: Info) -> list[ScheduleItemUser]:
100+
def speakers(self) -> list[ScheduleItemUser]:
102101
keynote_speakers = [
103102
speaker for speaker in self.speakers.all() if speaker.user_id
104103
]
105-
participants_by_conference = info.context._participants_data
106-
if participants_by_conference is None:
107-
participants_by_conference = {}
108-
info.context._participants_data = participants_by_conference
109-
110-
participants_data = participants_by_conference.setdefault(
111-
self.conference_id, {}
112-
)
113-
missing_user_ids = [
114-
speaker.user_id
115-
for speaker in keynote_speakers
116-
if speaker.user_id not in participants_data
117-
]
118-
if missing_user_ids:
119-
participants_data.update({user_id: None for user_id in missing_user_ids})
120-
participants_data.update(
121-
{
122-
participant.user_id: participant
123-
for participant in participant_models.Participant.objects.filter(
124-
user_id__in=missing_user_ids,
125-
conference_id=self.conference_id,
126-
).all()
127-
}
128-
)
129-
130104
return [
131105
ScheduleItemUser(
132106
id=speaker.user_id,
133107
fullname=speaker.user.full_name,
134108
full_name=speaker.user.full_name,
135-
participant=participants_data[speaker.user_id],
109+
participant=next(
110+
(
111+
participant
112+
for participant in speaker.user.participants.all()
113+
if participant.conference_id == self.conference_id
114+
),
115+
None,
116+
),
136117
)
137118
for speaker in keynote_speakers
138119
]

backend/api/context.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
from dataclasses import dataclass, field
1+
from dataclasses import dataclass
22
from typing import Any
33

44
from django.http.request import HttpRequest
55
from strawberry.types import Info as StrawberryInfo
66

7-
from participants import models as participant_models
87
from voting.models.vote import Vote
98

109

@@ -13,10 +12,6 @@ class Context:
1312
request: HttpRequest
1413
response: Any
1514
_user_can_vote: bool | None = None
16-
_participants_data: (
17-
dict[int, dict[int, participant_models.Participant | None]] | None
18-
) = None
19-
_schedule_participants_loaded_conferences: set[int] = field(default_factory=set)
2015
_my_votes: dict[int, Vote] | None = None
2116

2217

backend/api/schedule/queries/search_events_for_schedule.py

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@
77
from api.permissions import CanEditSchedule
88
from api.submissions.types import Submission
99
from conferences.models import Keynote as KeynoteModel
10-
from participants import models as participant_models
1110
from submissions.models import Submission as SubmissionModel
1211

1312

1413
@strawberry.type
1514
class SearchEventsForScheduleResult:
16-
conference_id: strawberry.Private[int]
1715
proposals: strawberry.Private[QuerySet[SubmissionModel]]
1816
keynotes: strawberry.Private[QuerySet[KeynoteModel]]
1917

@@ -22,53 +20,15 @@ def results(self, info: Info) -> list[Submission | Keynote]:
2220
# The mixed union has to become a list here, so optimize each queryset
2321
# before Strawberry loses the opportunity to inspect its model type.
2422
# Keep title explicit because the frontend selects its resolver twice
25-
# under different aliases. Always prefetch keynote speakers so their
26-
# IDs can share the participant batch even when a participant is absent.
23+
# under different aliases.
2724
proposals = list(
2825
optimize(
2926
self.proposals,
3027
info,
3128
store=OptimizerStore.with_hints(only=["title", "speaker_id"]),
3229
)
3330
)
34-
keynotes = list(
35-
optimize(
36-
self.keynotes,
37-
info,
38-
store=OptimizerStore.with_hints(prefetch_related=["speakers"]),
39-
)
40-
)
41-
42-
# Participant deliberately has no reverse User relation, so batch the
43-
# speakers from both sides of the union into the shared request cache.
44-
speaker_ids = {proposal.speaker_id for proposal in proposals}
45-
speaker_ids.update(
46-
speaker.user_id
47-
for keynote in keynotes
48-
for speaker in keynote.speakers.all()
49-
if speaker.user_id
50-
)
51-
participants_by_conference = info.context._participants_data
52-
if participants_by_conference is None:
53-
participants_by_conference = {}
54-
info.context._participants_data = participants_by_conference
55-
56-
participants_data = participants_by_conference.setdefault(
57-
self.conference_id, {}
58-
)
59-
missing_speaker_ids = speaker_ids - participants_data.keys()
60-
participants_data.update(
61-
{speaker_id: None for speaker_id in missing_speaker_ids}
62-
)
63-
participants_data.update(
64-
{
65-
participant.user_id: participant
66-
for participant in participant_models.Participant.objects.filter(
67-
conference_id=self.conference_id,
68-
user_id__in=missing_speaker_ids,
69-
)
70-
}
71-
)
31+
keynotes = list(optimize(self.keynotes, info))
7232

7333
return [*proposals, *keynotes]
7434

@@ -97,7 +57,6 @@ def search_events_for_schedule(
9757
)
9858

9959
return SearchEventsForScheduleResult(
100-
conference_id=int(conference_id),
10160
proposals=proposals,
10261
keynotes=keynotes,
10362
)

backend/api/schedule/tests/test_search_events_for_schedule.py

Lines changed: 2 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import pytest
2-
from django.db import connection
3-
from django.test.utils import CaptureQueriesContext
42

53
from conferences.tests.factories import (
64
ConferenceFactory,
@@ -9,7 +7,6 @@
97
KeynoteSpeakerFactory,
108
)
119
from i18n.strings import LazyI18nString
12-
from participants import models as participant_models
1310
from participants.tests.factories import ParticipantFactory
1411
from submissions.models import Submission
1512
from submissions.tests.factories import SubmissionFactory, SubmissionTypeFactory
@@ -68,7 +65,6 @@
6865
query SearchEvents(
6966
$firstConferenceId: ID!
7067
$secondConferenceId: ID!
71-
$uncachedSubmissionId: ID!
7268
) {
7369
first: searchEventsForSchedule(
7470
conferenceId: $firstConferenceId
@@ -84,13 +80,6 @@
8480
}
8581
}
8682
}
87-
uncachedSubmission: submission(id: $uncachedSubmissionId) {
88-
speaker {
89-
participant {
90-
speakerAvailabilities
91-
}
92-
}
93-
}
9483
second: searchEventsForSchedule(
9584
conferenceId: $secondConferenceId
9685
query: "Shared"
@@ -109,32 +98,6 @@
10998
"""
11099

111100

112-
CACHED_PARTICIPANT_SEARCH_EVENTS_QUERY = """
113-
query SearchEvents($code: String!, $conferenceId: ID!) {
114-
conference(code: $code) {
115-
keynotes {
116-
speakers {
117-
participant {
118-
speakerAvailabilities
119-
}
120-
}
121-
}
122-
}
123-
searchEventsForSchedule(conferenceId: $conferenceId, query: "Cached") {
124-
results {
125-
... on Keynote {
126-
speakers {
127-
participant {
128-
speakerAvailabilities
129-
}
130-
}
131-
}
132-
}
133-
}
134-
}
135-
"""
136-
137-
138101
def _search_events_for_schedule(client, **input):
139102
return client.query(
140103
"""query SearchEventsForSchedule($conferenceId: ID!, $query: String!) {
@@ -156,7 +119,7 @@ def _search_events_for_schedule(client, **input):
156119

157120
@pytest.mark.parametrize(
158121
("event_count", "expected_queries"),
159-
[(1, 9), (4, 9)],
122+
[(1, 10), (4, 10)],
160123
)
161124
@pytest.mark.parametrize("has_participant", [True, False])
162125
def test_frontend_search_events_query(
@@ -311,27 +274,13 @@ def test_frontend_search_events_query_keeps_participants_scoped_by_conference(
311274
title=LazyI18nString({"en": f"Shared Talk {index}", "it": ""}),
312275
)
313276

314-
uncached_speaker = UserFactory(full_name="Uncached Speaker")
315-
ParticipantFactory(
316-
conference=conferences[1],
317-
user=uncached_speaker,
318-
speaker_availabilities={"uncached": True},
319-
)
320-
uncached_submission = SubmissionFactory(
321-
conference=conferences[1],
322-
speaker=uncached_speaker,
323-
status=Submission.STATUS.accepted,
324-
title=LazyI18nString({"en": "Unmatched Talk", "it": ""}),
325-
)
326-
327277
admin_graphql_api_client.force_login(admin_superuser)
328-
with django_assert_num_queries(13):
278+
with django_assert_num_queries(10):
329279
response = admin_graphql_api_client.query(
330280
MULTI_CONFERENCE_SEARCH_EVENTS_QUERY,
331281
variables={
332282
"firstConferenceId": str(conferences[0].id),
333283
"secondConferenceId": str(conferences[1].id),
334-
"uncachedSubmissionId": uncached_submission.hashid,
335284
},
336285
)
337286

@@ -346,9 +295,6 @@ def test_frontend_search_events_query_keeps_participants_scoped_by_conference(
346295
}
347296
]
348297
},
349-
"uncachedSubmission": {
350-
"speaker": {"participant": {"speakerAvailabilities": {"uncached": True}}}
351-
},
352298
"second": {
353299
"results": [
354300
{
@@ -361,42 +307,6 @@ def test_frontend_search_events_query_keeps_participants_scoped_by_conference(
361307
}
362308

363309

364-
def test_frontend_search_events_query_reuses_cached_participants(
365-
admin_graphql_api_client,
366-
admin_superuser,
367-
):
368-
conference = ConferenceFactory()
369-
keynote = KeynoteFactory(
370-
conference=conference,
371-
title=LazyI18nString({"en": "Cached Keynote", "it": ""}),
372-
)
373-
speaker = KeynoteSpeakerFactory(keynote=keynote).user
374-
ParticipantFactory(
375-
conference=conference,
376-
user=speaker,
377-
speaker_availabilities={"cached": True},
378-
)
379-
380-
admin_graphql_api_client.force_login(admin_superuser)
381-
with CaptureQueriesContext(connection) as queries:
382-
response = admin_graphql_api_client.query(
383-
CACHED_PARTICIPANT_SEARCH_EVENTS_QUERY,
384-
variables={
385-
"code": conference.code,
386-
"conferenceId": str(conference.id),
387-
},
388-
)
389-
390-
participant_table = participant_models.Participant._meta.db_table
391-
assert sum(f'FROM "{participant_table}"' in query["sql"] for query in queries) == 1
392-
assert "errors" not in response
393-
participant = {"participant": {"speakerAvailabilities": {"cached": True}}}
394-
assert response["data"] == {
395-
"conference": {"keynotes": [{"speakers": [participant]}]},
396-
"searchEventsForSchedule": {"results": [{"speakers": [participant]}]},
397-
}
398-
399-
400310
@pytest.mark.parametrize("user_to_test", ["admin_user", "user", "not_authenticated"])
401311
def test_cannot_search_without_permission(
402312
admin_graphql_api_client,

backend/api/schedule/types/schedule_item.py

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from api.schedule.types.room import Room
1313
from api.schedule.types.schedule_item_user import ScheduleItemUser
1414
from api.submissions.types import Submission
15-
from participants import models as participant_models
1615
from schedule import models
1716

1817
if TYPE_CHECKING: # pragma: no cover
@@ -168,43 +167,12 @@ def user_is_talk_manager(self, info: Info) -> bool:
168167
only=["conference_id"],
169168
select_related=["submission__speaker"],
170169
prefetch_related=[
171-
"keynote__speakers__user",
172-
"additional_speakers__user",
170+
"submission__speaker__participants",
171+
"keynote__speakers__user__participants",
172+
"additional_speakers__user__participants",
173173
],
174174
)
175-
def speakers(self, info: Info) -> list[ScheduleItemUser]:
176-
participants_by_conference = info.context._participants_data
177-
if participants_by_conference is None:
178-
participants_by_conference = {}
179-
info.context._participants_data = participants_by_conference
180-
181-
participants_data = participants_by_conference.setdefault(
182-
self.conference_id, {}
183-
)
184-
loaded_conferences = info.context._schedule_participants_loaded_conferences
185-
if self.conference_id not in loaded_conferences:
186-
schedule_items = models.ScheduleItem.objects.filter(
187-
conference_id=self.conference_id
188-
)
189-
submission_speakers = schedule_items.values("submission__speaker_id")
190-
keynote_speakers = schedule_items.values("keynote__speakers__user_id")
191-
additional_speakers = schedule_items.values("additional_speakers__user_id")
192-
participants_data.update(
193-
{
194-
participant.user_id: participant
195-
for participant in participant_models.Participant.objects.filter(
196-
conference_id=self.conference_id
197-
)
198-
.filter(
199-
django_models.Q(user_id__in=submission_speakers)
200-
| django_models.Q(user_id__in=keynote_speakers)
201-
| django_models.Q(user_id__in=additional_speakers)
202-
)
203-
.select_related("user")
204-
}
205-
)
206-
loaded_conferences.add(self.conference_id)
207-
175+
def speakers(self) -> list[ScheduleItemUser]:
208176
schedule_item_speakers = []
209177
if self.submission_id:
210178
schedule_item_speakers.append(self.submission.speaker)
@@ -228,7 +196,14 @@ def speakers(self, info: Info) -> list[ScheduleItemUser]:
228196
id=speaker.id,
229197
fullname=speaker.fullname,
230198
full_name=speaker.full_name,
231-
participant=participants_data.get(speaker.id),
199+
participant=next(
200+
(
201+
participant
202+
for participant in speaker.participants.all()
203+
if participant.conference_id == self.conference_id
204+
),
205+
None,
206+
),
232207
)
233208
)
234209

0 commit comments

Comments
 (0)