44from django .urls import reverse
55from django .utils import translation
66
7- from applications .models import Application , Score
7+ from applications .models import Answer , Application , Question , Score
88from applications .views import application_list
99
1010
@@ -203,12 +203,16 @@ def test_changing_application_status_errors(client, admin_client, future_event,
203203
204204 # lack of state parameter
205205 resp = admin_client .post (
206- reverse ("applications:change_state" , args = [future_event .page_url ]), {"application" : application_submitted .id }
206+ reverse ("applications:change_state" , args = [future_event .page_url ]),
207+ {"application" : application_submitted .id },
207208 )
208209 assert "error" in resp .json ()
209210
210211 # lack of application parameter
211- resp = admin_client .post (reverse ("applications:change_state" , args = [future_event .page_url ]), {"state" : "accepted" })
212+ resp = admin_client .post (
213+ reverse ("applications:change_state" , args = [future_event .page_url ]),
214+ {"state" : "accepted" },
215+ )
212216 assert "error" in resp .json ()
213217
214218
@@ -222,13 +226,15 @@ def test_changing_application_rsvp_errors(client, admin_client, future_event, ap
222226
223227 # lack of rsvp_status parameter
224228 resp = admin_client .post (
225- reverse ("applications:change_rsvp" , args = [future_event .page_url ]), {"application" : application_submitted .id }
229+ reverse ("applications:change_rsvp" , args = [future_event .page_url ]),
230+ {"application" : application_submitted .id },
226231 )
227232 assert "error" in resp .json ()
228233
229234 # lack of application parameter
230235 resp = admin_client .post (
231- reverse ("applications:change_rsvp" , args = [future_event .page_url ]), {"rsvp_status" : Application .RSVP_YES }
236+ reverse ("applications:change_rsvp" , args = [future_event .page_url ]),
237+ {"rsvp_status" : Application .RSVP_YES },
232238 )
233239 assert "error" in resp .json ()
234240
@@ -238,7 +244,10 @@ def test_changing_application_status_in_bulk(admin_client, future_event, applica
238244 assert application_rejected .state == "rejected"
239245 resp = admin_client .post (
240246 reverse ("applications:change_state" , args = [future_event .page_url ]),
241- {"state" : "accepted" , "application" : [application_submitted .id , application_rejected .id ]},
247+ {
248+ "state" : "accepted" ,
249+ "application" : [application_submitted .id , application_rejected .id ],
250+ },
242251 )
243252 assert resp .status_code == 200
244253 application_submitted = Application .objects .get (id = application_submitted .id )
@@ -258,3 +267,52 @@ def test_application_scores_is_queried_once(admin_client, future_event, scored_a
258267
259268 # The first query is for the annotation in get_applications_for_event, the second is for the scores themselves
260269 assert len (score_queries ) == 2
270+
271+
272+ def test_application_detail_queries_optimized (admin_client , future_event , application_submitted , future_event_form ):
273+ """Regression test to ensure application_detail view doesn't have N+1 query problem with answers/questions."""
274+ # Create multiple questions and answers for the application
275+ questions = []
276+ for i in range (5 ):
277+ question = Question .objects .create (
278+ form = future_event_form ,
279+ title = f"Test Question { i } " ,
280+ question_type = "text" ,
281+ order = i ,
282+ )
283+ questions .append (question )
284+ Answer .objects .create (
285+ application = application_submitted ,
286+ question = question ,
287+ answer = f"Test Answer { i } " ,
288+ )
289+
290+ application_detail_url = reverse (
291+ "applications:application_detail" ,
292+ kwargs = {
293+ "page_url" : future_event .page_url ,
294+ "app_number" : application_submitted .number ,
295+ },
296+ )
297+
298+ with CaptureQueriesContext (connection ) as queries :
299+ resp = admin_client .get (application_detail_url )
300+ assert resp .status_code == 200
301+
302+ # Check that we're not making separate queries for each question
303+ question_table_name = Question ._meta .db_table
304+ answer_table_name = Answer ._meta .db_table
305+
306+ question_queries = [q for q in queries .captured_queries if question_table_name in q ["sql" ]]
307+ answer_queries = [q for q in queries .captured_queries if answer_table_name in q ["sql" ]]
308+
309+ # With select_related, we should have only 1 query that joins answers and questions
310+ # The query should join both tables, so it will appear in both lists
311+ # We're checking that there's at most 1 query involving answers (with joined questions)
312+ assert len (answer_queries ) <= 1 , f"Expected at most 1 answer query, got { len (answer_queries )} "
313+
314+ # Verify that if there are question queries, they're part of the joined query with answers
315+ # There should not be 5 separate queries (one per question)
316+ assert len (question_queries ) <= 1 , (
317+ f"Expected at most 1 question query (joined with answers), got { len (question_queries )} "
318+ )
0 commit comments