Skip to content

Updated generate_tar to reflect changes in export course tar file#2318

Closed
melissajinn wants to merge 29 commits intomasterfrom
export-tar
Closed

Updated generate_tar to reflect changes in export course tar file#2318
melissajinn wants to merge 29 commits intomasterfrom
export-tar

Conversation

@melissajinn
Copy link
Copy Markdown

New tar format example:

test-course
test-course/course.rb
test-course/test-course.yml
test-course/llomac-lab/assignment_config.yml
test-course/llomac-lab/llomac-lab.rb
test-course/llomac-lab/llomac-lab.yml
test-course/llomac-lab/log.txt
test-course/hello_python/autograde-Makefile
test-course/hello_python/autograde.tar
test-course/hello_python/hello.py
test-course/hello_python/hello_python.rb
test-course/hello_python/hello_python.yml
test-course/hello_python/log.txt
test-course/hello_python/writeup
test-course/hello_python/writeup/writeup.html

20wildmanj and others added 29 commits November 14, 2024 13:30
…and disable access key fields if not enabled
Add g4dn.xlarge, g4dn.2xlarge, and g5.xlarge instance type options
merged if-else branches
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

This PR introduces EC2 SSH support for autograders, enabling AWS credential management with encrypted storage and instance type selection. It adds conditional UI tabs, updates model validations, implements job state derivation logic, and removes the ScoresController while enhancing scoreboard nil-handling. Database schema updates include new encrypted credential columns and a feature flag system.

Changes

Cohort / File(s) Summary
Autograder EC2 & Credential Management
app/controllers/autograders_controller.rb, app/models/autograder.rb, app/assets/javascripts/autograder.js
Added credential toggle logic in controller's create/update actions, encrypted two sensitive fields, introduced INSTANCE_TYPES constant with validation, added conditional credential validations when use_access_key? is true, and wired form UI behavior for checkbox-driven credential field enable/disable.
Autograder Views & UI Partials
app/views/autograders/_form.html.erb, app/views/autograders/_basic_settings.html.erb, app/views/autograders/_ec2_settings.html.erb
Refactored monolithic form into tabbed UI with separate partials for basic (VM image, timeout, release score, makefile/tar) and EC2 settings (credentials, instance type selection with grouped options).
Autograder Database & Migrations
db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb, db/migrate/20241211042124_add_use_access_key_to_autograder.rb, db/migrate/20251021034813_encrypt_autograder_credentials.rb, db/schema.rb
Added instance_type, use_access_key columns; encrypted credential storage via access_key_ciphertext/access_key_id_ciphertext; updated schema version and added new ssh_keys table.
Autograder Test Coverage
spec/factories/autograders.rb, spec/models/autograder_spec.rb, spec/helpers/assessment_autograde_core_spec.rb
Updated factory defaults for instance_type and use_access_key; added validations spec for instance types and conditional credential requirements; added comprehensive Tango job payload composition specs covering EC2 enabled/disabled and credential presence scenarios.
Autograder Helper & Tango Integration
app/helpers/assessment_autograde_core.rb
Refactored tango_add_job to conditionally include EC2 fields (ec2Vmms, instanceType, accessKey, accessKeyId) when Rails.configuration.x.ec2_ssh is enabled and credentials are configured.
Feature Flags & Configuration
config/environments/development.rb, config/environments/production.rb.template, config/initializers/filter_parameter_logging.rb
Introduced config.x.ec2_ssh flag (enabled in development, disabled in production); added :access_key and :access_key_id to filtered parameter logging to prevent secrets in logs.
Controller Authorization & Parameters
app/controllers/assessments_controller.rb, app/controllers/announcements_controller.rb
Added authorization mappings for bulkExport, bulkGrade_complete, regradeBatch actions; tightened edit_assessment_params to explicitly delete internal fields (:id, :created_at, :updated_at, :course_id, :late_penalty_id, :version_penalty_id); conditioned :system permission on administrator role.
Job State & Scoreboard Logic
app/controllers/jobs_controller.rb, app/controllers/scoreboards_controller.rb, app/views/scoreboards/show.html.erb
Replaced unconditional Completed fallback with three-way trace-based classification (Succeeded/Failed/Unknown State); added robust error handling and JSON parsing with logging in scoreboard entry creation; implemented nil-safe colspec validation with padding; updated view to defensively render error icons for nil entries.
Course Export & Data Structure
app/models/course.rb
Changed tar root directory and config filename to fixed names (course, course_config.yml); added conditional per-user and per-CUD YAML exports controlled by export_configs; refactored assessment/submission export to use fixed directory structure with per-item files.
Score Controller Removal
app/controllers/scores_controller.rb, config/routes.rb, spec/controllers/scores_controller_spec.rb
Removed ScoresController entirely (including create, show, update actions and authorization/filtering logic); removed corresponding routes from nested assessments scope.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant Controller as AutogradersController
    participant Model as Autograder Model
    participant Helper as AssessmentAutogradeCore
    participant TangoClient

    Browser->>Controller: POST/PATCH form (with use_access_key flag)
    Controller->>Controller: Normalize use_access_key to boolean<br/>Clear credentials if disabled
    Controller->>Model: update(params)
    Model->>Model: Validate instance_type<br/>Validate credentials if use_access_key?<br/>Encrypt access_key/access_key_id
    Model-->>Controller: ✓ saved
    
    rect rgba(0, 150, 200, 0.5)
    Note over Controller,TangoClient: EC2 SSH Submission Flow
    Controller->>Helper: tango_add_job(...)
    alt EC2 SSH Enabled
        Helper->>Helper: Build job_properties<br/>with ec2Vmms=true<br/>instanceType=t3.micro
        alt Use Access Key
            Helper->>Helper: Add accessKey<br/>Add accessKeyId
        end
    else EC2 SSH Disabled
        Helper->>Helper: Build base job_properties only
    end
    Helper->>TangoClient: addjob(JSON payload)
    TangoClient-->>Helper: job_id
    end
    
    Helper-->>Controller: ✓ queued
    Controller-->>Browser: success response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Autograder improvements #2209: Modifies autograder UI form partial, AutogradersController edit/update/download_file actions, and routes for autograder file management—directly related at the code/feature level.

Suggested reviewers

  • coder6583
  • dwang3851
  • jhs-panda
🚥 Pre-merge checks | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title focuses on the generate_tar method in Course model, but the PR involves extensive changes across 27+ files including autograders, assessments, jobs, scores controller removal, and database migrations that far exceed just the generate_tar modification. Revise the title to better reflect the scope, such as 'Add EC2 SSH support with access key encryption and refactor course/assessment export' to more accurately capture the PR's full intent.
Description check ⚠️ Warning The description only shows an example of the new tar format but completely omits critical context about the broader changes: autograder enhancements, access key encryption, scores controller removal, database migrations, and significant controller/helper modifications. Expand the description to include sections on all major changes (EC2 SSH, encryption, scores removal, assessment updates) and explicitly link to the related issue. Follow the provided template structure including Motivation, How Has This Been Tested, and Types of Changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch export-tar

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
Copy Markdown
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: 13

🧹 Nitpick comments (7)
app/views/autograders/_form.html.erb (2)

3-15: Consider hiding tab bar when only one tab exists.

When EC2 SSH is disabled, the tab bar shows only a single "Basic" tab, which may look unnecessary. Consider conditionally rendering the tab navigation only when multiple tabs exist.

💡 Suggested improvement
+<% show_tabs = Rails.configuration.x.ec2_ssh.presence %>
+<% if show_tabs %>
 <div class="row">
   <div class="col s12">
     <ul id="tabs" class="tabs tabs-fixed-width">
-      <% list = ["basic"] %>
-      <% if Rails.configuration.x.ec2_ssh.presence %>
-        <% list.append("ec2") %>
-      <% end %>
+      <% list = ["basic", "ec2"] %>
       <% list.each do |tab_name| %>
         ...
       <% end %>
     </ul>
   </div>
 </div>
+<% end %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_form.html.erb` around lines 3 - 15, The tab bar is
always rendered even when only the "basic" tab exists; change the ERB to build
the list (currently via list = ["basic"] and optionally list.append("ec2")) and
then conditionally render the <ul id="tabs" ...> block only when list.length > 1
so the tabs are hidden for a single tab. Keep the existing logic that iterates
over list and uses params[:active_tab] to mark the active tab (the
tag.li/link_to block) but wrap that rendering in an if list.size > 1 check so
the tabs bar is omitted when there's only one entry.

21-23: Active tab is not preserved on form submission.

Unlike the assessments edit form (which appends active_tab to the form URL), this form always submits to the same endpoint without preserving the active tab. On validation failure, users will be redirected back to the default tab instead of staying on the EC2 tab where they may have entered invalid data.

💡 Suggested fix to preserve active tab
-    <%= form_for `@autograder`, url: course_assessment_autograder_path(`@course`, `@assessment`, `@autograder`),
+    <%= form_for `@autograder`, url: "#{course_assessment_autograder_path(`@course`, `@assessment`, `@autograder`)}?active_tab=#{params[:active_tab] || 'basic'}",
                               builder: FormBuilderWithDateTimeInput,
                               html: { multipart: true } do |f| %>

And in AutogradersController#update, redirect with the active_tab parameter:

redirect_to(edit_course_assessment_autograder_path(`@course`, `@assessment`, active_tab: params[:active_tab] || 'basic'))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_form.html.erb` around lines 21 - 23, The form is not
preserving the active tab on submit; update the form_for call (the form_for
`@autograder` with url: course_assessment_autograder_path(...)) to include the
current active_tab (either by adding active_tab: params[:active_tab] to the URL
options or by emitting a hidden input named "active_tab" with
params[:active_tab]) so the param is sent on submit, and modify
AutogradersController#update to redirect back with that param (use
edit_course_assessment_autograder_path(`@course`, `@assessment`, active_tab:
params[:active_tab] || 'basic')) so validation failures return the user to the
same tab.
spec/models/autograder_spec.rb (1)

14-18: Non-deterministic test may cause flaky failures.

Using Autograder::INSTANCE_TYPES.sample makes this test non-deterministic. While unlikely, if the constant were ever empty, this would fail unexpectedly. Consider using a fixed value or the first element for consistent test behavior.

💡 Suggested fix for deterministic test
     it "allows listed instance types" do
-      autograder.instance_type = Autograder::INSTANCE_TYPES.sample
+      autograder.instance_type = Autograder::INSTANCE_TYPES.first

       expect(autograder).to be_valid
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/models/autograder_spec.rb` around lines 14 - 18, The test in
spec/models/autograder_spec.rb uses Autograder::INSTANCE_TYPES.sample making it
non-deterministic; change the assignment in the example block that sets
autograder.instance_type = Autograder::INSTANCE_TYPES.sample to use a stable
value (e.g., Autograder::INSTANCE_TYPES.first or a specific known-good string)
so the example "allows listed instance types" reliably passes and cannot fail if
the constant is empty or ordering changes.
db/migrate/20251021034813_encrypt_autograder_credentials.rb (1)

24-42: Down migration should also use guards for consistency.

The down method should include column_exists? guards to handle cases where the migration may have been partially run or the schema is in an unexpected state.

💡 Suggested fix with guards
   def down
     # Add back plaintext columns
-    add_column :autograders, :access_key, :string, default: ""
-    add_column :autograders, :access_key_id, :string, default: ""
+    add_column :autograders, :access_key, :string, default: "" unless column_exists?(:autograders, :access_key)
+    add_column :autograders, :access_key_id, :string, default: "" unless column_exists?(:autograders, :access_key_id)

     # Migrate encrypted data back to plaintext columns
-    Autograder.reset_column_information
-    Autograder.find_each do |autograder|
-      if autograder.access_key_ciphertext.present?
-        autograder.update_column(:access_key, autograder.access_key || "")
-      end
-      if autograder.access_key_id_ciphertext.present?
-        autograder.update_column(:access_key_id, autograder.access_key_id || "")
-      end
-    end
+    if column_exists?(:autograders, :access_key_ciphertext)
+      Autograder.reset_column_information
+      Autograder.find_each do |autograder|
+        if autograder.access_key_ciphertext.present?
+          autograder.update_column(:access_key, autograder.access_key || "")
+        end
+        if autograder.access_key_id_ciphertext.present?
+          autograder.update_column(:access_key_id, autograder.access_key_id || "")
+        end
+      end
+    end

     # Remove encrypted columns
-    remove_column :autograders, :access_key_ciphertext
-    remove_column :autograders, :access_key_id_ciphertext
+    remove_column :autograders, :access_key_ciphertext if column_exists?(:autograders, :access_key_ciphertext)
+    remove_column :autograders, :access_key_id_ciphertext if column_exists?(:autograders, :access_key_id_ciphertext)
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20251021034813_encrypt_autograder_credentials.rb` around lines 24
- 42, Update the down method to guard every schema operation with column_exists?
checks: before add_column calls for :access_key and :access_key_id, wrap them in
column_exists?(:autograders, :access_key) / column_exists?(:autograders,
:access_key_id) logic (or the inverse via unless) so you don't error if they
already exist; after calling Autograder.reset_column_information and inside the
Autograder.find_each loop, check column_exists?(:autograders,
:access_key_ciphertext) and column_exists?(:autograders,
:access_key_id_ciphertext) before referencing autograder.access_key_ciphertext /
access_key_id_ciphertext; and before remove_column calls for
:access_key_ciphertext and :access_key_id_ciphertext, ensure those columns exist
with column_exists?(:autograders, ...) to avoid failures when removing missing
columns.
app/models/autograder.rb (1)

21-24: Redundant validation: inclusion duplicates numericality constraints.

The numericality validation on lines 22-23 already enforces >= 10 and <= 900. The inclusion validation on line 24 is redundant.

♻️ Proposed fix
   # Validations
   validates :autograde_timeout,
             numericality: { greater_than_or_equal_to: 10, less_than_or_equal_to: 900 }
-  validates :autograde_timeout, inclusion: { in: 10..900 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/autograder.rb` around lines 21 - 24, The validations for
autograde_timeout are duplicated: the numericality check on :autograde_timeout
in the Autograder model already enforces >=10 and <=900, so remove the redundant
validates :autograde_timeout, inclusion: { in: 10..900 } line; keep the existing
numericality validation in the Autograder class to enforce the limits.
app/views/autograders/_basic_settings.html.erb (1)

30-35: Remove unnecessary file_path parameter from download links.

The download_file action (per app/controllers/autograders_controller.rb:104-121) only uses file_key to look up the file path from an allowlist. The file_path parameter is ignored server-side and unnecessarily exposes internal filesystem paths in URLs.

♻️ Proposed fix
 <% unless `@makefile_exists.nil`? %>
-  <%= link_to "Download Makefile", download_file_course_assessment_autograder_path(file_path: `@makefile_exists`, file_key: 'makefile'), class: "btn" %>
+  <%= link_to "Download Makefile", download_file_course_assessment_autograder_path(file_key: 'makefile'), class: "btn" %>
 <% end %>

 <% unless `@tar_exists.nil`? %>
-  <%= link_to "Download Tar", download_file_course_assessment_autograder_path(file_path: `@tar_exists`, file_key: 'tar'), class: "btn" %>
+  <%= link_to "Download Tar", download_file_course_assessment_autograder_path(file_key: 'tar'), class: "btn" %>
 <% end %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_basic_settings.html.erb` around lines 30 - 35, The
download links currently include an unnecessary and sensitive file_path query
param; update the two link_to calls in _basic_settings.html.erb that call
download_file_course_assessment_autograder_path (the ones guarded by
`@makefile_exists` and `@tar_exists`) to stop passing file_path and only pass
file_key and other needed options (e.g., class: "btn"), so the URL contains only
file_key used by the controller's download_file action.
spec/helpers/assessment_autograde_core_spec.rb (1)

31-39: Consider using a local variable instead of instance variable for captured payload.

Using @captured_payload_json as an instance variable works but is fragile. A more idiomatic RSpec approach would use let with a memoized reference or a dedicated capture mechanism.

♻️ Alternative approach using let with a wrapper
let(:captured_payloads) { [] }

before do
  helper_instance.instance_variable_set(:`@autograde_prop`, autograder)
  allow(TangoClient).to receive(:addjob) do |_queue, payload|
    captured_payloads << payload
    { "jobId" => 1 }
  end
end

# Then in tests:
let(:captured_payload) { JSON.parse(captured_payloads.last) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/helpers/assessment_autograde_core_spec.rb` around lines 31 - 39, The
test currently stores the stubbed TangoClient.addjob payload in an instance
variable `@captured_payload_json` which is brittle; replace it with a local
memoized capture: define let(:captured_payloads) { [] } and in the before block
(where helper_instance.instance_variable_set(:`@autograde_prop`, autograder) and
allow(TangoClient).to receive(:addjob) do |_queue, payload| ...) push payload
into captured_payloads (e.g. captured_payloads << payload) and return the job
hash; then redefine let(:captured_payload) to parse the last captured_payloads
entry (JSON.parse(captured_payloads.last)) so tests read from the let-based
capture instead of `@captured_payload_json`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/assets/javascripts/autograder.js`:
- Around line 30-33: The hover handler on the option elements
('#autograder_instance_type option' with .hover) is unreliable because most
browsers don't fire hover on <option>; remove this block from autograder.js and
either implement a custom dropdown component for hover highlighting or attach
hover handlers to a custom list/replica element instead; locate the selector
'#autograder_instance_type option' and the anonymous hover callbacks and delete
or replace them with handlers on a custom element that supports
mouseenter/mouseleave.

In `@app/controllers/jobs_controller.rb`:
- Around line 250-255: The last-trace check in jobs_controller.rb is too strict
(it only checks for "Error") so messages like "failed" or lowercase "error"
become "Unknown State"; update the conditional that reads rjob["trace"][-1] to
perform a case-insensitive match for either "fail" or "error" (e.g. downcase
include checks or a /fail|error/i regex) and set job[:state] = "Failed" when
either is present so failed/failed-like terminal messages are detected
consistently with tango_plot_data.

In `@app/controllers/scoreboards_controller.rb`:
- Around line 313-337: The current fix pads short parsed["scoreboard"] rows with
nil which makes them non-empty and bypasses the invalid-row fallback used by
scoreboardOrderSubmissions; instead of padding, detect when
parsed["scoreboard"].length < expected_length, log the warning, and mark the
entry as invalid so the existing invalid-row handling runs (for example clear
parsed["scoreboard"] or set a boolean flag on parsed like
parsed["_invalid_scoreboard"] = true) so later code (scoreboardOrderSubmissions
and the view rendering error_icon) treats the row as malformed and does not
include it in sorting; remove the fill/padding logic and ensure the rest of the
code checks the new invalid marker or empty value.

In `@app/helpers/assessment_autograde_core.rb`:
- Around line 183-186: The job_properties hash is being JSON-serialized before
calling TangoClient.addjob, but addjob currently posts the string without an
explicit Content-Type header; update either the call site or TangoClient.addjob
so the request sends the correct header: either pass the raw job_properties hash
to TangoClient.addjob (remove to_json in assessment_autograde_core.rb so
HTTParty can serialize and set headers) or modify TangoClient.addjob to accept
headers and set "Content-Type" => "application/json" (consistent with how
upload_file and upload_docker_image set headers). Ensure the change references
the job_properties usage in assessment_autograde_core.rb and the
TangoClient.addjob method.

In `@app/models/assessment.rb`:
- Around line 97-99: The current asmt_yaml_path method always returns
"assignment_config.yml" which breaks loading of legacy tarballs; update
load_yaml to try the new asmt_yaml_path first but fall back to the legacy
per-assessment filename (the original "<assignment>.yml" used before the rename)
if the new file is missing, while still writing/saving to asmt_yaml_path; modify
load_yaml to check both paths (asmt_yaml_path and the legacy filename) and use
whichever exists.

In `@app/models/autograder.rb`:
- Around line 8-9: In the Autograder model replace the Rails 7 `encrypts` calls
with Lockbox-compatible `has_encrypted` declarations: change the two occurrences
of `encrypts :access_key` and `encrypts :access_key_id` in the Autograder class
to use `has_encrypted :access_key` and `has_encrypted :access_key_id` (follow
the same pattern used in GithubIntegration model `has_encrypted` declarations).

In `@app/models/course.rb`:
- Around line 368-371: The export is currently writing the entire user record
via cud.user.attributes (in the course_user_data.each block where tar.add_file
is called), which leaks sensitive auth/PII fields; replace this by whitelisting
only required user fields for re-import/identity mapping (e.g. id,
login/username, name/display_name, email, and any other minimal mapping attrs)
instead of dumping all attributes. Implement this by defining an allowlist
(constant or method) and writing only user.slice(*ALLOWED_EXPORT_USER_FIELDS) or
a dedicated to_export_hash on User, then use that filtered hash (or its YAML)
when calling tar_file.write in the loop. Ensure no auth-related fields like
encrypted_password, reset/confirmation tokens, sign-in metadata, or admin flags
are included.
- Around line 387-405: The export currently writes flat assessment and
submission YAML from assessment.attributes.to_yaml, which loses the assignment
bundle and produces YAML that Assessment#load_yaml can't consume; change the
export to restore the per-assignment directory layout (create a directory per
assessment, e.g. "assessments/assessment_<id>/") and copy the full assignment
bundle files into that directory (assignment_config.yml,
"#{assessment.name}.rb", log.txt, writeup/autograde assets,
problems/autograder/scoreboard files) instead of emitting only
assessment.attributes; serialize assessment data using the same format expected
by Assessment#load_yaml (use the existing exporter/dump routine or build the
"general/dates/..." structure rather than assessment.attributes.to_yaml), and
when exporting submissions iterate assessment.submissions and include the actual
submission artifact files (submission files/artifacts) into the corresponding
assignment directory (or a parallel submission directory per assessment) rather
than writing submission.attributes.to_yaml.

In `@app/views/autograders/_basic_settings.html.erb`:
- Around line 26-28: Fix the typo in the confirmation message used in the
link_to "Delete Autograder" helper: update the data confirm string on the
link_to (the call that currently contains "Are you sure you want to delete the
Autograder for this assesssment?") to spell "assessment" correctly so the
message reads "Are you sure you want to delete the Autograder for this
assessment?".

In `@app/views/autograders/_ec2_settings.html.erb`:
- Around line 107-109: Fix the typo in the confirmation message of the "Delete
Autograder" link: update the data confirm string in the link_to call that uses
course_assessment_autograder_path(`@course`, `@assessment`) so "assesssment" is
corrected to "assessment" in the confirmation prompt.
- Around line 64-70: The view raises ArgumentError for an invalid
selected_instance_type which causes a 500; instead, detect invalid values from
the canonical Autograder::INSTANCE_TYPES (do not duplicate ec2_instance_options
logic), and either default selected_instance_type to a safe value (e.g., nil or
the first valid option) or set a visible error message to render in the
template; update the block where selected_instance_type is computed (currently
using ec2_instance_options and the local selected_instance_type) to validate
against Autograder::INSTANCE_TYPES and handle invalid cases gracefully (default
or set flash/inline error) rather than raising.

In `@app/views/scoreboards/show.html.erb`:
- Around line 80-89: The row collapses because the template only iterates
existing elements of grade[:entry]; when the controller falls back to
grade[:entry] = [] after createScoreboardEntry fails the view renders fewer
<td>s. Fix by ensuring the view renders a fixed number of cells: in
show.html.erb, replace the current conditional iteration over grade[:entry] with
a loop over the expected column count (calculate from the header or a known
constant) and for each index render <td><%= column_value %></td> if
grade[:entry][i] present else render the 'error_icon' placeholder; alternatively
ensure createScoreboardEntry always returns an array of the expected length
(e.g., Array.new(expected_count, nil)) so the existing per-index rendering still
produces placeholder cells. Ensure references to grade[:entry],
createScoreboardEntry, and the 'error_icon' partial are used to locate the code
to change.

In `@db/migrate/20251021034813_encrypt_autograder_credentials.rb`:
- Around line 1-22: The migration EncryptAutograderCredentials currently
unconditionally adds/removes columns and reads plaintext columns that may
already have been added/removed by a prior migration; update the up method to
guard each schema operation with column_exists? checks (use unless
column_exists?(:autograders, :access_key_ciphertext) / unless
column_exists?(:autograders, :access_key_id_ciphertext) before add_column, and
wrap remove_column calls with if column_exists?(:autograders, :access_key) / if
column_exists?(:autograders, :access_key_id)), and when iterating
Autograder.find_each only read and copy plaintext attributes if the plaintext
columns actually exist (guard reads of :access_key and :access_key_id),
preserving the existing save(validate: false) behavior and calling
Autograder.reset_column_information appropriately after any schema changes.

---

Nitpick comments:
In `@app/models/autograder.rb`:
- Around line 21-24: The validations for autograde_timeout are duplicated: the
numericality check on :autograde_timeout in the Autograder model already
enforces >=10 and <=900, so remove the redundant validates :autograde_timeout,
inclusion: { in: 10..900 } line; keep the existing numericality validation in
the Autograder class to enforce the limits.

In `@app/views/autograders/_basic_settings.html.erb`:
- Around line 30-35: The download links currently include an unnecessary and
sensitive file_path query param; update the two link_to calls in
_basic_settings.html.erb that call
download_file_course_assessment_autograder_path (the ones guarded by
`@makefile_exists` and `@tar_exists`) to stop passing file_path and only pass
file_key and other needed options (e.g., class: "btn"), so the URL contains only
file_key used by the controller's download_file action.

In `@app/views/autograders/_form.html.erb`:
- Around line 3-15: The tab bar is always rendered even when only the "basic"
tab exists; change the ERB to build the list (currently via list = ["basic"] and
optionally list.append("ec2")) and then conditionally render the <ul id="tabs"
...> block only when list.length > 1 so the tabs are hidden for a single tab.
Keep the existing logic that iterates over list and uses params[:active_tab] to
mark the active tab (the tag.li/link_to block) but wrap that rendering in an if
list.size > 1 check so the tabs bar is omitted when there's only one entry.
- Around line 21-23: The form is not preserving the active tab on submit; update
the form_for call (the form_for `@autograder` with url:
course_assessment_autograder_path(...)) to include the current active_tab
(either by adding active_tab: params[:active_tab] to the URL options or by
emitting a hidden input named "active_tab" with params[:active_tab]) so the
param is sent on submit, and modify AutogradersController#update to redirect
back with that param (use edit_course_assessment_autograder_path(`@course`,
`@assessment`, active_tab: params[:active_tab] || 'basic')) so validation failures
return the user to the same tab.

In `@db/migrate/20251021034813_encrypt_autograder_credentials.rb`:
- Around line 24-42: Update the down method to guard every schema operation with
column_exists? checks: before add_column calls for :access_key and
:access_key_id, wrap them in column_exists?(:autograders, :access_key) /
column_exists?(:autograders, :access_key_id) logic (or the inverse via unless)
so you don't error if they already exist; after calling
Autograder.reset_column_information and inside the Autograder.find_each loop,
check column_exists?(:autograders, :access_key_ciphertext) and
column_exists?(:autograders, :access_key_id_ciphertext) before referencing
autograder.access_key_ciphertext / access_key_id_ciphertext; and before
remove_column calls for :access_key_ciphertext and :access_key_id_ciphertext,
ensure those columns exist with column_exists?(:autograders, ...) to avoid
failures when removing missing columns.

In `@spec/helpers/assessment_autograde_core_spec.rb`:
- Around line 31-39: The test currently stores the stubbed TangoClient.addjob
payload in an instance variable `@captured_payload_json` which is brittle; replace
it with a local memoized capture: define let(:captured_payloads) { [] } and in
the before block (where helper_instance.instance_variable_set(:`@autograde_prop`,
autograder) and allow(TangoClient).to receive(:addjob) do |_queue, payload| ...)
push payload into captured_payloads (e.g. captured_payloads << payload) and
return the job hash; then redefine let(:captured_payload) to parse the last
captured_payloads entry (JSON.parse(captured_payloads.last)) so tests read from
the let-based capture instead of `@captured_payload_json`.

In `@spec/models/autograder_spec.rb`:
- Around line 14-18: The test in spec/models/autograder_spec.rb uses
Autograder::INSTANCE_TYPES.sample making it non-deterministic; change the
assignment in the example block that sets autograder.instance_type =
Autograder::INSTANCE_TYPES.sample to use a stable value (e.g.,
Autograder::INSTANCE_TYPES.first or a specific known-good string) so the example
"allows listed instance types" reliably passes and cannot fail if the constant
is empty or ordering changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9442a7d7-b30b-4146-afa6-cc255ab04f14

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • Gemfile.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (27)
  • app/assets/javascripts/autograder.js
  • app/controllers/announcements_controller.rb
  • app/controllers/assessments_controller.rb
  • app/controllers/autograders_controller.rb
  • app/controllers/jobs_controller.rb
  • app/controllers/scoreboards_controller.rb
  • app/controllers/scores_controller.rb
  • app/helpers/assessment_autograde_core.rb
  • app/models/assessment.rb
  • app/models/autograder.rb
  • app/models/course.rb
  • app/views/autograders/_basic_settings.html.erb
  • app/views/autograders/_ec2_settings.html.erb
  • app/views/autograders/_form.html.erb
  • app/views/scoreboards/show.html.erb
  • config/environments/development.rb
  • config/environments/production.rb.template
  • config/initializers/filter_parameter_logging.rb
  • config/routes.rb
  • db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb
  • db/migrate/20241211042124_add_use_access_key_to_autograder.rb
  • db/migrate/20251021034813_encrypt_autograder_credentials.rb
  • db/schema.rb
  • spec/controllers/scores_controller_spec.rb
  • spec/factories/autograders.rb
  • spec/helpers/assessment_autograde_core_spec.rb
  • spec/models/autograder_spec.rb
💤 Files with no reviewable changes (3)
  • spec/controllers/scores_controller_spec.rb
  • config/routes.rb
  • app/controllers/scores_controller.rb

Comment on lines +30 to +33
$('#autograder_instance_type option').hover(
function() { $(this).addClass('highlighted-option'); },
function() { $(this).removeClass('highlighted-option'); }
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

hover events on <option> elements are not reliably supported.

Most browsers do not fire hover events on <option> elements inside <select> dropdowns. This code will effectively be dead in Chrome, Firefox, and Safari. If visual feedback on hover is desired, consider using a custom dropdown component or simply remove this code.

🗑️ Suggested removal of unsupported code
-        $('#autograder_instance_type option').hover(
-            function() { $(this).addClass('highlighted-option'); },
-            function() { $(this).removeClass('highlighted-option'); }
-        );
📝 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
$('#autograder_instance_type option').hover(
function() { $(this).addClass('highlighted-option'); },
function() { $(this).removeClass('highlighted-option'); }
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/assets/javascripts/autograder.js` around lines 30 - 33, The hover handler
on the option elements ('#autograder_instance_type option' with .hover) is
unreliable because most browsers don't fire hover on <option>; remove this block
from autograder.js and either implement a custom dropdown component for hover
highlighting or attach hover handlers to a custom list/replica element instead;
locate the selector '#autograder_instance_type option' and the anonymous hover
callbacks and delete or replace them with handlers on a custom element that
supports mouseenter/mouseleave.

Comment on lines +250 to +255
elsif rjob["trace"][-1].include?("Autodriver returned normally")
job[:state] = "Succeeded"
elsif rjob["trace"][-1].split("|")[1].include? "Error"
job[:state] = "Failed"
else
job[:state] = "Completed"
job[:state] = "Failed" if rjob["trace"][-1].split("|")[1].include? "Error"
job[:state] = "Unknown State"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't narrow failed-job detection to exact Error in the last trace field.

This is stricter than the existing heuristics in tango_plot_data, which look for lowercase fail / error. Dead jobs with terminal messages like failed or error will now fall through to Unknown State, and app/views/jobs/index.html.erb only highlights rows when the state is Failed, so those failures become easy to miss.

Suggested fix
-    elsif rjob["trace"][-1].include?("Autodriver returned normally")
-        job[:state] = "Succeeded"
-    elsif rjob["trace"][-1].split("|")[1].include? "Error"
-      job[:state] = "Failed"
-    else
-      job[:state] = "Unknown State"
-    end
+    else
+      last_trace = rjob["trace"][-1].to_s
+
+      job[:state] =
+        if last_trace.include?("Autodriver returned normally")
+          "Succeeded"
+        elsif last_trace.match?(/error|fail/i)
+          "Failed"
+        else
+          "Unknown State"
+        end
+    end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/jobs_controller.rb` around lines 250 - 255, The last-trace
check in jobs_controller.rb is too strict (it only checks for "Error") so
messages like "failed" or lowercase "error" become "Unknown State"; update the
conditional that reads rjob["trace"][-1] to perform a case-insensitive match for
either "fail" or "error" (e.g. downcase include checks or a /fail|error/i regex)
and set job[:state] = "Failed" when either is present so failed/failed-like
terminal messages are detected consistently with tango_plot_data.

Comment on lines +313 to +337
# ADDED: Ensure the scoreboard entries align with colspec expectations
if @scoreboard && @scoreboard.colspec.present?
begin
colspec_parsed = ActiveSupport::JSON.decode(@scoreboard.colspec)
if colspec_parsed && colspec_parsed["scoreboard"] &&
colspec_parsed["scoreboard"].is_a?(Array)
expected_length = colspec_parsed["scoreboard"].length
# Ensure the entry has enough elements, pad with nil if needed
if parsed["scoreboard"].length < expected_length
Rails.logger.warn(
"Scoreboard entry too short (#{parsed['scoreboard'].length}) " \
"for user, expected #{expected_length}"
)
# Pad the array to match expected length
parsed["scoreboard"] = parsed["scoreboard"].fill(
nil,
parsed["scoreboard"].length,
expected_length - parsed["scoreboard"].length
)
end
end
rescue StandardError => e
Rails.logger.error("Error validating scoreboard entry against colspec: #{e.message}")
end
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't turn malformed scoreboard rows into sortable data.

Padding a short entry with nil makes it non-empty, so it skips the invalid-row fallback at Lines 112-113 and still goes through scoreboardOrderSubmissions at Lines 393-401. That changes ranking behavior for broken autograder output instead of treating the row as invalid. Since the view now renders error_icon for missing cells, keep short rows marked invalid for sorting rather than padding them here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/scoreboards_controller.rb` around lines 313 - 337, The
current fix pads short parsed["scoreboard"] rows with nil which makes them
non-empty and bypasses the invalid-row fallback used by
scoreboardOrderSubmissions; instead of padding, detect when
parsed["scoreboard"].length < expected_length, log the warning, and mark the
entry as invalid so the existing invalid-row handling runs (for example clear
parsed["scoreboard"] or set a boolean flag on parsed like
parsed["_invalid_scoreboard"] = true) so later code (scoreboardOrderSubmissions
and the view rendering error_icon) treats the row as malformed and does not
include it in sorting; remove the fill/padding logic and ensure the rest of the
code checks the new invalid marker or empty value.

Comment on lines +183 to 186
job_properties = job_properties.to_json

begin
response = TangoClient.addjob("#{course.name}-#{assessment.name}", job_properties)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how TangoClient.addjob handles the body parameter and if headers are set
rg -n -A 10 'def self\.addjob' lib/tango_client.rb

# Check if there's any content-type header configuration
rg -n 'content.type|Content-Type|headers' lib/tango_client.rb

Repository: autolab/Autolab

Length of output: 729


🏁 Script executed:

#!/bin/bash
# Check HTTParty configuration in TangoClient
rg -n -B 5 -A 5 'class TangoClient' lib/tango_client.rb

# Check if there are global headers configured
rg -n 'base_uri|default_params|headers' lib/tango_client.rb | head -20

# Look at the context in assessment_autograde_core.rb around line 183
sed -n '170,190p' app/helpers/assessment_autograde_core.rb

Repository: autolab/Autolab

Length of output: 1137


🏁 Script executed:

#!/bin/bash
# Search for all calls to TangoClient.addjob
rg -n 'TangoClient\.addjob|\.addjob' --type ruby

# Check git history to see if this was recently changed
git log -n 5 --oneline -- app/helpers/assessment_autograde_core.rb

# Look for any documentation or comments about addjob format
rg -n -B 3 'def self\.addjob' lib/tango_client.rb

Repository: autolab/Autolab

Length of output: 344


🏁 Script executed:

#!/bin/bash
# Check git diff for the recent changes to assessment_autograde_core.rb
git log --oneline -n 10 -- app/helpers/assessment_autograde_core.rb

# Look at the specific method/function around line 160-190 to understand the refactoring
git show HEAD:app/helpers/assessment_autograde_core.rb | sed -n '160,190p'

# Also check if there's a previous commit that might show the original implementation
git log -p --all -S 'addjob' -- app/helpers/assessment_autograde_core.rb | head -100

Repository: autolab/Autolab

Length of output: 1275


🏁 Script executed:

#!/bin/bash
# Use git blame to see when the .to_json line was added
git blame app/helpers/assessment_autograde_core.rb | grep -A 2 -B 2 'to_json'

# Check if there's a way to see the real history
git log --all --oneline -- app/helpers/assessment_autograde_core.rb | head -20

# Let's also check the actual TangoClient documentation or comments about what format it expects
rg -n -B 5 -A 15 'def self\.addjob' lib/tango_client.rb

Repository: autolab/Autolab

Length of output: 2231


🏁 Script executed:

#!/bin/bash
# Show the parent commit of ce48033
git show ce48033^:app/helpers/assessment_autograde_core.rb | sed -n '160,195p'

# Also check what changed in ce48033
git show ce48033 -- app/helpers/assessment_autograde_core.rb | head -150

Repository: autolab/Autolab

Length of output: 5358


Verify that TangoClient.addjob receives the correct Content-Type header for JSON data.

The job_properties hash is serialized to JSON at line 183 before passing to TangoClient.addjob. However, TangoClient.addjob passes this string directly to HTTParty.post(url, body: options) without setting an explicit Content-Type: application/json header. HTTParty will send the JSON string as-is without this header.

Compare this with other methods in TangoClient: upload_file explicitly sets headers (headers: { "filename" => filename }), and upload_docker_image sets headers: { "Content-Type": "application/octet-stream" }.

Either modify TangoClient.addjob to accept and set a Content-Type header, or pass the job_properties hash directly instead of pre-serializing it to JSON and let HTTParty handle the serialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/helpers/assessment_autograde_core.rb` around lines 183 - 186, The
job_properties hash is being JSON-serialized before calling TangoClient.addjob,
but addjob currently posts the string without an explicit Content-Type header;
update either the call site or TangoClient.addjob so the request sends the
correct header: either pass the raw job_properties hash to TangoClient.addjob
(remove to_json in assessment_autograde_core.rb so HTTParty can serialize and
set headers) or modify TangoClient.addjob to accept headers and set
"Content-Type" => "application/json" (consistent with how upload_file and
upload_docker_image set headers). Ensure the change references the
job_properties usage in assessment_autograde_core.rb and the TangoClient.addjob
method.

Comment thread app/models/assessment.rb
Comment on lines 97 to 99
def asmt_yaml_path
path "#{name}.yml"
path "assignment_config.yml"
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep the legacy assessment YAML filename as a read fallback.

load_yaml reads through asmt_yaml_path too, so this rename makes pre-existing course folders / tarballs that still contain <assignment>.yml fail with ENOENT. Keep assignment_config.yml as the new write path, but fall back to the legacy filename when loading.

🧩 Compatibility fallback
 def load_yaml
   return unless new_record?
 
-  props = YAML.safe_load(File.open(asmt_yaml_path, "r", &:read))
+  yaml_path = [asmt_yaml_path, path("#{name}.yml")].find { |candidate| File.exist?(candidate) }
+  raise Errno::ENOENT, asmt_yaml_path unless yaml_path
+
+  props = YAML.safe_load(File.read(yaml_path))
   validate_yaml(props)
   backwards_compatibility(props)
   deserialize(props)
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/assessment.rb` around lines 97 - 99, The current asmt_yaml_path
method always returns "assignment_config.yml" which breaks loading of legacy
tarballs; update load_yaml to try the new asmt_yaml_path first but fall back to
the legacy per-assessment filename (the original "<assignment>.yml" used before
the rename) if the new file is missing, while still writing/saving to
asmt_yaml_path; modify load_yaml to check both paths (asmt_yaml_path and the
legacy filename) and use whichever exists.

Comment on lines +26 to +28
<%= link_to "Delete Autograder", course_assessment_autograder_path(@course, @assessment),
method: :delete, class: "btn danger",
data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo in confirmation message: "assesssment" → "assessment".

✏️ Proposed fix
 <%= link_to "Delete Autograder", course_assessment_autograder_path(`@course`, `@assessment`),
             method: :delete, class: "btn danger",
-            data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
+            data: { confirm: "Are you sure you want to delete the Autograder for this assessment?" } %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_basic_settings.html.erb` around lines 26 - 28, Fix the
typo in the confirmation message used in the link_to "Delete Autograder" helper:
update the data confirm string on the link_to (the call that currently contains
"Are you sure you want to delete the Autograder for this assesssment?") to spell
"assessment" correctly so the message reads "Are you sure you want to delete the
Autograder for this assessment?".

Comment on lines +64 to +70
<%
selected_instance_type = @autograder.instance_type
valid_instance_types = ec2_instance_options.flat_map { |_group, instances| instances.map(&:last) }
if selected_instance_type.present? && !valid_instance_types.include?(selected_instance_type)
raise ArgumentError, "Invalid EC2 instance type: #{selected_instance_type}"
end
%>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Raising ArgumentError in a view causes a 500 error instead of a user-friendly message.

If an invalid instance_type is somehow stored (e.g., after changing the allowed list), this will crash the page. Consider rendering an error message or defaulting to a safe value instead.

Additionally, this validation duplicates the model's INSTANCE_TYPES constant. Consider referencing Autograder::INSTANCE_TYPES to ensure consistency.

🛠️ Proposed fix
 <%
   selected_instance_type = `@autograder.instance_type`
-  valid_instance_types = ec2_instance_options.flat_map { |_group, instances| instances.map(&:last) }
-  if selected_instance_type.present? && !valid_instance_types.include?(selected_instance_type)
-    raise ArgumentError, "Invalid EC2 instance type: #{selected_instance_type}"
+  if selected_instance_type.present? && !Autograder::INSTANCE_TYPES.include?(selected_instance_type)
+    # Fall back to blank selection and let the user pick a valid type
+    selected_instance_type = nil
+    flash.now[:error] ||= "Previously saved instance type is no longer valid. Please select a new one."
   end
 %>

Then update line 75:

 <%= f.select :instance_type,
-             grouped_options_for_select(ec2_instance_options, `@autograder.instance_type`),
+             grouped_options_for_select(ec2_instance_options, selected_instance_type),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_ec2_settings.html.erb` around lines 64 - 70, The view
raises ArgumentError for an invalid selected_instance_type which causes a 500;
instead, detect invalid values from the canonical Autograder::INSTANCE_TYPES (do
not duplicate ec2_instance_options logic), and either default
selected_instance_type to a safe value (e.g., nil or the first valid option) or
set a visible error message to render in the template; update the block where
selected_instance_type is computed (currently using ec2_instance_options and the
local selected_instance_type) to validate against Autograder::INSTANCE_TYPES and
handle invalid cases gracefully (default or set flash/inline error) rather than
raising.

Comment on lines +107 to +109
<%= link_to "Delete Autograder", course_assessment_autograder_path(@course, @assessment),
method: :delete, class: "btn danger",
data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same typo: "assesssment" → "assessment".

✏️ Proposed fix
 <%= link_to "Delete Autograder", course_assessment_autograder_path(`@course`, `@assessment`),
             method: :delete, class: "btn danger",
-            data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
+            data: { confirm: "Are you sure you want to delete the Autograder for this assessment?" } %>
📝 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
<%= link_to "Delete Autograder", course_assessment_autograder_path(@course, @assessment),
method: :delete, class: "btn danger",
data: { confirm: "Are you sure you want to delete the Autograder for this assesssment?" } %>
<%= link_to "Delete Autograder", course_assessment_autograder_path(`@course`, `@assessment`),
method: :delete, class: "btn danger",
data: { confirm: "Are you sure you want to delete the Autograder for this assessment?" } %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/autograders/_ec2_settings.html.erb` around lines 107 - 109, Fix the
typo in the confirmation message of the "Delete Autograder" link: update the
data confirm string in the link_to call that uses
course_assessment_autograder_path(`@course`, `@assessment`) so "assesssment" is
corrected to "assessment" in the confirmation prompt.

Comment on lines 80 to 89
<% if grade[:entry].is_a?(Array) %>
<% grade[:entry].each do |column| %>
<td><%= column %></td>
<% if !column.nil? %>
<td><%= column %></td>
<% else %>
<%= render 'error_icon' %>
<% end %>
<% end %>
<% else %>
<%= render 'error_icon' %>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default-scoreboard rows can still collapse when the entry is short.

This branch only iterates the cells that are present. When the controller falls back to grade[:entry] = [] after a createScoreboardEntry error (app/controllers/scoreboards_controller.rb Lines 83-87), the row stops after the Time column and the table becomes misaligned instead of showing placeholders for Total/problem columns.

Possible fix
-            <% if grade[:entry].is_a?(Array) %>
-              <% grade[:entry].each do |column| %>
-                <% if !column.nil? %>
-                  <td><%= column %></td>
-                <% else %>
-                  <%= render 'error_icon' %>
-                <% end %>
-              <% end %>
-            <% else %>
-              <%= render 'error_icon' %>
-            <% end %>
+            <% expected_columns = 1 + `@assessment.problems.length` %>
+            <% expected_columns.times do |idx| %>
+              <% column = grade[:entry].is_a?(Array) ? grade[:entry][idx] : nil %>
+              <% if column.nil? %>
+                <%= render 'error_icon' %>
+              <% else %>
+                <td><%= column %></td>
+              <% end %>
+            <% end %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/scoreboards/show.html.erb` around lines 80 - 89, The row collapses
because the template only iterates existing elements of grade[:entry]; when the
controller falls back to grade[:entry] = [] after createScoreboardEntry fails
the view renders fewer <td>s. Fix by ensuring the view renders a fixed number of
cells: in show.html.erb, replace the current conditional iteration over
grade[:entry] with a loop over the expected column count (calculate from the
header or a known constant) and for each index render <td><%= column_value
%></td> if grade[:entry][i] present else render the 'error_icon' placeholder;
alternatively ensure createScoreboardEntry always returns an array of the
expected length (e.g., Array.new(expected_count, nil)) so the existing per-index
rendering still produces placeholder cells. Ensure references to grade[:entry],
createScoreboardEntry, and the 'error_icon' partial are used to locate the code
to change.

Comment on lines +1 to +22
class EncryptAutograderCredentials < ActiveRecord::Migration[6.1]
def up
# Add encrypted columns for access keys
add_column :autograders, :access_key_ciphertext, :text
add_column :autograders, :access_key_id_ciphertext, :text

# Migrate existing plaintext data to encrypted columns
Autograder.reset_column_information
Autograder.find_each do |autograder|
if autograder.read_attribute(:access_key).present?
autograder.access_key = autograder.read_attribute(:access_key)
end
if autograder.read_attribute(:access_key_id).present?
autograder.access_key_id = autograder.read_attribute(:access_key_id)
end
autograder.save(validate: false) # Skip validation during migration
end

# Remove plaintext columns after migration
remove_column :autograders, :access_key
remove_column :autograders, :access_key_id
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the existing migration and potential conflicts
echo "=== Checking existing migration 20241205233214 ==="
cat db/migrate/20241205233214_add_ec2_ssh_fields_to_autograders.rb

echo ""
echo "=== Checking schema for current column state ==="
rg -n 'access_key' db/schema.rb || echo "schema.rb not found or no matches"

Repository: autolab/Autolab

Length of output: 1495


🏁 Script executed:

cat db/migrate/20251021034813_encrypt_autograder_credentials.rb

Repository: autolab/Autolab

Length of output: 1706


Migration conflicts with existing migration 20241205233214_add_ec2_ssh_fields_to_autograders.rb.

This migration will fail when run after 20241205233214 due to duplicate operations:

  1. Duplicate column additions: Migration 20241205233214 already adds access_key_ciphertext and access_key_id_ciphertext. Lines 4-5 will fail with "column already exists" errors since they lack guards.

  2. Missing source columns: Migration 20241205233214 removes the plaintext access_key and access_key_id columns. Lines 10 and 13 attempt to read from these columns, which no longer exist.

  3. Duplicate column removals: Lines 20-21 attempt to remove columns already removed by 20241205233214.

Add column_exists? guards throughout the migration to safely handle both initial runs and conditional scenarios:

Suggested fix
  def up
    # Add encrypted columns for access keys
-   add_column :autograders, :access_key_ciphertext, :text
-   add_column :autograders, :access_key_id_ciphertext, :text
+   add_column :autograders, :access_key_ciphertext, :text unless column_exists?(:autograders, :access_key_ciphertext)
+   add_column :autograders, :access_key_id_ciphertext, :text unless column_exists?(:autograders, :access_key_id_ciphertext)

    # Migrate existing plaintext data to encrypted columns
+   if column_exists?(:autograders, :access_key) && column_exists?(:autograders, :access_key_id)
      Autograder.reset_column_information
      Autograder.find_each do |autograder|
        if autograder.read_attribute(:access_key).present?
          autograder.access_key = autograder.read_attribute(:access_key)
        end
        if autograder.read_attribute(:access_key_id).present?
          autograder.access_key_id = autograder.read_attribute(:access_key_id)
        end
        autograder.save(validate: false) # Skip validation during migration
      end
+   end

    # Remove plaintext columns after migration
-   remove_column :autograders, :access_key
-   remove_column :autograders, :access_key_id
+   remove_column :autograders, :access_key if column_exists?(:autograders, :access_key)
+   remove_column :autograders, :access_key_id if column_exists?(:autograders, :access_key_id)
  end

  def down
    add_column :autograders, :access_key, :string, default: "" unless column_exists?(:autograders, :access_key)
    add_column :autograders, :access_key_id, :string, default: "" unless column_exists?(:autograders, :access_key_id)
    
    if column_exists?(:autograders, :access_key_ciphertext) && column_exists?(:autograders, :access_key_id_ciphertext)
      Autograder.reset_column_information
      Autograder.find_each do |autograder|
        if autograder.access_key_ciphertext.present?
          autograder.update_column(:access_key, autograder.access_key || "")
        end
        if autograder.access_key_id_ciphertext.present?
          autograder.update_column(:access_key_id, autograder.access_key_id || "")
        end
      end
    end

    remove_column :autograders, :access_key_ciphertext if column_exists?(:autograders, :access_key_ciphertext)
    remove_column :autograders, :access_key_id_ciphertext if column_exists?(:autograders, :access_key_id_ciphertext)
  end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20251021034813_encrypt_autograder_credentials.rb` around lines 1 -
22, The migration EncryptAutograderCredentials currently unconditionally
adds/removes columns and reads plaintext columns that may already have been
added/removed by a prior migration; update the up method to guard each schema
operation with column_exists? checks (use unless column_exists?(:autograders,
:access_key_ciphertext) / unless column_exists?(:autograders,
:access_key_id_ciphertext) before add_column, and wrap remove_column calls with
if column_exists?(:autograders, :access_key) / if column_exists?(:autograders,
:access_key_id)), and when iterating Autograder.find_each only read and copy
plaintext attributes if the plaintext columns actually exist (guard reads of
:access_key and :access_key_id), preserving the existing save(validate: false)
behavior and calling Autograder.reset_column_information appropriately after any
schema changes.

@melissajinn melissajinn deleted the export-tar branch April 14, 2026 20:24
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.

8 participants