Add the ability to duplicate modules.
Closes COMMS-148 Closes COMMS-147 Test Plan: * Create a module with a variety of items in it. * Duplicate it. * The new module should appear now. * Make sure that this all works in conjunction with various other ui actions on the page without needing a refresh. * The "duplicate" option should appear iff the module in question has no quizzes in it. Modules with quizzes cannot be duplicated for the moment. * Make sure that the ability to "duplicate" is updated properly if quizzes are added to/removed from the module. Change-Id: I288c56f42a89abad5c725fa3cbd91b02ea11a460 Reviewed-on: https://gerrit.instructure.com/128960 Reviewed-by: Felix Milea-Ciobanu <fmileaciobanu@instructure.com> QA-Review: Heath Hales <hhales@instructure.com> Tested-by: Jenkins Product-Review: Christi Wruck
This commit is contained in:
parent
625da046e6
commit
d352cfbd8b
|
@ -415,6 +415,36 @@ class ContextModulesApiController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def duplicate
|
||||||
|
if authorized_action(@context, @current_user, :manage_content)
|
||||||
|
old_module = @context.modules_visible_to(@current_user).find(params[:module_id])
|
||||||
|
if !@context.root_account.feature_enabled?(:duplicate_modules)
|
||||||
|
return render json: { error: 'duplicating objects not enabled' }, status: :bad_request
|
||||||
|
end
|
||||||
|
return render json: { error: 'unable to find module to duplicate' }, status: :bad_request unless old_module
|
||||||
|
return render json: { error: 'cannot duplicate this module' }, status: :bad_request unless old_module.can_be_duplicated?
|
||||||
|
|
||||||
|
new_module = old_module.duplicate
|
||||||
|
new_module.save!
|
||||||
|
new_module.insert_at(old_module.position + 1)
|
||||||
|
if new_module
|
||||||
|
result_json = new_module.as_json(include: :content_tags, methods: :workflow_state)
|
||||||
|
attachment_tags = new_module.content_tags.select do |content_tag|
|
||||||
|
content_tag.content_type == 'Attachment'
|
||||||
|
end
|
||||||
|
result_json['ENV_UPDATE'] = attachment_tags.map do |attachment_tag|
|
||||||
|
{ :id => attachment_tag.id.to_s,
|
||||||
|
:content_id => attachment_tag.content_id,
|
||||||
|
:content_details => content_details(attachment_tag, @current_user, :for_admin => true)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
render :json => result_json
|
||||||
|
else
|
||||||
|
render :json => { error: 'cannot save new module' }, status: :bad_request
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# @note API Update multiple modules
|
# @note API Update multiple modules
|
||||||
#
|
#
|
||||||
# Update multiple modules in an account.
|
# Update multiple modules in an account.
|
||||||
|
|
|
@ -75,6 +75,7 @@ class ContextModulesController < ApplicationController
|
||||||
module_file_details = load_module_file_details if @context.grants_right?(@current_user, session, :manage_content)
|
module_file_details = load_module_file_details if @context.grants_right?(@current_user, session, :manage_content)
|
||||||
js_env :course_id => @context.id,
|
js_env :course_id => @context.id,
|
||||||
:CONTEXT_URL_ROOT => polymorphic_path([@context]),
|
:CONTEXT_URL_ROOT => polymorphic_path([@context]),
|
||||||
|
:DUPLICATE_ENABLED => @domain_root_account.feature_enabled?(:duplicate_modules),
|
||||||
:FILES_CONTEXTS => [{asset_string: @context.asset_string}],
|
:FILES_CONTEXTS => [{asset_string: @context.asset_string}],
|
||||||
:MODULE_FILE_DETAILS => module_file_details,
|
:MODULE_FILE_DETAILS => module_file_details,
|
||||||
:MODULE_FILE_PERMISSIONS => {
|
:MODULE_FILE_PERMISSIONS => {
|
||||||
|
|
|
@ -174,7 +174,7 @@ class Assignment < ActiveRecord::Base
|
||||||
# override later. Just helps to avoid duplicate positions.
|
# override later. Just helps to avoid duplicate positions.
|
||||||
result.position = Assignment.active.where(assignment_group: assignment_group).maximum(:position) + 1
|
result.position = Assignment.active.where(assignment_group: assignment_group).maximum(:position) + 1
|
||||||
result.title =
|
result.title =
|
||||||
opts_with_default[:copy_title] ? opts_with_default[:copy_title] : get_copy_title(self, t("Copy"))
|
opts_with_default[:copy_title] ? opts_with_default[:copy_title] : get_copy_title(self, t("Copy"), self.title)
|
||||||
|
|
||||||
if self.wiki_page && opts_with_default[:duplicate_wiki_page]
|
if self.wiki_page && opts_with_default[:duplicate_wiki_page]
|
||||||
result.wiki_page = self.wiki_page.duplicate({
|
result.wiki_page = self.wiki_page.duplicate({
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
class ContextModule < ActiveRecord::Base
|
class ContextModule < ActiveRecord::Base
|
||||||
include Workflow
|
include Workflow
|
||||||
include SearchTermHelper
|
include SearchTermHelper
|
||||||
|
include DuplicatingObjects
|
||||||
|
|
||||||
belongs_to :context, polymorphic: [:course]
|
belongs_to :context, polymorphic: [:course]
|
||||||
has_many :context_module_progressions, :dependent => :destroy
|
has_many :context_module_progressions, :dependent => :destroy
|
||||||
|
@ -155,6 +156,81 @@ class ContextModule < ActiveRecord::Base
|
||||||
self.position
|
self.position
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_potentially_conflicting_titles(title_base)
|
||||||
|
ContextModule.not_deleted.where(context_id: self.context_id)
|
||||||
|
.starting_with_name(title_base).pluck("name").to_set
|
||||||
|
end
|
||||||
|
|
||||||
|
def duplicate_base_model(copy_title)
|
||||||
|
ContextModule.new({
|
||||||
|
:context_id => self.context_id,
|
||||||
|
:context_type => self.context_type,
|
||||||
|
:name => copy_title,
|
||||||
|
:position => ContextModule.not_deleted.where(context_id: self.context_id).maximum(:position) + 1,
|
||||||
|
:prerequisites => self.prerequisites,
|
||||||
|
:completion_requirements => self.completion_requirements,
|
||||||
|
:workflow_state => 'unpublished',
|
||||||
|
:unlock_at => self.unlock_at,
|
||||||
|
:require_sequential_progress => self.require_sequential_progress,
|
||||||
|
:completion_events => self.completion_events,
|
||||||
|
:requirement_count => self.requirement_count
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_be_duplicated?
|
||||||
|
self.content_tags.none? do |content_tag|
|
||||||
|
!content_tag.deleted? && content_tag.content_type_class == 'quiz'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This is intended for duplicating a content tag when we are duplicating a module
|
||||||
|
# Not intended for duplicating a content tag to keep in the original module
|
||||||
|
def duplicate_content_tag_base_model(original_content_tag)
|
||||||
|
ContentTag.new(
|
||||||
|
:content_id => original_content_tag.content_id,
|
||||||
|
:content_type => original_content_tag.content_type,
|
||||||
|
:context_id => original_content_tag.context_id,
|
||||||
|
:context_type => original_content_tag.context_type,
|
||||||
|
:title => original_content_tag.title,
|
||||||
|
:tag_type => original_content_tag.tag_type,
|
||||||
|
:position => original_content_tag.position,
|
||||||
|
:indent => original_content_tag.indent,
|
||||||
|
:learning_outcome_id => original_content_tag.learning_outcome_id,
|
||||||
|
:context_code => original_content_tag.context_code,
|
||||||
|
:mastery_score => original_content_tag.mastery_score,
|
||||||
|
:workflow_state => 'unpublished'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
private :duplicate_content_tag_base_model
|
||||||
|
|
||||||
|
# Intended for taking a content_tag in this module and duplicating it
|
||||||
|
# into a new module. Not intended for duplicating a content tag to be
|
||||||
|
# kept in the same module.
|
||||||
|
def duplicate_content_tag(original_content_tag)
|
||||||
|
new_tag = duplicate_content_tag_base_model(original_content_tag)
|
||||||
|
if original_content_tag.content&.respond_to?(:duplicate)
|
||||||
|
new_tag.content = original_content_tag.content.duplicate
|
||||||
|
# If we have multiple assignments (e.g.) make sure they each get unused titles.
|
||||||
|
# A title isn't marked used if the assignment hasn't been saved yet.
|
||||||
|
new_tag.content.save!
|
||||||
|
new_tag.title = nil
|
||||||
|
end
|
||||||
|
new_tag
|
||||||
|
end
|
||||||
|
private :duplicate_content_tag
|
||||||
|
|
||||||
|
def duplicate
|
||||||
|
copy_title = get_copy_title(self, t("Copy"), self.name)
|
||||||
|
new_module = duplicate_base_model(copy_title)
|
||||||
|
living_tags = self.content_tags.select do |content_tag|
|
||||||
|
!content_tag.deleted?
|
||||||
|
end
|
||||||
|
new_module.content_tags = living_tags.map do |content_tag|
|
||||||
|
duplicate_content_tag(content_tag)
|
||||||
|
end
|
||||||
|
new_module
|
||||||
|
end
|
||||||
|
|
||||||
def validate_prerequisites
|
def validate_prerequisites
|
||||||
positions = ContextModule.module_positions(self.context)
|
positions = ContextModule.module_positions(self.context)
|
||||||
@already_confirmed_valid_requirements = false
|
@already_confirmed_valid_requirements = false
|
||||||
|
@ -207,7 +283,9 @@ class ContextModule < ActiveRecord::Base
|
||||||
scope :active, -> { where(:workflow_state => 'active') }
|
scope :active, -> { where(:workflow_state => 'active') }
|
||||||
scope :unpublished, -> { where(:workflow_state => 'unpublished') }
|
scope :unpublished, -> { where(:workflow_state => 'unpublished') }
|
||||||
scope :not_deleted, -> { where("context_modules.workflow_state<>'deleted'") }
|
scope :not_deleted, -> { where("context_modules.workflow_state<>'deleted'") }
|
||||||
|
scope :starting_with_name, lambda { |name|
|
||||||
|
where('name ILIKE ?', "#{name}%")
|
||||||
|
}
|
||||||
alias_method :published?, :active?
|
alias_method :published?, :active?
|
||||||
|
|
||||||
def publish_items!
|
def publish_items!
|
||||||
|
|
|
@ -331,7 +331,8 @@ class DiscussionTopic < ActiveRecord::Base
|
||||||
:user => nil
|
:user => nil
|
||||||
}
|
}
|
||||||
opts_with_default = default_opts.merge(opts)
|
opts_with_default = default_opts.merge(opts)
|
||||||
copy_title = opts_with_default[:copy_title] ? opts_with_default[:copy_title] : get_copy_title(self, t("Copy"))
|
copy_title =
|
||||||
|
opts_with_default[:copy_title] ? opts_with_default[:copy_title] : get_copy_title(self, t("Copy"), self.title)
|
||||||
result = self.duplicate_base_model(copy_title, opts_with_default)
|
result = self.duplicate_base_model(copy_title, opts_with_default)
|
||||||
|
|
||||||
if self.assignment && opts_with_default[:duplicate_assignment]
|
if self.assignment && opts_with_default[:duplicate_assignment]
|
||||||
|
|
|
@ -424,7 +424,8 @@ class WikiPage < ActiveRecord::Base
|
||||||
}
|
}
|
||||||
opts_with_default = default_opts.merge(opts)
|
opts_with_default = default_opts.merge(opts)
|
||||||
result = WikiPage.new({
|
result = WikiPage.new({
|
||||||
:title => opts_with_default[:copy_title] ? opts_with_default[:copy_title] : get_copy_title(self, t("Copy")),
|
:title =>
|
||||||
|
opts_with_default[:copy_title] ? opts_with_default[:copy_title] : get_copy_title(self, t("Copy"), self.title),
|
||||||
:wiki_id => self.wiki_id,
|
:wiki_id => self.wiki_id,
|
||||||
:context_id => self.context_id,
|
:context_id => self.context_id,
|
||||||
:context_type => self.context_type,
|
:context_type => self.context_type,
|
||||||
|
|
|
@ -155,6 +155,13 @@
|
||||||
class="delete_module_link icon-trash"
|
class="delete_module_link icon-trash"
|
||||||
title="<%= t('links.title.delete_module', %{Delete this module}) %>"><%= t('links.text.delete_module', %{Delete}) %></a>
|
title="<%= t('links.title.delete_module', %{Delete this module}) %>"><%= t('links.text.delete_module', %{Delete}) %></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li role="presentation" class="duplicate_module_menu_item">
|
||||||
|
<a
|
||||||
|
href="/api/v1<%= context_url(@context, :context_url) %>/modules/<%= context_module ? context_module.id : "{{ id }}" %>/duplicate"
|
||||||
|
class="duplicate_module_link icon-copy-course"
|
||||||
|
aria-label="<%= t('Duplicate Module %{module_name}') %>"
|
||||||
|
title="<%= t(%{Delete this module}) %>"><%= t(%{Duplicate}) %></a>
|
||||||
|
</li>
|
||||||
<%= external_tools_menu_items(@menu_tools[:module_menu], {link_class: "menu_tool_link", settings_key: :module_menu, in_list: true, url_params: {:modules => [context_module ? context_module.id : "{{ id }}"]}}) %>
|
<%= external_tools_menu_items(@menu_tools[:module_menu], {link_class: "menu_tool_link", settings_key: :module_menu, in_list: true, url_params: {:modules => [context_module ? context_module.id : "{{ id }}"]}}) %>
|
||||||
</ul>
|
</ul>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1556,6 +1556,7 @@ CanvasRails::Application.routes.draw do
|
||||||
get "courses/:course_id/modules", action: :index, as: 'course_context_modules'
|
get "courses/:course_id/modules", action: :index, as: 'course_context_modules'
|
||||||
get "courses/:course_id/modules/:id", action: :show, as: 'course_context_module'
|
get "courses/:course_id/modules/:id", action: :show, as: 'course_context_module'
|
||||||
put "courses/:course_id/modules", action: :batch_update
|
put "courses/:course_id/modules", action: :batch_update
|
||||||
|
post "courses/:course_id/modules/:module_id/duplicate", action: :duplicate
|
||||||
post "courses/:course_id/modules", action: :create, as: 'course_context_module_create'
|
post "courses/:course_id/modules", action: :create, as: 'course_context_module_create'
|
||||||
put "courses/:course_id/modules/:id", action: :update, as: 'course_context_module_update'
|
put "courses/:course_id/modules/:id", action: :update, as: 'course_context_module_update'
|
||||||
delete "courses/:course_id/modules/:id", action: :destroy
|
delete "courses/:course_id/modules/:id", action: :destroy
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
module DuplicatingObjects
|
module DuplicatingObjects
|
||||||
# Lowercases title and replaces spaces with hyphens (to allow to check for
|
# Lowercases title and replaces spaces with hyphens (to allow to check for
|
||||||
# matching titles that differ only in case or space/hyphens
|
# matching titles that differ only in case or space/hyphens)
|
||||||
def normalize_title(title)
|
def normalize_title(title)
|
||||||
title.gsub(/ /, '-').downcase
|
title.gsub(/ /, '-').downcase
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,7 @@ module DuplicatingObjects
|
||||||
# should provide a function "get_potentially_conflicting_titles" that
|
# should provide a function "get_potentially_conflicting_titles" that
|
||||||
# returns a set of titles that might conflict with the entity's title.
|
# returns a set of titles that might conflict with the entity's title.
|
||||||
#
|
#
|
||||||
# If entity.title ends in "#{copy_suffix} #" (or without the number),
|
# If the given title ends in "#{copy_suffix} #" (or without the number),
|
||||||
# tries incrementing from # (or 2 if no number is given) until one is
|
# tries incrementing from # (or 2 if no number is given) until one is
|
||||||
# found that isn't in the set returned by entity.get_potentially_conflicting_titles
|
# found that isn't in the set returned by entity.get_potentially_conflicting_titles
|
||||||
#
|
#
|
||||||
|
@ -36,16 +36,16 @@ module DuplicatingObjects
|
||||||
#
|
#
|
||||||
# For the purposes of matching, conflicts are case-insensitive and also
|
# For the purposes of matching, conflicts are case-insensitive and also
|
||||||
# treats hyphens and spaces as the same thing.
|
# treats hyphens and spaces as the same thing.
|
||||||
def get_copy_title(entity, copy_suffix)
|
def get_copy_title(entity, copy_suffix, entity_title)
|
||||||
title = entity.title
|
is_multiple_copy = !(normalize_title(entity_title)=~
|
||||||
is_multiple_copy = !(normalize_title(title)=~
|
|
||||||
/#{Regexp.quote(copy_suffix.downcase)}-[0-9]*$/).nil?
|
/#{Regexp.quote(copy_suffix.downcase)}-[0-9]*$/).nil?
|
||||||
if title.end_with?(copy_suffix) || is_multiple_copy
|
normalized_suffix = normalize_title(copy_suffix)
|
||||||
potential_title = title
|
if normalize_title(entity_title).end_with?(normalized_suffix) || is_multiple_copy
|
||||||
possible_num_to_try = normalize_title(title).scan(/\d+$/).first
|
potential_title = entity_title
|
||||||
|
possible_num_to_try = normalize_title(entity_title).scan(/\d+$/).first
|
||||||
num_to_try = possible_num_to_try ? possible_num_to_try.to_i + 1 : 2
|
num_to_try = possible_num_to_try ? possible_num_to_try.to_i + 1 : 2
|
||||||
else
|
else
|
||||||
potential_title = "#{title} #{copy_suffix}"
|
potential_title = "#{entity_title} #{copy_suffix}"
|
||||||
num_to_try = 2
|
num_to_try = 2
|
||||||
end
|
end
|
||||||
title_base = !is_multiple_copy ? potential_title + " " : potential_title.gsub(/\d+$/, '')
|
title_base = !is_multiple_copy ? potential_title + " " : potential_title.gsub(/\d+$/, '')
|
||||||
|
|
|
@ -244,6 +244,15 @@ END
|
||||||
root_opt_in: true,
|
root_opt_in: true,
|
||||||
beta: true
|
beta: true
|
||||||
},
|
},
|
||||||
|
'duplicate_modules' =>
|
||||||
|
{
|
||||||
|
display_name: -> { I18n.t('Duplicate Modules') },
|
||||||
|
description: -> { I18n.t("Allows the duplicating of modules in Canvas") },
|
||||||
|
applies_to: 'Account',
|
||||||
|
state: 'hidden',
|
||||||
|
root_opt_in: true,
|
||||||
|
beta: true
|
||||||
|
},
|
||||||
'allow_opt_out_of_inbox' =>
|
'allow_opt_out_of_inbox' =>
|
||||||
{
|
{
|
||||||
display_name: -> { I18n.t('features.allow_opt_out_of_inbox', "Allow Users to Opt-out of the Inbox") },
|
display_name: -> { I18n.t('features.allow_opt_out_of_inbox', "Allow Users to Opt-out of the Inbox") },
|
||||||
|
|
|
@ -57,6 +57,13 @@ import './vendor/jquery.scrollTo'
|
||||||
import 'jqueryui/sortable'
|
import 'jqueryui/sortable'
|
||||||
import 'compiled/jquery.rails_flash_notifications'
|
import 'compiled/jquery.rails_flash_notifications'
|
||||||
|
|
||||||
|
function refreshDuplicateLinkStatus($module) {
|
||||||
|
if (ENV.DUPLICATE_ENABLED && !$module.find('.context_module_item.quiz').length) {
|
||||||
|
$module.find('.duplicate_module_menu_item').show();
|
||||||
|
} else {
|
||||||
|
$module.find('.duplicate_module_menu_item').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
// TODO: AMD don't export global, use as module
|
// TODO: AMD don't export global, use as module
|
||||||
/*global modules*/
|
/*global modules*/
|
||||||
window.modules = (function() {
|
window.modules = (function() {
|
||||||
|
@ -135,6 +142,9 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
$module.find(".content").errorBox(I18n.t('errors.reorder', 'Reorder failed, please try again.'));
|
$module.find(".content").errorBox(I18n.t('errors.reorder', 'Reorder failed, please try again.'));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
$('.context_module').each(function() {
|
||||||
|
refreshDuplicateLinkStatus($(this));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProgressions: function(callback) {
|
updateProgressions: function(callback) {
|
||||||
|
@ -424,6 +434,7 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
$before.before($item.show());
|
$before.before($item.show());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refreshDuplicateLinkStatus($module);
|
||||||
return $item;
|
return $item;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1022,6 +1033,38 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".duplicate_module_link").live('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const duplicateRequestUrl = $(this).attr('href');
|
||||||
|
const duplicatedModuleElement = $(this).parents(".context_module");
|
||||||
|
const duplicatedModuleElementId = duplicatedModuleElement[0].id;
|
||||||
|
|
||||||
|
const renderDuplicatedModule = function(response) {
|
||||||
|
response.data.ENV_UPDATE.forEach((newAttachmentItem) => {
|
||||||
|
ENV.MODULE_FILE_DETAILS[newAttachmentItem.id] = newAttachmentItem
|
||||||
|
});
|
||||||
|
const contextModule = response.data.context_module;
|
||||||
|
const newModuleId = response.data.context_module.id;
|
||||||
|
var context_response = response;
|
||||||
|
axios.get(location.href).then((response) => {
|
||||||
|
const $newContent = $(response.data);
|
||||||
|
const $newModule = $newContent.find("#context_module_" + newModuleId);
|
||||||
|
$newModule.insertAfter(duplicatedModuleElement);
|
||||||
|
modules.updateAssignmentData();
|
||||||
|
// Without these two 'die' commands, the event handler happens twice after
|
||||||
|
// initModuleManagement is called.
|
||||||
|
$('.delete_module_link').die();
|
||||||
|
$('.duplicate_module_link').die();
|
||||||
|
$(".context_module").find(".expand_module_link,.collapse_module_link").bind('click keyclick', toggleModuleCollapse);
|
||||||
|
modules.initModuleManagement();
|
||||||
|
}).catch(showFlashError(I18n.t('Error rendering duplicated module')));
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.post(duplicateRequestUrl, {})
|
||||||
|
.then(renderDuplicatedModule)
|
||||||
|
.catch(showFlashError(I18n.t('Error duplicating module')));
|
||||||
|
});
|
||||||
|
|
||||||
$(".delete_module_link").live('click', function(event) {
|
$(".delete_module_link").live('click', function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
$(this).parents(".context_module").confirmDelete({
|
$(this).parents(".context_module").confirmDelete({
|
||||||
|
@ -1139,6 +1182,7 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
var $currentCogLink = $(this).closest('.cog-menu-container').children('.al-trigger');
|
var $currentCogLink = $(this).closest('.cog-menu-container').children('.al-trigger');
|
||||||
// Get the previous cog item to focus after delete
|
// Get the previous cog item to focus after delete
|
||||||
var $allInCurrentModule = $(this).parents('.context_module_items').children()
|
var $allInCurrentModule = $(this).parents('.context_module_items').children()
|
||||||
|
var $currentModule = $(this).parents('.context_module');
|
||||||
var curIndex = $allInCurrentModule.index($(this).parents('.context_module_item'));
|
var curIndex = $allInCurrentModule.index($(this).parents('.context_module_item'));
|
||||||
var newIndex = curIndex - 1;
|
var newIndex = curIndex - 1;
|
||||||
var $previousCogLink;
|
var $previousCogLink;
|
||||||
|
@ -1156,6 +1200,7 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
modules.updateTaggedItems();
|
modules.updateTaggedItems();
|
||||||
$previousCogLink.focus();
|
$previousCogLink.focus();
|
||||||
|
refreshDuplicateLinkStatus($currentModule);
|
||||||
});
|
});
|
||||||
$.flashMessage(I18n.t("Module item %{module_item_name} was successfully deleted.", {module_item_name: data.content_tag.title}));
|
$.flashMessage(I18n.t("Module item %{module_item_name} was successfully deleted.", {module_item_name: data.content_tag.title}));
|
||||||
},
|
},
|
||||||
|
@ -1454,7 +1499,6 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
|
|
||||||
var initPublishButton = function($el, data) {
|
var initPublishButton = function($el, data) {
|
||||||
data = data || $el.data();
|
data = data || $el.data();
|
||||||
|
|
||||||
if(data.moduleType == 'attachment'){
|
if(data.moduleType == 'attachment'){
|
||||||
// Module isNew if it was created with an ajax request vs being loaded when the page loads
|
// Module isNew if it was created with an ajax request vs being loaded when the page loads
|
||||||
var moduleItem = {};
|
var moduleItem = {};
|
||||||
|
@ -1650,7 +1694,95 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var toggleModuleCollapse = function(event, goSlow) {
|
||||||
|
event.preventDefault();
|
||||||
|
var expandCallback = null;
|
||||||
|
if(goSlow && $.isFunction(goSlow)) {
|
||||||
|
expandCallback = goSlow;
|
||||||
|
goSlow = null;
|
||||||
|
}
|
||||||
|
var collapse = $(this).hasClass('collapse_module_link') ? '1' : '0';
|
||||||
|
var $module = $(this).parents(".context_module");
|
||||||
|
var reload_entries = $module.find(".content .context_module_items").children().length === 0;
|
||||||
|
var toggle = function(show) {
|
||||||
|
var callback = function() {
|
||||||
|
$module.find(".collapse_module_link").css('display', $module.find(".content:visible").length > 0 ? 'inline-block' : 'none');
|
||||||
|
$module.find(".expand_module_link").css('display', $module.find(".content:visible").length === 0 ? 'inline-block' : 'none');
|
||||||
|
if($module.find(".content:visible").length > 0) {
|
||||||
|
$module.find(".footer .manage_module").css('display', '');
|
||||||
|
$module.toggleClass('collapsed_module', false);
|
||||||
|
// Makes sure the resulting item has focus.
|
||||||
|
$module.find(".collapse_module_link").focus();
|
||||||
|
$.screenReaderFlashMessage(I18n.t('Expanded'));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$module.find(".footer .manage_module").css('display', ''); //'none');
|
||||||
|
$module.toggleClass('collapsed_module', true);
|
||||||
|
// Makes sure the resulting item has focus.
|
||||||
|
$module.find(".expand_module_link").focus();
|
||||||
|
$.screenReaderFlashMessage(I18n.t('Collapsed'));
|
||||||
|
}
|
||||||
|
if(expandCallback && $.isFunction(expandCallback)) {
|
||||||
|
expandCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if(show) {
|
||||||
|
$module.find(".content").show();
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
$module.find(".content").slideToggle(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if(reload_entries || goSlow) {
|
||||||
|
$module.loadingImage();
|
||||||
|
}
|
||||||
|
var url = $(this).attr('href');
|
||||||
|
if(goSlow) {
|
||||||
|
url = $module.find(".edit_module_link").attr('href');
|
||||||
|
}
|
||||||
|
$.ajaxJSON(url, (goSlow ? 'GET' : 'POST'), {collapse: collapse}, function(data) {
|
||||||
|
if(goSlow) {
|
||||||
|
$module.loadingImage('remove');
|
||||||
|
var items = data;
|
||||||
|
var next = function() {
|
||||||
|
var item = items.shift();
|
||||||
|
if(item) {
|
||||||
|
modules.addItemToModule($module, item.content_tag);
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
$module.find(".context_module_items.ui-sortable").sortable('refresh');
|
||||||
|
toggle(true);
|
||||||
|
modules.updateProgressionState($module);
|
||||||
|
$("#context_modules").triggerHandler('slow_load');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
if(reload_entries) {
|
||||||
|
$module.loadingImage('remove');
|
||||||
|
for(var idx in data) {
|
||||||
|
modules.addItemToModule($module, data[idx].content_tag);
|
||||||
|
}
|
||||||
|
$module.find(".context_module_items.ui-sortable").sortable('refresh');
|
||||||
|
toggle();
|
||||||
|
modules.updateProgressionState($module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, function(data) {
|
||||||
|
$module.loadingImage('remove');
|
||||||
|
});
|
||||||
|
if(collapse == '1' || !reload_entries) {
|
||||||
|
toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// THAT IS THE END
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
$('.context_module').each(function() {
|
||||||
|
refreshDuplicateLinkStatus($(this));
|
||||||
|
});
|
||||||
if (ENV.IS_STUDENT) {
|
if (ENV.IS_STUDENT) {
|
||||||
$('.context_module').addClass('student-view');
|
$('.context_module').addClass('student-view');
|
||||||
$('.context_module_item .ig-row').addClass('student-view');
|
$('.context_module_item .ig-row').addClass('student-view');
|
||||||
|
@ -1822,89 +1954,7 @@ import 'compiled/jquery.rails_flash_notifications'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".context_module").find(".expand_module_link,.collapse_module_link").bind('click keyclick', function(event, goSlow) {
|
$(".context_module").find(".expand_module_link,.collapse_module_link").bind('click keyclick', toggleModuleCollapse);
|
||||||
event.preventDefault();
|
|
||||||
var expandCallback = null;
|
|
||||||
if(goSlow && $.isFunction(goSlow)) {
|
|
||||||
expandCallback = goSlow;
|
|
||||||
goSlow = null;
|
|
||||||
}
|
|
||||||
var collapse = $(this).hasClass('collapse_module_link') ? '1' : '0';
|
|
||||||
var $module = $(this).parents(".context_module");
|
|
||||||
var reload_entries = $module.find(".content .context_module_items").children().length === 0;
|
|
||||||
var toggle = function(show) {
|
|
||||||
var callback = function() {
|
|
||||||
$module.find(".collapse_module_link").css('display', $module.find(".content:visible").length > 0 ? 'inline-block' : 'none');
|
|
||||||
$module.find(".expand_module_link").css('display', $module.find(".content:visible").length === 0 ? 'inline-block' : 'none');
|
|
||||||
if($module.find(".content:visible").length > 0) {
|
|
||||||
$module.find(".footer .manage_module").css('display', '');
|
|
||||||
$module.toggleClass('collapsed_module', false);
|
|
||||||
// Makes sure the resulting item has focus.
|
|
||||||
$module.find(".collapse_module_link").focus();
|
|
||||||
$.screenReaderFlashMessage(I18n.t('Expanded'));
|
|
||||||
|
|
||||||
} else {
|
|
||||||
$module.find(".footer .manage_module").css('display', ''); //'none');
|
|
||||||
$module.toggleClass('collapsed_module', true);
|
|
||||||
// Makes sure the resulting item has focus.
|
|
||||||
$module.find(".expand_module_link").focus();
|
|
||||||
$.screenReaderFlashMessage(I18n.t('Collapsed'));
|
|
||||||
}
|
|
||||||
if(expandCallback && $.isFunction(expandCallback)) {
|
|
||||||
expandCallback();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if(show) {
|
|
||||||
$module.find(".content").show();
|
|
||||||
callback();
|
|
||||||
} else {
|
|
||||||
$module.find(".content").slideToggle(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if(reload_entries || goSlow) {
|
|
||||||
$module.loadingImage();
|
|
||||||
}
|
|
||||||
var url = $(this).attr('href');
|
|
||||||
if(goSlow) {
|
|
||||||
url = $module.find(".edit_module_link").attr('href');
|
|
||||||
}
|
|
||||||
$.ajaxJSON(url, (goSlow ? 'GET' : 'POST'), {collapse: collapse}, function(data) {
|
|
||||||
if(goSlow) {
|
|
||||||
$module.loadingImage('remove');
|
|
||||||
var items = data;
|
|
||||||
var next = function() {
|
|
||||||
var item = items.shift();
|
|
||||||
if(item) {
|
|
||||||
modules.addItemToModule($module, item.content_tag);
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
$module.find(".context_module_items.ui-sortable").sortable('refresh');
|
|
||||||
toggle(true);
|
|
||||||
modules.updateProgressionState($module);
|
|
||||||
$("#context_modules").triggerHandler('slow_load');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
if(reload_entries) {
|
|
||||||
$module.loadingImage('remove');
|
|
||||||
for(var idx in data) {
|
|
||||||
modules.addItemToModule($module, data[idx].content_tag);
|
|
||||||
}
|
|
||||||
$module.find(".context_module_items.ui-sortable").sortable('refresh');
|
|
||||||
toggle();
|
|
||||||
modules.updateProgressionState($module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, function(data) {
|
|
||||||
$module.loadingImage('remove');
|
|
||||||
});
|
|
||||||
if(collapse == '1' || !reload_entries) {
|
|
||||||
toggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
$(document).fragmentChange(function(event, hash) {
|
$(document).fragmentChange(function(event, hash) {
|
||||||
if (hash == '#student_progressions') {
|
if (hash == '#student_progressions') {
|
||||||
$(".module_progressions_link").trigger('click');
|
$(".module_progressions_link").trigger('click');
|
||||||
|
|
|
@ -72,6 +72,66 @@ describe "Modules API", type: :request do
|
||||||
course_with_teacher(:course => @course, :active_all => true)
|
course_with_teacher(:course => @course, :active_all => true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "duplicating" do
|
||||||
|
it "can duplicate if no quiz" do
|
||||||
|
@course.account.enable_feature!(:duplicate_modules)
|
||||||
|
course_module = @course.context_modules.create!(:name => "empty module", :workflow_state => "published")
|
||||||
|
assignment = @course.assignments.create!(
|
||||||
|
:name => "some assignment to duplicate",
|
||||||
|
:workflow_state => "published"
|
||||||
|
)
|
||||||
|
course_module.add_item(:id => assignment.id, :type => 'assignment')
|
||||||
|
course_module.add_item(:type => 'context_module_sub_header', :title => 'some header')
|
||||||
|
course_module.save!
|
||||||
|
json = api_call(:post, "/api/v1/courses/#{@course.id}/modules/#{course_module.id}/duplicate",
|
||||||
|
{ :controller => "context_modules_api", :action => "duplicate", :format => "json",
|
||||||
|
:course_id => @course.id.to_s, :module_id => course_module.id.to_s },
|
||||||
|
{}, {},
|
||||||
|
{:expected_status => 200})
|
||||||
|
expect(json['context_module']['name']).to eq('empty module Copy')
|
||||||
|
expect(json['context_module']['workflow_state']).to eq('unpublished')
|
||||||
|
expect(json['context_module']['content_tags'].length).to eq(2)
|
||||||
|
content_tags = json['context_module']['content_tags']
|
||||||
|
expect(content_tags[0]['content_tag']['title']).to eq('some assignment to duplicate Copy')
|
||||||
|
expect(content_tags[0]['content_tag']['content_type']).to eq('Assignment')
|
||||||
|
expect(content_tags[0]['content_tag']['workflow_state']).to eq('unpublished')
|
||||||
|
expect(content_tags[1]['content_tag']['title']).to eq('some header')
|
||||||
|
expect(content_tags[1]['content_tag']['content_type']).to eq('ContextModuleSubHeader')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "cannot duplicate module with quiz" do
|
||||||
|
@course.account.enable_feature!(:duplicate_modules)
|
||||||
|
course_module = @course.context_modules.create!(:name => "empty module", :workflow_state => "published")
|
||||||
|
# To be rigorous, make a quiz and add it as an *assignment*
|
||||||
|
quiz = @course.quizzes.build(:title => "some quiz", :quiz_type => "assignment")
|
||||||
|
quiz.save!
|
||||||
|
course_module.add_item(:id => quiz.assignment_id, :type => 'assignment')
|
||||||
|
course_module.save!
|
||||||
|
api_call(:post, "/api/v1/courses/#{@course.id}/modules/#{course_module.id}/duplicate",
|
||||||
|
{ :controller => "context_modules_api", :action => "duplicate", :format => "json",
|
||||||
|
:course_id => @course.id.to_s, :module_id => course_module.id.to_s }, {}, {},
|
||||||
|
{ :expected_status => 400 })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "cannot duplicate nonexistent module" do
|
||||||
|
@course.account.enable_feature!(:duplicate_modules)
|
||||||
|
bad_module_id = ContextModule.maximum(:id) + 1
|
||||||
|
api_call(:post, "/api/v1/courses/#{@course.id}/modules/#{bad_module_id}/duplicate",
|
||||||
|
{ :controller => "context_modules_api", :action => "duplicate", :format => "json",
|
||||||
|
:course_id => @course.id.to_s, :module_id => bad_module_id.to_s }, {}, {},
|
||||||
|
{ :expected_status => 404 })
|
||||||
|
end
|
||||||
|
|
||||||
|
it "cannot duplicate if feature disabled" do
|
||||||
|
@course.account.disable_feature!(:duplicate_modules)
|
||||||
|
course_module = @course.context_modules.create!(:name => "empty module", :workflow_state => "published")
|
||||||
|
api_call(:post, "/api/v1/courses/#{@course.id}/modules/#{course_module.id}/duplicate",
|
||||||
|
{ :controller => "context_modules_api", :action => "duplicate", :format => "json",
|
||||||
|
:course_id => @course.id.to_s, :module_id => course_module.id.to_s }, {}, {},
|
||||||
|
{ :expected_status => 400 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "index" do
|
describe "index" do
|
||||||
it "should list published and unpublished modules" do
|
it "should list published and unpublished modules" do
|
||||||
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules",
|
json = api_call(:get, "/api/v1/courses/#{@course.id}/modules",
|
||||||
|
@ -703,6 +763,16 @@ describe "Modules API", type: :request do
|
||||||
expect(json['state']).to eq 'locked'
|
expect(json['state']).to eq 'locked'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "cannot duplicate" do
|
||||||
|
@course.account.enable_feature!(:duplicate_modules)
|
||||||
|
course_module = @course.context_modules.create!(:name => "empty module")
|
||||||
|
api_call(:post, "/api/v1/courses/#{@course.id}/modules/#{course_module.id}/duplicate",
|
||||||
|
{ :controller => "context_modules_api", :action => "duplicate", :format => "json",
|
||||||
|
:course_id => @course.id.to_s, :module_id => course_module.id.to_s },
|
||||||
|
{}, {},
|
||||||
|
{:expected_status => 401})
|
||||||
|
end
|
||||||
|
|
||||||
it "should show module progress" do
|
it "should show module progress" do
|
||||||
# to simplify things, eliminate the requirements on the quiz and discussion topic for this test
|
# to simplify things, eliminate the requirements on the quiz and discussion topic for this test
|
||||||
@module1.completion_requirements.reject! {|r| [@quiz_tag.id, @topic_tag.id].include? r[:id]}
|
@module1.completion_requirements.reject! {|r| [@quiz_tag.id, @topic_tag.id].include? r[:id]}
|
||||||
|
|
|
@ -34,30 +34,35 @@ describe DuplicatingObjects do
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_potentially_conflicting_titles(_title_base)
|
def get_potentially_conflicting_titles(_title_base)
|
||||||
[ 'Foo', 'Foo Copy', 'Foo Copy 1', 'Foo Copy 2', 'Foo Copy 5' ].to_set
|
[ 'Foo', 'assignment Copy', 'Foo Copy', 'Foo Copy 1', 'Foo Copy 2', 'Foo Copy 5' ].to_set
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_accessor :title
|
attr_accessor :title
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'copy treated as "Copy" but case is respected' do
|
||||||
|
entity = MockEntity.new('assignment copy')
|
||||||
|
expect(get_copy_title(entity, 'Copy', entity.title)).to eq('assignment copy 2')
|
||||||
|
end
|
||||||
|
|
||||||
it 'no conflicts' do
|
it 'no conflicts' do
|
||||||
entity = MockEntity.new('Bar')
|
entity = MockEntity.new('Bar')
|
||||||
expect(get_copy_title(entity, 'Copy')).to eq 'Bar Copy'
|
expect(get_copy_title(entity, 'Copy', entity.title)).to eq 'Bar Copy'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'conflict not ending in suffix' do
|
it 'conflict not ending in suffix' do
|
||||||
entity = MockEntity.new('Foo')
|
entity = MockEntity.new('Foo')
|
||||||
expect(get_copy_title(entity, 'Copy')).to eq 'Foo Copy 3'
|
expect(get_copy_title(entity, 'Copy', entity.title)).to eq 'Foo Copy 3'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'conflict ending in suffix' do
|
it 'conflict ending in suffix' do
|
||||||
entity = MockEntity.new('Foo Copy 1')
|
entity = MockEntity.new('Foo Copy 1')
|
||||||
expect(get_copy_title(entity, 'Copy')).to eq 'Foo Copy 3'
|
expect(get_copy_title(entity, 'Copy', entity.title)).to eq 'Foo Copy 3'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'increments from given number' do
|
it 'increments from given number' do
|
||||||
entity = MockEntity.new('Foo Copy 5')
|
entity = MockEntity.new('Foo Copy 5')
|
||||||
expect(get_copy_title(entity, 'Copy')).to eq 'Foo Copy 6'
|
expect(get_copy_title(entity, 'Copy', entity.title)).to eq 'Foo Copy 6'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -56,10 +56,75 @@ describe ContextModule do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "can_be_duplicated?" do
|
||||||
|
it "forbid quiz" do
|
||||||
|
course_module
|
||||||
|
quiz = @course.quizzes.build(:title => "some quiz", :quiz_type => "assignment")
|
||||||
|
quiz.save!
|
||||||
|
@module.add_item({:id => quiz.id, :type => 'quiz'})
|
||||||
|
assignment = @course.assignments.create!(:title => "some assignment")
|
||||||
|
@module.add_item({ id: assignment.id, :type => 'assignment' })
|
||||||
|
expect(@module.can_be_duplicated?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "forbid quiz even if added as assignment" do
|
||||||
|
course_module
|
||||||
|
quiz = @course.quizzes.build(:title => "some quiz", :quiz_type => "assignment")
|
||||||
|
quiz.save!
|
||||||
|
assignment = Assignment.find(quiz.assignment_id)
|
||||||
|
@module.add_item({:id => assignment.id, :type => 'assignment'})
|
||||||
|
expect(@module.can_be_duplicated?).to be_falsey
|
||||||
|
end
|
||||||
|
|
||||||
|
it "*deleted* quiz tags are ok" do
|
||||||
|
course_module
|
||||||
|
quiz = @course.quizzes.build(:title => "some quiz", :quiz_type => "assignment")
|
||||||
|
quiz.save!
|
||||||
|
assignment = Assignment.find(quiz.assignment_id)
|
||||||
|
@module.add_item({:id => assignment.id, :type => 'assignment'})
|
||||||
|
@module.content_tags[0].workflow_state = 'deleted'
|
||||||
|
expect(@module.can_be_duplicated?).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "ok if no quiz" do
|
||||||
|
course_module
|
||||||
|
assignment = @course.assignments.create!(:title => "some assignment")
|
||||||
|
@module.add_item({ id: assignment.id, :type => 'assignment' })
|
||||||
|
expect(@module.can_be_duplicated?).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "duplicate" do
|
||||||
|
course_module # name is "some module"
|
||||||
|
assignment1 = @course.assignments.create!(:title => "assignment")
|
||||||
|
assignment2 = @course.assignments.create!(:title => "assignment copy")
|
||||||
|
@module.add_item(type: 'context_module_sub_header', title: 'unpublished header')
|
||||||
|
@module.add_item({:id => assignment1.id, :type => 'assignment'})
|
||||||
|
quiz = @course.quizzes.build(:title => "some quiz", :quiz_type => "assignment")
|
||||||
|
quiz.save!
|
||||||
|
# It is permitted to duplicate a module with a deleted quiz tag, but the deleted
|
||||||
|
# item should not be duplicated.
|
||||||
|
@module.add_item({:id => quiz.id, :type => 'quiz'})
|
||||||
|
@module.content_tags[2].workflow_state = 'deleted'
|
||||||
|
@module.add_item({:id => assignment2.id, :type => 'assignment'})
|
||||||
|
|
||||||
|
@module.workflow_state = 'published'
|
||||||
|
@module.save!
|
||||||
|
new_module = @module.duplicate
|
||||||
|
expect(new_module.name).to eq "some module Copy"
|
||||||
|
expect(new_module.content_tags.length).to eq 3
|
||||||
|
# Stuff with actual content should get unique names, but not stuff like headers.
|
||||||
|
expect(new_module.content_tags[0].title).to eq('unpublished header')
|
||||||
|
expect(new_module.content_tags[1].content.title).to eq('assignment Copy 2')
|
||||||
|
# Respect original choice of "copy" if the thing I copied already made a decision.
|
||||||
|
expect(new_module.content_tags[2].content.title).to eq('assignment copy 3')
|
||||||
|
expect(new_module.workflow_state).to eq('unpublished')
|
||||||
|
end
|
||||||
|
|
||||||
describe "available_for?" do
|
describe "available_for?" do
|
||||||
it "should return true by default" do
|
it "should return true by default" do
|
||||||
course_module
|
course_module
|
||||||
expect(@module.available_for?(nil)).to eql(true)
|
expect(@module.available_for?(nil)).to be(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns true by default when require_sequential_progress is true and there are no requirements" do
|
it "returns true by default when require_sequential_progress is true and there are no requirements" do
|
||||||
|
|
Loading…
Reference in New Issue