Skip to content

re-organized course import tarball convention#2310

Open
lmaosoggypancakes wants to merge 2 commits intomasterfrom
josef_import_courses
Open

re-organized course import tarball convention#2310
lmaosoggypancakes wants to merge 2 commits intomasterfrom
josef_import_courses

Conversation

@lmaosoggypancakes
Copy link

The current format for tarballs to import is messy and confusing, especially when importing multiple courses.

Description

As it stood, the location and name for config files depend on the course name, making parsing messy and export/import courses harder than it should be. Config files should be the same name for all courses, just located in different folders, rather than having different names.

How Has This Been Tested?

tested on importing exported courses

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)

Other issues / help required

might need testing on courses with the same name to make sure there's a collision

If unsure, feel free to submit first and we'll help you along.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Changes update course YAML file naming convention from course_name.yml to course_config.yml in the courses controller, add credential validation guard to GitHub integration, modify the computed_score method signature from yield-based to parameter-based acceptance, initialize submission scores in submissions controller, add render-time measurement to the manage view, and adjust template whitespace in the submission panel.

Changes

Cohort / File(s) Summary
Course Configuration
app/controllers/courses_controller.rb
Updated validation and file handling to expect course_config.yml instead of course_name.yml; removed rescue blocks for error handling in create_from_tar; added explicit raise when no valid YAML config is found in get_course_from_config; introduced course_yml_path variable for improved error messaging.
Submission and Score Management
app/controllers/submissions_controller.rb, app/helpers/application_helper.rb
Initialize @submission_scores mapping in submissions index action; change computed_score method signature to accept value parameter directly instead of yield-based computation, altering how callers must invoke the method.
GitHub Integration
app/models/github_integration.rb
Add early guard clause in GithubIntegration.connected to return false when GitHub client credentials are blank, short-circuiting rate-limit checks.
View Updates
app/views/courses/manage.html.erb, app/views/assessments/_submission_panel.html.erb
Add render-time measurement to manage template displaying elapsed time; adjust whitespace and indentation in submission panel template with no functional impact.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • evanyeyeye
  • KesterTan
🚥 Pre-merge checks | ✅ 1 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers motivation, testing approach, and acknowledges edge cases, but the 'Types of changes' checkboxes remain unchecked and the PR appears to introduce breaking changes without marking them. Explicitly check the appropriate 'Types of changes' checkbox (likely 'Breaking change' given the YAML filename convention shift) to clearly communicate the impact to reviewers.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (12 files):

⚔️ .github/workflows/codeql-analysis.yml (content)
⚔️ .github/workflows/rubyonrails.yml (content)
⚔️ app/assets/javascripts/manage_submissions.js (content)
⚔️ app/assets/stylesheets/manage_submissions.css.scss (content)
⚔️ app/controllers/assessment/autograde.rb (content)
⚔️ app/controllers/courses_controller.rb (content)
⚔️ app/controllers/submissions_controller.rb (content)
⚔️ app/helpers/application_helper.rb (content)
⚔️ app/models/github_integration.rb (content)
⚔️ app/views/assessments/_submission_panel.html.erb (content)
⚔️ app/views/courses/manage.html.erb (content)
⚔️ app/views/submissions/index.html.erb (content)

These conflicts must be resolved before merging into master.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change—reorganizing the course import tarball convention from course-name-dependent filenames to a consistent naming structure.

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

✨ 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 josef_import_courses
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch josef_import_courses
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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: 5

Caution

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

⚠️ Outside diff range comments (3)
app/controllers/submissions_controller.rb (1)

72-72: ⚠️ Potential issue | 🔴 Critical

computed_score call with block syntax will raise ArgumentError after the signature change.

The computed_score method in ApplicationHelper was changed from a yield-based interface to requiring value as the first positional parameter (see application_helper.rb Line 111). These call sites still use the old block-based invocation with no positional arguments:

# Line 72
total = computed_score { submission.final_score(cud) }
# Line 75
submission_info[index]["late_penalty"] = computed_score { submission.late_penalty(cud) }

Both will fail at runtime with ArgumentError: wrong number of arguments (given 0, expected 1..3).

Proposed fix
-      total = computed_score { submission.final_score(cud) }
+      total = computed_score(submission.final_score(cud))
-      submission_info[index]["late_penalty"] = computed_score { submission.late_penalty(cud) }
+      submission_info[index]["late_penalty"] = computed_score(submission.late_penalty(cud))

Also applies to: 75-75

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

In `@app/controllers/submissions_controller.rb` at line 72, The call sites using
block-style invocation of computed_score (e.g., total = computed_score {
submission.final_score(cud) } and submission_info[index]["late_penalty"] =
computed_score { submission.late_penalty(cud) }) must be changed to the new
positional API: pass the computed value as the first argument instead of a block
— e.g., total = computed_score(submission.final_score(cud)) and
submission_info[index]["late_penalty"] =
computed_score(submission.late_penalty(cud)); if you need to supply the optional
second/third parameters supported by computed_score, pass them as additional
positional arguments.
app/controllers/courses_controller.rb (1)

1305-1334: ⚠️ Potential issue | 🟠 Major

Removing the depth check allows any .yml file in the archive to be treated as the course config.

Line 1308 comments out the guard entry.file? && entry.full_name.count('/') == 1, and Line 1310 only checks the file extension. This means a .yml file nested deep in an assessment directory (e.g., course/assessments/lab1/config.yml) could be picked up as the course configuration, leading to incorrect or failed imports.

At minimum, restore the entry.file? check and add a filename check:

Proposed fix
     tar_extract.each do |entry|
-      # next unless entry.file? && entry.full_name.count('/') == 1
-      # there should only be one file in the main directory with .yml extension
-      next unless File.extname(entry.full_name) == '.yml'
+      # Only match the course config YAML at the expected path
+      next unless entry.file? && File.basename(entry.full_name) == 'course_config.yml'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/courses_controller.rb` around lines 1305 - 1334, Restore the
original guard in get_course_from_config so only top-level files are considered:
require entry.file? and that entry.full_name.count('/') == 1, and also add a
filename check (e.g., File.basename(entry.full_name) == 'config.yml' or the
expected course filename) before accepting the .yml; update the conditional that
currently reads next unless File.extname(entry.full_name) == '.yml' to include
these checks so nested YAMLs (like in assessment subfolders) are ignored.
app/helpers/application_helper.rb (1)

111-117: ⚠️ Potential issue | 🔴 Critical

Signature change breaks 11 call sites across the codebase — update all callers to the new parameter-based API.

The method signature was changed from yield-based to parameter-based (value as first positional arg), but existing callers were not migrated. This breaks runtime execution in multiple locations:

Block-only calls (missing required value parameter → ArgumentError):

  • app/views/submissions/index.html.erb:205
  • app/views/gradebooks/student.html.erb:133, 158
  • app/views/assessments/_submission_history_row.html.erb:81
  • app/controllers/submissions_controller.rb:72, 75
  • app/helpers/application_helper.rb:132

Mixed calls (positional URL arg + ignored block → NoMethodError when calling .round(1) on string):

  • app/views/assessments/viewGradesheet.html.erb:61, 63, 69
  • app/helpers/application_helper.rb:127

Additionally, the rescue ScoreComputationException clause is now dead code — no yield exists to raise it, and .round(1) on numeric won't throw this exception.

All 11 call sites must be updated to pass computed values (not blocks) as the first argument, or the method signature must be reverted to support blocks.

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

In `@app/helpers/application_helper.rb` around lines 111 - 117, The computed_score
helper signature was changed from block-based to require a first positional
value (computed_score(value, link = nil, nil_to_dash = true)) which breaks 11
call sites that still call it with a block or pass a URL as the first arg;
update those call sites to supply the precomputed numeric value as the first
argument and pass the link as the second argument (e.g. replace computed_score {
... } with computed_score(computed_value, link) and replace computed_score(link)
where link was positional with computed_score(computed_value, link));
alternatively, revert computed_score to accept a block again (restore
yield-based behavior) if you prefer the old API, and if keeping the new API
remove or adjust the rescue ScoreComputationException in computed_score (or move
exception handling to where the value is computed) since .round(1) will not
raise that exception under the new contract.
🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@app/controllers/courses_controller.rb`:
- Line 1411: Fix the malformed and unsafe flash message: add the missing space
between "got" and "expected" and stop inserting unescaped user-derived values
into HTML by removing or not relying on flash[:html_safe]; instead HTML-escape
course_name and course_yml_path when building flash[:error] (e.g. use
ERB::Util.html_escape or CGI.escapeHTML on course_name and course_yml_path) so
the message uses the escaped values and reads "got <escaped_course_name>
expected <escaped_course_yml_path>" while avoiding setting flash[:html_safe].
- Around line 89-122: Remove the debug artifacts left in the manage action:
delete the commented-out Rails.logger.silence do and its matching `#end` around
the GradeMatrix/statistics block in the manage action (the code that builds
`@course_stats` using GradeMatrix and Statistics), and also remove the render-time
<h3> debug element from manage.html.erb; ensure the logic that constructs cols
and `@course_stats` (using GradeMatrix.new, matrix.has_assessment?,
matrix.cells_for_assessment, matrix.averages_for_category,
matrix.course_averages, and Statistics#stats) remains intact and unchanged aside
from removing the commented logger wrapper and stray HTML debug header.
- Around line 241-250: The commented-out begin/rescue removed error handling
around tar_extract.rewind, get_course_from_config(tar_extract) and
save_assessments_from_tar(tar_extract); restore protection by adding a
begin/rescue (or equivalent) around those calls to catch StandardError (or more
specific exceptions), set flash[:error] with the exception message,
render(action: "new") && return on error, and ensure tar_extract.close runs in
an ensure block so the tar is closed regardless of success or failure; reference
the existing methods get_course_from_config, save_assessments_from_tar,
tar_extract.rewind and the flash/render behavior when implementing the fix.
- Around line 1396-1397: Replace the incorrect double-nested path and filename
check: set course_yml_path to "#{course_name}/#{course_name}.yml" (instead of
File.join(course_name, course_name, "course_config.yml")) and update the boolean
assignment using that variable (course_yml_exists = course_yml_exists ||
pathname == course_yml_path); also update the error message around the later
check (referenced near the original line 1411) to mention "#{course_name}.yml"
(the actual filename produced by generate_tar in Course#generate_tar) and ensure
this logic is consistent with save_assessments_from_tar which expects a single
course_name/… nesting.
- Around line 1396-1397: The assignment of course_yml_path inside the per-entry
loop is redundant; move the computation of course_yml_path =
File.join(course_name, course_name, "course_config.yml") out of the loop so it
is calculated once after course_name has been determined (or after the loop
finishes), and keep the loop only setting course_yml_exists = course_yml_exists
|| pathname == course_yml_path (or compare against the precomputed path); update
references to course_yml_path accordingly in the surrounding code in
courses_controller.rb.
- Around line 1305-1334: Restore the original guard in get_course_from_config so
only top-level files are considered: require entry.file? and that
entry.full_name.count('/') == 1, and also add a filename check (e.g.,
File.basename(entry.full_name) == 'config.yml' or the expected course filename)
before accepting the .yml; update the conditional that currently reads next
unless File.extname(entry.full_name) == '.yml' to include these checks so nested
YAMLs (like in assessment subfolders) are ignored.

In `@app/controllers/submissions_controller.rb`:
- Around line 43-44: The current code forces every submission to score 0 by
using `@submissions.index_with` { 0 }, which can hide missing/nil scores; change
this to map to nil (e.g., `@submissions.index_with` { nil }) or only set scores
for submissions you actually have grades for so missing scores remain nil, then
update any view logic that reads `@submission_scores` to handle nil vs 0
explicitly (treat nil as "not graded" and 0 as a graded zero). Also either
remove the commented-out `@submission_styles` line or restore it with a short TODO
comment explaining its purpose (e.g., why ignored_submission_style(s) would be
needed) so intent is clear; update references to `@submission_scores` and
ignored_submission_style in the view/controller accordingly.
- Line 72: The call sites using block-style invocation of computed_score (e.g.,
total = computed_score { submission.final_score(cud) } and
submission_info[index]["late_penalty"] = computed_score {
submission.late_penalty(cud) }) must be changed to the new positional API: pass
the computed value as the first argument instead of a block — e.g., total =
computed_score(submission.final_score(cud)) and
submission_info[index]["late_penalty"] =
computed_score(submission.late_penalty(cud)); if you need to supply the optional
second/third parameters supported by computed_score, pass them as additional
positional arguments.

In `@app/helpers/application_helper.rb`:
- Around line 111-117: The computed_score helper signature was changed from
block-based to require a first positional value (computed_score(value, link =
nil, nil_to_dash = true)) which breaks 11 call sites that still call it with a
block or pass a URL as the first arg; update those call sites to supply the
precomputed numeric value as the first argument and pass the link as the second
argument (e.g. replace computed_score { ... } with
computed_score(computed_value, link) and replace computed_score(link) where link
was positional with computed_score(computed_value, link)); alternatively, revert
computed_score to accept a block again (restore yield-based behavior) if you
prefer the old API, and if keeping the new API remove or adjust the rescue
ScoreComputationException in computed_score (or move exception handling to where
the value is computed) since .round(1) will not raise that exception under the
new contract.

In `@app/models/github_integration.rb`:
- Around line 166-167: Replace the use of the low-precedence `or` operator in
the boolean guard with the idiomatic `||` operator to avoid precedence
surprises: update the conditional in the GitHub integration guard (the return
false if Rails.configuration.x.github.client_id.blank? or
Rails.configuration.x.github.client_secret.blank? line inside the
GithubIntegration class/method) to use `||` between the two blank? checks so
both boolean expressions are evaluated with normal precedence.

In `@app/views/courses/manage.html.erb`:
- Line 2: Remove the debug render-time measurement from the template: delete the
Time.current assignment (start_time) and the corresponding display block that
computes and shows render duration (the <h3> element that subtracts start_time
from Time.current), so neither the timing variable nor the HTML that exposes
render_time remains in manage.html.erb; search for the start_time assignment and
the render-time <h3> (or the expression Time.current - start_time) and remove
both occurrences.
🧹 Nitpick comments (3)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed.


In `@app/controllers/courses_controller.rb`:
- Around line 1396-1397: The assignment of course_yml_path inside the per-entry
loop is redundant; move the computation of course_yml_path =
File.join(course_name, course_name, "course_config.yml") out of the loop so it
is calculated once after course_name has been determined (or after the loop
finishes), and keep the loop only setting course_yml_exists = course_yml_exists
|| pathname == course_yml_path (or compare against the precomputed path); update
references to course_yml_path accordingly in the surrounding code in
courses_controller.rb.

In `@app/controllers/submissions_controller.rb`:
- Around line 43-44: The current code forces every submission to score 0 by
using `@submissions.index_with` { 0 }, which can hide missing/nil scores; change
this to map to nil (e.g., `@submissions.index_with` { nil }) or only set scores
for submissions you actually have grades for so missing scores remain nil, then
update any view logic that reads `@submission_scores` to handle nil vs 0
explicitly (treat nil as "not graded" and 0 as a graded zero). Also either
remove the commented-out `@submission_styles` line or restore it with a short TODO
comment explaining its purpose (e.g., why ignored_submission_style(s) would be
needed) so intent is clear; update references to `@submission_scores` and
ignored_submission_style in the view/controller accordingly.

In `@app/models/github_integration.rb`:
- Around line 166-167: Replace the use of the low-precedence `or` operator in
the boolean guard with the idiomatic `||` operator to avoid precedence
surprises: update the conditional in the GitHub integration guard (the return
false if Rails.configuration.x.github.client_id.blank? or
Rails.configuration.x.github.client_secret.blank? line inside the
GithubIntegration class/method) to use `||` between the two blank? checks so
both boolean expressions are evaluated with normal precedence.
app/controllers/courses_controller.rb (1)

1396-1397: course_yml_path is reassigned on every non-root entry iteration.

course_yml_path is set inside the else branch that runs for every file/directory entry (not just the root). Since it only depends on course_name, this redundant reassignment is harmless but wasteful and obscures intent. Move it outside the loop, after course_name is determined — or compute it once after the loop ends.

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

In `@app/controllers/courses_controller.rb` around lines 1396 - 1397, The
assignment of course_yml_path inside the per-entry loop is redundant; move the
computation of course_yml_path = File.join(course_name, course_name,
"course_config.yml") out of the loop so it is calculated once after course_name
has been determined (or after the loop finishes), and keep the loop only setting
course_yml_exists = course_yml_exists || pathname == course_yml_path (or compare
against the precomputed path); update references to course_yml_path accordingly
in the surrounding code in courses_controller.rb.
app/controllers/submissions_controller.rb (1)

43-44: Initializing all scores to 0 may mask missing/nil scores.

@submissions.index_with { 0 } maps every submission to 0. Verify the index view actually uses @submission_scores and that a default of 0 (rather than nil) is the correct semantic — a student with no graded scores is different from a student who scored 0.

Also, the commented-out line on Line 44 should either be removed or restored with a TODO explaining intent.

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

In `@app/controllers/submissions_controller.rb` around lines 43 - 44, The current
code forces every submission to score 0 by using `@submissions.index_with` { 0 },
which can hide missing/nil scores; change this to map to nil (e.g.,
`@submissions.index_with` { nil }) or only set scores for submissions you actually
have grades for so missing scores remain nil, then update any view logic that
reads `@submission_scores` to handle nil vs 0 explicitly (treat nil as "not
graded" and 0 as a graded zero). Also either remove the commented-out
`@submission_styles` line or restore it with a short TODO comment explaining its
purpose (e.g., why ignored_submission_style(s) would be needed) so intent is
clear; update references to `@submission_scores` and ignored_submission_style in
the view/controller accordingly.
app/models/github_integration.rb (1)

166-167: Prefer || over or for boolean conditions.

While functionally correct here, or has different (lower) precedence than || in Ruby and is generally reserved for control-flow idioms. Using || is the idiomatic choice for boolean expressions and avoids subtle precedence bugs in other contexts.

Proposed fix
-    return false if Rails.configuration.x.github.client_id.blank? or
-                     Rails.configuration.x.github.client_secret.blank?
+    return false if Rails.configuration.x.github.client_id.blank? ||
+                     Rails.configuration.x.github.client_secret.blank?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/github_integration.rb` around lines 166 - 167, Replace the use of
the low-precedence `or` operator in the boolean guard with the idiomatic `||`
operator to avoid precedence surprises: update the conditional in the GitHub
integration guard (the return false if
Rails.configuration.x.github.client_id.blank? or
Rails.configuration.x.github.client_secret.blank? line inside the
GithubIntegration class/method) to use `||` between the two blank? checks so
both boolean expressions are evaluated with normal precedence.

Comment on lines +89 to +122
# Rails.logger.silence do
matrix = GradeMatrix.new @course, @cud
cols = {}
# extract assessment final scores
@course.assessments.each do |asmt|
next unless matrix.has_assessment? asmt.id

cells = matrix.cells_for_assessment asmt.id
final_scores = cells.map { |c| c["final_score"] }
cols[asmt.name] = ["asmt", asmt, final_scores]
end

# category averages
@course.assessment_categories.each do |cat|
next unless matrix.has_category? cat
# category averages
@course.assessment_categories.each do |cat|
next unless matrix.has_category? cat

cols["#{cat} Average"] = ["avg", nil, matrix.averages_for_category(cat)]
end
cols["#{cat} Average"] = ["avg", nil, matrix.averages_for_category(cat)]
end

# course averages
cols["Course Average"] = ["avg", nil, matrix.course_averages]

# calculate statistics
# send course_stats back in the form of
# name of average / assesment -> [type, asmt, statistics]
# where type = "asmt" or "avg" (assessment or average)
# asmt = assessment object or nil if an average of category / class
# statistics (statistics pertaining to asmt/avg (mean, median, std dev, etc))
@course_stats = {}
stat = Statistics.new
cols.each do |key, values|
@course_stats[key] = [values[0], values[1], stat.stats(values[2])]
end
# course averages
cols["Course Average"] = ["avg", nil, matrix.course_averages]

# calculate statistics
# send course_stats back in the form of
# name of average / assesment -> [type, asmt, statistics]
# where type = "asmt" or "avg" (assessment or average)
# asmt = assessment object or nil if an average of category / class
# statistics (statistics pertaining to asmt/avg (mean, median, std dev, etc))
@course_stats = {}
stat = Statistics.new
cols.each do |key, values|
@course_stats[key] = [values[0], values[1], stat.stats(values[2])]
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.

🛠️ Refactor suggestion | 🟠 Major

Remove debug artifacts from manage action.

The commented-out Rails.logger.silence do (Line 89) and #end (Line 122) are debug leftovers. Along with the render-time <h3> in manage.html.erb, these suggest temporary profiling code that shouldn't be merged.

Proposed fix
-    # Rails.logger.silence do
-      matrix = GradeMatrix.new `@course`, `@cud`
+    matrix = GradeMatrix.new `@course`, `@cud`
 ...
-    `#end`
📝 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
# Rails.logger.silence do
matrix = GradeMatrix.new @course, @cud
cols = {}
# extract assessment final scores
@course.assessments.each do |asmt|
next unless matrix.has_assessment? asmt.id
cells = matrix.cells_for_assessment asmt.id
final_scores = cells.map { |c| c["final_score"] }
cols[asmt.name] = ["asmt", asmt, final_scores]
end
# category averages
@course.assessment_categories.each do |cat|
next unless matrix.has_category? cat
# category averages
@course.assessment_categories.each do |cat|
next unless matrix.has_category? cat
cols["#{cat} Average"] = ["avg", nil, matrix.averages_for_category(cat)]
end
cols["#{cat} Average"] = ["avg", nil, matrix.averages_for_category(cat)]
end
# course averages
cols["Course Average"] = ["avg", nil, matrix.course_averages]
# calculate statistics
# send course_stats back in the form of
# name of average / assesment -> [type, asmt, statistics]
# where type = "asmt" or "avg" (assessment or average)
# asmt = assessment object or nil if an average of category / class
# statistics (statistics pertaining to asmt/avg (mean, median, std dev, etc))
@course_stats = {}
stat = Statistics.new
cols.each do |key, values|
@course_stats[key] = [values[0], values[1], stat.stats(values[2])]
end
# course averages
cols["Course Average"] = ["avg", nil, matrix.course_averages]
# calculate statistics
# send course_stats back in the form of
# name of average / assesment -> [type, asmt, statistics]
# where type = "asmt" or "avg" (assessment or average)
# asmt = assessment object or nil if an average of category / class
# statistics (statistics pertaining to asmt/avg (mean, median, std dev, etc))
@course_stats = {}
stat = Statistics.new
cols.each do |key, values|
@course_stats[key] = [values[0], values[1], stat.stats(values[2])]
end
#end
matrix = GradeMatrix.new `@course`, `@cud`
cols = {}
# extract assessment final scores
`@course.assessments.each` do |asmt|
next unless matrix.has_assessment? asmt.id
cells = matrix.cells_for_assessment asmt.id
final_scores = cells.map { |c| c["final_score"] }
cols[asmt.name] = ["asmt", asmt, final_scores]
end
# category averages
`@course.assessment_categories.each` do |cat|
next unless matrix.has_category? cat
cols["#{cat} Average"] = ["avg", nil, matrix.averages_for_category(cat)]
end
# course averages
cols["Course Average"] = ["avg", nil, matrix.course_averages]
# calculate statistics
# send course_stats back in the form of
# name of average / assesment -> [type, asmt, statistics]
# where type = "asmt" or "avg" (assessment or average)
# asmt = assessment object or nil if an average of category / class
# statistics (statistics pertaining to asmt/avg (mean, median, std dev, etc))
`@course_stats` = {}
stat = Statistics.new
cols.each do |key, values|
`@course_stats`[key] = [values[0], values[1], stat.stats(values[2])]
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/courses_controller.rb` around lines 89 - 122, Remove the
debug artifacts left in the manage action: delete the commented-out
Rails.logger.silence do and its matching `#end` around the GradeMatrix/statistics
block in the manage action (the code that builds `@course_stats` using GradeMatrix
and Statistics), and also remove the render-time <h3> debug element from
manage.html.erb; ensure the logic that constructs cols and `@course_stats` (using
GradeMatrix.new, matrix.has_assessment?, matrix.cells_for_assessment,
matrix.averages_for_category, matrix.course_averages, and Statistics#stats)
remains intact and unchanged aside from removing the commented logger wrapper
and stray HTML debug header.

Comment on lines +241 to +250
# begin
tar_extract.rewind
@newCourse = get_course_from_config(tar_extract)
# save assessment directories
save_assessments_from_tar(tar_extract)
tar_extract.close
rescue StandardError => e
flash[:error] = "Error while extracting course to server -- #{e.message}."
render(action: "new") && return
end
#rescue StandardError => e
#flash[:error] = "Error while extracting course to server -- #{e.message}."
# render(action: "new") && return
#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

Commented-out rescue block removes error handling for course extraction.

The begin/rescue that previously caught errors from get_course_from_config and save_assessments_from_tar is commented out (and the begin on Line 241 is also commented). If either method raises, the user will see a raw 500 error instead of a helpful flash message. This is a regression from the previous behavior.

Either restore the error handling or replace it with equivalent protection.

Proposed fix
-#    begin
+    begin
       tar_extract.rewind
       `@newCourse` = get_course_from_config(tar_extract)
       # save assessment directories
       save_assessments_from_tar(tar_extract)
       tar_extract.close
-    `#rescue` StandardError => e
-      `#flash`[:error] = "Error while extracting course to server -- #{e.message}."
-     # render(action: "new") && return
-    `#end`
+    rescue StandardError => e
+      flash[:error] = "Error while extracting course to server -- #{e.message}."
+      render(action: "new") && return
+    end
📝 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
# begin
tar_extract.rewind
@newCourse = get_course_from_config(tar_extract)
# save assessment directories
save_assessments_from_tar(tar_extract)
tar_extract.close
rescue StandardError => e
flash[:error] = "Error while extracting course to server -- #{e.message}."
render(action: "new") && return
end
#rescue StandardError => e
#flash[:error] = "Error while extracting course to server -- #{e.message}."
# render(action: "new") && return
#end
begin
tar_extract.rewind
`@newCourse` = get_course_from_config(tar_extract)
# save assessment directories
save_assessments_from_tar(tar_extract)
tar_extract.close
rescue StandardError => e
flash[:error] = "Error while extracting course to server -- #{e.message}."
render(action: "new") && return
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/courses_controller.rb` around lines 241 - 250, The
commented-out begin/rescue removed error handling around tar_extract.rewind,
get_course_from_config(tar_extract) and save_assessments_from_tar(tar_extract);
restore protection by adding a begin/rescue (or equivalent) around those calls
to catch StandardError (or more specific exceptions), set flash[:error] with the
exception message, render(action: "new") && return on error, and ensure
tar_extract.close runs in an ensure block so the tar is closed regardless of
success or failure; reference the existing methods get_course_from_config,
save_assessments_from_tar, tar_extract.rewind and the flash/render behavior when
implementing the fix.

Comment on lines +1396 to +1397
course_yml_path = File.join(course_name, course_name, "course_config.yml")
course_yml_exists = course_yml_exists || pathname == course_yml_path
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the problematic code location first
echo "=== Code at lines 1396-1397 in courses_controller.rb ==="
sed -n '1390,1405p' app/controllers/courses_controller.rb
echo ""
echo "=== Code around line 1339 (save_assessments_from_tar) ==="
sed -n '1330,1360p' app/controllers/courses_controller.rb
echo ""
echo "=== search for generate_tar method ==="
rg -n 'def generate_tar' --type=ruby -A 30
echo ""
echo "=== Look for course_config.yml references ==="
rg -n 'course_config\.yml' --type=ruby -B 2 -A 2
echo ""
echo "=== Look for course_yml or course.yml references ==="
rg -n 'course\.yml|course_yml' --type=ruby -B 2 -A 2
echo ""
echo "=== Search for export methods ==="
rg -n 'def export' --type=ruby -A 15

Repository: autolab/Autolab

Length of output: 9859


Fix both the duplicated course_name and incorrect filename in the config path check.

The code at line 1396 has two issues:

  1. Duplicates course_name in the path (double nesting)
  2. Looks for course_config.yml, but the actual config filename generated by generate_tar (app/models/course.rb:360) is #{course_name}.yml

The generate_tar method creates the structure: course_name/course_name.yml, not course_name/course_name/course_config.yml. This is consistent with save_assessments_from_tar (line 1339), which expects single nesting at course_name/assessments/....

The error message at line 1411 is also incorrect and should reference the actual expected path.

Proposed fix
-        course_yml_path = File.join(course_name, course_name, "course_config.yml")
+        course_yml_path = File.join(course_name, "#{course_name}.yml")

Also update line 1411 error message to reference the correct path: course_name.yml instead of course_config.yml.

📝 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
course_yml_path = File.join(course_name, course_name, "course_config.yml")
course_yml_exists = course_yml_exists || pathname == course_yml_path
course_yml_path = File.join(course_name, "#{course_name}.yml")
course_yml_exists = course_yml_exists || pathname == course_yml_path
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/courses_controller.rb` around lines 1396 - 1397, Replace the
incorrect double-nested path and filename check: set course_yml_path to
"#{course_name}/#{course_name}.yml" (instead of File.join(course_name,
course_name, "course_config.yml")) and update the boolean assignment using that
variable (course_yml_exists = course_yml_exists || pathname == course_yml_path);
also update the error message around the later check (referenced near the
original line 1411) to mention "#{course_name}.yml" (the actual filename
produced by generate_tar in Course#generate_tar) and ensure this logic is
consistent with save_assessments_from_tar which expects a single course_name/…
nesting.

flash[:error] = "Errors found in tarball:"
if !course_yml_exists && !course_name.nil?
flash[:error] += "<br>Course yml file #{course_name}/#{course_name}.yml was not found"
flash[:error] += "<br>Course yml file #{course_name}/course_config.yml was not found: got " + course_name + "expected " + course_yml_path
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

Missing space in error message and XSS risk from user-derived content.

The string concatenation is missing a space before "expected":

"got " + course_name + "expected " + course_yml_path

This renders as "got mycoursexpected mycoursemycourse/course_config.yml".

Additionally, course_name is derived from the tarball's directory name (user-supplied input) and is interpolated into flash[:error] with flash[:html_safe] = true set on Line 1413. This is an XSS vector — a malicious tarball could set a directory name containing <script> tags.

Proposed fix
-        flash[:error] += "<br>Course yml file #{course_name}/course_config.yml was not found: got " + course_name + "expected " + course_yml_path
+        flash[:error] += "<br>Course yml file not found. Expected #{CGI.escapeHTML(course_yml_path.to_s)}, got #{CGI.escapeHTML(course_name.to_s)}"

Based on learnings: "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."

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

In `@app/controllers/courses_controller.rb` at line 1411, Fix the malformed and
unsafe flash message: add the missing space between "got" and "expected" and
stop inserting unescaped user-derived values into HTML by removing or not
relying on flash[:html_safe]; instead HTML-escape course_name and
course_yml_path when building flash[:error] (e.g. use ERB::Util.html_escape or
CGI.escapeHTML on course_name and course_yml_path) so the message uses the
escaped values and reads "got <escaped_course_name> expected
<escaped_course_yml_path>" while avoiding setting flash[:html_safe].

@@ -1,5 +1,5 @@
<% @title = "Manage Course" %>

<% start_time = Time.current %>
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

Remove debug render-time measurement from production template.

Lines 2 and 163–164 add an <h3> element displaying the ERB render time directly in the page HTML. This exposes internal performance metrics to all users (including students) and looks like leftover debug instrumentation. It should be removed before merging.

Proposed fix
 <% `@title` = "Manage Course" %>
-    <% start_time = Time.current %>
 <% content_for :stylesheets do %>
-<h3>rendered in <%= ((Time.current - start_time) * 1000).round(2) %> ms</h3>
-
 </div>

Also applies to: 163-164

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

In `@app/views/courses/manage.html.erb` at line 2, Remove the debug render-time
measurement from the template: delete the Time.current assignment (start_time)
and the corresponding display block that computes and shows render duration (the
<h3> element that subtracts start_time from Time.current), so neither the timing
variable nor the HTML that exposes render_time remains in manage.html.erb;
search for the start_time assignment and the render-time <h3> (or the expression
Time.current - start_time) and remove both occurrences.

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.

1 participant