Skip to content

Conversation

@dwang3851
Copy link
Contributor

Description

This changes implements server-side pagination for the manage submissions page to improve loading times.

Motivation and Context

Previously, it would take about 1.5 seconds for each page of the manage submissions page to load since we would render all of them at once. For example, for a submissions page with 400 submissions, it would take about 6 seconds. This change renders only one page at a time, to take a maximum of 1.5 seconds each time.

How Has This Been Tested?

Tested all of the manage submissions functions locally

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have run rubocop and erblint for style check. If you haven't, run overcommit --install && overcommit --sign to use pre-commit hook for linting
  • My change requires a change to the documentation, which is located at Autolab Docs
  • I have updated the documentation accordingly, included in this PR

@dwang3851 dwang3851 marked this pull request as ready for review October 30, 2025 22:22
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

📝 Walkthrough

Walkthrough

Switched submissions index to DataTables server-side rendering; controller now serves paginated/searchable/sortable JSON and a new formatter method. Client JS manages selection state, delegated events, and dynamic row rendering; view removed server-rendered rows and added a DataTables-driven tbody.

Changes

Cohort / File(s) Summary
DataTables Server-Side Integration
app/controllers/submissions_controller.rb, app/views/submissions/index.html.erb, app/assets/javascripts/manage_submissions.js
Replaced server-rendered table body with DataTables server-side processing. Controller responds with JSON supporting paging, searching (including email), multi-column sorting (including computed score), and per-row formatting via format_submission_for_datatable. View now supplies header + select-all cell; JS initializes DataTables and populates rows via AJAX.
Client Selection & State Management
app/assets/javascripts/manage_submissions.js
Added selectedStudentCids, selectedSubmissions, and submissions_to_cud state; implemented toggleAllRows, toggleRow, updateSelectAllCheckbox, and updateSelectedCount; delegated event handlers for dynamic elements and draw/createdRow logic to sync checkbox and row highlight state.
Row Rendering & Actions
app/controllers/submissions_controller.rb, app/assets/javascripts/manage_submissions.js
Centralized per-row HTML generation in controller formatter; JS columns/createdRow populate checkboxes, EXCUSED label, score modal trigger, view/download links, regrade/delete controls, and hidden IDs; drawCallback applies selection persistence.
View Simplification
app/views/submissions/index.html.erb
Removed previously rendered submissions_to_cud, per-row markup, selected-count placeholder, and Regrade All block; retained modals and annotation placeholders; simplified header generation to include a select-all checkbox and iconized sortable headers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser as Browser (manage_submissions.js)
    participant Server as Server (SubmissionsController)
    participant DB as Database

    rect rgb(230, 245, 255)
    Note over User,Browser: Page load
    User->>Browser: GET /submissions
    Browser->>Server: GET /submissions (HTML)
    Server->>DB: Query assignments/problems (page scaffold)
    DB-->>Server: Return data
    Server-->>Browser: Render index view (DataTables config)
    Browser->>Browser: Initialize DataTables
    end

    rect rgb(245, 230, 255)
    Note over Browser,Server: DataTables requests rows (server-side)
    Browser->>Server: AJAX GET /submissions.json (draw, start, length, search, order)
    Server->>DB: Query submissions with filters/sort/limit
    DB-->>Server: Return rows
    Server->>Server: format_submission_for_datatable per row
    Server-->>Browser: JSON response (data, draw, recordsTotal, recordsFiltered)
    Browser->>Browser: createdRow + drawCallback -> render checkboxes, actions, selection state
    end

    rect rgb(230, 255, 235)
    Note over User,Browser: Selection flow
    User->>Browser: Click row checkbox / select-all
    Browser->>Browser: toggleRow / toggleAllRows -> update selectedSubmissions, selectedStudentCids, UI state
    Browser->>Browser: updateSelectedCount(), enable/disable batch actions
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

  • Review areas needing extra attention:
    • app/controllers/submissions_controller.rb: search/sort logic (especially computed score sorting), record counts, and format_submission_for_datatable correctness and auth declaration.
    • app/assets/javascripts/manage_submissions.js: DataTables columns config, createdRow/drawCallback sync with selection state, and delegated event handlers for dynamic elements.
    • Integration between view headers and DataTables expectations (column ordering, hidden columns, and select-all behavior).

Possibly related PRs

Suggested reviewers

  • KesterTan
  • coder6583
  • jhs-panda

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Manage submissions speed" is clearly related to the primary objective of this changeset, which is to improve the loading performance of the manage submissions page. The title directly conveys both the context (manage submissions) and the goal (speed improvement) that motivates the changes. While the title does not specify the implementation approach (server-side pagination), it accurately captures the main intent and would be understandable to a developer reviewing the commit history. The title is neither vague nor misleading.
Description Check ✅ Passed The pull request description follows the required template structure and includes all essential sections: Description explains the change clearly, Motivation and Context provides specific performance metrics and the problem being solved, and How Has This Been Tested documents the testing approach. The author correctly selected "New feature" and marked the rubocop/erblint checklist item as complete, while appropriately leaving documentation items unchecked since this change does not require documentation updates. The description is sufficiently detailed and complete for understanding the scope and rationale of the changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch manage-submissions-speed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/assets/javascripts/manage_submissions.js (1)

558-562: Reset selections safely when paging.

selectedSubmissions.map(selectedSubmission => toggleRow(selectedSubmission, false)); mutates selectedSubmissions while you iterate it, so at least one previously selected ID survives. After changing pages, destructive operations (delete, excuse, etc.) still include that hidden submission. Iterate over a copy (or just clear the arrays) before toggling to keep selection state accurate.

-      selectedSubmissions.map(selectedSubmission => toggleRow(selectedSubmission, false));
+      [...selectedSubmissions].forEach(selectedSubmission => toggleRow(selectedSubmission, false));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 674efe9 and cfdbab4.

⛔ Files ignored due to path filters (1)
  • .github/workflows/codeql-analysis.yml is excluded by !**/*.yml
📒 Files selected for processing (3)
  • app/assets/javascripts/manage_submissions.js (3 hunks)
  • app/controllers/submissions_controller.rb (1 hunks)
  • app/views/submissions/index.html.erb (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-09-20T20:46:52.775Z
Learnt from: evanyeyeye
Repo: autolab/Autolab PR: 2209
File: app/controllers/autograders_controller.rb:0-0
Timestamp: 2024-09-20T20:46:52.775Z
Learning: In the `AutogradersController`, ensure that the `download_file` method has `action_auth_level :download_file, :instructor` to restrict access to instructors.

Applied to files:

  • app/controllers/submissions_controller.rb
📚 Learning: 2024-06-10T19:38:42.782Z
Learnt from: jlge
Repo: autolab/Autolab PR: 2011
File: app/controllers/assessments_controller.rb:28-29
Timestamp: 2024-06-10T19:38:42.782Z
Learning: `destroy_no_redirect` in `app/controllers/assessments_controller.rb` is an internal method used within the controller and not a public endpoint. It should not be included in the `before_action :set_assessment` exclusion list.

Applied to files:

  • app/controllers/submissions_controller.rb
🧬 Code graph analysis (1)
app/controllers/submissions_controller.rb (4)
app/controllers/application_controller.rb (1)
  • action_auth_level (53-71)
app/helpers/application_helper.rb (1)
  • computed_score (111-118)
app/models/submission.rb (1)
  • final_score (327-335)
app/models/assessment_user_datum.rb (1)
  • final_score (106-109)

Comment on lines 42 to 73
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Clamp the sort direction to ASC/DESC.

order_direction is interpolated straight into the ORDER BY clause, so a crafted order[0][dir] parameter lets an attacker inject arbitrary SQL (e.g., desc; DROP TABLE ...). Please whitelist the direction before using it in SQL.

-        order_direction = params.dig(:order, '0', :dir) || 'desc'
+        direction_param = params.dig(:order, '0', :dir).to_s.downcase
+        order_direction = %w[asc desc].include?(direction_param) ? direction_param : 'desc'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
direction_param = params.dig(:order, '0', :dir).to_s.downcase
order_direction = %w[asc desc].include?(direction_param) ? direction_param : '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
🤖 Prompt for AI Agents
In app/controllers/submissions_controller.rb around lines 42 to 75,
order_direction is taken directly from params and interpolated into the ORDER BY
clause which allows SQL injection; replace that assignment with a whitelist that
maps the incoming param (case-insensitive) to either "ASC" or "DESC" (defaulting
to "DESC") and use that sanitized value in the order call (or use a conditional
that only uses the order when it matches the allowed values). Ensure you perform
this clamp before any query construction so only "ASC" or "DESC" can ever reach
the SQL string.

@KesterTan KesterTan requested review from a team and KesterTan and removed request for a team October 30, 2025 22:51
@dwang3851 dwang3851 force-pushed the manage-submissions-speed branch from cfdbab4 to a6f0c1e Compare October 30, 2025 22:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/assets/javascripts/manage_submissions.js (2)

527-531: Remove redundant selectedStudentCids filtering.

Lines 528-530 already conditionally remove selectedCid from selectedStudentCids only when no other submissions from that student are selected. Line 531 then unconditionally removes it again with _.without(), which is redundant and could incorrectly remove CIDs that should remain selected.

Apply this diff:

         const hasOtherSelectedSubmissions = selectedSubmissions.some(id => submissions_to_cud[id] === selectedCid);
         if (!hasOtherSelectedSubmissions) {
           selectedStudentCids = selectedStudentCids.filter(cid => cid !== selectedCid);
         }
-        selectedStudentCids = _.without(selectedStudentCids, selectedCid);
       }

558-562: Reconsider clearing selections on pagination.

This handler clears all selected submissions when the user navigates to a different page. Typically, users expect selections to persist across pages so they can select submissions from multiple pages for batch operations (e.g., regrade, delete, download).

Consider removing this handler to allow selections to persist, or add a clear indicator to users that changing pages will clear their selections.

♻️ Duplicate comments (1)
app/controllers/submissions_controller.rb (1)

42-42: SQL injection vulnerability remains unfixed.

The order_direction parameter is still taken directly from params without validation and later interpolated into SQL (line 84). This allows SQL injection attacks.

As noted in the previous review, apply this fix:

-        order_direction = params.dig(:order, '0', :dir) || 'desc'
+        direction_param = params.dig(:order, '0', :dir).to_s.downcase
+        order_direction = %w[asc desc].include?(direction_param) ? direction_param : 'desc'
🧹 Nitpick comments (1)
app/views/submissions/index.html.erb (1)

14-20: Remove unused additional_data array.

With the migration to DataTables server-side processing, this additional_data array appears unused. The JavaScript now builds submissions_to_cud dynamically from the JSON response (see manage_submissions.js lines 243-248). The server-side loop over @submissions here no longer serves a purpose.

Apply this diff to remove the unused code:

-  // additional data for each row
-  additional_data = [
-      <% for s in @submissions do %>
-      {
-          "submission-id": "<%= s.id.to_s %>",
-      },
-      <% end %>
-  ];
-
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfdbab4 and a6f0c1e.

📒 Files selected for processing (3)
  • app/assets/javascripts/manage_submissions.js (3 hunks)
  • app/controllers/submissions_controller.rb (1 hunks)
  • app/views/submissions/index.html.erb (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-06-10T19:38:42.782Z
Learnt from: najclark
Repo: autolab/Autolab PR: 2011
File: app/controllers/courses_controller.rb:1316-1320
Timestamp: 2024-06-10T19:38:42.782Z
Learning: The user confirmed the concern about potential security risks when setting `flash[:error]` and `flash[:html_safe]` with user-supplied input, specifically mentioning `course_name` derived from a tar file's contents. It's important to ensure proper sanitization of user-supplied input to prevent XSS vulnerabilities, especially when `flash[:html_safe]` is used, which indicates the content should not be escaped.

Applied to files:

  • app/controllers/submissions_controller.rb
📚 Learning: 2024-09-20T20:46:52.775Z
Learnt from: evanyeyeye
Repo: autolab/Autolab PR: 2209
File: app/controllers/autograders_controller.rb:0-0
Timestamp: 2024-09-20T20:46:52.775Z
Learning: In the `AutogradersController`, ensure that the `download_file` method has `action_auth_level :download_file, :instructor` to restrict access to instructors.

Applied to files:

  • app/controllers/submissions_controller.rb
📚 Learning: 2024-06-10T19:38:42.782Z
Learnt from: jlge
Repo: autolab/Autolab PR: 2011
File: app/controllers/assessments_controller.rb:28-29
Timestamp: 2024-06-10T19:38:42.782Z
Learning: `destroy_no_redirect` in `app/controllers/assessments_controller.rb` is an internal method used within the controller and not a public endpoint. It should not be included in the `before_action :set_assessment` exclusion list.

Applied to files:

  • app/controllers/submissions_controller.rb
🧬 Code graph analysis (1)
app/controllers/submissions_controller.rb (3)
app/models/submission.rb (1)
  • final_score (327-335)
app/controllers/application_controller.rb (1)
  • action_auth_level (53-71)
app/helpers/application_helper.rb (1)
  • computed_score (111-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (6)
app/views/submissions/index.html.erb (1)

120-147: LGTM: Header structure and DataTables placeholder.

The streamlined header structure with the select-all checkbox, conditional sort icons, and DataTables-driven tbody placeholder is well-implemented and aligns with the server-side processing migration.

app/controllers/submissions_controller.rb (2)

19-34: LGTM: HTML format initialization.

The HTML format branch correctly initializes instance variables and loads a limited set of submissions (100) for the initial page render. The @submissions_to_cud mapping is efficiently created using pluck and to_h.


105-137: LGTM: DataTables formatter method.

The format_submission_for_datatable method is properly authorized at the :instructor level and returns a well-structured array for DataTables consumption. The use of instance variables and helper methods is appropriate.

app/assets/javascripts/manage_submissions.js (3)

76-96: LGTM: Delegated event handler.

The use of $(document).on('click', '.score-details', ...) is the correct pattern for handling clicks on dynamically created DataTables rows. This ensures the event handler works for elements rendered after page load.


236-306: LGTM: DataTables server-side configuration.

The DataTables initialization with server-side processing is well-implemented:

  • AJAX endpoint correctly targets the JSON format
  • dataSrc callback builds the submissions_to_cud mapping from response data
  • Column definitions properly handle rendering, sorting, and visibility
  • createdRow and drawCallback maintain selection state across redraws

315-321: LGTM: Select-all checkbox synchronization.

The updateSelectAllCheckbox function correctly synchronizes the select-all checkbox state with individual row selections.

Comment on lines +68 to +93
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider performance implications of in-memory score sorting.

When sorting by calculated_score, all filtered submissions are loaded into memory, scored, sorted in Ruby, then paginated. For assessments with many submissions matching the search filter, this could consume significant memory and time.

While this is a reasonable tradeoff given that final_score is computed dynamically rather than stored in the database, consider monitoring performance for large datasets or implementing score materialization if this becomes a bottleneck.

🤖 Prompt for AI Agents
In app/controllers/submissions_controller.rb around lines 68-93, the controller
loads all filtered submissions into memory to compute and sort by
calculated_score which can be very expensive; change this to avoid full
in-memory sorting by either (A) materializing/calculating and persisting the
score on submissions (or a denormalized column on course_user_datum) and
updating it when relevant so the DB can order and paginate, or (B) if
materialization is not feasible now, fetch only the paginated IDs first (apply
the same filters and search in SQL with limit/offset), load those submissions,
compute final_score for that page and format — additionally add monitoring
(metrics/timing) around this path to detect when it becomes a bottleneck and
consider adding a background job to backfill scores if you implement
materialization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants