435 lines
19 KiB
Ruby
435 lines
19 KiB
Ruby
#
|
|
# Copyright (C) 2011 - 2014 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/>.
|
|
#
|
|
|
|
class ContextModulesController < ApplicationController
|
|
before_filter :require_context
|
|
add_crumb(proc { t('#crumbs.modules', "Modules") }) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_context_modules_url }
|
|
before_filter { |c| c.active_tab = "modules" }
|
|
|
|
module ModuleIndexHelper
|
|
def load_modules
|
|
@modules = @context.modules_visible_to(@current_user)
|
|
@collapsed_modules = ContextModuleProgression.for_user(@current_user).for_modules(@modules).select([:context_module_id, :collapsed]).select{|p| p.collapsed? }.map(&:context_module_id)
|
|
|
|
@menu_tools = {}
|
|
[:assignment_menu, :discussion_topic_menu, :file_menu, :module_menu, :quiz_menu, :wiki_page_menu].each do |type|
|
|
@menu_tools[type] = ContextExternalTool.all_tools_for(@context, :type => type,
|
|
:root_account => @domain_root_account, :current_user => @current_user)
|
|
end
|
|
end
|
|
end
|
|
include ModuleIndexHelper
|
|
|
|
def index
|
|
if authorized_action(@context, @current_user, :read)
|
|
log_asset_access("modules:#{@context.asset_string}", "modules", "other")
|
|
load_modules
|
|
if @context.grants_right?(@current_user, session, :participate_as_student)
|
|
return unless tab_enabled?(@context.class::TAB_MODULES)
|
|
ActiveRecord::Associations::Preloader.new(@modules, :content_tags).run
|
|
@modules.each{|m| m.evaluate_for(@current_user) }
|
|
session[:module_progressions_initialized] = true
|
|
end
|
|
js_env :course_id => @context.id
|
|
end
|
|
end
|
|
|
|
def item_redirect
|
|
if authorized_action(@context, @current_user, :read)
|
|
@tag = @context.context_module_tags.not_deleted.find(params[:id])
|
|
|
|
if !(@tag.unpublished? || @tag.context_module.unpublished?) || authorized_action(@tag.context_module, @current_user, :update)
|
|
reevaluate_modules_if_locked(@tag)
|
|
@progression = @tag.context_module.evaluate_for(@current_user) if @tag.context_module
|
|
@progression.uncollapse! if @progression && @progression.collapsed?
|
|
content_tag_redirect(@context, @tag, :context_context_modules_url, :modules)
|
|
end
|
|
end
|
|
end
|
|
|
|
def module_redirect
|
|
if authorized_action(@context, @current_user, :read)
|
|
@module = @context.context_modules.not_deleted.find(params[:context_module_id])
|
|
@tags = @module.content_tags_visible_to(@current_user)
|
|
if params[:last]
|
|
@tags.pop while @tags.last && @tags.last.content_type == 'ContextModuleSubHeader'
|
|
else
|
|
@tags.shift while @tags.first && @tags.first.content_type == 'ContextModuleSubHeader'
|
|
end
|
|
@tag = params[:last] ? @tags.last : @tags.first
|
|
if !@tag
|
|
flash[:notice] = t 'module_empty', %{There are no items in the module "%{module}"}, :module => @module.name
|
|
redirect_to named_context_url(@context, :context_context_modules_url, :anchor => "module_#{@module.id}")
|
|
return
|
|
end
|
|
|
|
reevaluate_modules_if_locked(@tag)
|
|
@progression = @tag.context_module.evaluate_for(@current_user) if @tag && @tag.context_module
|
|
@progression.uncollapse! if @progression && @progression.collapsed?
|
|
content_tag_redirect(@context, @tag, :context_context_modules_url)
|
|
end
|
|
end
|
|
|
|
def reevaluate_modules_if_locked(tag)
|
|
# if the object is locked for this user, reevaluate all the modules and clear the cache so it will be checked again when loaded
|
|
if tag.content && tag.content.respond_to?(:locked_for?)
|
|
locked = tag.content.locked_for?(@current_user, :context => @context)
|
|
if locked
|
|
@context.context_modules.active.each { |m| m.evaluate_for(@current_user) }
|
|
if tag.content.respond_to?(:clear_locked_cache)
|
|
tag.content.clear_locked_cache(@current_user)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def create
|
|
if authorized_action(@context.context_modules.scoped.new, @current_user, :create)
|
|
@module = @context.context_modules.build
|
|
@module.workflow_state = 'unpublished'
|
|
@module.attributes = params[:context_module]
|
|
respond_to do |format|
|
|
if @module.save
|
|
format.html { redirect_to named_context_url(@context, :context_context_modules_url) }
|
|
format.json { render :json => @module.as_json(:include => :content_tags, :methods => :workflow_state, :permissions => {:user => @current_user, :session => session}) }
|
|
else
|
|
format.html
|
|
format.json { render :json => @module.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def reorder
|
|
if authorized_action(@context.context_modules.scoped.new, @current_user, :update)
|
|
m = @context.context_modules.not_deleted.first
|
|
|
|
m.update_order(params[:order].split(","))
|
|
# Need to invalidate the ordering cache used by context_module.rb
|
|
@context.touch
|
|
|
|
# I'd like to get rid of this saving every module, but we have to
|
|
# update the list of prerequisites since a reorder can cause
|
|
# prerequisites to no longer be valid
|
|
@modules = @context.context_modules.not_deleted
|
|
@modules.each{|m| m.save_without_touching_context }
|
|
@context.touch
|
|
|
|
# # Background this, not essential that it happen right away
|
|
# ContextModule.send_later(:update_tag_order, @context)
|
|
respond_to do |format|
|
|
format.json { render :json => @modules.map{ |m| m.as_json(include: :content_tags, methods: :workflow_state) } }
|
|
end
|
|
end
|
|
end
|
|
|
|
def content_tag_assignment_data
|
|
if authorized_action(@context, @current_user, :read)
|
|
info = {}
|
|
@context.module_items_visible_to(@current_user).each do |tag|
|
|
info[tag.id] = Rails.cache.fetch([tag, @current_user, "content_tag_assignment_info"].cache_key) do
|
|
if tag.assignment
|
|
tag.assignment.context_module_tag_info(@current_user)
|
|
else
|
|
{:points_possible => nil, :due_date => (tag.content.due_at.utc.iso8601 rescue nil)}
|
|
end
|
|
end
|
|
end
|
|
render :json => info
|
|
end
|
|
end
|
|
|
|
def prerequisites_needing_finishing_for(mod, progression, before_tag=nil)
|
|
tags = mod.content_tags_visible_to(@current_user)
|
|
pres = []
|
|
tags.each do |tag|
|
|
if req = (mod.completion_requirements || []).detect{|r| r[:id] == tag.id }
|
|
progression.requirements_met ||= []
|
|
if !progression.requirements_met.any?{|r| r[:id] == req[:id] && r[:type] == req[:type] }
|
|
if !before_tag || tag.position <= before_tag.position
|
|
pre = {
|
|
:url => named_context_url(@context, :context_context_modules_item_redirect_url, tag.id),
|
|
:id => tag.id,
|
|
:context_module_id => mod.id,
|
|
:title => tag.title
|
|
}
|
|
pre[:requirement] = req
|
|
pre[:requirement_description] = ContextModule.requirement_description(req)
|
|
pre[:available] = !progression.locked? && (!mod.require_sequential_progress || tag.position <= progression.current_position)
|
|
pres << pre
|
|
end
|
|
end
|
|
end
|
|
end
|
|
pres
|
|
end
|
|
protected :prerequisites_needing_finishing_for
|
|
|
|
def content_tag_prerequisites_needing_finishing
|
|
code = params[:code].split("_")
|
|
id = code.pop
|
|
raise ActiveRecord::RecordNotFound if id !~ Api::ID_REGEX
|
|
type = code.join("_").classify
|
|
if type == 'ContentTag'
|
|
@tag = @context.context_module_tags.active.where(id: id).first
|
|
else
|
|
@tag = @context.context_module_tags.active.where(context_module_id: params[:context_module_id], content_id: id, content_type: type).first
|
|
end
|
|
@module = @context.context_modules.active.find(params[:context_module_id])
|
|
@progression = @module.evaluate_for(@current_user)
|
|
@progression.current_position ||= 0 if @progression
|
|
res = {};
|
|
if !@progression
|
|
elsif @progression.locked?
|
|
res[:locked] = true
|
|
res[:modules] = []
|
|
previous_modules = @context.context_modules.active.where('position<?', @module.position).order(:position).all
|
|
previous_modules.reverse!
|
|
valid_previous_modules = []
|
|
prereq_ids = @module.prerequisites.select{|p| p[:type] == 'context_module' }.map{|p| p[:id] }
|
|
previous_modules.each do |mod|
|
|
if prereq_ids.include?(mod.id)
|
|
valid_previous_modules << mod
|
|
prereq_ids += mod.prerequisites.select{|p| p[:type] == 'context_module' }.map{|p| p[:id] }
|
|
end
|
|
end
|
|
valid_previous_modules.reverse!
|
|
valid_previous_modules.each do |mod|
|
|
prog = mod.evaluate_for(@current_user)
|
|
res[:modules] << {
|
|
:id => mod.id,
|
|
:name => mod.name,
|
|
:prerequisites => prerequisites_needing_finishing_for(mod, prog),
|
|
:locked => prog.locked?
|
|
} unless prog.completed?
|
|
end
|
|
elsif @module.require_sequential_progress && @progression.current_position && @tag && @tag.position && @progression.current_position < @tag.position
|
|
res[:locked] = true
|
|
pres = prerequisites_needing_finishing_for(@module, @progression, @tag)
|
|
res[:modules] = [{
|
|
:id => @module.id,
|
|
:name => @module.name,
|
|
:prerequisites => pres,
|
|
:locked => false
|
|
}]
|
|
else
|
|
res[:locked] = false
|
|
end
|
|
render :json => res
|
|
end
|
|
|
|
def toggle_collapse
|
|
if authorized_action(@context, @current_user, :read)
|
|
@module = @context.modules_visible_to(@current_user).find(params[:context_module_id])
|
|
@progression = @module.evaluate_for(@current_user) #context_module_progressions.find_by_user_id(@current_user)
|
|
@progression ||= ContextModuleProgression.new
|
|
if params[:collapse] == '1'
|
|
@progression.collapsed = true
|
|
elsif params[:collapse]
|
|
@progression.uncollapse!
|
|
else
|
|
@progression.collapsed = !@progression.collapsed
|
|
end
|
|
@progression.save
|
|
respond_to do |format|
|
|
format.html { redirect_to named_context_url(@context, :context_context_modules_url) }
|
|
format.json { render :json => (@progression.collapsed ? @progression : @module.content_tags_visible_to(@current_user) )}
|
|
end
|
|
end
|
|
end
|
|
|
|
def show
|
|
@module = @context.modules_visible_to(@current_user).find(params[:id])
|
|
respond_to do |format|
|
|
format.html { redirect_to named_context_url(@context, :context_context_modules_url, :anchor => "module_#{params[:id]}") }
|
|
format.json { render :json => @module.content_tags_visible_to(@current_user) }
|
|
end
|
|
end
|
|
|
|
def reorder_items
|
|
@module = @context.context_modules.not_deleted.find(params[:context_module_id])
|
|
if authorized_action(@module, @current_user, :update)
|
|
order = params[:order].split(",").map{|id| id.to_i}
|
|
tags = @context.context_module_tags.not_deleted.where(id: order)
|
|
affected_module_ids = (tags.map(&:context_module_id) + [@module.id]).uniq.compact
|
|
affected_items = []
|
|
items = order.map{|id| tags.detect{|t| t.id == id.to_i } }.compact.uniq
|
|
items.each_with_index do |item, idx|
|
|
item.position = idx
|
|
item.context_module_id = @module.id
|
|
if item.changed?
|
|
item.skip_touch = true
|
|
item.save
|
|
affected_items << item
|
|
end
|
|
end
|
|
ContentTag.touch_context_modules(affected_module_ids)
|
|
ContentTag.update_could_be_locked(affected_items)
|
|
@context.touch
|
|
@module.reload
|
|
respond_to do |format|
|
|
format.json { render :json => @module.as_json(:include => :content_tags, :methods => :workflow_state, :permissions => {:user => @current_user, :session => session}) }
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
def item_details
|
|
if authorized_action(@context, @current_user, :read)
|
|
# namespaced models are separated by : in the url
|
|
code = params[:id].gsub(":", "/").split("_")
|
|
id = code.pop.to_i
|
|
type = code.join("_").classify
|
|
@modules = @context.modules_visible_to(@current_user)
|
|
@tags = @context.context_module_tags.active.sort_by{|t| t.position ||= 999}
|
|
result = {}
|
|
possible_tags = @tags.find_all {|t| t.content_type == type && t.content_id == id }
|
|
if possible_tags.size > 1
|
|
# if there's more than one tag for the item, but the caller didn't
|
|
# specify which one they want, we don't want to return any information.
|
|
# this way the module item prev/next links won't appear with misleading navigation info.
|
|
if params[:module_item_id]
|
|
result[:current_item] = possible_tags.detect { |t| t.id == params[:module_item_id].to_i }
|
|
end
|
|
else
|
|
result[:current_item] = possible_tags.first
|
|
if !result[:current_item]
|
|
obj = @context.find_asset(params[:id], [:attachment, :discussion_topic, :assignment, :quiz, :wiki_page, :content_tag])
|
|
if obj.is_a?(ContentTag)
|
|
result[:current_item] = @tags.detect{|t| t.id == obj.id }
|
|
elsif obj.is_a?(DiscussionTopic) && obj.assignment_id
|
|
result[:current_item] = @tags.detect{|t| t.content_type == 'Assignment' && t.content_id == obj.assignment_id }
|
|
elsif obj.is_a?(Quizzes::Quiz) && obj.assignment_id
|
|
result[:current_item] = @tags.detect{|t| t.content_type == 'Assignment' && t.content_id == obj.assignment_id }
|
|
end
|
|
end
|
|
end
|
|
result[:current_item].evaluate_for(@current_user) rescue nil
|
|
if result[:current_item] && result[:current_item].position
|
|
result[:previous_item] = @tags.reverse.detect{|t| t.id != result[:current_item].id && t.context_module_id == result[:current_item].context_module_id && t.position && t.position <= result[:current_item].position && t.content_type != "ContextModuleSubHeader" }
|
|
result[:next_item] = @tags.detect{|t| t.id != result[:current_item].id && t.context_module_id == result[:current_item].context_module_id && t.position && t.position >= result[:current_item].position && t.content_type != "ContextModuleSubHeader" }
|
|
current_module = @modules.detect{|m| m.id == result[:current_item].context_module_id}
|
|
if current_module
|
|
result[:previous_module] = @modules.reverse.detect{|m| (m.position || 0) < (current_module.position || 0) }
|
|
result[:next_module] = @modules.detect{|m| (m.position || 0) > (current_module.position || 0) }
|
|
end
|
|
end
|
|
render :json => result
|
|
end
|
|
end
|
|
|
|
include ContextModulesHelper
|
|
def add_item
|
|
@module = @context.context_modules.not_deleted.find(params[:context_module_id])
|
|
if authorized_action(@module, @current_user, :update)
|
|
@tag = @module.add_item(params[:item])
|
|
json = @tag.as_json
|
|
json['content_tag'].merge!(
|
|
publishable: module_item_publishable?(@tag),
|
|
published: @tag.published?,
|
|
publishable_id: module_item_publishable_id(@tag),
|
|
unpublishable: module_item_unpublishable?(@tag),
|
|
graded: @tag.graded?
|
|
)
|
|
render json: json
|
|
end
|
|
end
|
|
|
|
def remove_item
|
|
@tag = @context.context_module_tags.not_deleted.find(params[:id])
|
|
if authorized_action(@tag.context_module, @current_user, :update)
|
|
@module = @tag.context_module
|
|
@tag.destroy
|
|
render :json => @tag
|
|
end
|
|
end
|
|
|
|
def update_item
|
|
@tag = @context.context_module_tags.not_deleted.find(params[:id])
|
|
if authorized_action(@tag.context_module, @current_user, :update)
|
|
@tag.title = params[:content_tag][:title] if params[:content_tag] && params[:content_tag][:title]
|
|
@tag.url = params[:content_tag][:url] if %w(ExternalUrl ContextExternalTool).include?(@tag.content_type) && params[:content_tag] && params[:content_tag][:url]
|
|
@tag.indent = params[:content_tag][:indent] if params[:content_tag] && params[:content_tag][:indent]
|
|
@tag.new_tab = params[:content_tag][:new_tab] if params[:content_tag] && params[:content_tag][:new_tab]
|
|
@tag.save
|
|
@tag.update_asset_name! if params[:content_tag][:title]
|
|
render :json => @tag
|
|
end
|
|
end
|
|
|
|
def progressions
|
|
if authorized_action(@context, @current_user, :read)
|
|
if request.format == :json
|
|
if @context.grants_right?(@current_user, session, :view_all_grades)
|
|
if params[:user_id] && @user = @context.students.find(params[:user_id])
|
|
@progressions = @context.context_modules.active.map{|m| m.evaluate_for(@user) }
|
|
else
|
|
if @context.large_roster
|
|
@progressions = []
|
|
else
|
|
context_module_ids = @context.context_modules.active.pluck(:id)
|
|
@progressions = ContextModuleProgression.where(:context_module_id => context_module_ids).each{|p| p.evaluate }
|
|
end
|
|
end
|
|
elsif @context.grants_right?(@current_user, session, :participate_as_student)
|
|
@progressions = @context.context_modules.active.order(:id).map{|m| m.evaluate_for(@current_user) }
|
|
else
|
|
@progressions = []
|
|
end
|
|
render :json => @progressions
|
|
elsif !@context.grants_right?(@current_user, session, :view_all_grades)
|
|
@restrict_student_list = true
|
|
student_ids = @context.observer_enrollments.for_user(@current_user).map(&:associated_user_id)
|
|
student_ids << @current_user.id if @context.user_is_student?(@current_user)
|
|
students = UserSearch.scope_for(@context, @current_user, {:enrollment_type => 'student'}).where(:id => student_ids)
|
|
@visible_students = students.map { |u| user_json(u, @current_user, session) }
|
|
end
|
|
end
|
|
end
|
|
|
|
def update
|
|
@module = @context.context_modules.not_deleted.find(params[:id])
|
|
if authorized_action(@module, @current_user, :update)
|
|
if params[:publish]
|
|
@module.publish
|
|
@module.publish_items!
|
|
elsif params[:unpublish]
|
|
@module.unpublish
|
|
end
|
|
respond_to do |format|
|
|
if @module.update_attributes(params[:context_module])
|
|
format.json { render :json => @module.as_json(:include => :content_tags, :methods => :workflow_state, :permissions => {:user => @current_user, :session => session}) }
|
|
else
|
|
format.json { render :json => @module.errors, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
@module = @context.context_modules.not_deleted.find(params[:id])
|
|
if authorized_action(@module, @current_user, :delete)
|
|
@module.destroy
|
|
respond_to do |format|
|
|
format.html { redirect_to named_context_url(@context, :context_context_modules_url) }
|
|
format.json { render :json => @module.as_json(:methods => :workflow_state) }
|
|
end
|
|
end
|
|
end
|
|
end
|