diff --git a/Changelog.md b/Changelog.md index 424707692d..66c9db8f00 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ ### 🚨 Breaking changes ### ✨ New features and improvements +- Added more download options to automated test result summaries (#7599) ### 🐛 Bug fixes diff --git a/app/assets/stylesheets/common/_markus.scss b/app/assets/stylesheets/common/_markus.scss index 7867e82139..7ff050a4dd 100644 --- a/app/assets/stylesheets/common/_markus.scss +++ b/app/assets/stylesheets/common/_markus.scss @@ -1530,3 +1530,76 @@ canvas { .jcrop-centered { display: inline-block; } + +.feature-option { + margin: 0.5rem; +} + +.feature-option input[type='radio'] { + display: none; +} + +.feature-option label { + width: 100px; + background: linear-gradient(to top, $background-main, $background-support); + border: 1px solid $primary-three; + border-radius: $radius; + box-shadow: inset 0 -1px 0 $primary-two; + color: $line; + cursor: pointer; + display: inline-block; + font: 400 1em $fonts; + min-width: 150px; + outline: none; + padding: 0.5em 1.5em; + text-align: center; + transition: all $time-quick; +} + +.feature-option input[type='radio']:checked + label { + background: linear-gradient(to bottom, $primary-one, $primary-three); + border: 1px solid $primary-one; + color: $background-main; + box-shadow: inset 0 1px 0 $primary-one; +} + +.feature-option label:hover { + border-color: $primary-one; + box-shadow: + inset 0 -1px 0 $primary-two, + 0 1px 0 $background-main; +} + +.feature-option input[type='radio']:disabled + label { + color: $line; + cursor: not-allowed; + background: linear-gradient(to top, $background-main, $disabled-area); + border-color: $disabled-area; + box-shadow: none; +} + +.feature-option input[type='radio']:disabled + label:hover { + border-color: $disabled-area; +} + +.feature-grid-wrap { + display: grid; + grid-template-columns: repeat(3, max-content); + column-gap: 0; +} + +.feature-row { + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + column-gap: inherit; + width: 150px; +} + +.feature-submit-group { + display: flex; + flex-direction: row; + gap: 1rem; + justify-content: right; + margin-top: 2rem; +} diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb index 42cf411688..dd0f37a257 100644 --- a/app/controllers/assignments_controller.rb +++ b/app/controllers/assignments_controller.rb @@ -271,22 +271,54 @@ def summary def download_test_results @assignment = record + + latest = params[:latest] == 'true' + student_run = params[:student_run] == 'true' + instructor_run = params[:instructor_run] == 'true' + + test_results = SummaryTestResultsHelper::SummaryTestResults.fetch( + test_groups: @assignment.test_groups, + latest:, + student_run:, + instructor_run: + ) + respond_to do |format| format.json do - data = @assignment.summary_test_result_json - filename = "#{@assignment.short_identifier}_test_results.json" - send_data data, - disposition: 'attachment', - type: 'application/json', - filename: filename + data = test_results.as_json + + if latest + send_data( + data, + type: 'application/json', + disposition: 'attachment', + filename: "#{@assignment.short_identifier}_test_results.json" + ) + else + zip_path = "tmp/#{@assignment.short_identifier}_test_results.zip" + Zip::File.open(zip_path, create: true) do |zip_file| + zip_file.get_output_stream('test_results.json') do |f| + f.write(data) + end + end + + send_file( + zip_path, + disposition: 'attachment', + filename: "#{@assignment.short_identifier}_test_results.zip" + ) + end end + format.csv do - data = @assignment.summary_test_result_csv filename = "#{@assignment.short_identifier}_test_results.csv" - send_data data, - disposition: 'attachment', - type: 'text/csv', - filename: filename + + send_data( + test_results.as_csv, + disposition: 'attachment', + type: 'text/csv', + filename: filename + ) end end end diff --git a/app/helpers/summary_test_results_helper.rb b/app/helpers/summary_test_results_helper.rb new file mode 100644 index 0000000000..63f4945d94 --- /dev/null +++ b/app/helpers/summary_test_results_helper.rb @@ -0,0 +1,97 @@ +module SummaryTestResultsHelper + class SummaryTestResults + class << self + def fetch(test_groups:, latest:, student_run:, instructor_run:) + query = base_query(latest:) + + query = query.student_run if student_run && !instructor_run + query = query.instructor_run if !student_run && instructor_run + + test_results = fetch_with_query(test_groups:, query:) + + SummaryTestResult.new(test_results:) + end + + private + + def base_query(latest:) + if latest + TestRun.group('grouping_id').select('MAX(created_at) as test_runs_created_at', 'grouping_id') + else + TestRun.select('created_at as test_runs_created_at', 'grouping_id') + end + end + + def fetch_with_query(test_groups:, query:) + latest_test_runs = TestRun + .joins(grouping: :group) + .joins("INNER JOIN (#{query.to_sql}) latest_test_run_by_grouping \ + ON latest_test_run_by_grouping.grouping_id = test_runs.grouping_id \ + AND latest_test_run_by_grouping.test_runs_created_at = test_runs.created_at") + .select('id', 'test_runs.grouping_id', 'groups.group_name') + .to_sql + + 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) + end + end + end + + class SummaryTestResult + def initialize(test_results:) + @test_results = test_results + end + + def as_csv + results = {} + headers = Set.new + + summary_test_results = @test_results.as_json + + summary_test_results.each do |test_result| + header = "#{test_result['name']}:#{test_result['test_result_name']}" + + if results.key?(test_result['group_name']) + results[test_result['group_name']][header] = test_result['status'] + else + results[test_result['group_name']] = { header => test_result['status'] } + end + + headers << header + end + headers = headers.sort + + CSV.generate do |csv| + csv << [nil, *headers] + + results.sort_by(&:first).each do |(group_name, _test_group)| + row = [group_name] + + headers.each do |header| + if results[group_name].key?(header) + row << results[group_name][header] + else + row << nil + end + end + csv << row + end + end + end + + def as_json + @test_results.group_by(&:group_name).transform_values do |grouping| + grouping.group_by(&:name) + end.to_json + end + end +end diff --git a/app/javascript/Components/Modals/download_test_results_modal.jsx b/app/javascript/Components/Modals/download_test_results_modal.jsx index 35f12f18a0..33e42608d3 100644 --- a/app/javascript/Components/Modals/download_test_results_modal.jsx +++ b/app/javascript/Components/Modals/download_test_results_modal.jsx @@ -2,6 +2,16 @@ import React from "react"; import Modal from "react-modal"; class DownloadTestResultsModal extends React.Component { + constructor(props) { + super(props); + this.state = { + studentRun: true, + instructorRun: false, + latest: true, + format: "json", + }; + } + componentDidMount() { Modal.setAppElement("body"); } @@ -18,49 +28,123 @@ class DownloadTestResultsModal extends React.Component { item: I18n.t("activerecord.models.test_result.other"), })} -
- -