@@ -7499,3 +7499,227 @@ def test_update_challenge_attributes_when_not_a_staff(self):
74997499
75007500 self .assertEqual (response .data , expected )
75017501 self .assertEqual (response .status_code , status .HTTP_401_UNAUTHORIZED )
7502+
7503+
7504+ class GetChallengeSubmissionMetricsByPkTest (BaseAPITestClass ):
7505+ """Tests for the get_challenge_submission_metrics_by_pk endpoint."""
7506+
7507+ def setUp (self ):
7508+ super ().setUp ()
7509+
7510+ with self .settings (MEDIA_ROOT = "/tmp/evalai" ):
7511+ self .challenge_phase1 = ChallengePhase .objects .create (
7512+ name = "Phase 1" ,
7513+ description = "Description for Phase 1" ,
7514+ leaderboard_public = False ,
7515+ is_public = True ,
7516+ start_date = timezone .now () - timedelta (days = 2 ),
7517+ end_date = timezone .now () + timedelta (days = 1 ),
7518+ challenge = self .challenge ,
7519+ codename = "phase_1_codename" ,
7520+ test_annotation = SimpleUploadedFile (
7521+ "test_annotation1.txt" ,
7522+ b"Dummy file content" ,
7523+ content_type = "text/plain" ,
7524+ ),
7525+ )
7526+
7527+ self .challenge_phase2 = ChallengePhase .objects .create (
7528+ name = "Phase 2" ,
7529+ description = "Description for Phase 2" ,
7530+ leaderboard_public = False ,
7531+ is_public = True ,
7532+ start_date = timezone .now () - timedelta (days = 2 ),
7533+ end_date = timezone .now () + timedelta (days = 1 ),
7534+ challenge = self .challenge ,
7535+ codename = "phase_2_codename" ,
7536+ test_annotation = SimpleUploadedFile (
7537+ "test_annotation2.txt" ,
7538+ b"Dummy file content" ,
7539+ content_type = "text/plain" ,
7540+ ),
7541+ )
7542+
7543+ self .url = reverse_lazy (
7544+ "challenges:get_challenge_submission_metrics_by_pk" ,
7545+ kwargs = {"pk" : self .challenge .pk },
7546+ )
7547+
7548+ def _create_submissions (self , phase , status_counts ):
7549+ """Helper to create submissions with given status counts.
7550+
7551+ Note: Submission.save() always forces status='submitted' for new
7552+ records, so we create them first and then use queryset .update()
7553+ to set the desired status without triggering the save override.
7554+ """
7555+ with self .settings (MEDIA_ROOT = "/tmp/evalai" ):
7556+ for sub_status , count in status_counts .items ():
7557+ pks = []
7558+ for _ in range (count ):
7559+ sub = Submission .objects .create (
7560+ participant_team = self .participant_team ,
7561+ challenge_phase = phase ,
7562+ created_by = self .user ,
7563+ status = "submitted" ,
7564+ input_file = SimpleUploadedFile (
7565+ "test_file.txt" ,
7566+ b"Dummy file content" ,
7567+ content_type = "text/plain" ,
7568+ ),
7569+ )
7570+ pks .append (sub .pk )
7571+ if sub_status != "submitted" :
7572+ Submission .objects .filter (pk__in = pks ).update (
7573+ status = sub_status
7574+ )
7575+
7576+ def test_returns_403_for_non_staff_user (self ):
7577+ """Non-staff users should be denied access."""
7578+ self .user .is_staff = False
7579+ self .user .save ()
7580+ self .client .force_authenticate (user = self .user )
7581+
7582+ response = self .client .get (self .url )
7583+ self .assertEqual (response .status_code , status .HTTP_403_FORBIDDEN )
7584+
7585+ def test_returns_all_status_keys_for_empty_challenge (self ):
7586+ """Even with no submissions, all STATUS_OPTIONS keys should appear as 0."""
7587+ self .user .is_staff = True
7588+ self .user .save ()
7589+ self .client .force_authenticate (user = self .user )
7590+
7591+ response = self .client .get (self .url )
7592+ self .assertEqual (response .status_code , status .HTTP_200_OK )
7593+
7594+ expected_statuses = [s [0 ] for s in Submission .STATUS_OPTIONS ]
7595+ for s in expected_statuses :
7596+ self .assertIn (s , response .data )
7597+ self .assertEqual (response .data [s ], 0 )
7598+
7599+ def test_returns_correct_counts_single_phase (self ):
7600+ """Counts should be correct when submissions exist in one phase."""
7601+ self .user .is_staff = True
7602+ self .user .save ()
7603+ self .client .force_authenticate (user = self .user )
7604+
7605+ self ._create_submissions (
7606+ self .challenge_phase1 ,
7607+ {"submitted" : 3 , "finished" : 5 , "failed" : 2 },
7608+ )
7609+
7610+ response = self .client .get (self .url )
7611+ self .assertEqual (response .status_code , status .HTTP_200_OK )
7612+ self .assertEqual (response .data ["submitted" ], 3 )
7613+ self .assertEqual (response .data ["finished" ], 5 )
7614+ self .assertEqual (response .data ["failed" ], 2 )
7615+ self .assertEqual (response .data ["running" ], 0 )
7616+
7617+ def test_returns_correct_counts_multiple_phases (self ):
7618+ """Counts should aggregate across all phases of the challenge."""
7619+ self .user .is_staff = True
7620+ self .user .save ()
7621+ self .client .force_authenticate (user = self .user )
7622+
7623+ self ._create_submissions (
7624+ self .challenge_phase1 ,
7625+ {"submitted" : 2 , "finished" : 4 },
7626+ )
7627+ self ._create_submissions (
7628+ self .challenge_phase2 ,
7629+ {"submitted" : 1 , "finished" : 3 , "failed" : 7 },
7630+ )
7631+
7632+ response = self .client .get (self .url )
7633+ self .assertEqual (response .status_code , status .HTTP_200_OK )
7634+ self .assertEqual (response .data ["submitted" ], 3 ) # 2 + 1
7635+ self .assertEqual (response .data ["finished" ], 7 ) # 4 + 3
7636+ self .assertEqual (response .data ["failed" ], 7 )
7637+ self .assertEqual (response .data ["running" ], 0 )
7638+
7639+ def test_does_not_count_submissions_from_other_challenges (self ):
7640+ """Submissions belonging to a different challenge must not be counted."""
7641+ self .user .is_staff = True
7642+ self .user .save ()
7643+ self .client .force_authenticate (user = self .user )
7644+
7645+ other_challenge = Challenge .objects .create (
7646+ title = "Other Challenge" ,
7647+ short_description = "Other short desc" ,
7648+ description = "Other description" ,
7649+ terms_and_conditions = "Other terms" ,
7650+ submission_guidelines = "Other guidelines" ,
7651+ creator = self .challenge_host_team ,
7652+ published = False ,
7653+ is_registration_open = True ,
7654+ enable_forum = True ,
7655+ anonymous_leaderboard = False ,
7656+ start_date = timezone .now () - timedelta (days = 2 ),
7657+ end_date = timezone .now () + timedelta (days = 1 ),
7658+ approved_by_admin = False ,
7659+ )
7660+ with self .settings (MEDIA_ROOT = "/tmp/evalai" ):
7661+ other_phase = ChallengePhase .objects .create (
7662+ name = "Other Phase" ,
7663+ description = "Other phase desc" ,
7664+ leaderboard_public = False ,
7665+ is_public = True ,
7666+ start_date = timezone .now () - timedelta (days = 2 ),
7667+ end_date = timezone .now () + timedelta (days = 1 ),
7668+ challenge = other_challenge ,
7669+ codename = "other_phase_codename" ,
7670+ test_annotation = SimpleUploadedFile (
7671+ "test_annotation_other.txt" ,
7672+ b"Dummy file content" ,
7673+ content_type = "text/plain" ,
7674+ ),
7675+ )
7676+
7677+ self ._create_submissions (
7678+ self .challenge_phase1 ,
7679+ {"finished" : 2 },
7680+ )
7681+ self ._create_submissions (
7682+ other_phase ,
7683+ {"finished" : 10 },
7684+ )
7685+
7686+ response = self .client .get (self .url )
7687+ self .assertEqual (response .status_code , status .HTTP_200_OK )
7688+ self .assertEqual (response .data ["finished" ], 2 )
7689+
7690+ def test_query_count_is_constant (self ):
7691+ """
7692+ The endpoint should use a constant number of DB queries regardless
7693+ of how many statuses have submissions. The old code ran 10 separate
7694+ COUNT queries; the fix uses a single aggregated GROUP BY query.
7695+ """
7696+ self .user .is_staff = True
7697+ self .user .save ()
7698+ self .client .force_authenticate (user = self .user )
7699+
7700+ self ._create_submissions (
7701+ self .challenge_phase1 ,
7702+ {
7703+ "submitted" : 1 ,
7704+ "running" : 1 ,
7705+ "failed" : 1 ,
7706+ "finished" : 1 ,
7707+ "cancelled" : 1 ,
7708+ "queued" : 1 ,
7709+ },
7710+ )
7711+
7712+ with CaptureQueriesContext (connection ) as context :
7713+ response = self .client .get (self .url )
7714+
7715+ self .assertEqual (response .status_code , status .HTTP_200_OK )
7716+ # Auth (token check, user load) + challenge lookup + 1 aggregated query
7717+ # should be well under 10. The old code alone produced 10 COUNT
7718+ # queries.
7719+ self .assertLessEqual (
7720+ len (context .captured_queries ),
7721+ 8 ,
7722+ "Query count too high – possible N+1 regression in "
7723+ "get_challenge_submission_metrics_by_pk. "
7724+ f"Queries: { [q ['sql' ] for q in context .captured_queries ]} " ,
7725+ )
0 commit comments