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:
parent
80ce61ea62
commit
506e786db4
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) %>
|
||||
|
|
|
@ -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} %>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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') },
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
|
@ -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",
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue