master courses: do some locked restrictions+icons for quizzes

closes #MC-62

Change-Id: I39f72492d622749d44fc6c29e96768b25f5b1a7a
Reviewed-on: https://gerrit.instructure.com/99812
Tested-by: Jenkins
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
Product-Review: James Williams  <jamesw@instructure.com>
QA-Review: James Williams  <jamesw@instructure.com>
This commit is contained in:
James Williams 2017-01-17 09:29:35 -07:00
parent ba69f5e14f
commit 0517c58e71
12 changed files with 136 additions and 12 deletions

View File

@ -513,6 +513,7 @@ class Quizzes::QuizzesApiController < ApplicationController
# @returns Quiz
def destroy
if authorized_action(@quiz, @current_user, :delete)
return render_unauthorized_action if editing_restricted?(@quiz)
@quiz.destroy
if accepts_jsonapi?
head :no_content

View File

@ -264,6 +264,7 @@ class Quizzes::QuizzesController < ApplicationController
def edit
if authorized_action(@quiz, @current_user, :update)
return render_unauthorized_action if editing_restricted?(@quiz)
add_crumb(@quiz.title, named_context_url(@context, :context_quiz_url, @quiz))
@assignment = @quiz.assignment
@ -479,6 +480,7 @@ class Quizzes::QuizzesController < ApplicationController
def destroy
if authorized_action(@quiz, @current_user, :delete)
return render_unauthorized_action if editing_restricted?(@quiz)
respond_to do |format|
if @quiz.destroy
format.html { redirect_to course_quizzes_url(@context) }

View File

@ -176,6 +176,7 @@ module Importers
end
end
item ||= context.quizzes.temp_record
item.mark_as_importing!(migration)
new_record = item.new_record? || item.deleted?
hash[:due_at] ||= hash[:due_date]
@ -223,7 +224,8 @@ module Importers
item.save!
build_assignment = false
if question_data
skip_questions = migration.for_master_course_import? && item.edit_types_locked_for_overwrite_on_import.include?(:content)
if question_data && !skip_questions
question_data[:qq_ids] ||= {}
hash[:questions] ||= []

View File

@ -82,6 +82,17 @@ class Quizzes::Quiz < ActiveRecord::Base
# simply_versioned callback updating the version.
after_save :link_assignment_overrides, :if => :new_assignment_id?
include MasterCourses::Restrictor
restrict_columns :content, [:title, :description]
restrict_columns :settings, [
:quiz_type, :assignment_group_id, :shuffle_answers, :time_limit,
:anonymous_submissions, :scoring_policy, :allowed_attempts, :hide_results,
:one_time_results, :show_correct_answers, :show_correct_answers_last_attempt,
:hide_correct_answers_at, :one_question_at_a_time, :cant_go_back, :access_code,
:ip_filter, :require_lockdown_browser, :require_lockdown_browser_for_results,
:lock_at, :unlock_at
]
# override has_one relationship provided by simply_versioned
def current_version_unidirectional
versions.limit(1)

View File

@ -51,6 +51,10 @@ class Quizzes::QuizQuestion < ActiveRecord::Base
serialize :question_data
after_save :update_quiz
include MasterCourses::CollectionRestrictor
self.collection_owner_association = :quiz
restrict_columns :content, [:question_data, :position, :quiz_group_id]
workflow do
state :active
state :deleted

View File

@ -260,6 +260,9 @@ module Quizzes
hash['links']['quiz_statistics'] = hash.delete(:quiz_statistics_url)
hash['links']['quiz_reports'] = hash.delete(:quiz_reports_url)
end
if serializer_option(:include_master_course_restrictions)
hash.merge!(quiz.master_course_api_restriction_data)
end
hash
end

View File

@ -24,7 +24,7 @@ $answersSprite: url(/images/answers_sprite.png);
// quiz index
li.quiz {
cursor: pointer;
.ellipses, i[class^=icon-]:not(.icon-publish):not(.icon-quiz):not(.icon-post-to-sis) {
.ellipses, i[class^=icon-]:not(.icon-publish):not(.icon-quiz):not(.icon-post-to-sis):not(.icon-lock) {
color: darken($ic-color-neutral, 40);
}
.ig-title {
@ -2256,3 +2256,11 @@ li.quiz.quiz-loading-overrides {
margin-left: 2px;
}
}
.master-course-cell i {
margin-right: 12px;
}
.options-spacer {
width: 52px;
}

View File

@ -46,9 +46,21 @@
</span>
{{/if}}
<span class="sis-button" data-view="sis-button"></span>
<span class="master-course-cell">
{{#if is_master_course_content}}
{{#if restricted_by_master_course}}
<i class="icon-lock"></i>
{{else}}
<i class="icon-unlock icon-Line"></i>
{{/if}}
{{/if}}
</span>
<span data-view="publish-icon" class="publish-icon"></span>
<div class="inline-block">
{{#if restricted_by_master_course}}
<div class="options-spacer"></div>
{{else}}
<a class="al-trigger al-trigger-gray" role="button" aria-haspopup="true" aria-owns="ui-id-{{id}}-1" href="#">
<i class="icon-settings"></i>
<i class="icon-mini-arrow-down"></i>
@ -73,6 +85,7 @@
</li>
{{>ExternalTools/external_tools_menu quiz_menu_tools}}
</ul>
{{/if}}
</div>
</div>
{{/if}}

View File

@ -112,7 +112,7 @@
<% if can_do(@quiz, @current_user, :update) || can_do(@quiz, @current_user, :grade) || can_do(@quiz, @current_user, :review_grades) %>
<div class="header-group-right">
<% if can_do(@quiz, @current_user, :update) %>
<% if can_do(@quiz, @current_user, :update) && !editing_restricted?(@quiz) %>
<a href="<%= edit_course_quiz_path(@context, @quiz) %>" class='btn edit_assignment_link quiz-edit-button' role='button'>
<i class="icon-edit"></i> <%= admin_edit_title %>
</a>
@ -144,7 +144,7 @@
</li>
<% end %>
<% if can_do(@quiz, @current_user, :update) %>
<% if can_do(@quiz, @current_user, :update) && !editing_restricted?(@quiz) %>
<% if @quiz.locked? %>
<%= form_for @quiz, as: :quiz, method: :put, :url => context_url(@context, :context_quiz_url, @quiz.id), :html => { :id => 'quiz_unlock_form' } do |f| %>
<%= hidden_field_tag 'quiz[locked]', false %>
@ -215,7 +215,7 @@
<% end %>
<li role="presentation">
<% if @quiz.grants_right?(@current_user, :delete) %>
<% if @quiz.grants_right?(@current_user, :delete) && !editing_restricted?(@quiz) %>
<a href="<%= context_url(@context, :context_quiz_url, @quiz) %>" class="delete_quiz_link" tabindex="-1" role="menuitem">
<i class="icon-trash"><span class="screenreader-only"><%= admin_delete_title %></span></i> <%= admin_delete_title %>
</a>

View File

@ -52,6 +52,12 @@ module Api::V1::Quiz
def quizzes_json(quizzes, context, user, session, options={})
options.merge!(description_formatter: description_formatter(context, user))
check_for_restrictions = master_courses? && context.grants_right?(user, session, :manage_assignments)
if check_for_restrictions
MasterCourses::Restrictor.preload_restrictions(quizzes)
options[:include_master_course_restrictions] = true
end
quizzes.map do |quiz|
quiz_json(quiz, context, user, session, options)
end

View File

@ -403,39 +403,51 @@ describe MasterCourses::MasterMigration do
@copy_to = course_factory
sub = @template.add_child_course!(@copy_to)
#TODO: quizzes and quiz questions
#quiz = @copy_from.quizzes.create!
#qq = quiz.quiz_questions.create!(:question_data => {'question_name' => 'test question', 'question_type' => 'essay_question'})
quiz = @copy_from.quizzes.create!
qq = quiz.quiz_questions.create!(:question_data => {'question_name' => 'test question', 'question_type' => 'essay_question'})
bank = @copy_from.assessment_question_banks.create!(:title => 'bank')
aq = bank.assessment_questions.create!(:question_data => {'question_name' => 'test question', 'question_type' => 'essay_question'})
run_master_migration
#copied_quiz = @copy_to.quizzes.where(:migration_id => mig_id(quiz)).first
#copied_qq = copied_quiz.quiz_questions.where(:migration_id => mig_id(qq)).first
copied_quiz = @copy_to.quizzes.where(:migration_id => mig_id(quiz)).first
copied_qq = copied_quiz.quiz_questions.where(:migration_id => mig_id(qq)).first
copied_bank = @copy_to.assessment_question_banks.where(:migration_id => mig_id(bank)).first
copied_aq = copied_bank.assessment_questions.where(:migration_id => mig_id(aq)).first
new_child_text = "some childish text"
copied_aq.question_data['question_text'] = new_child_text
copied_aq.save!
copied_qd = copied_qq.question_data
copied_qd['question_text'] = new_child_text
copied_qq.question_data = copied_qd
copied_qq.save!
bank_child_tag = sub.child_content_tags.polymorphic_where(:content => copied_bank).first
expect(bank_child_tag.downstream_changes).to include("assessment_questions_content") # treats all assessment questions like a column
quiz_child_tag = sub.child_content_tags.polymorphic_where(:content => copied_quiz).first
expect(quiz_child_tag.downstream_changes).to include("quiz_questions_content") # treats all assessment questions like a column
new_master_text = "some mastery text"
bank.update_attribute(:title, new_master_text)
aq.question_data['question_text'] = new_master_text
aq.save!
quiz.update_attribute(:title, new_master_text)
qd = qq.question_data
qd['question_text'] = new_master_text
qq.question_data = qd
qq.save!
[bank].each {|c| c.class.where(:id => c).update_all(:updated_at => 2.seconds.from_now)} # ensure it gets copied
[bank, quiz].each {|c| c.class.where(:id => c).update_all(:updated_at => 2.seconds.from_now)} # ensure it gets copied
run_master_migration # re-copy all the content - but don't actually overwrite anything because it got changed downstream
expect(copied_bank.reload.title).to_not eq new_master_text
expect(copied_aq.reload.question_data['question_text']).to_not eq new_master_text
expect(copied_quiz.reload.title).to_not eq new_master_text
expect(copied_qq.reload.question_data['question_text']).to_not eq new_master_text
[bank].each do |c|
[bank, quiz].each do |c|
mtag = @template.content_tag_for(c)
Timecop.freeze(2.seconds.from_now) do
mtag.update_attribute(:restrictions, {:content => true}) # should touch the content
@ -446,6 +458,8 @@ describe MasterCourses::MasterMigration do
expect(copied_bank.reload.title).to eq new_master_text
expect(copied_aq.reload.question_data['question_text']).to eq new_master_text
expect(copied_quiz.reload.title).to eq new_master_text
expect(copied_qq.reload.question_data['question_text']).to eq new_master_text
end
context "master courses + external migrations" do

View File

@ -0,0 +1,60 @@
require_relative '../common'
describe "master courses - child courses - quiz locking" do
include_context "in-process server selenium tests"
before :once do
Account.default.enable_feature!(:master_courses)
@copy_from = course_factory(:active_all => true)
@template = MasterCourses::MasterTemplate.set_as_master_course(@copy_from)
@original_quiz = @copy_from.quizzes.create!(:title => "blah", :description => "bloo")
@tag = @template.create_content_tag_for!(@original_quiz)
course_with_teacher(:active_all => true)
@copy_to = @course
@quiz_copy = @copy_to.quizzes.new(:title => "blah", :description => "bloo") # just create a copy directly instead of doing a real migration
@quiz_copy.migration_id = @tag.migration_id
@quiz_copy.save!
end
before :each do
user_session(@teacher)
end
it "should not show the cog-menu options on the index when locked" do
@tag.update_attribute(:restrictions, {:content => true, :settings => true})
get "/courses/#{@copy_to.id}/quizzes"
expect(f('.master-course-cell')).to contain_css('.icon-lock')
expect(f('.quiz')).to_not contain_css('.al-trigger')
end
it "should show the cog-menu options on the index when not locked" do
get "/courses/#{@copy_to.id}/quizzes"
expect(f('.master-course-cell')).to contain_css('.icon-unlock')
expect(f('.quiz')).to contain_css('.al-trigger')
end
it "should not show the edit/delete options on the show page when locked" do
@tag.update_attribute(:restrictions, {:content => true, :settings => true})
get "/courses/#{@copy_to.id}/quizzes/#{@quiz_copy.id}"
expect(f('#content')).to_not contain_css('.quiz-edit-button')
f('.al-trigger').click
expect(f('.al-options')).to_not contain_css('.delete_quiz_link')
end
it "should show the edit/delete cog-menu options on the show when not locked" do
get "/courses/#{@copy_to.id}/quizzes/#{@quiz_copy.id}"
expect(f('#content')).to contain_css('.quiz-edit-button')
f('.al-trigger').click
expect(f('.al-options')).to contain_css('.delete_quiz_link')
end
end