feat: grades upload flow#8008
Conversation
Coverage Report for CI Build 28135968175Coverage increased (+0.03%) to 90.215%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
david-yz-liu
left a comment
There was a problem hiding this comment.
@YheChen nice work, I left a few comments. Please also make sure to update the screenshots in the PR description and the documentation Wiki.
| class AssignmentPolicy < ApplicationPolicy | ||
| default_rule :manage? | ||
| alias_rule :summary?, to: :view? | ||
| alias_rule :upload_grades?, to: :manage? |
There was a problem hiding this comment.
This isn't necessary because of the default_rule :manage? call
| .joins(grouping: :group) | ||
| .includes(:marks, grouping: :group) | ||
| .index_by { |result| result.grouping.group.group_name } | ||
| results_by_user_name = current_results |
There was a problem hiding this comment.
Don't include this. The group name is required; the list of users is optional, and should be ignored if it's in the CSV upload. There is no need to have a fallback query (see my further comment below on result_for_uploaded_marks_row)
|
|
||
| raise CsvInvalidLineError if criterion_columns == :invalid | ||
|
|
||
| result = result_for_uploaded_marks_row(row, group_name_index, user_name_index, |
There was a problem hiding this comment.
I wouldn't define a helper function here, it's not worth it. Do a lookup on row[group_name_index]. This will either be a Result or nil, and is the only thin you need to use.
| mark = result.marks.find_or_initialize_by(criterion: criterion) | ||
| next if !overwrite && !mark.mark.nil? | ||
|
|
||
| mark_value = parse_uploaded_mark(row[column_index]) |
There was a problem hiding this comment.
No need to make this a helper function, inline the logic here
| next if !overwrite && !mark.mark.nil? | ||
|
|
||
| mark_value = parse_uploaded_mark(row[column_index]) | ||
| unless mark.update(mark: mark_value, |
There was a problem hiding this comment.
Doing this one record at a time will be quite slow, even with the transaction. Modify the logic to accumulate a set of updates and use upsert_all
| elsif criterion_columns.nil? | ||
| begin | ||
| criterion_columns = assignment_upload_criterion_columns(headers, row, criteria_by_name) | ||
| rescue CsvInvalidLineError |
There was a problem hiding this comment.
I'm not sure what the purpose of this exception handling and re-raising is
| next | ||
| elsif criterion_columns.nil? | ||
| begin | ||
| criterion_columns = assignment_upload_criterion_columns(headers, row, criteria_by_name) |
There was a problem hiding this comment.
Similar to my above comment, I think this is overly complex and doesn't require a helper. Ignore the "totals" row; determine Criterion object based on the name, which is found only in the headers row.
You can ignore the "totals" row by skipping any row with a blank "group name" column, which I believe you already have some logic to do.
| criteria.exists? ? criteria.last.position + 1 : 1 | ||
| end | ||
|
|
||
| def criteria_by_upload_name |
There was a problem hiding this comment.
This helper function isn't necessary; again, inline the code above.
First, to create a hash use the pattern of
ta_criteria.map do |criteria|
...
# end with an array of [key, value]
end.to_h
Second, the most complex part of this logic is the conditional adding of "bonus". This appears elsewhere in the code for generating CSV files, so actually what I would suggest is to define a Criterion helper method called export_name that handles this logic, and then just call criterion.export_name here. Also modify the existing places in the CSV exporting to use this method.
The .to_s and strip calls are unnecessary. They are not found in the corresponding CSV export method.
| upload_file_requirement: Filename %{file_name} is not allowed for this assignment. | ||
| upload_file_requirement_in_folder: Filename %{file_name} in %{file_path} is not allowed for this assignment. | ||
| upload_grades: | ||
| invalid_criteria: 'No matching criteria were found in the uploaded CSV. Unknown criteria: %{criteria}' |
There was a problem hiding this comment.
Reword this message to "The following criteria were not found for this assignment: %{criteria}"
| example_form_params[:assignment][:submission_rule_attributes][:periods_attributes] = submission_rule.id | ||
| example_form_params | ||
| end | ||
|
|
There was a problem hiding this comment.
Please revert the blank line changes in this file (here and below)
There was a problem hiding this comment.
FYI I discovered that RuboCop had a rule RSpec/EmptyLineAfterFinalLet that was creating these blank lines, which I bypassed in my commit by doing
SKIP=rubocop git commit -m "Remove incidental controller spec blank lines"
| criteria_by_name = ta_criteria.index_by(&:export_name) | ||
| ignored_headers = [Group.human_attribute_name(:group_name), | ||
| I18n.t('results.total_mark'), | ||
| 'Bonus/Deductions'] + |
There was a problem hiding this comment.
Sorry I just noticed this; please internationalize this string (and replace here and in the CSV export)
|
|
||
| if uploaded_criterion_columns.empty? || unknown_columns.present? | ||
| criterion_columns = [] | ||
| raise CsvInvalidLineError, |
There was a problem hiding this comment.
Ah okay I understand a bit better now. When the criteria columns are invalid, we shouldn't raise a CsvInvalidLineError (which is handled by MarkusCsv.parse), but instead you can just raise the string directly. This will terminate this entire function; then, handle the error in the controller and report the specific error message to the user.
| next | ||
| end | ||
|
|
||
| raise CsvInvalidLineError if criterion_columns.empty? |
There was a problem hiding this comment.
Doing the above comment will allow you to remove this check
| else | ||
| new_mark_updates[key] = attrs.merge(created_at: now) | ||
| end | ||
| marks_by_result_and_criterion[key] = { id: mark&.[](:id), mark: attrs[:mark] } |
There was a problem hiding this comment.
I don't think this line has any effect (marks_by_result_and_criterion is not used further in this function)
| if mark&.[](:id) | ||
| mark_updates[mark[:id]] = attrs.merge(id: mark[:id]) | ||
| else | ||
| new_mark_updates[key] = attrs.merge(created_at: now) |
There was a problem hiding this comment.
Hmm I don't think the created_at timestamp is necessary? I thought this was address in Rails v7 (rails/rails#43003).
Proposed Changes
Adds CSV upload support for criterion marks from the assignment Grades tab.
This PR adds an Upload button for instructors on the assignment Grades tab. The upload flow accepts a CSV in the same general format as the existing Grades tab CSV export / grade entry form uploads, updates marks for matching assignment criteria, supports an overwrite option, and ignores total mark and bonus/deduction columns.
Also adds backend import handling, authorization, translations, and model/controller tests.
Fixes #7794
Screenshots of your changes (if applicable)
Upload Button
Upload Modal
Associated documentation repository pull request (if applicable)
MarkUsProject/Wiki#268
Type of Change
Checklist
Before opening your pull request:
After opening your pull request:
Questions and Comments