allow using an item in modules more than once

closes #8769

An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.

test plan:

For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.

Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.

Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.

Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.

Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.

Rename the item. Verify that all instances of it in modules get renamed.

Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
This commit is contained in:
Brian Palmer 2012-06-18 16:18:43 -06:00
parent c53a4eaa16
commit 73380d2bc8
24 changed files with 294 additions and 101 deletions

View File

@ -920,16 +920,17 @@ class ApplicationController < ActionController::Base
end
def content_tag_redirect(context, tag, error_redirect_symbol)
url_params = { :module_item_id => tag.id }
if tag.content_type == 'Assignment'
redirect_to named_context_url(context, :context_assignment_url, tag.content_id)
redirect_to named_context_url(context, :context_assignment_url, tag.content_id, url_params)
elsif tag.content_type == 'WikiPage'
redirect_to named_context_url(context, :context_wiki_page_url, tag.content.url)
redirect_to named_context_url(context, :context_wiki_page_url, tag.content.url, url_params)
elsif tag.content_type == 'Attachment'
redirect_to named_context_url(context, :context_file_url, tag.content_id)
redirect_to named_context_url(context, :context_file_url, tag.content_id, url_params)
elsif tag.content_type == 'Quiz'
redirect_to named_context_url(context, :context_quiz_url, tag.content_id)
redirect_to named_context_url(context, :context_quiz_url, tag.content_id, url_params)
elsif tag.content_type == 'DiscussionTopic'
redirect_to named_context_url(context, :context_discussion_topic_url, tag.content_id)
redirect_to named_context_url(context, :context_discussion_topic_url, tag.content_id, url_params)
elsif tag.content_type == 'ExternalUrl'
@tag = tag
@module = tag.context_module

View File

@ -67,7 +67,7 @@ class AssignmentsController < ApplicationController
end
@locked = @assignment.locked_for?(@current_user, :check_policies => true, :deep_check_if_needed => true)
@unlocked = !@locked || @assignment.grants_rights?(@current_user, session, :update)[:update]
@assignment_module = @assignment.context_module_tag
@assignment_module = ContextModuleItem.find_tag_with_preferred([@assignment], params[:module_item_id])
@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).find(:all, :select => 'user_id').map(&:user_id)

View File

@ -269,16 +269,25 @@ class ContextModulesController < ApplicationController
@modules = @context.context_modules.active
@tags = @context.context_module_tags.active.sort_by{|t| t.position ||= 999}
result = {}
result[:current_item] = @tags.detect{|t| t.content_type == type && t.content_id == id }
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?(Quiz) && obj.assignment_id
result[:current_item] = @tags.detect{|t| t.content_type == 'Assignment' && t.content_id == obj.assignment_id }
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?(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

View File

@ -191,6 +191,8 @@ class DiscussionTopicsController < ApplicationController
else
format.html do
@context_module_tag = ContextModuleItem.find_tag_with_preferred([@topic, @topic.root_topic, @topic.assignment], params[:module_item_id])
@sequence_asset = @context_module_tag.try(:content)
env_hash = {
:TOPIC => {
:ID => @topic.id,

View File

@ -124,8 +124,6 @@ class FilesController < ApplicationController
if authorized_action(@attachment,@current_user,:read)
if @attachment.grants_right?(@current_user, nil, :download)
@headers = false
@tag = @attachment.context_module_tag
@module = @attachment.context_module_tag.context_module rescue nil
render
else
show

View File

@ -134,6 +134,8 @@ class QuizzesController < ApplicationController
@locked_reason = @quiz.locked_for?(@current_user, :check_policies => true, :deep_check_if_needed => true)
@locked = @locked_reason && !@quiz.grants_right?(@current_user, session, :update)
@context_module_tag = ContextModuleItem.find_tag_with_preferred([@quiz, @quiz.assignment], params[:module_item_id])
@sequence_asset = @context_module_tag.try(:content)
@quiz.context_module_action(@current_user, :read) if !@locked
@assignment = @quiz.assignment

View File

@ -24,6 +24,7 @@ class Assignment < ActiveRecord::Base
include HasContentTags
include CopyAuthorizedLinks
include Mutable
include ContextModuleItem
attr_accessible :title, :name, :description, :due_at, :points_possible,
:min_score, :max_score, :mastery_score, :grading_type, :submission_types,
@ -40,7 +41,6 @@ class Assignment < ActiveRecord::Base
has_one :quiz
belongs_to :assignment_group
has_one :discussion_topic, :conditions => ['discussion_topics.root_topic_id IS NULL'], :order => 'created_at'
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:context_module_progressions, :content_tags]}
has_many :learning_outcome_tags, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND content_tags.workflow_state != ?', 'learning_outcome', 'deleted'], :include => :learning_outcome
has_one :rubric_association, :as => :association, :conditions => ['rubric_associations.purpose = ?', "grading"], :order => :created_at, :include => :rubric
has_one :rubric, :through => :rubric_association
@ -335,12 +335,13 @@ class Assignment < ActiveRecord::Base
end
def context_module_action(user, action, points=nil)
self.context_module_tag.context_module_action(user, action, points) if self.context_module_tag
if self.submission_types == 'discussion_topic' && self.discussion_topic && self.discussion_topic.context_module_tag
self.discussion_topic.context_module_tag.context_module_action(user, action, points)
elsif self.submission_types == 'online_quiz' && self.quiz && self.quiz.context_module_tag
self.quiz.context_module_tag.context_module_action(user, action, points)
tags_to_update = self.context_module_tags.to_a
if self.submission_types == 'discussion_topic' && self.discussion_topic
tags_to_update += self.discussion_topic.context_module_tags
elsif self.submission_types == 'online_quiz' && self.quiz
tags_to_update += self.quiz.context_module_tags
end
tags_to_update.each { |tag| tag.context_module_action(user, action, points) }
end
set_broadcast_policy do |p|
@ -680,17 +681,16 @@ class Assignment < ActiveRecord::Base
end
def locked_for?(user=nil, opts={})
@locks ||= {}
locked = false
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.unlock_at && self.unlock_at > Time.now)
locked = {:asset_string => self.asset_string, :unlock_at => self.unlock_at}
elsif (self.lock_at && self.lock_at <= Time.now)
locked = {:asset_string => self.asset_string, :lock_at => self.lock_at}
elsif (self.could_be_locked && self.context_module_tag && self.context_module_tag.locked_for?(user, opts[:deep_check_if_needed]))
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
elsif self.could_be_locked && item = locked_by_module_item?(user, opts[:deep_check_if_needed])
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
end
locked
end
@ -1237,10 +1237,6 @@ class Assignment < ActiveRecord::Base
named_scope :no_graded_quizzes_or_topics, :conditions=>"submission_types NOT IN ('online_quiz', 'discussion_topic')"
named_scope :with_context_module_tags, lambda {
{:include => :context_module_tag }
}
named_scope :with_submissions, lambda {
{:include => :submissions }
}

View File

@ -20,7 +20,8 @@
class Attachment < ActiveRecord::Base
attr_accessible :context, :folder, :filename, :display_name, :user, :locked, :position, :lock_at, :unlock_at, :uploaded_data
include HasContentTags
include ContextModuleItem
belongs_to :context, :polymorphic => true
belongs_to :cloned_item
belongs_to :folder
@ -29,7 +30,6 @@ class Attachment < ActiveRecord::Base
has_one :media_object
has_many :submissions
has_many :attachment_associations
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => :context_module_progressions}
belongs_to :root_attachment, :class_name => 'Attachment'
belongs_to :scribd_mime_type
belongs_to :scribd_account
@ -990,17 +990,16 @@ class Attachment < ActiveRecord::Base
end
def locked_for?(user, opts={})
@locks ||= {}
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
return {:manually_locked => true} if self.locked || (self.folder && self.folder.locked?)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.unlock_at && Time.now < self.unlock_at)
locked = {:asset_string => self.asset_string, :unlock_at => self.unlock_at}
elsif (self.lock_at && Time.now > self.lock_at)
locked = {:asset_string => self.asset_string, :lock_at => self.lock_at}
elsif (self.could_be_locked && self.context_module_tag && !self.context_module_tag.available_for?(user, opts[:deep_check_if_needed]))
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
elsif self.could_be_locked && item = locked_by_module_item?(user, opts[:deep_check_if_needed])
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
end
locked
end
@ -1033,7 +1032,7 @@ class Attachment < ActiveRecord::Base
end
def context_module_action(user, action)
self.context_module_tag.context_module_action(user, action) if self.context_module_tag
self.context_module_tags.each { |tag| tag.context_module_action(user, action) }
end
include Workflow

View File

@ -33,7 +33,7 @@ class ContentTag < ActiveRecord::Base
validates_presence_of :context, :unless => proc { |tag| tag.context_id && tag.context_type }
validates_length_of :comments, :maximum => maximum_text_length, :allow_nil => true, :allow_blank => true
before_save :default_values
after_save :enforce_unique_in_modules
after_save :update_could_be_locked
after_save :touch_context_module
after_save :touch_context_if_learning_outcome
include CustomValidations
@ -86,23 +86,17 @@ class ContentTag < ActiveRecord::Base
def context_name
self.context.name rescue ""
end
def enforce_unique_in_modules
if self.workflow_state != 'deleted' && self.content_id && self.content_id > 0 && self.tag_type == 'context_module' && self.content_type != 'ContextExternalTool'
tags = ContentTag.find_all_by_content_id_and_content_type_and_tag_type_and_context_id_and_context_type(self.content_id, self.content_type, 'context_module', self.context_id, self.context_type)
tags.select{|t| t != self }.each do |tag|
tag.destroy
end
end
def update_could_be_locked
if self.content_id && self.content_type
klass = self.content_type.constantize
if klass.new.respond_to?(:could_be_locked=)
self.content_type.constantize.update_all({:could_be_locked => true}, {:id => self.content_id}) rescue nil
klass.update_all({:could_be_locked => true}, {:id => self.content_id})
end
end
true
end
def confirm_valid_module_requirements
self.context_module && self.context_module.confirm_valid_requirements
end

View File

@ -303,7 +303,6 @@ class ContextModule < ActiveRecord::Base
added_item
else
return nil unless item
added_item ||= ContentTag.find_by_content_id_and_content_type_and_context_id_and_context_type_and_tag_type(item.id, item.class.to_s, self.context_id, self.context_type, 'context_module')
title = params[:title] || (item.title rescue item.name)
added_item ||= self.content_tags.build(:context => context)
added_item.attributes = {

View File

@ -0,0 +1,61 @@
#
# Copyright (C) 2011 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/>.
#
# This isn't a record on its own, but a module included in other records such
# as Attachment and Assignment.
#
# ContextModules contain items indirectly, through ContentTags that contain the
# information on position in the module, progression requirements, etc.
module ContextModuleItem
# set up the association for the AR class that included this module
def self.included(klass)
klass.has_many :context_module_tags, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND content_tags.workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:context_module_progressions, :content_tags]}
end
# Check if this item is locked for the given user.
# If we are locked, this will return the module item (ContentTag) that is
# locking the item for the given user
def locked_by_module_item?(user, deep_check)
if self.context_module_tags.present? && self.context_module_tags.all? { |tag| tag.locked_for?(user, deep_check) }
item = self.context_module_tags.first
end
item || false
end
# searches the ContextModuleItems in objs_to_search, in order, for the first
# context module tag -- returns the tag with id preferred_id if given and it
# exists
#
# If no preferred is found, but more than one tag exists for the same obj, we
# return nothing, since we can't know which tag is appropriate to return.
def self.find_tag_with_preferred(objs_to_search, preferred_id)
objs_to_search.each do |obj|
next unless obj.present?
tag = obj.context_module_tags.find_by_id(preferred_id)
return tag if tag
end
objs_to_search.each do |obj|
next unless obj.present?
tags = obj.context_module_tags.to_a
return nil if tags.size > 1
tag = tags.first
return tag if tag
end
return nil
end
end

View File

@ -23,6 +23,7 @@ class DiscussionTopic < ActiveRecord::Base
include HasContentTags
include CopyAuthorizedLinks
include TextHelper
include ContextModuleItem
attr_accessible :title, :message, :user, :delayed_post_at, :assignment,
:plaintext_message, :podcast_enabled, :podcast_has_student_posts,
@ -39,7 +40,6 @@ class DiscussionTopic < ActiveRecord::Base
has_many :discussion_entries, :order => :created_at, :dependent => :destroy
has_many :root_discussion_entries, :class_name => 'DiscussionEntry', :include => [:user], :conditions => ['discussion_entries.parent_id IS NULL AND discussion_entries.workflow_state != ?', 'deleted']
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:content_tags, :context_module_progressions]}
has_one :external_feed_entry, :as => :asset
belongs_to :external_feed
belongs_to :context, :polymorphic => true
@ -135,8 +135,8 @@ class DiscussionTopic < ActiveRecord::Base
attr_accessor :saved_by
def update_assignment
if !self.assignment_id && @old_assignment_id && self.context_module_tag
self.context_module_tag.confirm_valid_module_requirements
if !self.assignment_id && @old_assignment_id
self.context_module_tags.each { |tag| tag.confirm_valid_module_requirements }
end
if @old_assignment_id
Assignment.update_all({:workflow_state => 'deleted', :updated_at => Time.now.utc}, {:id => @old_assignment_id, :context_id => self.context_id, :context_type => self.context_type, :submission_types => 'discussion_topic'})
@ -542,11 +542,12 @@ class DiscussionTopic < ActiveRecord::Base
end
def context_module_action(user, action, points=nil)
self.context_module_tag.context_module_action(user, action, points) if self.context_module_tag
tags_to_update = self.context_module_tags.to_a
if self.for_assignment?
self.assignment.context_module_tag.context_module_action(user, action, points) if self.assignment.context_module_tag
tags_to_update += self.assignment.context_module_tags
self.ensure_submission(user) if self.assignment.context.students.include?(user) && action == :contributed
end
tags_to_update.each { |tag| tag.context_module_action(user, action, points) }
end
def ensure_submission(user)
@ -597,16 +598,15 @@ class DiscussionTopic < ActiveRecord::Base
end
def locked_for?(user=nil, opts={})
@locks ||= {}
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.delayed_post_at && self.delayed_post_at > Time.now)
locked = {:asset_string => self.asset_string, :unlock_at => self.delayed_post_at}
elsif (self.assignment && l = self.assignment.locked_for?(user, opts))
locked = l
elsif (self.could_be_locked && self.context_module_tag && !self.context_module_tag.available_for?(user, opts[:deep_check_if_needed]))
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
elsif self.could_be_locked && item = locked_by_module_item?(user, opts[:deep_check_if_needed])
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
elsif (self.root_topic && l = self.root_topic.locked_for?(user, opts))
locked = l
end

View File

@ -24,6 +24,8 @@ class Quiz < ActiveRecord::Base
include CopyAuthorizedLinks
include ActionView::Helpers::SanitizeHelper
extend ActionView::Helpers::SanitizeHelper::ClassMethods
include ContextModuleItem
attr_accessible :title, :description, :points_possible, :assignment_id, :shuffle_answers,
:show_correct_answers, :time_limit, :allowed_attempts, :scoring_policy, :quiz_type,
:lock_at, :unlock_at, :due_at, :access_code, :anonymous_submissions, :assignment_group_id,
@ -36,7 +38,6 @@ class Quiz < ActiveRecord::Base
has_many :quiz_questions, :dependent => :destroy, :order => 'position'
has_many :quiz_submissions, :dependent => :destroy
has_many :quiz_groups, :dependent => :destroy, :order => 'position'
has_one :context_module_tag, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:context_module_progressions, :content_tags]}
belongs_to :context, :polymorphic => true
belongs_to :assignment
belongs_to :cloned_item
@ -218,8 +219,8 @@ class Quiz < ActiveRecord::Base
attr_accessor :saved_by
def update_assignment
send_later_if_production(:set_unpublished_question_count) if self.id
if !self.assignment_id && @old_assignment_id && self.context_module_tag
self.context_module_tag.confirm_valid_module_requirements
if !self.assignment_id && @old_assignment_id
self.context_module_tags.each { |tag| tag.confirm_valid_module_requirements }
end
if !self.graded? && (@old_assignment_id || self.last_assignment_id)
Assignment.update_all({:workflow_state => 'deleted', :updated_at => Time.now.utc}, {:id => [@old_assignment_id, self.last_assignment_id].compact, :submission_types => 'online_quiz'})
@ -588,9 +589,8 @@ class Quiz < ActiveRecord::Base
alias_method :to_s, :quiz_title
def locked_for?(user=nil, opts={})
@locks ||= {}
return false if opts[:check_policies] && self.grants_right?(user, nil, :update)
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
locked = false
if (self.unlock_at && self.unlock_at > Time.now)
sub = user && quiz_submissions.find_by_user_id(user.id)
@ -607,10 +607,10 @@ class Quiz < ActiveRecord::Base
if !sub || !sub.manually_unlocked
locked = l
end
elsif (self.context_module_tag && !self.context_module_tag.available_for?(user, opts[:deep_check_if_needed]))
elsif item = locked_by_module_item?(user, opts[:deep_check_if_needed])
sub = user && quiz_submissions.find_by_user_id(user.id)
if !sub || !sub.manually_unlocked
locked = {:asset_string => self.asset_string, :context_module => self.context_module_tag.context_module.attributes}
locked = {:asset_string => self.asset_string, :context_module => item.context_module.attributes}
end
end
locked
@ -618,8 +618,11 @@ class Quiz < ActiveRecord::Base
end
def context_module_action(user, action, points=nil)
self.context_module_tag.context_module_action(user, action, points) if self.context_module_tag
self.assignment.context_module_tag.context_module_action(user, action, points) if self.assignment && self.assignment.context_module_tag
tags_to_update = self.context_module_tags.to_a
if self.assignment
tags_to_update += self.assignment.context_module_tags
end
tags_to_update.each { |tag| tag.context_module_action(user, action, points) }
end
# virtual attribute

View File

@ -24,12 +24,12 @@ class WikiPage < ActiveRecord::Base
include Workflow
include HasContentTags
include CopyAuthorizedLinks
include ContextModuleItem
belongs_to :wiki, :touch => true
belongs_to :wiki_with_participants, :class_name => 'Wiki', :foreign_key => 'wiki_id', :include => {:wiki_namespaces => :context }
belongs_to :cloned_item
belongs_to :user
has_many :context_module_tags, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND workflow_state != ?', 'context_module', 'deleted'], :include => {:context_module => [:content_tags, :context_module_progressions]}
has_many :wiki_page_comments, :order => "created_at DESC"
acts_as_url :title, :scope => [:wiki_id, :not_deleted], :sync_url => true
@ -195,8 +195,7 @@ class WikiPage < ActiveRecord::Base
def locked_for?(context, user, opts={})
return false unless self.could_be_locked
@locks ||= {}
@locks[user ? user.id : 0] ||= Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
Rails.cache.fetch(locked_cache_key(user), :expires_in => 1.minute) do
m = context_module_tag_for(context, user).context_module rescue nil
locked = false
if (m && !m.available_for?(user))

View File

@ -104,5 +104,5 @@ $(document).ready(function() {
<%= render :partial => "shared/rubric_forms" %>
<% end %>
<%= render :partial => "shared/aligned_outcomes", :locals => {:asset => @assignment} %>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => @assignment} if @assignment.context_module_tag %>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => @assignment} if !@assignment.context_module_tags.empty? %>
<% end %>

View File

@ -12,8 +12,10 @@
'other' => image_tag("blank.png", :class => "image", :alt => '')
}
%>
<% criterion = completion_criteria && completion_criteria.find{|c| c[:id] == tag.id} %>
<table id="context_module_item_<%= tag ? tag.id : "blank" %>" class="context_module_item <%= module_item.content_type_class if module_item %> <%= 'also_assignment' if module_item && module_item.graded? %> indent_<%= tag.try_rescue(:indent) || '0' %> <%= 'progression_requirement' if criterion %> <%= criterion[:type] if criterion %>_requirement" style="<%= hidden unless module_item %>">
<% criterion = completion_criteria && completion_criteria.find{|c| c[:id] == tag.id}
item_class = "#{module_item.content_type}_#{module_item.content_id}" if module_item
%>
<table id="context_module_item_<%= tag ? tag.id : "blank" %>" class="context_module_item <%= module_item.content_type_class if module_item %> <%= 'also_assignment' if module_item && module_item.graded? %> indent_<%= tag.try_rescue(:indent) || '0' %> <%= 'progression_requirement' if criterion %> <%= criterion[:type] if criterion %>_requirement <%= item_class %>" style="<%= hidden unless module_item %>">
<tr>
<td class="module_item_icons">
<div class="nobr">

View File

@ -225,10 +225,7 @@
</div>
<%=
sequence_asset = @topic
sequence_asset = @topic.root_topic if @topic.root_topic && !@topic.context_module_tag && @topic.root_topic.context_module_tag
sequence_asset = @topic.assignment if @topic.assignment && !@topic.context_module_tag && @topic.assignment.context_module_tag
render :partial => "shared/sequence_footer", :locals => {:asset => sequence_asset, :context => sequence_asset.context} if sequence_asset.context_module_tag
render :partial => "shared/sequence_footer", :locals => {:asset => @sequence_asset, :context => @sequence_asset.context} if @sequence_asset
%>
<% end %>
<% if @headers == false || @locked %>

View File

@ -220,8 +220,4 @@
<%= render :partial => "shared/message_students" %>
<% end %>
<% end %>
<%
sequence_asset = @quiz
sequence_asset = @quiz.assignment if @quiz.assignment && !@quiz.context_module_tag && @quiz.assignment.context_module_tag
%>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => sequence_asset} if sequence_asset.context_module_tag %>
<%= render :partial => "shared/sequence_footer", :locals => {:asset => @sequence_asset} if @sequence_asset %>

View File

@ -9,7 +9,7 @@
<%= image_tag "forward.png" %> <span class="text"><%= t(:next, "Next") %></span>
<span class="title ellipsis"></span>
</a>
<a href="<%= context_url(context, :context_context_modules_item_details_url, asset.asset_string) %>" style="display: none;" class="sequence_details_url">&nbsp;</a>
<a href="<%= context_url(context, :context_context_modules_item_details_url, asset.asset_string, :module_item_id => params[:module_item_id]) %>" style="display: none;" class="sequence_details_url">&nbsp;</a>
<a href="<%= context_url(context, :context_context_modules_item_redirect_url, "{{ id }}") %>" class="module_item_url" style="display: none;">&nbsp;</a>
<a href="<%= context_url(context, :context_context_module_url, "{{ id }}") %>" class="module_url" style="display: none;">&nbsp;</a>
<div class="all">

View File

@ -182,6 +182,14 @@ define([
}, function() {
});
},
itemClass: function(content_tag) {
return content_tag.content_type + "_" + content_tag.content_id;
},
updateAllItemInstances: function(content_tag) {
$(".context_module_item."+modules.itemClass(content_tag)+" .title").each(function() {
$(this).text(content_tag.title);
});
},
editModule: function($module) {
var $form = $("#add_context_module_form");
$form.data('current_module', $module);
@ -279,6 +287,7 @@ define([
$item.removeClass('indent_' + idx);
}
$item.addClass('indent_' + (data.indent || 0));
$item.addClass(modules.itemClass(data));
// don't just tack onto the bottom, put it in its correct position
var $before = null;
$module.find(".context_module_items").children().each(function() {
@ -663,6 +672,7 @@ define([
var $module = $("#context_module_" + data.content_tag.context_module_id);
var $item = modules.addItemToModule($module, data.content_tag);
$module.find(".context_module_items").sortable('refresh');
modules.updateAllItemInstances(data.content_tag);
modules.updateAssignmentData();
$(this).dialog('close');
},

View File

@ -55,10 +55,10 @@ describe ContextModulesController do
header2 = @module.add_item :type => 'context_module_sub_header'
get 'module_redirect', :course_id => @course.id, :context_module_id => @module.id, :first => 1
response.should redirect_to course_assignment_url(@course.id, assignment1.id)
response.should redirect_to course_assignment_url(@course.id, assignment1.id, :module_item_id => assignmentTag1.id)
get 'module_redirect', :course_id => @course.id, :context_module_id => @module.id, :last => 1
response.should redirect_to course_assignment_url(@course.id, assignment2.id)
response.should redirect_to course_assignment_url(@course.id, assignment2.id, :module_item_id => assignmentTag2.id)
assignmentTag1.destroy
assignmentTag2.destroy
@ -152,7 +152,7 @@ describe ContextModulesController do
get 'item_redirect', :course_id => @course.id, :id => assignmentTag1.id
response.should be_redirect
response.should redirect_to course_assignment_url(@course, assignment1)
response.should redirect_to course_assignment_url(@course, assignment1, :module_item_id => assignmentTag1.id)
end
it "should redirect to a discussion page" do
@ -165,7 +165,7 @@ describe ContextModulesController do
get 'item_redirect', :course_id => @course.id, :id => topicTag.id
response.should be_redirect
response.should redirect_to course_discussion_topic_url(@course, topic)
response.should redirect_to course_discussion_topic_url(@course, topic, :module_item_id => topicTag.id)
end
it "should redirect to a quiz page" do
@ -178,7 +178,7 @@ describe ContextModulesController do
get 'item_redirect', :course_id => @course.id, :id => tag.id
response.should be_redirect
response.should redirect_to course_quiz_url(@course, quiz)
response.should redirect_to course_quiz_url(@course, quiz, :module_item_id => tag.id)
end
end
@ -217,4 +217,4 @@ describe ContextModulesController do
ct1.context_module.should == m1
end
end
end
end

View File

@ -132,7 +132,7 @@ describe ContextModule do
get next_link
response.should be_redirect
response.location.ends_with?(@test_url).should be_true
response.location.ends_with?(@test_url + "?module_item_id=#{@tag2.id}").should be_true
get @test_url
response.should be_success

View File

@ -136,6 +136,22 @@ describe ContextModule do
@tag.content.should eql(@file)
@module.content_tags.should be_include(@tag)
end
it "should allow adding items more than once" do
course_module
@assignment = @course.assignments.create!(:title => "some assignment")
@tag1 = @module.add_item(:id => @assignment.id, :type => "assignment")
@tag2 = @module.add_item(:id => @assignment.id, :type => "assignment")
@tag1.should_not == @tag2
@module.content_tags.should be_include(@tag1)
@module.content_tags.should be_include(@tag2)
@mod2 = @course.context_modules.create!(:name => "mod2")
@tag3 = @mod2.add_item(:id => @assignment.id, :type => "assignment")
@tag3.should_not == @tag1
@tag3.should_not == @tag2
@mod2.content_tags.should == [@tag3]
end
end
describe "completion_requirements=" do
@ -249,6 +265,32 @@ describe ContextModule do
@progression.should be_locked
end
describe "multi-items" do
it "should be locked if all tags are locked" do
course_module
@user = User.create!(:name => "some name")
@course.enroll_student(@user)
@a1 = @course.assignments.create!(:title => "some assignment")
@tag1 = @module.add_item({:id => @a1.id, :type => 'assignment'})
@module.require_sequential_progress = true
@module.completion_requirements = {@tag1.id => {:type => 'must_submit'}}
@module.save!
@a2 = @course.assignments.create!(:title => "locked assignment")
@a2.locked_for?(@user).should be_false
@tag2 = @module.add_item({:id => @a2.id, :type => 'assignment'})
@a2.reload.locked_for?(@user).should be_true
@mod2 = @course.context_modules.create!(:name => "mod2")
@tag3 = @mod2.add_item({:id => @a2.id, :type => 'assignment'})
# not locked, because the second tag allows access
@a2.reload.locked_for?(@user).should be_false
@mod2.prerequisites = "module_#{@module.id}"
@mod2.save!
# now locked, because mod2 is locked
@a2.reload.locked_for?(@user).should be_true
end
end
it "should not be available if previous module is incomplete" do
course_module
@assignment = @course.assignments.create!(:title => "some assignment")
@ -448,7 +490,7 @@ describe ContextModule do
@progression.current_position.should eql(@tag2.position)
@assignment.reload; @assignment2.reload
@assignment.locked_for?(@user).should eql(false)
@assignment2.locked_for?(@user).should_not eql(false)
@assignment2.locked_for?(@user).should eql(false)
@module.completion_requirements = {@tag.id => {:type => 'min_score', :min_score => 5}}
@module.save

View File

@ -85,6 +85,15 @@ describe "context_modules" do
@module = @course.context_modules.create!(:name => "some module")
end
def edit_module_item(module_item)
driver.execute_script("$(arguments[0]).addClass('context_module_item_hover')", module_item)
module_item.find_element(:css, '.edit_item_link').click
edit_form = driver.find_element(:id, 'edit_item_form')
yield edit_form
submit_form(edit_form)
wait_for_ajaximations
end
context "context modules as a teacher" do
before (:each) do
@ -255,12 +264,9 @@ describe "context_modules" do
item_edit_text = "Assignment Edit 1"
module_item = add_existing_module_item('#assignments_select', 'Assignment', @assignment.title)
tag = ContentTag.last
driver.execute_script("$('#context_module_item_#{tag.id}').addClass('context_module_item_hover')")
module_item.find_element(:css, '.edit_item_link').click
edit_form = driver.find_element(:id, 'edit_item_form')
replace_content(edit_form.find_element(:id, 'content_tag_title'), item_edit_text)
submit_form(edit_form)
wait_for_ajaximations
edit_module_item(module_item) do |edit_form|
replace_content(edit_form.find_element(:id, 'content_tag_title'), item_edit_text)
end
module_item = driver.find_element(:id, "context_module_item_#{tag.id}")
module_item.should include_text(item_edit_text)
@ -275,6 +281,41 @@ describe "context_modules" do
add_existing_module_item('#assignments_select', 'Assignment', @assignment.title)
end
it "should allow adding an item twice" do
item1 = add_existing_module_item('#assignments_select', 'Assignment', @assignment.title)
item2 = add_existing_module_item('#assignments_select', 'Assignment', @assignment.title)
item1.should_not == item2
@assignment.reload.context_module_tags.size.should == 2
end
it "should rename all instances of an item" do
item1 = add_existing_module_item('#assignments_select', 'Assignment', @assignment.title)
item2 = add_existing_module_item('#assignments_select', 'Assignment', @assignment.title)
edit_module_item(item2) do |edit_form|
replace_content(edit_form.find_element(:id, 'content_tag_title'), "renamed assignment")
end
all_items = ff(".context_module_item.Assignment_#{@assignment.id}")
all_items.size.should == 2
all_items.each { |i| i.find_element(:css, '.title').text.should == 'renamed assignment' }
@assignment.reload.title.should == 'renamed assignment'
run_jobs
@assignment.context_module_tags.each { |tag| tag.title.should == 'renamed assignment' }
# reload the page and renaming should still work on existing items
get "/courses/#{@course.id}/modules"
wait_for_ajaximations
item3 = add_existing_module_item('#assignments_select', 'Assignment', @assignment.title)
edit_module_item(item3) do |edit_form|
replace_content(edit_form.find_element(:id, 'content_tag_title'), "again")
end
all_items = ff(".context_module_item.Assignment_#{@assignment.id}")
all_items.size.should == 3
all_items.each { |i| i.find_element(:css, '.title').text.should == 'again' }
@assignment.reload.title.should == 'again'
run_jobs
@assignment.context_module_tags.each { |tag| tag.title.should == 'again' }
end
it "should add a quiz to a module" do
add_existing_module_item('#quizs_select', 'Quiz', @quiz.title)
end
@ -338,7 +379,7 @@ describe "context_modules" do
driver.find_element(:css, '.ui-dialog .add_prerequisite_link').click
wait_for_animations
#have to do it this way because the select has no css attributes on it
click_option(':input:visible.eq(3)', first_module_name)
click_option('.criterion select', "the module, #{first_module_name}")
submit_form(add_form)
wait_for_ajaximations
db_module = ContextModule.last
@ -347,7 +388,7 @@ describe "context_modules" do
driver.find_element(:css, "#context_module_#{db_module.id} .edit_module_link").click
driver.find_element(:css, '.ui-dialog').should be_displayed
wait_for_ajaximations
prereq_select = find_all_with_jquery(':input:visible')[3]
prereq_select = find_with_jquery('.criterion select')
option = first_selected_option(prereq_select)
option.text.should == 'the module, ' + first_module_name
end
@ -627,6 +668,48 @@ describe "context_modules" do
end
describe "sequence footer" do
it "should show the right nav when an item is in modules multiple times" do
@assignment = @course.assignments.create!(:title => "some assignment")
@atag1 = @module_1.add_item(:id => @assignment.id, :type => "assignment")
@after1 = @module_1.add_item(:type => "external_url", :title => "url1", :url => "http://example.com/1")
@atag2 = @module_2.add_item(:id => @assignment.id, :type => "assignment")
@after2 = @module_2.add_item(:type => "external_url", :title => "url2", :url => "http://example.com/2")
get "/courses/#{@course.id}/modules/items/#{@atag1.id}"
wait_for_ajaximations
prev = driver.find_element(:css, '#sequence_footer a.prev')
URI.parse(prev.attribute('href')).path.should == "/courses/#{@course.id}/modules/items/#{@tag_1.id}"
nxt = driver.find_element(:css, '#sequence_footer a.next')
URI.parse(nxt.attribute('href')).path.should == "/courses/#{@course.id}/modules/items/#{@after1.id}"
get "/courses/#{@course.id}/modules/items/#{@atag2.id}"
wait_for_ajaximations
prev = driver.find_element(:css, '#sequence_footer a.prev')
URI.parse(prev.attribute('href')).path.should == "/courses/#{@course.id}/modules/items/#{@tag_2.id}"
nxt = driver.find_element(:css, '#sequence_footer a.next')
URI.parse(nxt.attribute('href')).path.should == "/courses/#{@course.id}/modules/items/#{@after2.id}"
# if the user didn't get here from a module link, we show no nav,
# because we can't know which nav to show
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
wait_for_ajaximations
prev = driver.find_element(:css, '#sequence_footer a.prev')
prev.should_not be_displayed
nxt = driver.find_element(:css, '#sequence_footer a.next')
nxt.should_not be_displayed
end
it "should show the nav when going straight to the item if there's only one tag" do
@assignment = @course.assignments.create!(:title => "some assignment")
@atag1 = @module_1.add_item(:id => @assignment.id, :type => "assignment")
@after1 = @module_1.add_item(:type => "external_url", :title => "url1", :url => "http://example.com/1")
get "/courses/#{@course.id}/assignments/#{@assignment.id}"
wait_for_ajaximations
prev = driver.find_element(:css, '#sequence_footer a.prev')
URI.parse(prev.attribute('href')).path.should == "/courses/#{@course.id}/modules/items/#{@tag_1.id}"
nxt = driver.find_element(:css, '#sequence_footer a.next')
URI.parse(nxt.attribute('href')).path.should == "/courses/#{@course.id}/modules/items/#{@after1.id}"
end
it "should show module navigation for group assignment discussions" do
group_assignment_discussion(:course => @course)
@group.users << @student