diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 376e2a90b..631f302e9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -44,7 +44,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -71,4 +71,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 diff --git a/app/assets/javascripts/manage_submissions.js b/app/assets/javascripts/manage_submissions.js index c89539aa9..74c7ab758 100644 --- a/app/assets/javascripts/manage_submissions.js +++ b/app/assets/javascripts/manage_submissions.js @@ -11,7 +11,10 @@ const buttonIDs = ['#regrade-selected', '#delete-selected', '#download-selected' let tweaks = []; let currentPage = 0; $(document).ready(function() { - var submission_info = {} + var submission_info = {}; + var selectedStudentCids = []; + var selectedSubmissions = []; + var submissions_to_cud = {}; // Build dynamically from server data const EditTweakButton = (totalSum) => { if (totalSum == null) { return ` @@ -70,7 +73,7 @@ $(document).ready(function() { $(document).ready(function () { $('.modal').modal(); - $('.score-details').on('click', function () { + $(document).on('click', '.score-details', function () { // Get the email const course_user_datum_id = $(this).data('cuid'); const email = $(this).data('email'); @@ -229,41 +232,79 @@ $(document).ready(function() { }); }); - var selectedStudentCids = []; - var selectedSubmissions = []; - + // Initialize DataTable with server-side processing var table = $('#submissions').DataTable({ - 'dom': 'f<"selected-buttons">rtip', // show buttons, search, table - 'paging': true, - 'createdRow': completeRow, - 'sPaginationType': 'full_numbers', - 'pageLength': 100, - 'info': true, - 'deferRender': true, + dom: 'f<"selected-buttons">rtip', + processing: true, + serverSide: true, + ajax: { + url: window.location.pathname + '.json', + type: 'GET', + dataSrc: function (json) { + json.data.forEach(row => { + submissions_to_cud[row[7]] = row[8]; + }); + return json.data; + }, + error: function (xhr, error, code) { + console.error('DataTables error:', error, code); + } + }, + columns: [ + { + data: null, orderable: false, className: 'submissions-td submissions-checkbox', + render: function (data, type, row, meta) { + return `
`; + } + }, + { + data: 1, className: 'submissions-td', + render: function (data, type, row, meta) { + var excusedLabel = data.excused ? 'EXCUSED' : ''; + return `
${data.name || ''}${excusedLabel}
${data.email}`; + } + }, + {data: 2, className: 'submissions-td'}, + { + data: 3, className: 'submissions-td', + render: function (data, type, row, meta) { + var score = data.score != null ? data.score : '-'; + return `
${score}
zoom_in
`; + } + }, + {data: 4, className: 'submissions-td', render: d => `${d}`}, + { + data: 5, + orderable: false, + className: 'submissions-td', + render: d => d.has_file ? `
zoom_in

View File

` : 'None' + }, + { + data: 6, orderable: false, className: 'submissions-td exclude-click', render: function (data, row) { + var regradeBtn = data.is_autograded ? `
autorenew

Regrade

` : ''; + return `${regradeBtn}
delete_outline

Delete

`; + } + }, + {data: 7, visible: false}, + {data: 8, visible: false} + ], + pageLength: 100, + lengthMenu: [[25, 50, 100, 200], [25, 50, 100, 200]], + order: [[4, 'desc']], + createdRow: function (row, data) { + var submissionId = data[7]; + $(row).attr('id', 'row-' + submissionId).attr('data-submission-id', submissionId).addClass('submission-row'); + }, + drawCallback: function () { + $('#submissions tbody .cbox').each(function () { + var submissionId = parseInt($(this).attr('id').replace('cbox-', ''), 10); + $(this).prop('checked', selectedSubmissions.includes(submissionId)); + if (selectedSubmissions.includes(submissionId)) $(this).closest('tr').addClass('selected'); + }); + updateSelectAllCheckbox(); + } }); - // Check if the table is empty - if (table.data().count() === 0) { - $('#submissions').closest('.dataTables_wrapper').hide(); // Hide the table and its controls - $('#no-data-message').show(); // Optionally show a custom message - } else { - $('#no-data-message').hide(); // Hide custom message when there is data - } - - function completeRow(row, data, index) { - var submission = additional_data[index]; - $(row).attr('data-submission-id', submission['submission-id']); - } - - $('thead').on('click', function(e) { - if (currentPage < 0) { - currentPage = 0 - } - if (currentPage > table.page.info().pages) { - currentPage = table.page.info().pages - 1 - } - table.page(currentPage).draw(false); - }) // Listen for select-all checkbox click $('#cbox-select-all').on('click', async function(e) { @@ -271,6 +312,14 @@ $(document).ready(function() { await toggleAllRows(selectAll); }); + function updateSelectAllCheckbox() { + var allChecked = true, anyChecked = false; + $('#submissions tbody .cbox').each(function () { + if ($(this).prop('checked')) anyChecked = true; else allChecked = false; + }); + $('#cbox-select-all').prop('checked', allChecked && anyChecked); + } + // Function to toggle all checkboxes function toggleAllRows(selectAll) { $('#submissions tbody .cbox').each(function() { @@ -396,7 +445,6 @@ $(document).ready(function() { return; } $(document).off("click", id).on("click", id, function (event) { - console.log(`${id} button clicked`); event.preventDefault(); if (selectedSubmissions.length === 0) { alert("No submissions selected."); diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 9496bb399..1941e9332 100755 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -17,29 +17,123 @@ class SubmissionsController < ApplicationController action_auth_level :index, :instructor def index - # cache ids instead of entire entries - submission_ids = Rails.cache.fetch(["submission_ids", @assessment.id], expires_in: 1.day) do - @assessment.submissions.order("created_at DESC").pluck(:id) - end - @submissions = Submission.where(id: submission_ids).includes({ course_user_datum: :user }) @autograded = @assessment.has_autograder? + @problems = @assessment.problems.to_a + @excused_cids = AssessmentUserDatum + .where(assessment_id: @assessment.id, grade_type: AssessmentUserDatum::EXCUSED) + .pluck(:course_user_datum_id) - @submissions_to_cud = - Rails.cache.fetch(["submissions_to_cud", @assessment.id], expires_in: 1.day) do - submissions_to_cud = {} - @submissions.each do |submission| - submissions_to_cud[submission.id] = submission.course_user_datum_id + respond_to do |format| + format.html do + @submissions = @assessment.submissions + .includes(course_user_datum: :user) + .order(created_at: :desc) + .limit(100) + + @submissions_to_cud = @submissions.pluck(:id, :course_user_datum_id).to_h + end + + format.json do + draw = params[:draw].to_i + start = params[:start].to_i + length = params[:length].to_i + search_value = params.dig(:search, :value) + order_column_index = params.dig(:order, '0', :column).to_i + order_direction = params.dig(:order, '0', :dir) || 'desc' + + columns_map = { + 1 => 'users.email', + 2 => 'submissions.version', + 3 => 'calculated_score', + 4 => 'submissions.created_at', + } + + order_column = columns_map[order_column_index] || 'submissions.created_at' + + base_query = @assessment.submissions + .joins(course_user_datum: :user) + + if search_value.present? + base_query = base_query.where( + "users.email LIKE :search", + search: "%#{search_value}%" + ) end - submissions_to_cud.to_json + + total_records = @assessment.submissions.count + filtered_records = base_query.count + + # If sorting by score, we have to load the data into ruby b/c we don't stored + # final scores in our database + if order_column == 'calculated_score' + all_submissions = base_query.includes(course_user_datum: :user).to_a + submissions_with_scores = all_submissions.map do |submission| + cud = submission.course_user_datum + [submission, submission.final_score(cud)] + end + sorted_submissions = submissions_with_scores.sort_by { |_, score| score } + sorted_submissions.reverse! if order_direction == 'desc' + paginated_submissions = sorted_submissions[start, length] || [] + + data = paginated_submissions.map do |submission, score| + format_submission_for_datatable(submission, score) + end + else + submissions = base_query + .includes(course_user_datum: :user) + .order("#{order_column} #{order_direction}") + .limit(length) + .offset(start) + + data = submissions.map do |submission| + cud = submission.course_user_datum + score = submission.final_score(cud) + format_submission_for_datatable(submission, score) + end + end + + render json: { + draw:, + recordsTotal: total_records, + recordsFiltered: filtered_records, + data:, + } end + end + end - @excused_cids = [] - excused_students = AssessmentUserDatum.where( - assessment_id: @assessment.id, - grade_type: AssessmentUserDatum::EXCUSED - ) - @excused_cids = excused_students.pluck(:course_user_datum_id) - @problems = @assessment.problems.to_a + action_auth_level :format_submission_for_datatable, :instructor + def format_submission_for_datatable(submission, score = nil) + cud = submission.course_user_datum + user = cud.user + excused = @excused_cids.include?(cud.id) + score ||= submission.final_score(cud) + [ + '', + { + name: [user.first_name, user.last_name].reject(&:blank?).join(' '), + email: user.email, + excused:, + cud_id: cud.id + }, + submission.version, + { + score: computed_score { score }, + email: user.email, + cud_id: cud.id + }, + submission.created_at.in_time_zone.to_s, + { + has_file: submission.filename.present?, + submission_id: submission.id + }, + { + submission_id: submission.id, + is_autograded: @autograded + }, + submission.id, + cud.id + ] end action_auth_level :score_details, :instructor diff --git a/app/views/submissions/index.html.erb b/app/views/submissions/index.html.erb index f1fb124d6..3ceac69bf 100755 --- a/app/views/submissions/index.html.erb +++ b/app/views/submissions/index.html.erb @@ -10,8 +10,6 @@