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 ``;
+ }
+ },
+ {data: 4, className: 'submissions-td', render: d => `${d}`},
+ {
+ data: 5,
+ orderable: false,
+ className: 'submissions-td',
+ render: d => d.has_file ? `` : 'None'
+ },
+ {
+ data: 6, orderable: false, className: 'submissions-td exclude-click', render: function (data, row) {
+ var regradeBtn = data.is_autograded ? `` : '';
+ 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 +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 @@