From f0a5d37b50c9e56261994dfc7836243db84b1fd6 Mon Sep 17 00:00:00 2001 From: David Wang Date: Thu, 30 Oct 2025 18:07:13 -0400 Subject: [PATCH 1/4] Bump github/codeql-action from 3 to 4 (#2298) --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 806cc0fec016b6883c48169333cbf8fd19c83a44 Mon Sep 17 00:00:00 2001 From: David Wang Date: Thu, 30 Oct 2025 18:07:28 -0400 Subject: [PATCH 2/4] Using pagination for manage submissions page --- app/assets/javascripts/manage_submissions.js | 160 ++++++++++++------- app/controllers/submissions_controller.rb | 117 +++++++++++--- app/views/submissions/index.html.erb | 158 +++--------------- 3 files changed, 224 insertions(+), 211 deletions(-) diff --git a/app/assets/javascripts/manage_submissions.js b/app/assets/javascripts/manage_submissions.js index c89539aa9..40f0cbaec 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'); @@ -105,9 +108,9 @@ $(document).ready(function() { tweaks = []; const submissions_body = data.submissions.map((submission) => { - const Tweak = new AutolabComponent(`tweak-value-${submission.id}`, { amount: null }); + const Tweak = new AutolabComponent(`tweak-value-${submission.id}`, {amount: null}); Tweak.template = function () { - return EditTweakButton( this.state.amount ); + return EditTweakButton(this.state.amount); } tweaks.push({tweak: Tweak, submission_id: submission.id, submission}); @@ -118,10 +121,10 @@ $(document).ready(function() { // Convert to human readable date with timezone const human_readable_created_at = - moment(submission.created_at).format('MMM Do YY, h:mma z UTC Z'); + moment(submission.created_at).format('MMM Do YY, h:mma z UTC Z'); const view_button = submission.filename ? - `
+ `` - : "None"; + : "None"; const download_button = - /text/.test(submission.detected_mime_type) ? + /text/.test(submission.detected_mime_type) ? `
${submission.total} - ${submission.problems. - map((problem) => - ` + ${submission.problems.map((problem) => + ` ${data.scores[submission.id]?.[problem.id]?.['score'] !== undefined - ? ` + ? ` ${data.scores[submission.id][problem.id]['score'].toFixed(1)} ` - : "-"} + : "-"} ` - ).join('')} + ).join('')} ${submission.late_penalty} @@ -185,7 +187,7 @@ $(document).ready(function() { }).join(''); const submissions_table = - `

Click on non-autograded problem scores to edit or leave a comment.

+ `

Click on non-autograded problem scores to edit or leave a comment.

@@ -211,10 +213,10 @@ $(document).ready(function() { "order": [[0, "desc"]], "paging": false, "info": false, - "searching": false,}); + "searching": false, + }); return data.submissions; - }).then((submissions) => { $('.tweak-button').on('click', selectTweak(submissions)); }).catch((err) => { @@ -229,41 +231,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}
`; + } + }, + {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}`; + } + }, + {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 +311,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() { @@ -297,21 +345,17 @@ $(document).ready(function() { var downloadHTML = $('#download-batch-html').html(); var excuseHTML = $('#excuse-batch-html').html(); $('div.selected-buttons').html(`
${regradeHTML}${deleteHTML}${downloadHTML}${excuseHTML}
`); - - // add ids to each selected button $('#selected-buttons > a').each(function () { let idText = this.title.split(' ')[0].toLowerCase() + '-selected'; this.setAttribute('id', idText); }); - if (!is_autograded) { $('#regrade-selected').hide(); $('#regrade-all-html').hide(); } - // base URLs for selected buttons var baseURLs = {}; - buttonIDs.forEach(function(id) { + buttonIDs.forEach(function (id) { baseURLs[id] = $(id).prop('href'); }); @@ -374,7 +418,6 @@ $(document).ready(function() { } }); }); - function changeButtonStates(state) { buttonIDs.forEach((id) => { const button = $(id); @@ -396,14 +439,13 @@ $(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."); return; } const endpoint = manage_submissions_endpoints[id.replace("#", "")]; - const requestData = { submission_ids: selectedSubmissions }; + const requestData = {submission_ids: selectedSubmissions}; if (id === "#delete-selected") { if (!confirm("Deleting will delete all checked submissions and cannot be undone. Are you sure you want to delete these submissions?")) { return; @@ -457,6 +499,8 @@ $(document).ready(function() { // SELECTING STUDENT CHECKBOXES function toggleRow(submissionId, forceSelect = null) { + console.log('toggleRow called:', submissionId, 'forceSelect:', forceSelect); + console.log('selectedSubmissions before:', [...selectedSubmissions]); var selectedCid = submissions_to_cud[submissionId]; const isSelected = selectedSubmissions.includes(submissionId); const shouldSelect = forceSelect !== null ? forceSelect : !isSelected; @@ -489,6 +533,8 @@ $(document).ready(function() { $('#cbox-select-all').prop('checked', numericSelectedSubmissions.length === $('#submissions tbody .cbox').length); updateSelectedCount(numericSelectedSubmissions); changeButtonStates(disableButtons); + console.log('selectedSubmissions after:', [...selectedSubmissions]); + console.log('Checkbox state:', $('#cbox-' + submissionId).prop('checked')); } $('#submissions').on('click', '.exclude-click i', function (e) { diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 9496bb399..ed06ac897 100755 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -17,29 +17,110 @@ 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) + + 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 + total_records = @assessment.submissions.count + filtered_records = base_query.count + submissions_query = base_query + .includes(course_user_datum: :user) + .left_joins(:scores) + .select('submissions.*') + .select('COALESCE(SUM(scores.score), 0) as calculated_score') + .group('submissions.id') + + # Apply sorting + submissions_query = + if order_column == 'calculated_score' + submissions_query.order("calculated_score #{order_direction}") + else + submissions_query.order("#{order_column} #{order_direction}") + end + # Apply pagination + submissions = submissions_query.limit(length).offset(start) - @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 + data = submissions.map do |submission| + format_submission_for_datatable(submission) end - submissions_to_cud.to_json + + 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) + cud = submission.course_user_datum + user = cud.user + excused = @excused_cids.include?(cud.id) + [ + '', + { + name: [user.first_name, user.last_name].reject(&:blank?).join(' '), + email: user.email, + excused:, + cud_id: cud.id + }, + submission.version, + { + score: computed_score { submission.final_score(@cud) }, + 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..e57e82fc0 100755 --- a/app/views/submissions/index.html.erb +++ b/app/views/submissions/index.html.erb @@ -10,8 +10,6 @@