canvas-lms/app/controllers/assignments_controller.rb

509 lines
23 KiB
Ruby

#
# Copyright (C) 2012 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# @API Assignments
class AssignmentsController < ApplicationController
include Api::V1::Section
include Api::V1::Assignment
include Api::V1::AssignmentOverride
include Api::V1::AssignmentGroup
include Api::V1::Outcome
include Api::V1::ExternalTools
include KalturaHelper
before_filter :require_context
add_crumb(proc { t '#crumbs.assignments', "Assignments" }, :except => [:destroy, :syllabus, :index]) { |c| c.send :course_assignments_path, c.instance_variable_get("@context") }
before_filter { |c| c.active_tab = "assignments" }
before_filter :normalize_title_param, :only => [:new, :edit]
def index
return redirect_to(dashboard_url) if @context == @current_user
if authorized_action(@context, @current_user, :read)
return unless tab_enabled?(@context.class::TAB_ASSIGNMENTS)
log_asset_access("assignments:#{@context.asset_string}", 'assignments', 'other')
add_crumb(t('#crumbs.assignments', "Assignments"), named_context_url(@context, :context_assignments_url))
# It'd be nice to do this as an after_create, but it's not that simple
# because of course import/copy.
@context.require_assignment_group
permissions = @context.rights_status(@current_user, :manage_assignments, :manage_grades)
permissions[:manage] = permissions[:manage_assignments]
js_env({
:URLS => {
:new_assignment_url => new_polymorphic_url([@context, :assignment]),
:course_url => api_v1_course_url(@context),
:sort_url => reorder_course_assignment_groups_url,
:assignment_sort_base_url => course_assignment_groups_url,
:context_modules_url => api_v1_course_context_modules_path(@context),
:course_student_submissions_url => api_v1_course_student_submissions_url(@context)
},
:PERMISSIONS => permissions,
:DIFFERENTIATED_ASSIGNMENTS_ENABLED => @context.feature_enabled?(:differentiated_assignments),
:assignment_menu_tools => external_tools_display_hashes(:assignment_menu),
:discussion_topic_menu_tools => external_tools_display_hashes(:discussion_topic_menu),
:quiz_menu_tools => external_tools_display_hashes(:quiz_menu),
:current_user_has_been_observer_in_this_course => @context.user_has_been_observer?(@current_user),
:observed_student_ids => ObserverEnrollment.observed_student_ids(@context, @current_user)
})
respond_to do |format|
format.html do
@padless = true
render :action => :new_index
end
end
end
end
def show
@assignment ||= @context.assignments.find(params[:id])
if @assignment.deleted?
respond_to do |format|
flash[:notice] = t 'notices.assignment_delete', "This assignment has been deleted"
format.html { redirect_to named_context_url(@context, :context_assignments_url) }
end
return
end
if authorized_action(@assignment, @current_user, :read)
if (da_on = @context.feature_enabled?(:differentiated_assignments)) &&
@current_user && @assignment &&
!@assignment.visible_to_user?(@current_user, differentiated_assignments: da_on)
respond_to do |format|
flash[:error] = t 'notices.assignment_not_available', "The assignment you requested is not available to your course section."
format.html { redirect_to named_context_url(@context, :context_assignments_url) }
end
return
end
@assignment = AssignmentOverrideApplicator.assignment_overridden_for(@assignment, @current_user)
@assignment.ensure_assignment_group
if @assignment.submission_types.include?("online_upload") || @assignment.submission_types.include?("online_url")
@external_tools = ContextExternalTool.all_tools_for(@context, :user => @current_user, :type => :homework_submission)
else
@external_tools = []
end
js_env({
:ROOT_OUTCOME_GROUP => outcome_group_json(@context.root_outcome_group, @current_user, session),
:COURSE_ID => @context.id,
:ASSIGNMENT_ID => @assignment.id,
:EXTERNAL_TOOLS => external_tools_json(@external_tools, @context, @current_user, session)
})
@locked = @assignment.locked_for?(@current_user, :check_policies => true, :deep_check_if_needed => true)
@locked.delete(:lock_at) if @locked.is_a?(Hash) && @locked.has_key?(:unlock_at) # removed to allow proper translation on show page
@unlocked = !@locked || @assignment.grants_right?(@current_user, session, :update)
@assignment.context_module_action(@current_user, :read) if @unlocked && !@assignment.new_record?
if @assignment.grants_right?(@current_user, session, :grade)
visible_student_ids = @context.enrollments_visible_to(@current_user).pluck(:user_id)
@current_student_submissions = @assignment.submissions.where("submissions.submission_type IS NOT NULL").where(:user_id => visible_student_ids).all
end
if @assignment.grants_right?(@current_user, session, :read_own_submission) && @context.grants_right?(@current_user, session, :read_grades)
@current_user_submission = @assignment.submissions.where(user_id: @current_user).first if @current_user
@current_user_submission = nil if @current_user_submission && !@current_user_submission.grade && !@current_user_submission.submission_type
@current_user_rubric_assessment = @assignment.rubric_association.rubric_assessments.where(user_id: @current_user).first if @current_user && @assignment.rubric_association
@current_user_submission.send_later(:context_module_action) if @current_user_submission
end
begin
google_docs = google_docs_connection
@google_docs_token = google_docs.retrieve_access_token
rescue GoogleDocs::NoTokenError
#do nothing
end
add_crumb(@assignment.title, polymorphic_url([@context, @assignment]))
log_asset_access(@assignment, "assignments", @assignment.assignment_group)
@assignment_menu_tools = external_tools_display_hashes(:assignment_menu)
respond_to do |format|
if @assignment.submission_types == 'online_quiz' && @assignment.quiz
format.html { redirect_to named_context_url(@context, :context_quiz_url, @assignment.quiz.id) }
elsif @assignment.submission_types == 'discussion_topic' && @assignment.discussion_topic && @assignment.discussion_topic.grants_right?(@current_user, session, :read)
format.html { redirect_to named_context_url(@context, :context_discussion_topic_url, @assignment.discussion_topic.id) }
elsif @assignment.submission_types == 'attendance'
format.html { redirect_to named_context_url(@context, :context_attendance_url, :anchor => "assignment/#{@assignment.id}") }
elsif @assignment.submission_types == 'external_tool' && @assignment.external_tool_tag && @unlocked
tag_type = params[:module_item_id].present? ? :modules : :assignments
format.html { content_tag_redirect(@context, @assignment.external_tool_tag, :context_url, tag_type) }
else
format.html { render :action => 'show' }
end
format.json { render :json => @assignment.as_json(:permissions => {:user => @current_user, :session => session}) }
end
end
end
def list_google_docs
assignment ||= @context.assignments.find(params[:id])
# prevent masquerading users from accessing google docs
if assignment.allow_google_docs_submission? && @real_current_user.blank?
docs = {}
begin
google_docs = google_docs_connection
docs = google_docs.list_with_extension_filter(assignment.allowed_extensions)
rescue GoogleDocs::NoTokenError
#do nothing
rescue => e
ErrorReport.log_exception(:oauth, e)
raise e
end
respond_to do |format|
format.json { render :json => docs.to_hash }
end
else
error_object = {:errors =>
{:base => t('errors.google_docs_masquerade_rejected', "Unable to connect to Google Docs as a masqueraded user.")}
}
respond_to do |format|
format.json { render :json => error_object, :status => :bad_request }
end
end
end
def rubric
@assignment = @context.assignments.active.find(params[:assignment_id])
@root_outcome_group = outcome_group_json(@context.root_outcome_group, @current_user, session).to_json
if authorized_action(@assignment, @current_user, :read)
render :partial => 'shared/assignment_rubric_dialog'
end
end
def assign_peer_reviews
@assignment = @context.assignments.active.find(params[:assignment_id])
if authorized_action(@assignment, @current_user, :grade)
cnt = params[:peer_review_count].to_i
@assignment.peer_review_count = cnt if cnt > 0
@assignment.assign_peer_reviews
respond_to do |format|
format.html { redirect_to named_context_url(@context, :context_assignment_peer_reviews_url, @assignment.id) }
end
end
end
def assign_peer_review
@assignment = @context.assignments.active.find(params[:assignment_id])
@student = @context.students_visible_to(@current_user).find params[:reviewer_id]
@reviewee = @context.students_visible_to(@current_user).find params[:reviewee_id]
if authorized_action(@assignment, @current_user, :grade)
@request = @assignment.assign_peer_review(@student, @reviewee)
respond_to do |format|
format.html { redirect_to named_context_url(@context, :context_assignment_peer_reviews_url, @assignment.id) }
format.json { render :json => @request.as_json(:methods => :asset_user_name) }
end
end
end
def remind_peer_review
@assignment = @context.assignments.active.find(params[:assignment_id])
if authorized_action(@assignment, @current_user, :grade)
@request = AssessmentRequest.where(id: params[:id]).first if params[:id].present?
respond_to do |format|
if @request.asset.assignment == @assignment && @request.send_reminder!
format.html { redirect_to named_context_url(@context, :context_assignment_peer_reviews_url) }
format.json { render :json => @request }
else
format.html { redirect_to named_context_url(@context, :context_assignment_peer_reviews_url) }
format.json { render :json => {:errors => {:base => t('errors.reminder_failed', "Reminder failed")}}, :status => :bad_request }
end
end
end
end
def delete_peer_review
@assignment = @context.assignments.active.find(params[:assignment_id])
if authorized_action(@assignment, @current_user, :grade)
@request = AssessmentRequest.where(id: params[:id]).first if params[:id].present?
respond_to do |format|
if @request.asset.assignment == @assignment && @request.destroy
format.html { redirect_to named_context_url(@context, :context_assignment_peer_reviews_url) }
format.json { render :json => @request }
else
format.html { redirect_to named_context_url(@context, :context_assignment_peer_reviews_url) }
format.json { render :json => {:errors => {:base => t('errors.delete_reminder_failed', "Delete failed")}}, :status => :bad_request }
end
end
end
end
def peer_reviews
@assignment = @context.assignments.active.find(params[:assignment_id])
if authorized_action(@assignment, @current_user, :grade)
if !@assignment.has_peer_reviews?
redirect_to named_context_url(@context, :context_assignment_url, @assignment.id)
return
end
student_scope = if @assignment.differentiated_assignments_applies?
@context.students_visible_to(@current_user).able_to_see_assignment_in_course_with_da(@assignment.id, @context.id)
else
@context.students_visible_to(@current_user)
end
@students = student_scope.uniq.order_by_sortable_name
@submissions = @assignment.submissions.include_assessment_requests
end
end
def syllabus
add_crumb t '#crumbs.syllabus', "Syllabus"
active_tab = "Syllabus"
if authorized_action(@context, @current_user, [:read, :read_syllabus])
return unless tab_enabled?(@context.class::TAB_SYLLABUS)
@groups = @context.assignment_groups.active.order(:position, AssignmentGroup.best_unicode_collation_key('name')).all
@assignment_groups = @groups
@events = @context.events_for(@current_user)
@undated_events = @events.select {|e| e.start_at == nil}
@dates = (@events.select {|e| e.start_at != nil}).map {|e| e.start_at.to_date}.uniq.sort.sort
if @context.grants_right?(@current_user, session, :read)
@syllabus_body = api_user_content(@context.syllabus_body, @context)
else
# the requesting user may not have :read if the course syllabus is public, in which
# case, we pass nil as the user so verifiers are added to links in the syllabus body
# (ability for the user to read the syllabus was checked above as :read_syllabus)
@syllabus_body = api_user_content(@context.syllabus_body, @context, nil, {}, true)
end
hash = { :CONTEXT_ACTION_SOURCE => :syllabus }
append_sis_data(hash)
js_env(hash)
log_asset_access("syllabus:#{@context.asset_string}", "syllabus", 'other')
respond_to do |format|
format.html
end
end
end
def toggle_mute
return nil unless authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
@assignment = @context.assignments.active.find(params[:assignment_id])
method = if params[:status] == "true" then :mute! else :unmute! end
respond_to do |format|
if @assignment && @assignment.send(method)
format.json { render :json => @assignment }
else
format.json { render :json => @assignment, :status => :bad_request }
end
end
end
def create
params[:assignment][:time_zone_edited] = Time.zone.name if params[:assignment]
group = get_assignment_group(params[:assignment])
@assignment ||= @context.assignments.build(params[:assignment])
@assignment.workflow_state ||= "unpublished"
@assignment.post_to_sis ||= @context.feature_enabled?(:post_to_sis) ? true : false
@assignment.updating_user = @current_user
@assignment.content_being_saved_by(@current_user)
@assignment.assignment_group = group if group
# if no due_at was given, set it to 11:59 pm in the creator's time zone
@assignment.infer_times
if authorized_action(@assignment, @current_user, :create)
respond_to do |format|
if @assignment.save
flash[:notice] = t 'notices.created', "Assignment was successfully created."
format.html { redirect_to named_context_url(@context, :context_assignment_url, @assignment.id) }
format.json { render :json => @assignment.as_json(:permissions => {:user => @current_user, :session => session}), :status => :created}
else
format.html { render :action => "new" }
format.json { render :json => @assignment.errors, :status => :bad_request }
end
end
end
end
def new
@assignment ||= @context.assignments.scoped.new
@assignment.workflow_state = 'unpublished'
add_crumb t :create_new_crumb, "Create new"
if params[:submission_types] == 'online_quiz'
redirect_to new_course_quiz_url(@context, index_edit_params)
elsif params[:submission_types] == 'discussion_topic'
redirect_to new_polymorphic_url([@context, :discussion_topic], index_edit_params)
else
edit
end
end
def edit
@assignment ||= @context.assignments.active.find(params[:id])
if authorized_action(@assignment, @current_user, @assignment.new_record? ? :create : :update)
@assignment.title = params[:title] if params[:title]
@assignment.due_at = params[:due_at] if params[:due_at]
@assignment.points_possible = params[:points_possible] if params[:points_possible]
@assignment.submission_types = params[:submission_types] if params[:submission_types]
@assignment.assignment_group_id = params[:assignment_group_id] if params[:assignment_group_id]
@assignment.ensure_assignment_group(false)
@assignment.post_to_sis = params[:post_to_sis] if params[:post_to_sis]
if @assignment.submission_types == 'online_quiz' && @assignment.quiz
return redirect_to edit_course_quiz_url(@context, @assignment.quiz, index_edit_params)
elsif @assignment.submission_types == 'discussion_topic' && @assignment.discussion_topic
return redirect_to edit_polymorphic_url([@context, @assignment.discussion_topic], index_edit_params)
end
assignment_groups = @context.assignment_groups.active
group_categories = @context.group_categories.
select { |c| !c.student_organized? }.
map { |c| { :id => c.id, :name => c.name } }
json_for_assignment_groups = assignment_groups.map do |group|
assignment_group_json(group, @current_user, session, [], {stringify_json_ids: true})
end
hash = {
:ASSIGNMENT_GROUPS => json_for_assignment_groups,
:GROUP_CATEGORIES => group_categories,
:KALTURA_ENABLED => !!feature_enabled?(:kaltura),
:POST_TO_SIS => @context.feature_enabled?(:post_grades),
:SECTION_LIST => (@context.course_sections.active.map { |section|
{
:id => section.id,
:name => section.name,
:start_at => section.start_at,
:end_at => section.end_at,
:override_course_dates => section.restrict_enrollments_to_section_dates
}
}),
:ASSIGNMENT_OVERRIDES =>
(assignment_overrides_json(
@assignment.overrides_for(@current_user)
)),
:ASSIGNMENT_INDEX_URL => polymorphic_url([@context, :assignments]),
:DIFFERENTIATED_ASSIGNMENTS_ENABLED => @context.feature_enabled?(:differentiated_assignments),
:COURSE_DATE_RANGE => {
:start_at => @context.start_at,
:end_at => @context.conclude_at,
:override_term_dates => @context.restrict_enrollments_to_course_dates
},
:TERM_DATE_RANGE => {
:start_at => @context.enrollment_term.start_at,
:end_at => @context.enrollment_term.end_at
}
}
hash[:ASSIGNMENT] = assignment_json(@assignment, @current_user, session, override_dates: false)
hash[:ASSIGNMENT][:has_submitted_submissions] = @assignment.has_submitted_submissions?
hash[:URL_ROOT] = polymorphic_url([:api_v1, @context, :assignments])
hash[:CANCEL_TO] = @assignment.new_record? ? polymorphic_url([@context, :assignments]) : polymorphic_url([@context, @assignment])
hash[:CONTEXT_ID] = @context.id
hash[:CONTEXT_ACTION_SOURCE] = :assignments
append_sis_data(hash)
js_env(hash)
@padless = true
render :action => "edit"
end
end
def update
@assignment = @context.assignments.find(params[:id])
if authorized_action(@assignment, @current_user, :update)
params[:assignment][:time_zone_edited] = Time.zone.name if params[:assignment]
params[:assignment] ||= {}
@assignment.post_to_sis = params[:assignment][:post_to_sis]
@assignment.updating_user = @current_user
if params[:assignment][:default_grade]
params[:assignment][:overwrite_existing_grades] = (params[:assignment][:overwrite_existing_grades] == "1")
@assignment.set_default_grade(params[:assignment])
render :json => @assignment.submissions.map{ |s| s.as_json(:include => :quiz_submission) }
return
end
params[:assignment].delete :default_grade
params[:assignment].delete :overwrite_existing_grades
if params[:publish]
@assignment.workflow_state = 'published'
end
if params[:assignment_type] == "quiz"
params[:assignment][:submission_types] = "online_quiz"
elsif params[:assignment_type] == "attendance"
params[:assignment][:submission_types] = "attendance"
elsif params[:assignment_type] == "discussion_topic"
params[:assignment][:submission_types] = "discussion_topic"
elsif params[:assignment_type] == "external_tool"
params[:assignment][:submission_types] = "external_tool"
end
respond_to do |format|
@assignment.content_being_saved_by(@current_user)
group = get_assignment_group(params[:assignment])
@assignment.assignment_group = group if group
if @assignment.update_attributes(params[:assignment])
log_asset_access(@assignment, "assignments", @assignment_group, 'participate')
@assignment.context_module_action(@current_user, :contributed)
@assignment.reload
flash[:notice] = t 'notices.updated', "Assignment was successfully updated."
format.html { redirect_to named_context_url(@context, :context_assignment_url, @assignment) }
format.json { render :json => @assignment.as_json(:permissions => {:user => @current_user, :session => session}, :include => [:quiz, :discussion_topic]), :status => :ok }
else
format.html { render :action => "edit" }
format.json { render :json => @assignment.errors, :status => :bad_request }
end
end
end
end
# @API Delete an assignment
#
# Delete the given assignment.
#
# @example_request
# curl https://<canvas>/api/v1/courses/<course_id>/assignments/<assignment_id> \
# -X DELETE \
# -H 'Authorization: Bearer <token>'
# @returns Assignment
def destroy
@assignment = @context.assignments.active.find(params[:id])
if authorized_action(@assignment, @current_user, :delete)
@assignment.destroy
respond_to do |format|
format.html { redirect_to(named_context_url(@context, :context_assignments_url)) }
format.json { render :json => assignment_json(@assignment, @current_user, session) }
end
end
end
protected
def get_assignment_group(assignment_params)
return unless assignment_params
if (group_id = assignment_params[:assignment_group_id]).present?
@context.assignment_groups.find(group_id)
end
end
def normalize_title_param
params[:title] ||= params[:name]
end
def index_edit_params
params.slice(*[:title, :due_at, :points_possible, :assignment_group_id])
end
end