From 1b7f64031e95d10d27460565cf8a2395827e6767 Mon Sep 17 00:00:00 2001 From: Naragod Date: Wed, 3 Dec 2025 12:36:45 -0500 Subject: [PATCH 1/2] ISSUE-7711: Add GET - api/.../groups/test_results api route --- app/controllers/api/groups_controller.rb | 43 ++++++++ config/routes.rb | 1 + .../controllers/api/groups_controller_spec.rb | 97 +++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/app/controllers/api/groups_controller.rb b/app/controllers/api/groups_controller.rb index b571f04938..1304938b7a 100644 --- a/app/controllers/api/groups_controller.rb +++ b/app/controllers/api/groups_controller.rb @@ -203,6 +203,49 @@ def annotations end end + def test_results + test_runs = grouping&.test_runs&.includes(test_group_results: [:test_group, + :test_results])&.order(created_at: :desc) + + if test_runs.blank? + return render 'shared/http_status', + locals: { code: '404', message: 'No test results found for this group' }, + status: :not_found + end + + results_data = test_runs.map do |test_run| + { + id: test_run.id, + status: test_run.status, + created_at: test_run.created_at, + problems: test_run.problems, + test_groups: test_run.test_group_results.map do |test_group_result| + { + name: test_group_result.test_group.name, + marks_earned: test_group_result.marks_earned, + marks_total: test_group_result.marks_total, + time: test_group_result.time, + tests: test_group_result.test_results.order(:position).map do |test| + { + name: test.name, + status: test.status, + marks_earned: test.marks_earned, + marks_total: test.marks_total, + output: test.output, + time: test.time + } + end + } + end + } + end + + respond_to do |format| + format.xml { render xml: results_data.to_xml(root: 'test_runs', skip_types: 'true') } + format.json { render json: results_data } + end + end + def add_annotations result = self.grouping&.current_result return page_not_found('No submission exists for that group') if result.nil? diff --git a/config/routes.rb b/config/routes.rb index 13ccf02ad1..ae2928818f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,6 +42,7 @@ put 'remove_tag' post 'collect_submission' post 'add_test_run' + get 'test_results' end resources :submission_files, only: [:index, :create] do collection do diff --git a/spec/controllers/api/groups_controller_spec.rb b/spec/controllers/api/groups_controller_spec.rb index b17c6ee583..e20984fcab 100644 --- a/spec/controllers/api/groups_controller_spec.rb +++ b/spec/controllers/api/groups_controller_spec.rb @@ -1306,5 +1306,102 @@ end end end + + context 'GET test_results' do + let(:grouping) { create(:grouping_with_inviter, assignment: assignment) } + let(:test_group) { create(:test_group, assignment: assignment) } + + context 'when the group has test results' do + let!(:test_run) { create(:test_run, grouping: grouping, role: instructor, status: :complete) } + let!(:test_group_result) do + create(:test_group_result, test_run: test_run, test_group: test_group, + marks_earned: 5.0, marks_total: 10.0, time: 1000) + end + + before do + create(:test_result, test_group_result: test_group_result, name: 'Test 1', + status: 'pass', marks_earned: 3.0, marks_total: 5.0, position: 1) + end + + context 'expecting json response' do + before do + request.env['HTTP_ACCEPT'] = 'application/json' + get :test_results, params: { id: grouping.group.id, assignment_id: assignment.id, course_id: course.id } + end + + it 'should be successful' do + expect(response).to have_http_status(:ok) + end + + it 'should return test run data' do + expect(response.parsed_body.first['id']).to eq(test_run.id) + expect(response.parsed_body.first['status']).to eq('complete') + end + + it 'should return test group data' do + test_group_data = response.parsed_body.first['test_groups'].first + expect(test_group_data['name']).to eq(test_group.name) + expect(test_group_data['marks_earned']).to eq(5.0) + end + + it 'should return individual test data' do + test_data = response.parsed_body.first['test_groups'].first['tests'].first + expect(test_data['name']).to eq('Test 1') + expect(test_data['status']).to eq('pass') + end + end + + context 'expecting xml response' do + before do + request.env['HTTP_ACCEPT'] = 'application/xml' + get :test_results, params: { id: grouping.group.id, assignment_id: assignment.id, course_id: course.id } + end + + it 'should be successful' do + expect(response).to have_http_status(:ok) + end + + it 'should return xml content' do + xml_data = Hash.from_xml(response.body) + expect(xml_data).to have_key('test_runs') + end + end + end + + context 'authorization check' do + it_behaves_like 'for a different course' do + before do + request.env['HTTP_ACCEPT'] = 'application/json' + get :test_results, params: { id: grouping.group.id, assignment_id: assignment.id, course_id: course.id } + end + end + end + + context 'when the group has no test results' do + before do + request.env['HTTP_ACCEPT'] = 'application/json' + get :test_results, params: { id: grouping.group.id, assignment_id: assignment.id, course_id: course.id } + end + + it 'should return 404 status' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when multiple test runs exist' do + let!(:older_test_run) { create(:test_run, grouping: grouping, role: instructor, created_at: 2.days.ago) } + let!(:newer_test_run) { create(:test_run, grouping: grouping, role: instructor, created_at: 1.hour.ago) } + + before do + request.env['HTTP_ACCEPT'] = 'application/json' + get :test_results, params: { id: grouping.group.id, assignment_id: assignment.id, course_id: course.id } + end + + it 'should return newest first' do + test_run_ids = response.parsed_body.pluck('id') + expect(test_run_ids).to eq([newer_test_run.id, older_test_run.id]) + end + end + end end end From e80cef8d4b1058e1a01e2ec3cfdcf391c60e9974 Mon Sep 17 00:00:00 2001 From: Naragod Date: Wed, 3 Dec 2025 12:43:23 -0500 Subject: [PATCH 2/2] ISSUE-7711: Simplify api response --- Changelog.md | 1 + app/controllers/api/groups_controller.rb | 52 ++++------ app/models/assignment.rb | 26 ++--- .../controllers/api/groups_controller_spec.rb | 97 +++++++++++++++---- 4 files changed, 110 insertions(+), 66 deletions(-) diff --git a/Changelog.md b/Changelog.md index f9e0f49e40..77e57a7757 100644 --- a/Changelog.md +++ b/Changelog.md @@ -20,6 +20,7 @@ - Enable zip downloads of test results (#7733) - Create rake task to remove orphaned end users (#7741) - Enable scanned assignments the ability to add inactive students (#7737) +- Enable test results downloads through the API (#7754) ### 🐛 Bug fixes - Fix name column search in graders table (#7693) diff --git a/app/controllers/api/groups_controller.rb b/app/controllers/api/groups_controller.rb index 1304938b7a..60a0a194ac 100644 --- a/app/controllers/api/groups_controller.rb +++ b/app/controllers/api/groups_controller.rb @@ -204,48 +204,30 @@ def annotations end def test_results - test_runs = grouping&.test_runs&.includes(test_group_results: [:test_group, - :test_results])&.order(created_at: :desc) + return render_no_grouping_error unless grouping - if test_runs.blank? - return render 'shared/http_status', - locals: { code: '404', message: 'No test results found for this group' }, - status: :not_found - end + # Use the existing Assignment#summary_test_results method filtered for this specific group + # This ensures format consistency with the UI download (summary_test_result_json) + group_name = grouping.group.group_name + results = assignment.summary_test_results([group_name]) - results_data = test_runs.map do |test_run| - { - id: test_run.id, - status: test_run.status, - created_at: test_run.created_at, - problems: test_run.problems, - test_groups: test_run.test_group_results.map do |test_group_result| - { - name: test_group_result.test_group.name, - marks_earned: test_group_result.marks_earned, - marks_total: test_group_result.marks_total, - time: test_group_result.time, - tests: test_group_result.test_results.order(:position).map do |test| - { - name: test.name, - status: test.status, - marks_earned: test.marks_earned, - marks_total: test.marks_total, - output: test.output, - time: test.time - } - end - } - end - } - end + return render_no_grouping_error if results.blank? + + # Group by test_group name to match the summary_test_result_json format + results_by_test_group = results.group_by(&:name) respond_to do |format| - format.xml { render xml: results_data.to_xml(root: 'test_runs', skip_types: 'true') } - format.json { render json: results_data } + format.xml { render xml: results_by_test_group.to_xml(root: 'test_results', skip_types: 'true') } + format.json { render json: results_by_test_group } end end + def render_no_grouping_error + render 'shared/http_status', + locals: { code: '404', message: 'No test results found for this group' }, + status: :not_found + end + def add_annotations result = self.grouping&.current_result return page_not_found('No submission exists for that group') if result.nil? diff --git a/app/models/assignment.rb b/app/models/assignment.rb index 70e28fde19..a281ad07a8 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -668,7 +668,7 @@ def summary_json(user) end # Generates the summary of the most test results associated with an assignment. - def summary_test_results + def summary_test_results(group_names = nil) latest_test_run_by_grouping = TestRun.group('grouping_id').select('MAX(created_at) as test_runs_created_at', 'grouping_id') .where.not(submission_id: nil) @@ -682,17 +682,21 @@ def summary_test_results .select('id', 'test_runs.grouping_id', 'groups.group_name') .to_sql - self.test_groups.joins(test_group_results: :test_results) - .joins("INNER JOIN (#{latest_test_runs}) latest_test_runs \ + query = self.test_groups.joins(test_group_results: :test_results) + .joins("INNER JOIN (#{latest_test_runs}) latest_test_runs \ ON test_group_results.test_run_id = latest_test_runs.id") - .select('test_groups.name', - 'test_groups.id as test_groups_id', - 'latest_test_runs.group_name', - 'test_results.name as test_result_name', - 'test_results.status', - 'test_results.marks_earned', - 'test_results.marks_total', - :output, :extra_info, :error_type) + + # Optionally - filters specific groups if provided + query = query.where('latest_test_runs.group_name': group_names) if group_names.present? + + query.select('test_groups.name', + 'test_groups.id as test_groups_id', + 'latest_test_runs.group_name', + 'test_results.name as test_result_name', + 'test_results.status', + 'test_results.marks_earned', + 'test_results.marks_total', + :output, :extra_info, :error_type) end # Generate a JSON summary of the most recent test results associated with an assignment. diff --git a/spec/controllers/api/groups_controller_spec.rb b/spec/controllers/api/groups_controller_spec.rb index e20984fcab..1a043a6389 100644 --- a/spec/controllers/api/groups_controller_spec.rb +++ b/spec/controllers/api/groups_controller_spec.rb @@ -1310,9 +1310,12 @@ context 'GET test_results' do let(:grouping) { create(:grouping_with_inviter, assignment: assignment) } let(:test_group) { create(:test_group, assignment: assignment) } + let(:submission) { create(:version_used_submission, grouping: grouping) } context 'when the group has test results' do - let!(:test_run) { create(:test_run, grouping: grouping, role: instructor, status: :complete) } + let!(:test_run) do + create(:test_run, grouping: grouping, role: instructor, status: :complete, submission: submission) + end let!(:test_group_result) do create(:test_group_result, test_run: test_run, test_group: test_group, marks_earned: 5.0, marks_total: 10.0, time: 1000) @@ -1333,21 +1336,19 @@ expect(response).to have_http_status(:ok) end - it 'should return test run data' do - expect(response.parsed_body.first['id']).to eq(test_run.id) - expect(response.parsed_body.first['status']).to eq('complete') + it 'should return data grouped by test group name' do + expect(response.parsed_body).to have_key(test_group.name) end - it 'should return test group data' do - test_group_data = response.parsed_body.first['test_groups'].first - expect(test_group_data['name']).to eq(test_group.name) - expect(test_group_data['marks_earned']).to eq(5.0) - end - - it 'should return individual test data' do - test_data = response.parsed_body.first['test_groups'].first['tests'].first - expect(test_data['name']).to eq('Test 1') - expect(test_data['status']).to eq('pass') + it 'should return test results for the group' do + test_results = response.parsed_body[test_group.name] + expect(test_results).to be_an(Array) + expect(test_results.first).to include( + 'test_result_name' => 'Test 1', + 'status' => 'pass', + 'marks_earned' => 3.0, + 'marks_total' => 5.0 + ) end end @@ -1363,7 +1364,34 @@ it 'should return xml content' do xml_data = Hash.from_xml(response.body) - expect(xml_data).to have_key('test_runs') + expect(xml_data).to have_key('test_results') + end + end + + context 'with multiple test groups' do + let(:test_group_two) { create(:test_group, assignment: assignment, name: 'Group B') } + let!(:test_group_result_two) do + create(:test_group_result, test_run: test_run, test_group: test_group_two) + end + + before do + create(:test_result, test_group_result: test_group_result_two, name: 'Test B1', + status: 'pass', marks_earned: 2.0, marks_total: 5.0, position: 1) + request.env['HTTP_ACCEPT'] = 'application/json' + get :test_results, params: { id: grouping.group.id, assignment_id: assignment.id, course_id: course.id } + end + + it 'should be successful' do + expect(response).to have_http_status(:ok) + end + + it 'should return results keyed by each test group name' do + expect(response.parsed_body.keys).to contain_exactly(test_group.name, test_group_two.name) + end + + it 'should return correct test results for each group' do + expect(response.parsed_body[test_group.name].first['test_result_name']).to eq('Test 1') + expect(response.parsed_body[test_group_two.name].first['test_result_name']).to eq('Test B1') end end end @@ -1388,18 +1416,47 @@ end end + context 'when the group does not exist' do + before do + request.env['HTTP_ACCEPT'] = 'application/json' + get :test_results, params: { id: 999_999, assignment_id: assignment.id, course_id: course.id } + end + + it 'should return 404 status' do + expect(response).to have_http_status(:not_found) + end + end + context 'when multiple test runs exist' do - let!(:older_test_run) { create(:test_run, grouping: grouping, role: instructor, created_at: 2.days.ago) } - let!(:newer_test_run) { create(:test_run, grouping: grouping, role: instructor, created_at: 1.hour.ago) } + let!(:older_test_run) do + create(:test_run, grouping: grouping, role: instructor, created_at: 2.days.ago, status: :complete, + submission: submission) + end + let!(:newer_test_run) do + create(:test_run, grouping: grouping, role: instructor, created_at: 1.hour.ago, status: :complete, + submission: submission) + end + let!(:older_test_group_result) do + create(:test_group_result, test_run: older_test_run, test_group: test_group) + end + let!(:newer_test_group_result) do + create(:test_group_result, test_run: newer_test_run, test_group: test_group) + end before do + create(:test_result, test_group_result: older_test_group_result, name: 'Old Test', + marks_earned: 1.0, marks_total: 5.0, status: 'pass', position: 1) + create(:test_result, test_group_result: newer_test_group_result, name: 'New Test', + marks_earned: 4.0, marks_total: 5.0, status: 'pass', position: 1) request.env['HTTP_ACCEPT'] = 'application/json' get :test_results, params: { id: grouping.group.id, assignment_id: assignment.id, course_id: course.id } end - it 'should return newest first' do - test_run_ids = response.parsed_body.pluck('id') - expect(test_run_ids).to eq([newer_test_run.id, older_test_run.id]) + it 'should return only the latest test run results' do + test_results = response.parsed_body[test_group.name] + expect(test_results.length).to eq(1) + expect(test_results.first['test_result_name']).to eq('New Test') + expect(test_results.first['marks_earned']).to eq(4.0) end end end