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 b571f04938..60a0a194ac 100644 --- a/app/controllers/api/groups_controller.rb +++ b/app/controllers/api/groups_controller.rb @@ -203,6 +203,31 @@ def annotations end end + def test_results + return render_no_grouping_error unless grouping + + # 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]) + + 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_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/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..1a043a6389 100644 --- a/spec/controllers/api/groups_controller_spec.rb +++ b/spec/controllers/api/groups_controller_spec.rb @@ -1306,5 +1306,159 @@ end end end + + 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) 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) + 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 data grouped by test group name' do + expect(response.parsed_body).to have_key(test_group.name) + end + + 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 + + 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_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 + + 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 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) 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 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 end end