Skip to content

Commit

Permalink
Merge pull request #7027 from donny-wong/v2.4.7
Browse files Browse the repository at this point in the history
V2.4.7
  • Loading branch information
pretendWhale authored Apr 3, 2024
2 parents bf989ac + 534d1db commit e61759c
Show file tree
Hide file tree
Showing 27 changed files with 610 additions and 102 deletions.
3 changes: 3 additions & 0 deletions .dockerfiles/entrypoint-dev-rails.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ npm list &> /dev/null || npm ci
./venv/bin/python3 -m pip install -r requirements-scanner.txt > /dev/null
./venv/bin/python3 -m pip install -r requirements-qr.txt > /dev/null

# install chromium (for nbconvert webpdf conversion)
./venv/bin/python3 -m playwright install chromium

# setup the database (checks for db existence first)
until pg_isready -q; do
echo "waiting for database to start up"
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/test_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get -yqq install libpq-dev cmake ghostscript pandoc imagemagick libmagickwand-dev git libgl1 tesseract-ocr
sudo apt-get -yqq install libpq-dev cmake ghostscript pandoc imagemagick libmagickwand-dev git libgl1 tesseract-ocr pandoc
- name: Set up ruby and cache gems
uses: ruby/setup-ruby@v1
with:
Expand All @@ -69,10 +69,12 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('requirements-jupyter.txt') }}-${{ hashFiles('requirements-scanner.txt') }}-${{ hashFiles('requirements-qr.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install python packages
- name: Install python packages and playwright dependencies
run: |
python3.9 -m venv venv
./venv/bin/pip install -r requirements-jupyter.txt -r requirements-scanner.txt -r requirements-qr.txt
./venv/bin/playwright install chromium
./venv/bin/playwright install-deps chromium
- name: Configure server
run: |
sudo rm -f /etc/localtime
Expand Down
5 changes: 5 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [v2.4.7]
- Support Jupyter notebooks for results printing (#6993)
- Enable bulk download of print PDFs for an assignments (#6998)
- Fixed long annotations being cut off in the annotation table (#7001)

## [v2.4.6]
- Disallow students from uploading .git file and .git folder in their repository (#6963)

Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.7.3)
rack (2.2.8)
rack-cors (2.0.1)
rack (2.2.8.1)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4)
Expand Down Expand Up @@ -390,7 +390,7 @@ GEM
rubyzip (2.3.2)
rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6)
rugged (1.7.1)
rugged (1.7.2)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
sassc (2.4.0)
Expand Down
2 changes: 1 addition & 1 deletion app/MARKUS_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION=v2.4.6,PATCH_LEVEL=DEV
VERSION=v2.4.7,PATCH_LEVEL=DEV
14 changes: 14 additions & 0 deletions app/assets/javascripts/Components/Modals/roster_sync_modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class LtiRosterModal extends React.Component {
this.state = {
include_tas: true,
include_students: true,
include_instructors: true,
};
}

Expand All @@ -34,6 +35,7 @@ class LtiRosterModal extends React.Component {
data: {
include_students: this.state.include_students,
include_tas: this.state.include_tas,
include_instructors: this.state.include_instructors,
lti_deployment_id: this.props.roster_deployment_id,
},
});
Expand Down Expand Up @@ -75,6 +77,18 @@ class LtiRosterModal extends React.Component {
{I18n.t("lti.sync_tas")}
</label>
</p>
<p>
<label>
<input
type="checkbox"
name="include_instructors"
key="1"
defaultChecked="true"
onChange={this.handleChange}
/>
{I18n.t("lti.sync_instructors")}
</label>
</p>

<section className={"modal-container dialog-actions"}>
<input type="submit" value={I18n.t("lti.roster_sync")} />
Expand Down
3 changes: 3 additions & 0 deletions app/assets/javascripts/Components/Result/annotation_table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ export class AnnotationTable extends React.Component {
className="auto-overflow"
data={this.props.annotations}
columns={allColumns}
getTdProps={() => {
return {className: "-wrap"};
}}
filterable
resizable
defaultSorted={[{id: "deduction"}, {id: "filename"}, {id: "number"}]}
Expand Down
20 changes: 18 additions & 2 deletions app/assets/javascripts/Components/submission_table.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ class RawSubmissionTable extends React.Component {
}).then(this.fetchData);
};

prepareGroupingFiles = () => {
prepareGroupingFiles = print => {
if (window.confirm(I18n.t("submissions.marking_incomplete_warning"))) {
$.post({
url: Routes.zip_groupings_files_course_assignment_submissions_url(
Expand All @@ -310,6 +310,7 @@ class RawSubmissionTable extends React.Component {
),
data: {
groupings: this.props.selection,
print: print,
},
});
}
Expand Down Expand Up @@ -426,7 +427,8 @@ class RawSubmissionTable extends React.Component {
collectSubmissions={() => {
this.setState({showCollectSubmissionsModal: true});
}}
downloadGroupingFiles={this.prepareGroupingFiles}
printGroupingFiles={() => this.prepareGroupingFiles(true)}
downloadGroupingFiles={() => this.prepareGroupingFiles(false)}
showReleaseUrls={() => this.setState({showReleaseUrlsModal: true})}
selection={this.props.selection}
runTests={this.runTests}
Expand Down Expand Up @@ -603,12 +605,26 @@ class SubmissionsActionBox extends React.Component {
</button>
);

let printGroupingFilesButton = (
<button
onClick={this.props.printGroupingFiles}
disabled={this.props.disabled}
title={I18n.t("print_the", {item: I18n.t("activerecord.models.submission.other")})}
>
<FontAwesomeIcon icon="fa-solid fa-print" />
<span className="button-text">
{I18n.t("print_the", {item: I18n.t("activerecord.models.submission.other")})}
</span>
</button>
);

return (
<div className="rt-action-box">
{completeButton}
{incompleteButton}
{collectButton}
{downloadGroupingFilesButton}
{printGroupingFilesButton}
{runTestsButton}
{releaseMarksButton}
{unreleaseMarksButton}
Expand Down
3 changes: 3 additions & 0 deletions app/controllers/courses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ def sync_roster
if params[:include_students] == 'true'
roles.append(LtiDeployment::LTI_ROLES[:learner])
end
if params[:include_instructors] == 'true'
roles.append(LtiDeployment::LTI_ROLES[:instructor])
end
@current_job = LtiRosterSyncJob.perform_later(deployment, @current_course,
roles,
can_create_users: allowed_to?(:lti_manage?, with: UserPolicy),
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/results_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ def download_view_tokens
def print
pdf_report = record.generate_print_pdf
send_data pdf_report.to_pdf,
filename: "#{record.submission.assignment.short_identifier}_#{record.grouping.group.group_name}.pdf",
filename: record.print_pdf_filename,
type: 'application/pdf'
end

Expand Down
3 changes: 2 additions & 1 deletion app/controllers/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,8 @@ def zip_groupings_files
groupings = groupings.joins(:ta_memberships).where('memberships.role_id': current_role.id)
end

@current_job = DownloadSubmissionsJob.perform_later(groupings.ids, zip_path.to_s, assignment.id, course.id)
@current_job = DownloadSubmissionsJob.perform_later(groupings.ids, zip_path.to_s, assignment.id, course.id,
print: params[:print] == 'true')
session[:job_id] = @current_job.job_id

render 'shared/_poll_job'
Expand Down
29 changes: 25 additions & 4 deletions app/helpers/lti_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ def roster_sync(lti_deployment, course, role_types, can_create_users: false, can
auth_data = lti_deployment.lti_client.get_oauth_token([LtiDeployment::LTI_SCOPES[:names_role]])
names_service = lti_deployment.lti_services.find_by!(service_type: 'namesrole')
membership_uri = URI(names_service.url)
req = Net::HTTP::Get.new(membership_uri)
res = lti_deployment.send_lti_request!(req, membership_uri, auth_data, [LtiDeployment::LTI_SCOPES[:names_role]])
member_info = JSON.parse(res.body)
user_data = member_info['members'].filter_map do |user|
member_info, follow = get_member_data(lti_deployment, membership_uri, auth_data)
while follow != false
additional_data, follow = get_member_data(lti_deployment, follow, auth_data)
member_info.concat(additional_data)
end
user_data = member_info.filter_map do |user|
unless user['status'] == 'Inactive' || user['roles'].include?(LtiDeployment::LTI_ROLES['test_user']) ||
role_types.none? { |role| user['roles'].include?(role) }
{ user_name: user['lis_person_sourcedid'].nil? ? user['name'].delete(' ') : user['lis_person_sourcedid'],
Expand Down Expand Up @@ -43,6 +45,8 @@ def roster_sync(lti_deployment, course, role_types, can_create_users: false, can
if course_role.nil? && can_create_roles
if lms_user[:roles].include?(LtiDeployment::LTI_ROLES[:ta])
course_role = Ta.create!(user: markus_user, course: lti_deployment.course)
elsif lms_user[:roles].include?(LtiDeployment::LTI_ROLES[:instructor])
course_role = Instructor.create!(user: markus_user, course: lti_deployment.course)
elsif lms_user[:roles].include?(LtiDeployment::LTI_ROLES[:learner])
course_role = Student.create!(user: markus_user, course: lti_deployment.course)
end
Expand Down Expand Up @@ -149,4 +153,21 @@ def get_current_results(lti_line_item, auth_data, scopes)
curr_results = lti_line_item.lti_deployment.send_lti_request!(result_req, results_uri, auth_data, scopes)
JSON.parse(curr_results.body)
end

def get_member_data(lti_deployment, url, auth_data)
req = Net::HTTP::Get.new(url)
res = lti_deployment.send_lti_request!(req, url, auth_data, [LtiDeployment::LTI_SCOPES[:names_role]])
member_info = JSON.parse(res.body)
links = res['link']
unless links
return [member_info['members'], false]
end
split_links = links.split(',')
split_next = split_links.find { |link| link.include?('next') }
next_link = split_next&.split(';')&.[](0)&.tr('<>', '')&.strip
next_uri = URI(next_link) if next_link
follow_link = false
follow_link = next_uri if next_uri
[member_info['members'], follow_link]
end
end
29 changes: 19 additions & 10 deletions app/jobs/download_submissions_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,32 @@ def self.completed_message(status)
self.status.update(assignment_id: job.arguments[2], course_id: job.arguments[3])
end

def perform(grouping_ids, zip_path, _assignment_id, _course_id)
def perform(grouping_ids, zip_path, assignment_id, _course_id, print: false)
## delete the old file if it exists
FileUtils.rm_f(zip_path)

progress.total = grouping_ids.length
Zip::File.open(zip_path, create: true) do |zip_file|
Grouping.includes(:group, :current_submission_used).where(id: grouping_ids).each do |grouping|
revision_id = grouping.current_submission_used&.revision_identifier
group_name = grouping.group.group_name
grouping.access_repo do |repo|
revision = repo.get_revision(revision_id)
repo.send_tree_to_zip(grouping.assignment.repository_folder, zip_file, revision, zip_subdir: group_name)
rescue Repository::RevisionDoesNotExist
next
ensure
if print
assignment = Assignment.find(assignment_id)
assignment.current_results.where('groupings.id': grouping_ids).each do |result|
pdf_report = result.generate_print_pdf
zip_file.get_output_stream(result.print_pdf_filename) { |f| f.write pdf_report.to_pdf }
progress.increment
end
else
Grouping.includes(:group, :current_submission_used).where(id: grouping_ids).each do |grouping|
revision_id = grouping.current_submission_used&.revision_identifier
group_name = grouping.group.group_name
grouping.access_repo do |repo|
revision = repo.get_revision(revision_id)
repo.send_tree_to_zip(grouping.assignment.repository_folder, zip_file, revision, zip_subdir: group_name)
rescue Repository::RevisionDoesNotExist
next
ensure
progress.increment
end
end
end
end
end
Expand Down
41 changes: 41 additions & 0 deletions app/models/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,29 @@ def generate_print_pdf
combined_page << annotation_page
end

input_files = submission.submission_files.where("filename LIKE '%.ipynb'").order(:path, :filename)
grouping.access_repo do |repo|
input_files.each do |sf|
contents = sf.retrieve_file(repo: repo)
tmp_path = File.join(workdir, 'tmp_file.pdf')
FileUtils.rm_rf(tmp_path)
args = [
Rails.application.config.python,
'-m', 'nbconvert',
'--to', 'webpdf',
'--stdin',
'--output', File.join(workdir, File.basename(tmp_path.to_s, '.pdf')) # Can't include the .pdf extension
]
_stdout, stderr, status = Open3.capture3(*args, stdin_data: contents)
if status.success?
input_pdf = CombinePDF.load(tmp_path)
combined_pdf << input_pdf
else
raise stderr
end
end
end

# Finally, insert cover page at the front
combined_pdf >> CombinePDF.load("#{workdir}/front.pdf")

Expand All @@ -337,6 +360,24 @@ def generate_print_pdf
combined_pdf
end

# Generate a filename to be used for the printed PDF.
# For individual submissions, we use the form "{id_number} - {FAMILY NAME}, {Given Name} ({username}).pdf".
# This is the form requested by the University of Toronto Arts & Science Exams Office (for final exams).
# For group submissions, we use the form "{group name}.pdf"
def print_pdf_filename
if submission.grouping.accepted_students.size == 1
student = submission.grouping.accepted_students.first.user
"#{student.id_number} - #{student.last_name.upcase}, #{student.first_name} (#{student.user_name}).pdf"
else
members = submission.grouping.accepted_students.includes(:user).map { |s| s.user.user_name }.sort
if members.empty?
"#{submission.grouping.group.group_name}.pdf"
else
"#{submission.grouping.group.group_name} (#{members.join(', ')}).pdf"
end
end
end

private

# Do not allow the marking state to be changed to incomplete if the result is released
Expand Down
1 change: 1 addition & 0 deletions config/locales/common/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ en:
start_roster_sync: Syncing Roster
sync_grades: Sync Grades
sync_grades_lms: Sync grades with LMS
sync_instructors: Sync Instructor roster
sync_students: Sync Student roster
sync_tas: Sync TA roster
unlink_confirm: This will unlink the course and any assessments from this LMS. Continue?
Expand Down
1 change: 1 addition & 0 deletions config/locales/defaults/download_upload/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ en:
download_json: Download in JSON Format
download_the: Download %{item}
download_yml: Download in YML Format
print_the: Print %{item}
rename: Rename
submit_the: Submit %{item}
upload: Upload
Expand Down
1 change: 1 addition & 0 deletions doc/markus-contributors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ Ignas Panero Armoska
Irene Fung
Isabelle Chan
Ishan Thukral
Jackson Lee
Jakub Subczynski
Jason Mai
Jay Parekh
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ services:
- MARKUS__REPOSITORY__SSH_URL=ssh://markus@localhost:2222/~/csc108
- CAPYBARA_SERVER_PORT=${CAPYBARA_SERVER_PORT:-3434}
- CAPYBARA_SERVER_HOST=${CAPYBARA_SERVER_HOST:-0.0.0.0}
- PLAYWRIGHT_BROWSERS_PATH=/app/tmp/pw-browsers
extra_hosts:
- localhost:host-gateway
ports:
Expand Down
Loading

0 comments on commit e61759c

Please sign in to comment.