DA - modules

fixes CNVS-14095
fixes CNVS-15157

test plan:
  - with DA on and off
    - go to modules page
      - only the right assignments show
      - completion requirements ignore
        assignments that cant be seen by a user
    - modules page works with changing sections
      - complete/incomplete updates properly
      - as does locked/unlocked
    - modules page shows correct content when
      a students grade is deleted and this makes
      them lose assignment visibility
    - modules api doesnt return any content that
      a student cannot see

Change-Id: Ia1acfd919214823cdfc3b45e974876b4529bb14d
Reviewed-on: https://gerrit.instructure.com/38970
Product-Review: Hilary Scharton <hilary@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Mike Nomitch <mnomitch@instructure.com>
This commit is contained in:
Michael Nomitch 2014-08-07 16:52:54 -05:00 committed by Mike Nomitch
parent 80ce61ea62
commit 506e786db4
21 changed files with 479 additions and 52 deletions

View File

@ -370,6 +370,21 @@ class ContextModulesApiController < ApplicationController
SearchTermHelper.validate_search_term(params[:search_term])
opts[:search_term] = params[:search_term]
end
if @context.feature_enabled?(:differentiated_assignments) && includes.include?('items')
user_ids = (@student || @current_user).id
if @context.user_has_been_observer?(@student || @current_user)
opts[:observed_student_ids] = ObserverEnrollment.observed_student_ids(self.context, (@student || @current_user) )
user_ids.concat(opts[:observed_student_ids])
end
opts[:assignment_visibilities] = AssignmentStudentVisibility.visible_assignment_ids_for_user(user_ids, @context.id)
opts[:discussion_visibilities] = DiscussionTopic.where(course_id: @context.id).visible_to_students_with_da_enabled(user_ids).pluck(:id)
# TODO: uncomment once quiz visibilities view is in master
# opts[:quiz_visibilities] = QuizStudentVisibility.visible_quiz_ids_for_user(user_ids, @context.id)
end
render :json => modules_and_progressions.map { |mod, prog| module_json(mod, @student || @current_user, session, prog, includes, opts) }.compact
end
end

View File

@ -64,7 +64,7 @@ class ContextModulesController < ApplicationController
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.active
@tags = @module.content_tags_visible_to(@current_user)
if params[:last]
@tags.pop while @tags.last && @tags.last.content_type == 'ContextModuleSubHeader'
else
@ -117,11 +117,11 @@ class ContextModulesController < ApplicationController
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
@ -132,7 +132,7 @@ class ContextModulesController < ApplicationController
@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|
@ -140,11 +140,11 @@ class ContextModulesController < ApplicationController
end
end
end
def content_tag_assignment_data
if authorized_action(@context, @current_user, :read)
info = {}
@context.context_module_tags.not_deleted.each do |tag|
@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)
@ -158,7 +158,7 @@ class ContextModulesController < ApplicationController
end
def prerequisites_needing_finishing_for(mod, progression, before_tag=nil)
tags = mod.content_tags.active
tags = mod.content_tags_visible_to(@current_user)
pres = []
tags.each do |tag|
if req = (mod.completion_requirements || []).detect{|r| r[:id] == tag.id }
@ -251,7 +251,7 @@ class ContextModulesController < ApplicationController
@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.active) }
format.json { render :json => (@progression.collapsed ? @progression : @module.content_tags_visible_to(@current_user) )}
end
end
end
@ -433,5 +433,4 @@ class ContextModulesController < ApplicationController
end
end
end
end

View File

@ -17,9 +17,13 @@
#
module ContextModulesHelper
def cache_if_module(context_module, editable, draft_state, &block)
def cache_if_module(context_module, editable, draft_state, differentiated_assignments, user, &block)
if context_module
cache_key = ['context_module_render_11_', context_module.cache_key, editable, draft_state, Time.zone].join('/')
# TODO: add quizzes once quiz visibilities view is in master
visible_assignments = differentiated_assignments ? user.assignments_visibile_in_course(context_module.context).pluck(:id) : []
cache_key_items = ['context_module_render_11_', context_module.cache_key, editable, draft_state, Time.zone]
cache_key_items << visible_assignments if differentiated_assignments
cache_key = cache_key_items.join('/')
cache_key = add_menu_tools_to_cache_key(cache_key)
cache(cache_key, nil, &block)
else

View File

@ -417,6 +417,18 @@ class ContentTag < ActiveRecord::Base
scope :learning_outcome_alignments, -> { where(:tag_type => 'learning_outcome') }
scope :learning_outcome_links, -> { where(:tag_type => 'learning_outcome_association', :associated_asset_type => 'LearningOutcomeGroup', :content_type => 'LearningOutcome') }
# TODO: add quizzes to this scope once the quiz visibilities view makes it into master
scope :visible_to_students_with_da_enabled, lambda { |user_ids|
joins("LEFT JOIN discussion_topics ON discussion_topics.id = content_tags.content_id AND content_type = 'DiscussionTopic'").
joins("LEFT JOIN assignment_student_visibilities ON ((assignment_student_visibilities.assignment_id = content_tags.content_id AND content_type = 'Assignment')
OR (assignment_student_visibilities.assignment_id = discussion_topics.assignment_id AND content_type = 'DiscussionTopic'))").
where("content_tags.content_type NOT IN ('Assignment','DiscussionTopic')
OR ((discussion_topics.id IS NOT NULL AND discussion_topics.assignment_id IS NULL)
OR (assignment_student_visibilities.assignment_id IS NOT NULL AND assignment_student_visibilities.user_id IN (?))
)", user_ids).
uniq
}
# only intended for learning outcome links
def self.outcome_title_order_by_clause
best_unicode_collation_key("learning_outcomes.short_description")

View File

@ -286,8 +286,9 @@ class ContextModule < ActiveRecord::Base
completion_requirements.select { |cr| valid_ids.include? cr[:id] }
end
def content_tags_visible_to(user)
if self.content_tags.loaded?
def content_tags_visible_to(user, opts={})
opts[:tags_loaded] = self.content_tags.loaded?
tags = if opts[:tags_loaded]
if self.grants_right?(user, :update)
self.content_tags.select{|tag| tag.workflow_state != 'deleted'}
else
@ -300,6 +301,47 @@ class ContextModule < ActiveRecord::Base
self.content_tags.active
end
end
if !self.grants_right?(user, :update) && self.context.feature_enabled?(:differentiated_assignments) && user
tags = filter_tags_for_da(tags, user, opts)
end
tags
end
def filter_tags_for_da(tags, user, opts={})
scope_filter = Proc.new{|tags, user_ids, course_id, opts|
tags.visible_to_students_with_da_enabled(user_ids)
}
array_filter = Proc.new{|tags, user_ids, course_id, opts|
visible_assignments = opts[:assignment_visibilities] || AssignmentStudentVisibility.visible_assignment_ids_for_user(user_ids, course_id)
visible_discussions = opts[:discussion_visibilities] || DiscussionTopic.where(context_id: course_id).visible_to_students_with_da_enabled(user_ids).pluck(:id)
# TODO: uncomment once quiz visibilities sql view makes it into master
# visible_quizzes = opts[:quiz_visibilities] || QuizStudentVisibility.visible_assignment_ids_for_user(user_ids, course_id)
tags.select{|tag|
case tag.content_type;
when 'Assignment'; visible_assignments.include?(tag.content_id);
when 'DiscussionTopic'; visible_discussions.include?(tag.content_id);
# when 'Quizzes::Quiz'; visible_quizzes.include?(tag.content_id);
else; true; end
}
}
filter = opts[:tags_loaded] ? array_filter : scope_filter
student_ids = [user.id]
if self.context.user_has_been_observer?(user)
observed_student_ids = opts[:observed_student_ids] || ObserverEnrollment.observed_student_ids(self.context, user)
student_ids.concat(observed_student_ids)
# if no observed_students, allow observer to see all content_tags
tags = filter.call(tags, student_ids, self.context_id, opts) if observed_student_ids.any?
else
tags = filter.call(tags, student_ids, self.context_id, opts)
end
tags
end
def add_item(params, added_item=nil, opts={})
@ -462,19 +504,6 @@ class ContextModule < ActiveRecord::Base
self.prerequisites.select{|pre| pre[:type] == 'context_module' && active_ids.member?(pre[:id])}
end
def reload
clear_cached_lookups
super
end
def clear_cached_lookups
@cached_active_tags = nil
end
def cached_active_tags
@cached_active_tags ||= self.content_tags.active
end
def confirm_valid_requirements(do_save=false)
return if @already_confirmed_valid_requirements
@already_confirmed_valid_requirements = true

View File

@ -134,7 +134,7 @@ class ContextModuleProgression < ActiveRecord::Base
calc = CompletedRequirementCalculator.new(self.requirements_met || [])
completion_requirements.each do |req|
# create the hash inside the loop in case the completion_requirements is empty (performance)
tags_hash ||= context_module.cached_active_tags.index_by(&:id)
tags_hash ||= context_module.content_tags_visible_to(self.user).index_by(&:id)
tag = tags_hash[req[:id]]
next unless tag
@ -264,7 +264,7 @@ class ContextModuleProgression < ActiveRecord::Base
completion_requirements = context_module.completion_requirements || []
requirements_met = self.requirements_met || []
context_module.cached_active_tags.each do |tag|
context_module.content_tags_visible_to(self.user).each do |tag|
self.current_position = tag.position if tag.position
all_met = completion_requirements.select{|r| r[:id] == tag.id }.all? do |req|
requirements_met.any?{|r| r[:id] == req[:id] && r[:type] == req[:type] }

View File

@ -303,10 +303,24 @@ class Course < ActiveRecord::Base
def module_items_visible_to(user)
if self.grants_right?(user, :manage_content)
self.context_module_tags.not_deleted.joins(:context_module).where("context_modules.workflow_state <> 'deleted'")
tags = self.context_module_tags.not_deleted.joins(:context_module).where("context_modules.workflow_state <> 'deleted'")
else
self.context_module_tags.active.joins(:context_module).where(:context_modules => {:workflow_state => 'active'})
tags = self.context_module_tags.active.joins(:context_module).where(:context_modules => {:workflow_state => 'active'})
end
if !self.grants_any_right?(user, :manage_content, :read_as_admin, :manage_grades, :manage_assignments) && self.feature_enabled?(:differentiated_assignments) && user
student_ids = [user.id]
if self.user_has_been_observer?(user)
observed_student_ids = ObserverEnrollment.observed_student_ids(self, user)
student_ids.concat(observed_student_ids)
# if no observed_students, allow observer to see all content_tags
tags = tags.visible_to_students_with_da_enabled(student_ids) if observed_student_ids.any?
else
tags = tags.visible_to_students_with_da_enabled(student_ids)
end
end
tags
end
def verify_unique_sis_source_id

View File

@ -46,7 +46,7 @@ class CourseProgress
def current_content_tag
return unless in_progress?
current_module.content_tags.active.where(:position => current_position).first
current_module.content_tags_visible_to(user).where(:position => current_position).first
end
def requirements

View File

@ -18,8 +18,26 @@
class StudentEnrollment < Enrollment
belongs_to :student, :foreign_key => :user_id, :class_name => 'User'
after_save :evaluate_modules, if: Proc.new{ |e|
# if enrollment switches sections or is created
e.course_section_id_changed? || e.course_id_changed? ||
# or if an enrollment is deleted and they are in another section of the course
(e.workflow_state_changed? && e.workflow_state == 'deleted' &&
e.user.enrollments.where('id != ?',e.id).active.where(course_id: e.course_id).exists?)
}
def student?
true
end
def evaluate_modules
if self.course.feature_enabled?(:differentiated_assignments)
ContextModuleProgression.for_user(self.user_id).
joins(:context_module).
where(:context_modules => { :context_type => 'Course', :context_id => self.course_id}).
each do |prog|
prog.mark_as_outdated!
end
end
end
end

View File

@ -225,7 +225,11 @@ TEXT
class="ig-list <%= 'editable' if can_do(@context, @current_user, :manage_content) %>"
>
<% editable = can_do(@context, @current_user, :manage_content) %>
<% cache_key = [@context.cache_key, editable, 'all_context_modules_draft_6', collection_cache_key(@modules), Time.zone].join('/') %>
<% differentiated_assignments_on = @context.feature_enabled?(:differentiated_assignments) %>
<% visible_assignments = differentiated_assignments_on ? @current_user.assignments_visibile_in_course(@context).pluck(:id) : [] %>
<% cache_key_items = [@context.cache_key, editable, 'all_context_modules_draft_6', collection_cache_key(@modules), Time.zone]%>
<% cache_key_items << visible_assignments if differentiated_assignments_on %>
<% cache_key = cache_key_items.join('/') %>
<% cache_key = add_menu_tools_to_cache_key(cache_key) %>
<% cache(cache_key) do %>
<% ContextModule.send(:preload_associations, @modules, :content_tags => :content) %>

View File

@ -1,9 +1,10 @@
<%
editable ||= can_do(@context, @current_user, :manage_content)
workflow_state = context_module && context_module.workflow_state
differentiated_assignments_on = @context.feature_enabled?(:differentiated_assignments)
@modules ||= []
%>
<% cache_if_module(context_module, editable, false) do %>
<% cache_if_module(context_module, editable, false, differentiated_assignments_on, @current_user) do %>
<div class="context_module bordered <%= 'unpublished_module' if workflow_state == "unpublished" %> <%= 'editable_context_module' if editable %>" tabindex="0" aria-label="<%= context_module ? context_module.name : "" %>" data-workflow-state="<%= context_module ? context_module.workflow_state : "{{ workflow_state }}"%>" data-module-url="<%= context_url(@context, :context_url) %>/modules/<%= context_module ? context_module.id : "{{ id }}" %>" data-module-id="<%= context_module ? context_module.id : "{{ id }}" %>" id="context_module_<%= context_module ? context_module.id : "blank" %>" style="<%= hidden unless context_module %>">
<a name="module_<%= context_module.id if context_module %>"></a>
@ -73,8 +74,7 @@
<div class="items context_module_items <%= 'manageable' if editable %>" style="min-height: 10px;">
<% if context_module %>
<%
tags = context_module.content_tags
tags = can_do(@context, @current_user, :manage_content) ? tags.not_deleted : tags.active
tags = context_module.content_tags_visible_to(@current_user)
tags.each do |tag|
%>
<%= render :partial => 'context_modules/module_item', :object => tag, :locals => {:completion_criteria => context_module.completion_requirements, :editable => editable} %>

View File

@ -3,9 +3,10 @@
editable ||= can_do(@context, @current_user, :manage_content)
workflow_state = context_module && context_module.workflow_state
published_status = context_module && context_module.published? ? 'published' : 'unpublished'
differentiated_assignments_on = @context.feature_enabled?(:differentiated_assignments)
@modules ||= []
%>
<% cache_if_module(context_module, editable, true) do %>
<% cache_if_module(context_module, editable, true, differentiated_assignments_on, @current_user) do %>
<div
class="item-group-condensed context_module

View File

@ -37,7 +37,8 @@ module Api::V1::ContextModule
end
has_update_rights = context_module.grants_right?(current_user, :update)
hash['published'] = context_module.active? if has_update_rights
tags = context_module.content_tags_visible_to(@current_user)
# TODO: add "quiz_visibilities: opts[:quiz_visibilities]" to args below once quiz visibilities makes it into master
tags = context_module.content_tags_visible_to(@current_user, assignment_visibilities: opts[:assignment_visibilities], discussion_visibilities: opts[:discussion_visibilities], observed_student_ids: opts[:observed_student_ids])
count = tags.count
hash['items_count'] = count
hash['items_url'] = polymorphic_url([:api_v1, context_module.context, context_module, :items])

View File

@ -184,19 +184,6 @@ Aspire (SIS2000), JMC, and any other SIF-enabled SIS that accepts the SIF elemen
root_opt_in: true,
development: true
},
'differentiated_assignments' =>
{
display_name: -> { I18n.t('features.differentiated_assignments', 'Differentiated Assignments') },
description: -> { I18n.t('differentiated_assignments_description', <<-END) },
Differentiated Assignments is a *beta* feature that enables choosing which section(s) an assignment applies to.
Sections that are not given an assignment will not see it in their course content and their final grade will be
calculated without those points.
END
applies_to: 'Course',
state: 'hidden',
root_opt_in: true,
development: true
},
'k12' =>
{
display_name: -> { I18n.t('features.k12', 'K-12 specific features') },

View File

@ -38,6 +38,8 @@ module FeatureFlags
flag = self.feature_flags.where(feature: feature).first
flag ||= self.feature_flags.build(feature: feature)
flag.state = state
@feature_flag_cache ||= {}
@feature_flag_cache[feature] = flag
flag.save!
end

View File

@ -0,0 +1,70 @@
#
# Copyright (C) 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/>.
#
module Features
module DifferentiatedAssignments
class ModuleEvaluator < Struct.new(:course_id)
def perform
ContextModuleProgression.joins(:context_module).
where(:context_modules => { :context_type => 'Course', :context_id => course_id}).
find_each do |prog|
prog.mark_as_outdated!
end
end
end
end
end
Feature.register('differentiated_assignments' =>
{
display_name: -> { I18n.t('features.differentiated_assignments', 'Differentiated Assignments') },
description: -> { I18n.t('differentiated_assignments_description', <<-END) },
Differentiated Assignments is a *beta* feature that enables choosing which section(s) an assignment applies to.
Sections that are not given an assignment will not see it in their course content and their final grade will be
calculated without those points.
END
applies_to: 'Course',
state: 'hidden',
root_opt_in: true,
development: true,
custom_transition_proc: ->(user, context, from_state, transitions) do
if context.is_a?(Course) && from_state == 'on'
transitions['off']['message'] = I18n.t('features.differentiated_assignments_course_disable_warning', <<END)
Disabling differentiated assignments will make all published assignments visible to all students.
END
elsif context.is_a?(Account) && from_state != 'off'
site_admin = Account.site_admin.grants_right?(user, :read)
warning = I18n.t('features.differentiated_assignments_account_disable_warning', <<END)
Turning this feature off will impact existing courses. For assistance in disabling this feature, please contact
your Canvas Success Manager.
END
%w(allowed off).each do |target_state|
if transitions.has_key?(target_state)
transitions[target_state]['message'] = warning
transitions[target_state]['locked'] = true unless site_admin
end
end
end
end,
after_state_change_proc: ->(context, old_state, new_state) do
if context.is_a?(Course)
Delayed::Job.enqueue(Features::DifferentiatedAssignments::ModuleEvaluator.new(context.id), max_attempts: 1)
end
end
}
)

View File

@ -914,6 +914,62 @@ describe "Module Items API", type: :request do
json.map{|item| item['id']}.sort.should == @module2.content_tags.map(&:id).sort
end
context 'differentiated_assignments' do
before do
@new_section = @course.course_sections.create!(name: "test section")
@student.enrollments.each(&:destroy!)
student_in_section(@new_section, user: @student)
@assignment.only_visible_to_overrides = true
@assignment.save!
end
context 'enabled' do
before {@course.enable_feature!(:differentiated_assignments)}
context 'with override' do
before{create_section_override_for_assignment(@assignment, {course_section: @new_section})}
it "should list all assignments" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items",
:controller => "context_module_items_api", :action => "index", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}")
json.map{|item| item['id']}.sort.should == @module1.content_tags.map(&:id).sort
end
end
context 'without override' do
it "should exclude unassigned assignments" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items",
:controller => "context_module_items_api", :action => "index", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}")
json.map{|item| item['id']}.sort.should_not == @module1.content_tags.map(&:id).sort
end
end
end
context 'disabled' do
before {@course.disable_feature!(:differentiated_assignments)}
context 'with override' do
before{create_section_override_for_assignment(@assignment, {course_section: @new_section})}
it "should list all assignments" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items",
:controller => "context_module_items_api", :action => "index", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}")
json.map{|item| item['id']}.sort.should == @module1.content_tags.map(&:id).sort
end
end
context 'without override' do
it "should list all assignments" do
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items",
:controller => "context_module_items_api", :action => "index", :format => "json",
:course_id => "#{@course.id}", :module_id => "#{@module1.id}")
json.map{|item| item['id']}.sort.should == @module1.content_tags.map(&:id).sort
end
end
end
end
context 'index including content details' do
let(:json) do
api_call(:get, "/api/v1/courses/#{@course.id}/modules/#{@module1.id}/items?include[]=content_details",

View File

@ -249,7 +249,7 @@ describe Assignment do
end
context "draft state on" do
before(:once) {@course.enable_feature!(:draft_state)}
before {@course.enable_feature!(:draft_state)}
context "differentiated_assignment on" do
before {@course.enable_feature!(:differentiated_assignments)}
it "should return assignments only when a student has overrides" do
@ -338,7 +338,7 @@ describe Assignment do
end
context "differentiated_assignment off" do
before(:once) { @course.disable_feature!(:differentiated_assignments) }
before{ @course.disable_feature!(:differentiated_assignments) }
context "observing only a section with visibility" do
before do
@observer_enrollment = @course.enroll_user(@observer, 'ObserverEnrollment', :section => @section, :enrollment_state => 'active')

View File

@ -516,6 +516,70 @@ describe ContentTag do
end
end
describe "visible_to_students_with_da_enabled" do
before do
course_with_student(active_all: true)
@section = @course.course_sections.create!(name: "test section")
student_in_section(@section, user: @student)
end
context "assignments" do
before do
@assignment = @course.assignments.create!(:title => "some assignment", :only_visible_to_overrides => true)
@module = @course.context_modules.create!(:name => "module")
@tag = @module.add_item({
:type => 'assignment',
:title => 'some assignment',
:id => @assignment.id
})
end
it "returns assignments if there is visibility" do
create_section_override_for_assignment(@assignment, {course_section: @section})
ContentTag.visible_to_students_with_da_enabled(@student.id).should include(@tag)
end
it "does not return assignments if there is no visibility" do
ContentTag.visible_to_students_with_da_enabled(@student.id).should_not include(@tag)
end
end
context "discussions" do
def set_up_discussion
@assignment = @course.assignments.create!(:title => "some discussion assignment",only_visible_to_overrides: true)
@assignment.submission_types = 'discussion_topic'
@assignment.save!
@topic.assignment_id = @assignment.id
@topic.save!
end
before do
discussion_topic_model(:user => @course.instructors.first, :context => @course)
@module = @course.context_modules.create!(:name => "module")
@tag = @module.add_item({
:type => 'discussion_topic',
:title => 'some discussion',
:id => @topic.id
})
end
it "returns discussions without attached assignments" do
ContentTag.visible_to_students_with_da_enabled(@student.id).should include(@tag)
end
it "returns discussions with attached assignments if there is visibility" do
set_up_discussion
create_section_override_for_assignment(@assignment, {course_section: @section})
ContentTag.visible_to_students_with_da_enabled(@student.id).should include(@tag)
end
it "does not return discussions with attached assignments if there is no visibility" do
set_up_discussion
ContentTag.visible_to_students_with_da_enabled(@student.id).should_not include(@tag)
end
end
context "other" do
it "it properly returns wiki pages" do
@page = @course.wiki.wiki_pages.create!(:title => "some page")
@module = @course.context_modules.create!(:name => "module")
@tag = @module.add_item({:type => 'WikiPage', :title => 'oh noes!' * 35, :id => @page.id})
ContentTag.visible_to_students_with_da_enabled(@student.id).should include(@tag)
end
end
end
describe "destroy" do
it "updates completion requirements on its associated ContextModule" do
course_with_teacher(:active_all => true)

View File

@ -161,6 +161,7 @@ describe ContextModule do
context "when draft state is enabled" do
before do
Course.any_instance.stubs(:feature_enabled?).with(:draft_state).returns(true)
Course.any_instance.stubs(:feature_enabled?).with(:differentiated_assignments).returns(false)
end
it "adds external tools with a default workflow_state of anonymous" do
@ -618,6 +619,47 @@ describe ContextModule do
@submission = @assign.submit_homework(@student, submission_type: 'online_text_entry', body: '42')
@module.evaluate_for(@student).should be_completed
end
context "differentiated assignements" do
before do
course_module
@student_1 = student_in_course(course: @course, active_all: true).user
@student_2 = student_in_course(course: @course, active_all: true).user
@student_1.enrollments.each(&:destroy!)
@overriden_section = @course.course_sections.create!(name: "test section")
student_in_section(@overriden_section, user: @student_1)
@assign = @course.assignments.create!(title: 'how many roads must a man walk down?', submission_types: 'online_text_entry')
@assign.only_visible_to_overrides = true
@assign.save!
create_section_override_for_assignment(@assign, {course_section: @overriden_section})
@tag = @module.add_item({id: @assign.id, type: 'assignment'})
@module.completion_requirements = {@tag.id => {type: 'must_submit'}}
@module.save!
end
context "enabled" do
before {@course.enable_feature!(:differentiated_assignments)}
it "should properly require differentiated assignments" do
@module.evaluate_for(@student_1).should be_unlocked
@submission = @assign.submit_homework(@student_1, submission_type: 'online_text_entry', body: '42')
@module.evaluate_for(@student_1).should be_completed
@module.evaluate_for(@student_2).should be_completed
end
end
context "disabled" do
before {@course.disable_feature!(:differentiated_assignments)}
it "should properly require all assignments" do
@module.evaluate_for(@student_1).should be_unlocked
@submission = @assign.submit_homework(@student_1, submission_type: 'online_text_entry', body: '42')
@module.evaluate_for(@student_1).should be_completed
@module.evaluate_for(@student_2).should be_unlocked
end
end
end
end
describe "require_sequential_progress" do
@ -828,6 +870,89 @@ describe ContextModule do
end
end
describe "content_tags_visible_to" do
before do
course_module
@student_1 = student_in_course(course: @course, active_all: true).user
@student_2 = student_in_course(course: @course, active_all: true).user
@student_1.enrollments.each(&:destroy!)
@overriden_section = @course.course_sections.create!(name: "test section")
student_in_section(@overriden_section, user: @student_1)
@assignment = @course.assignments.create!(title: 'how many roads must a man walk down?', submission_types: 'online_text_entry')
@assignment.only_visible_to_overrides = true
@assignment.save!
create_section_override_for_assignment(@assignment, {course_section: @overriden_section})
@tag = @module.add_item({id: @assignment.id, type: 'assignment'})
end
context "differentiated_assignments enabled" do
before {@course.enable_feature!(:differentiated_assignments)}
it "should properly return differentiated assignments" do
@module.content_tags_visible_to(@teacher).map(&:content).include?(@assignment).should be_true
@module.content_tags_visible_to(@student_1).map(&:content).include?(@assignment).should be_true
@module.content_tags_visible_to(@student_2).map(&:content).include?(@assignment).should be_false
end
it "should properly return unpublished assignments" do
@assignment.workflow_state = "unpublished"
@assignment.save!
@module.content_tags_visible_to(@teacher).map(&:content).include?(@assignment).should be_true
@module.content_tags_visible_to(@student_1).map(&:content).include?(@assignment).should be_false
@module.content_tags_visible_to(@student_2).map(&:content).include?(@assignment).should be_false
end
# if tags are preloaded we shouldn't filter by a scope (as that requires re-fetching the tags)
it "should not reload the tags if already loaded" do
ContentTag.expects(:visible_to_students_with_da_enabled).never
ContextModule.send(:preload_associations, [@module], {:content_tags => :content})
@module.content_tags_visible_to(@student_1)
end
# if tags are not preloaded we should filter by a scope (as will be quicker than filtering an array)
it "should filter use a cope to filter content tags if they arent already loaded" do
ContentTag.expects(:visible_to_students_with_da_enabled).once
@module.content_tags_visible_to(@student_1)
end
it "should filter differentiated discussions" do
discussion_topic_model(:user => @teacher, :context => @course)
@discussion_assignment = @course.assignments.create!(:title => "some discussion assignment",only_visible_to_overrides: true)
@discussion_assignment.submission_types = 'discussion_topic'
@discussion_assignment.save!
@topic.assignment_id = @discussion_assignment.id
@topic.save!
create_section_override_for_assignment(@discussion_assignment, {course_section: @overriden_section})
@module.add_item({id: @topic.id, type: 'discussion_topic'})
@module.content_tags_visible_to(@teacher).map(&:content).include?(@topic).should be_true
@module.content_tags_visible_to(@student_1).map(&:content).include?(@topic).should be_true
@module.content_tags_visible_to(@student_2).map(&:content).include?(@topic).should be_false
end
it "should work for observers" do
@observer = User.create
@observer_enrollment = @course.enroll_user(@observer, 'ObserverEnrollment', :section => @overriden_section, :enrollment_state => 'active')
@observer_enrollment.update_attribute(:associated_user_id, @student_2.id)
@module.content_tags_visible_to(@observer).map(&:content).include?(@assignment).should be_false
@observer_enrollment.update_attribute(:associated_user_id, @student_1.id)
@module.content_tags_visible_to(@observer).map(&:content).include?(@assignment).should be_true
end
end
context "differentiated_assignments disabled" do
before {@course.disable_feature!(:differentiated_assignments)}
it "should return all published assignments" do
@module.content_tags_visible_to(@teacher).map(&:content).include?(@assignment).should be_true
@module.content_tags_visible_to(@student_1).map(&:content).include?(@assignment).should be_true
@module.content_tags_visible_to(@student_2).map(&:content).include?(@assignment).should be_true
end
it "should not return unpublished assignments" do
@assignment.workflow_state = "unpublished"
@assignment.save!
@module.content_tags_visible_to(@teacher).map(&:content).include?(@assignment).should be_true
@module.content_tags_visible_to(@student_1).map(&:content).include?(@assignment).should be_false
@module.content_tags_visible_to(@student_2).map(&:content).include?(@assignment).should be_false
end
end
end
describe "#find_or_create_progression" do
it "should not create progressions for non-enrolled users" do
course = Course.create!

View File

@ -177,6 +177,32 @@ describe "context_modules" do
driver.current_url.should match %r{/courses/#{@course.id}/quizzes/#{@quiz_1.id}}
end
it "should validate that a students cannot see unassigned differentiated assignments" do
@assignment_2.only_visible_to_overrides = true
@assignment_2.save!
@course.enable_feature!(:differentiated_assignments)
@student.enrollments.each(&:destroy)
@overriden_section = @course.course_sections.create!(name: "test section")
student_in_section(@overriden_section, user: @student)
go_to_modules
context_modules = ff('.context_module')
context_modules[0].find_element(:css, '.context_module_items').should_not include_text(@assignment_2.name)
context_modules[1].find_element(:css, '.context_module_items').should_not include_text(@assignment_2.name)
# Should not redirect to the hidden assignment
get "/courses/#{@course.id}/modules/#{@module_2.id}/items/first"
driver.current_url.should_not match %r{/courses/#{@course.id}/assignments/#{@assignment_2.id}}
create_section_override_for_assignment(@assignment_2, {course_section: @overriden_section})
# Should redirect to the now visible assignment
get "/courses/#{@course.id}/modules/#{@module_2.id}/items/first"
driver.current_url.should match %r{/courses/#{@course.id}/assignments/#{@assignment_2.id}}
end
it "should allow a student view student to progress through module content" do
course_with_teacher_logged_in(:course => @course, :active_all => true)
@fake_student = @course.student_view_student