Skip to content

Commit

Permalink
Merge branch 'master' into allen/assessment-edit
Browse files Browse the repository at this point in the history
  • Loading branch information
kxmbrian authored Mar 23, 2017
2 parents 5069782 + ac4abbf commit 3aba80e
Show file tree
Hide file tree
Showing 18 changed files with 554 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,35 @@ def reload_answer

# Publish all the graded submissions.
def publish_all
# TODO: Modify assessment ability to implement this in a cleaner way
authorize!(:publish_all, Course::Assessment::Submission.new(assessment: @assessment))
graded_submissions = @assessment.submissions.with_graded_state
if !graded_submissions.empty?
job =
Course::Assessment::Submission::PublishingJob.perform_later(@assessment, current_user).job
job = Course::Assessment::Submission::PublishingJob.
perform_later(@assessment, current_user).job
redirect_to(job_path(job))
else
redirect_to course_assessment_submissions_path(current_course, @assessment),
notice: t('.notice')
end
end

# Download either all of or a subset of submissions for an assessment.
def download_all
authorize!(:manage, @assessment)
if !@assessment.downloadable?
redirect_to course_assessment_submissions_path(current_course, @assessment),
notice: t('.not_downloadable')
elsif @assessment.submissions.confirmed.empty?
redirect_to course_assessment_submissions_path(current_course, @assessment),
notice: t('.no_submissions')
else
job = Course::Assessment::Submission::ZipDownloadJob.
perform_later(current_course_user, @assessment, params[:students]).job
redirect_to job_path(job)
end
end

private

def create_params
Expand Down
17 changes: 17 additions & 0 deletions app/jobs/course/assessment/submission/zip_download_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true
class Course::Assessment::Submission::ZipDownloadJob < ApplicationJob
include TrackableJob

protected

# Performs the download service.
#
# @param [CourseUser] course_user The course user downloading the submissions.
# @param [Course::Assessment] assessment The assessments to download submissions for.
# @param [String|nil] students The subset of students whose submissions to download.
def perform_tracked(course_user, assessment, students = nil)
zip_file = Course::Assessment::Submission::ZipDownloadService.
download_and_zip(course_user, assessment, students)
redirect_to SendFile.send_file(zip_file, Pathname.normalize_filename(assessment.title) + '.zip')
end
end
4 changes: 4 additions & 0 deletions app/models/course/assessment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ def password_protected?
password.present?
end

def downloadable?
questions.any?(&:downloadable?)
end

def initialize_duplicate(duplicator, other)
copy_attributes(other, duplicator.time_shift)
self.folder = duplicator.duplicate(other.folder)
Expand Down
9 changes: 9 additions & 0 deletions app/models/course/assessment/answer/programming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,13 @@ def attempting_times_left
def grade_inline?
false
end

def download(dir)
files.each do |src_file|
dst_path = File.join(dir, src_file.filename)
File.open(dst_path, 'w') do |dst_file|
dst_file.write(src_file.content)
end
end
end
end
24 changes: 24 additions & 0 deletions app/models/course/assessment/answer/text_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@ def reset_answer
acting_as
end

def download(dir)
download_answer(dir) unless question.actable.file_upload_question?
download_attachment(dir) if attachment
end

def download_answer(dir)
answer_path = File.join(dir, 'answer.txt')
File.open(answer_path, 'w') do |file|
file.write(answer_text)
end
end

def download_attachment(dir)
name_generator = FileName.new(File.join(dir, attachment.name), position: :middle,
format: '(%d)',
delimiter: ' ')
attachment_path = name_generator.create
File.open(attachment_path, 'wb') do |file|
attachment.open(binmode: true) do |attachment_stream|
FileUtils.copy_stream(attachment_stream, file)
end
end
end

private

def set_default
Expand Down
11 changes: 11 additions & 0 deletions app/models/course/assessment/question.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ def display_title
question_number: question_number, title: title)
end

# Whether the answer has downloadable content.
#
# @return [Boolean]
def downloadable?
if actable.self_respond_to?(:downloadable?)
actable.downloadable?
else
false
end
end

# Copy attributes for question from the object being duplicated.
#
# @param other [Object] The source object to copy attributes from.
Expand Down
4 changes: 4 additions & 0 deletions app/models/course/assessment/question/programming.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def test_cases_by_type
test_cases.group_by(&:test_case_type)
end

def downloadable?
true
end

def initialize_duplicate(duplicator, other)
copy_attributes(other)

Expand Down
4 changes: 4 additions & 0 deletions app/models/course/assessment/question/text_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def attempt(submission, last_attempt = nil)
answer.acting_as
end

def downloadable?
true
end

def initialize_duplicate(duplicator, other)
copy_attributes(other)
self.solutions = duplicator.duplicate(other.solutions)
Expand Down
86 changes: 86 additions & 0 deletions app/services/course/assessment/submission/zip_download_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# frozen_string_literal: true
class Course::Assessment::Submission::ZipDownloadService
class << self
# Downloads the submissions and zip them.
#
# @param [CourseUser] course_user The course user downloading the submissions.
# @param [Course::Assessment] assessment The assessments to download submissions from.
# @param [String|nil] students The subset of students whose submissions to download.
# Accepted values: 'my_students', 'students', 'others'
# @return [String] The path to the zip file.
def download_and_zip(course_user, assessment, students)
service = new(course_user, assessment, students)
ActsAsTenant.without_tenant do
service.send(:download_to_base_dir)
end
service.send(:zip_base_dir)
end
end

STUDENTS = { my: 'my', phantom: 'phantom' }.freeze

private

def initialize(course_user, assessment, students)
@course_user = course_user
@assessment = assessment
@questions = assessment.questions.map { |q| [q.id, q] }.to_h
@students = students
@base_dir = Dir.mktmpdir('coursemology-download-')
end

# Downloads each submission to its own folder in the base directory.
def download_to_base_dir
submissions = @assessment.submissions.by_users(student_ids).
includes(:answers, experience_points_record: :course_user)
submissions.find_each do |submission|
submission_dir = create_folder(@base_dir, submission.course_user.name)
download_answers(submission, submission_dir)
end
end

# Downloads each answer to its own folder in the submission directory.
def download_answers(submission, submission_dir)
answers = submission.answers.includes(:question).latest_answers.
select { |answer| @questions[answer.question_id]&.downloadable? }
answers.each do |answer|
answer_dir = create_folder(submission_dir, @questions[answer.question_id].display_title)
answer.specific.download(answer_dir)
end
end

def create_folder(parent, folder_name)
normalized_name = Pathname.normalize_filename(folder_name)
name_generator = FileName.new(File.join(parent, normalized_name),
format: '(%d)', delimiter: ' ')
name_generator.create.tap do |dir|
Dir.mkdir(dir)
end
end

# Zip the directory and write to the file.
#
# @return [String] The path to the zip file.
def zip_base_dir
output_file = @base_dir + '.zip'
Zip::File.open(output_file, Zip::File::CREATE) do |zip_file|
Dir["#{@base_dir}/**/**"].each do |file|
zip_file.add(file.sub(File.join(@base_dir + '/'), ''), file)
end
end

output_file
end

def student_ids
@student_ids ||=
case @students
when STUDENTS[:my]
@course_user.my_students
when STUDENTS[:phantom]
@course_user.course.course_users.students.phantom
else
@course_user.course.course_users.students.without_phantom_users
end.select(:user_id)
end
end
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
h3
= title
- if @assessment.downloadable? && course_users.map { |u| submissions[u] }.any? { |s| s && s.workflow_state != 'attempting' }
div.pull-right
= link_to t('.download'), download_all_course_assessment_submissions_path(current_course, @assessment, students: students),
class: ['btn', 'btn-primary'], target: '_blank'

table.table.submissions
thead
tr
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
class: ['btn', 'btn-danger'], method: :patch, data: { confirm: t('.confirm_publish') }

- submissions = @submissions.map { |s| [s.course_user, s] }.to_h
- types = Course::Assessment::Submission::ZipDownloadService::STUDENTS

- if !@my_students.empty?
h3 = t('.my_student_header')
= render partial: 'submissions', object: submissions, locals: { course_users: @my_students }
= render partial: 'submissions', object: submissions,
locals: { course_users: @my_students, title: t('.my_student_header'), students: types[:my] }

h3 = t('.student_header')
- students = @course_students.without_phantom_users
= render partial: 'submissions', object: submissions, locals: { course_users: students }
= render partial: 'submissions', object: submissions,
locals: { course_users: students, title: t('.student_header'), students: nil }

h3 = t('.other_header')
- other_users = @course_students - students
= render partial: 'submissions', object: submissions, locals: { course_users: other_users }
= render partial: 'submissions', object: submissions,
locals: { course_users: other_users, title: t('.other_header'), students: types[:phantom] }
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ span.pull-right.btn-group
title: t('.upload'), class: ['btn', 'btn-primary'] do
= fa_icon 'upload'.freeze
= link_to download_course_material_folder_path(current_course, @folder),
title: t('.download'), class: ['btn', 'btn-primary'] do
title: t('.download'), class: ['btn', 'btn-primary'], target: '_blank' do
= fa_icon 'download'.freeze

- if @folder.concrete? && can?(:edit, @folder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ en:
publish_all:
success: 'All graded submissions have been published.'
notice: 'There are no graded submissions.'
download_all:
not_downloadable: 'This assessment has no downloadable files.'
no_submissions: 'There are no confirmed submissions.'
buttons:
save: 'Save'
save_draft: 'Save Draft'
Expand Down Expand Up @@ -71,6 +74,7 @@ en:
status: 'Submission Status'
grade: 'Grade'
experience_points: 'Experience Points'
download: 'Download'
submission:
graded_not_published_warning: >
The grade and experience points are in a draft state and cannot be seen by
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
resources :submissions, only: [:index, :create, :edit, :update] do
post :auto_grade, on: :member
post :reload_answer, on: :member
get :download_all, on: :collection
patch :publish_all, on: :collection
resources :logs, only: [:index]
scope module: :answer do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,32 @@
end
end

describe '#publish_all' do
subject do
patch :publish_all, course_id: course, assessment_id: assessment
end

context 'when a student tries to publish submissions' do
let(:course) { create(:course) }
let(:user) { create(:course_student, course: course).user }

it { expect { subject }.to raise_exception(CanCan::AccessDenied) }
end
end

describe '#download_all' do
subject do
get :download_all, course_id: course, assessment_id: assessment
end

context 'when a student tries to download submissions' do
let(:course) { create(:course) }
let(:user) { create(:course_student, course: course).user }

it { expect { subject }.to raise_exception(CanCan::AccessDenied) }
end
end

context 'when the assessment is autograded' do
let(:assessment) { create(:assessment, :autograded, :with_mrq_question, course: course) }
let!(:answer) do
Expand Down
Loading

0 comments on commit 3aba80e

Please sign in to comment.