move N.Q to Quizzes Page
(flag=newquizzes_on_quiz_page) - populate N.Q quizzes to assignment quizzes list - display kebab menu based on quiz types (old quizze and new quizzes) - items in kebab menu are functional closes QUIZ-6790, QUIZ-6792, QUIZ-6789, QUIZ-6786 test plan: - With the newquizzes_on_quiz_page flag disabled everything should behave like in production - With the newquizzes_on_quiz_page flag enabled 1) N.Q quizzes show up on Quizzes Page 2) N.Q quiz shells have correct kebab menu 3) each menu items (delete, duplicate) should work Change-Id: Ie4a78bb0f0a69f4d6e248135d1c486f1ca0ffe7f Reviewed-on: https://gerrit.instructure.com/209993 Tested-by: Jenkins Reviewed-by: Jeremy Stanley <jeremy@instructure.com> Reviewed-by: Stephen Kacsmark <skacsmark@instructure.com> Reviewed-by: Jon Willesen <jonw+gerrit@instructure.com> QA-Review: Robin Kuss <rkuss@instructure.com> Product-Review: Kevin Dougherty III <jdougherty@instructure.com>
This commit is contained in:
parent
8067ee5ce5
commit
6e65e2ef31
|
@ -25,9 +25,10 @@ import DateGroupCollection from '../collections/DateGroupCollection'
|
|||
import I18n from 'i18n!modelsQuiz'
|
||||
import 'jquery.ajaxJSON'
|
||||
import 'jquery.instructure_misc_helpers'
|
||||
import PandaPubPoller from '../util/PandaPubPoller'
|
||||
|
||||
export default class Quiz extends Backbone.Model {
|
||||
initialize(attributes, options = {}) {
|
||||
initialize() {
|
||||
this.publish = this.publish.bind(this)
|
||||
this.unpublish = this.unpublish.bind(this)
|
||||
this.dueAt = this.dueAt.bind(this)
|
||||
|
@ -51,6 +52,7 @@ export default class Quiz extends Backbone.Model {
|
|||
this.objectType = this.objectType.bind(this)
|
||||
|
||||
super.initialize(...arguments)
|
||||
this.initId()
|
||||
this.initAssignment()
|
||||
this.initAssignmentOverrides()
|
||||
this.initUrls()
|
||||
|
@ -62,6 +64,10 @@ export default class Quiz extends Backbone.Model {
|
|||
}
|
||||
|
||||
// initialize attributes
|
||||
initId() {
|
||||
this.id = this.isQuizzesNext() ? `assignment_${this.get('id')}` : this.get('id')
|
||||
}
|
||||
|
||||
initAssignment() {
|
||||
if (this.attributes.assignment) {
|
||||
this.set('assignment', new Assignment(this.attributes.assignment))
|
||||
|
@ -78,12 +84,11 @@ export default class Quiz extends Backbone.Model {
|
|||
|
||||
initUrls() {
|
||||
if (this.get('html_url')) {
|
||||
this.set('base_url', this.get('html_url').replace(/quizzes\/\d+/, 'quizzes'))
|
||||
|
||||
this.set('base_url', this.get('html_url').replace(/(quizzes|assignments)\/\d+/, '$1'))
|
||||
this.set('url', `${this.get('base_url')}/${this.get('id')}`)
|
||||
this.set('edit_url', `${this.get('base_url')}/${this.get('id')}/edit`)
|
||||
this.set('publish_url', `${this.get('base_url')}/publish`)
|
||||
this.set('unpublish_url', `${this.get('base_url')}/unpublish`)
|
||||
this.set('publish_url', this.publish_url())
|
||||
this.set('unpublish_url', this.unpublish_url())
|
||||
this.set(
|
||||
'toggle_post_to_sis_url',
|
||||
`${this.get('base_url')}/${this.get('id')}/toggle_post_to_sis`
|
||||
|
@ -103,7 +108,9 @@ export default class Quiz extends Backbone.Model {
|
|||
|
||||
initQuestionsCount() {
|
||||
const cnt = this.get('question_count')
|
||||
return this.set('question_count_label', I18n.t('question_count', 'Question', {count: cnt}))
|
||||
if (cnt) {
|
||||
this.set('question_count_label', I18n.t('question_count', 'Question', {count: cnt}))
|
||||
}
|
||||
}
|
||||
|
||||
initPointsCount() {
|
||||
|
@ -115,10 +122,28 @@ export default class Quiz extends Backbone.Model {
|
|||
return this.set('possible_points_label', text)
|
||||
}
|
||||
|
||||
isQuizzesNext() {
|
||||
return this.get('quiz_type') === 'quizzes.next'
|
||||
}
|
||||
|
||||
isUngradedSurvey() {
|
||||
return this.get('quiz_type') === 'survey'
|
||||
}
|
||||
|
||||
publish_url() {
|
||||
if (this.isQuizzesNext()) {
|
||||
return `${this.get('base_url')}/publish/quiz`
|
||||
}
|
||||
return `${this.get('base_url')}/publish`
|
||||
}
|
||||
|
||||
unpublish_url() {
|
||||
if (this.isQuizzesNext()) {
|
||||
return `${this.get('base_url')}/unpublish/quiz`
|
||||
}
|
||||
return `${this.get('base_url')}/unpublish`
|
||||
}
|
||||
|
||||
initAllDates() {
|
||||
let allDates
|
||||
if ((allDates = this.get('all_dates')) != null) {
|
||||
|
@ -127,7 +152,6 @@ export default class Quiz extends Backbone.Model {
|
|||
}
|
||||
|
||||
// publishing
|
||||
|
||||
publish() {
|
||||
this.set('published', true)
|
||||
return $.ajaxJSON(this.get('publish_url'), 'POST', {quizzes: [this.get('id')]})
|
||||
|
@ -162,6 +186,10 @@ export default class Quiz extends Backbone.Model {
|
|||
return this.set('lock_at', date)
|
||||
}
|
||||
|
||||
isDuplicating() {
|
||||
return this.get('workflow_state') === 'duplicating'
|
||||
}
|
||||
|
||||
name(newName) {
|
||||
if (!(arguments.length > 0)) return this.get('title')
|
||||
return this.set('title', newName)
|
||||
|
@ -171,13 +199,73 @@ export default class Quiz extends Backbone.Model {
|
|||
return this.get('url')
|
||||
}
|
||||
|
||||
destroy(options) {
|
||||
const opts = {
|
||||
url: this.htmlUrl(),
|
||||
...options
|
||||
}
|
||||
Backbone.Model.prototype.destroy.call(this, opts)
|
||||
}
|
||||
|
||||
defaultDates() {
|
||||
let group
|
||||
return (group = new DateGroup({
|
||||
return new DateGroup({
|
||||
due_at: this.get('due_at'),
|
||||
unlock_at: this.get('unlock_at'),
|
||||
lock_at: this.get('lock_at')
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
// caller is original assignments
|
||||
duplicate(callback) {
|
||||
const course_id = this.get('course_id')
|
||||
const assignment_id = this.get('id')
|
||||
return $.ajaxJSON(
|
||||
`/api/v1/courses/${course_id}/assignments/${assignment_id}/duplicate`,
|
||||
'POST',
|
||||
{quizzes: [assignment_id], result_type: 'Quiz'},
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
// caller is failed assignments
|
||||
duplicate_failed(callback) {
|
||||
const target_course_id = this.get('course_id')
|
||||
const target_assignment_id = this.get('id')
|
||||
const original_course_id = this.get('original_course_id')
|
||||
const original_assignment_id = this.get('original_assignment_id')
|
||||
let query_string = `?target_assignment_id=${target_assignment_id}`
|
||||
if (original_course_id !== target_course_id) {
|
||||
// when it's a course copy failure
|
||||
query_string += `&target_course_id=${target_course_id}`
|
||||
}
|
||||
$.ajaxJSON(
|
||||
`/api/v1/courses/${original_course_id}/assignments/${original_assignment_id}/duplicate${query_string}`,
|
||||
'POST',
|
||||
{},
|
||||
callback
|
||||
)
|
||||
}
|
||||
|
||||
pollUntilFinishedLoading(interval) {
|
||||
if (this.isDuplicating()) {
|
||||
this.pollUntilFinished(interval)
|
||||
}
|
||||
}
|
||||
|
||||
pollUntilFinished(interval) {
|
||||
const course_id = this.get('course_id')
|
||||
const id = this.get('id')
|
||||
const poller = new PandaPubPoller(interval, interval * 5, done => {
|
||||
this.fetch({url: `/api/v1/courses/${course_id}/assignments/${id}?result_type=Quiz`}).always(
|
||||
() => {
|
||||
done()
|
||||
if (!this.isDuplicating()) {
|
||||
return poller.stop()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
poller.start()
|
||||
}
|
||||
|
||||
multipleDueDates() {
|
||||
|
@ -193,10 +281,9 @@ export default class Quiz extends Backbone.Model {
|
|||
}
|
||||
|
||||
allDates() {
|
||||
let result
|
||||
const groups = this.get('all_dates')
|
||||
const models = (groups && groups.models) || []
|
||||
return (result = _.map(models, group => group.toJSON()))
|
||||
return _.map(models, group => group.toJSON())
|
||||
}
|
||||
|
||||
singleSectionDueDate() {
|
||||
|
|
|
@ -28,6 +28,7 @@ import DateAvailableColumnView from '../assignments/DateAvailableColumnView'
|
|||
import SisButtonView from '../SisButtonView'
|
||||
import template from 'jst/quizzes/QuizItemView'
|
||||
import 'jquery.disableWhileLoading'
|
||||
import Quiz from '../../models/Quiz'
|
||||
|
||||
export default class ItemView extends Backbone.View {
|
||||
static initClass() {
|
||||
|
@ -47,7 +48,11 @@ export default class ItemView extends Backbone.View {
|
|||
'click .delete-item': 'onDelete',
|
||||
'click .migrate': 'migrateQuiz',
|
||||
'click .quiz-copy-to': 'copyQuizTo',
|
||||
'click .quiz-send-to': 'sendQuizTo'
|
||||
'click .quiz-send-to': 'sendQuizTo',
|
||||
'click .duplicate_assignment': 'onDuplicate',
|
||||
'click .duplicate-failed-retry': 'onDuplicateFailedRetry',
|
||||
'click .duplicate-failed-cancel': 'onDuplicateOrImportFailedCancel',
|
||||
'click .import-failed-cancel': 'onDuplicateOrImportFailedCancel'
|
||||
}
|
||||
|
||||
this.prototype.messages = {
|
||||
|
@ -61,6 +66,7 @@ export default class ItemView extends Backbone.View {
|
|||
initialize(options) {
|
||||
this.initializeChildViews()
|
||||
this.observeModel()
|
||||
this.model.pollUntilFinishedLoading(3000)
|
||||
return super.initialize(...arguments)
|
||||
}
|
||||
|
||||
|
@ -121,7 +127,8 @@ export default class ItemView extends Backbone.View {
|
|||
}
|
||||
|
||||
migrateQuizEnabled() {
|
||||
return ENV.FLAGS && ENV.FLAGS.migrate_quiz_enabled
|
||||
const isOldQuiz = this.model.get('quiz_type') !== 'quizzes.next'
|
||||
return ENV.FLAGS && ENV.FLAGS.migrate_quiz_enabled && isOldQuiz
|
||||
}
|
||||
|
||||
migrateQuiz(e) {
|
||||
|
@ -152,12 +159,14 @@ export default class ItemView extends Backbone.View {
|
|||
}
|
||||
|
||||
// delete quiz item
|
||||
delete() {
|
||||
delete(opts) {
|
||||
this.$el.hide()
|
||||
return this.model.destroy({
|
||||
success: () => {
|
||||
this.$el.remove()
|
||||
return $.flashMessage(this.messages.deleteSuccessful)
|
||||
if (opts.silent !== true) {
|
||||
$.flashMessage(this.messages.deleteSuccessful)
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.$el.show()
|
||||
|
@ -178,7 +187,8 @@ export default class ItemView extends Backbone.View {
|
|||
|
||||
observeModel() {
|
||||
this.model.on('change:published', this.updatePublishState, this)
|
||||
return this.model.on('change:loadingOverrides', this.render, this)
|
||||
this.model.on('change:loadingOverrides', this.render, this)
|
||||
this.model.on('change:workflow_state', this.render, this)
|
||||
}
|
||||
|
||||
updatePublishState() {
|
||||
|
@ -189,6 +199,55 @@ export default class ItemView extends Backbone.View {
|
|||
return ENV.PERMISSIONS.manage
|
||||
}
|
||||
|
||||
canDuplicate() {
|
||||
const userIsAdmin = _.includes(ENV.current_user_roles, 'admin')
|
||||
const canManage = this.canManage()
|
||||
const canDuplicate = this.model.get('can_duplicate')
|
||||
return (userIsAdmin || canManage) && canDuplicate
|
||||
}
|
||||
|
||||
onDuplicate(e) {
|
||||
if (!this.canDuplicate()) return
|
||||
e.preventDefault()
|
||||
this.model.duplicate(this.addQuizToList.bind(this))
|
||||
}
|
||||
|
||||
addQuizToList(response) {
|
||||
if (!response) return
|
||||
const quiz = new Quiz(response)
|
||||
if (ENV.PERMISSIONS.by_assignment_id) {
|
||||
ENV.PERMISSIONS.by_assignment_id[quiz.id] =
|
||||
ENV.PERMISSIONS.by_assignment_id[quiz.originalAssignmentID()]
|
||||
}
|
||||
this.model.collection.add(quiz)
|
||||
this.focusOnQuiz(response)
|
||||
}
|
||||
|
||||
focusOnQuiz(quiz) {
|
||||
$(`#assignment_${quiz.id}`)
|
||||
.attr('tabindex', -1)
|
||||
.focus()
|
||||
}
|
||||
|
||||
onDuplicateOrImportFailedCancel(e) {
|
||||
e.preventDefault()
|
||||
this.delete({silent: true})
|
||||
}
|
||||
|
||||
onDuplicateFailedRetry(e) {
|
||||
e.preventDefault()
|
||||
const button = $(e.target)
|
||||
button.prop('disabled', true)
|
||||
this.model
|
||||
.duplicate_failed(response => {
|
||||
this.addQuizToList(response)
|
||||
this.delete({silent: true})
|
||||
})
|
||||
.always(() => {
|
||||
button.prop('disabled', false)
|
||||
})
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const base = _.extend(this.model.toJSON(), this.options)
|
||||
base.quiz_menu_tools = ENV.quiz_menu_tools
|
||||
|
@ -206,6 +265,9 @@ export default class ItemView extends Backbone.View {
|
|||
}
|
||||
|
||||
base.migrateQuizEnabled = this.migrateQuizEnabled
|
||||
base.canDuplicate = this.canDuplicate()
|
||||
base.isDuplicating = this.model.get('workflow_state') === 'duplicating'
|
||||
base.failedToDuplicate = this.model.get('workflow_state') === 'failed_to_duplicate'
|
||||
base.showAvailability = this.model.multipleDueDates() || !this.model.defaultDates().available()
|
||||
base.showDueDate = this.model.multipleDueDates() || this.model.singleSectionDueDate()
|
||||
|
||||
|
|
|
@ -618,6 +618,7 @@ class AssignmentsApiController < ApplicationController
|
|||
include Api::V1::Assignment
|
||||
include Api::V1::Submission
|
||||
include Api::V1::AssignmentOverride
|
||||
include Api::V1::Quiz
|
||||
|
||||
# @API List assignments
|
||||
# Returns the paginated list of assignments for the current course or assignment group.
|
||||
|
@ -700,9 +701,16 @@ class AssignmentsApiController < ApplicationController
|
|||
if assignment_topic&.pinned && !assignment_topic&.position.nil?
|
||||
new_assignment.discussion_topic.insert_at(assignment_topic.position + 1)
|
||||
end
|
||||
# Include the updated positions in the response so the frontend can
|
||||
# update them appropriately
|
||||
result_json = assignment_json(new_assignment, @current_user, session)
|
||||
# return assignment json based on requested result type
|
||||
# Serializing an assignment into a quiz format is required by N.Q Quiz shells on Quizzes Page
|
||||
result_json = if use_quiz_json?
|
||||
quiz_json(new_assignment, @context, @current_user, session, {}, QuizzesNext::QuizSerializer)
|
||||
else
|
||||
# Include the updated positions in the response so the frontend can
|
||||
# update them appropriately
|
||||
assignment_json(new_assignment, @current_user, session)
|
||||
end
|
||||
|
||||
result_json['new_positions'] = positions_hash
|
||||
render :json => result_json
|
||||
else
|
||||
|
@ -851,13 +859,22 @@ class AssignmentsApiController < ApplicationController
|
|||
@assignment.context_module_action(@current_user, :read) unless locked && !locked[:can_view]
|
||||
log_api_asset_access(@assignment, "assignments", @assignment.assignment_group)
|
||||
|
||||
render :json => assignment_json(@assignment, @current_user, session,
|
||||
submission: submissions,
|
||||
override_dates: override_dates,
|
||||
include_visibility: include_visibility,
|
||||
needs_grading_count_by_section: needs_grading_count_by_section,
|
||||
include_all_dates: include_all_dates,
|
||||
include_overrides: include_override_objects)
|
||||
options = {
|
||||
submission: submissions,
|
||||
override_dates: override_dates,
|
||||
include_visibility: include_visibility,
|
||||
needs_grading_count_by_section: needs_grading_count_by_section,
|
||||
include_all_dates: include_all_dates,
|
||||
include_overrides: include_override_objects
|
||||
}
|
||||
|
||||
result_json = if use_quiz_json?
|
||||
quiz_json(@assignment, @context, @current_user, session, {}, QuizzesNext::QuizSerializer)
|
||||
else
|
||||
assignment_json(@assignment, @current_user, session, options)
|
||||
end
|
||||
|
||||
render :json => result_json
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1323,4 +1340,8 @@ class AssignmentsApiController < ApplicationController
|
|||
def course_copy_retry?
|
||||
target_course_for_duplicate != @context
|
||||
end
|
||||
|
||||
def use_quiz_json?
|
||||
params[:result_type] == 'Quiz' && @context.root_account.feature_enabled?(:newquizzes_on_quiz_page)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -681,6 +681,42 @@ class AssignmentsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# pulish a N.Q assignment from Quizzes Page
|
||||
def publish_quizzes
|
||||
if authorized_action(@context, @current_user, :manage_assignments)
|
||||
@assignments = @context.assignments.active.where(id: params[:quizzes])
|
||||
@assignments.each(&:publish!)
|
||||
|
||||
flash[:notice] = t('notices.quizzes_published',
|
||||
{ :one => "1 quiz successfully published!",
|
||||
:other => "%{count} quizzes successfully published!" },
|
||||
:count => @assignments.length)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to named_context_url(@context, :context_quizzes_url) }
|
||||
format.json { render :json => {}, :status => :ok }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# unpulish a N.Q assignment from Quizzes Page
|
||||
def unpublish_quizzes
|
||||
if authorized_action(@context, @current_user, :manage_assignments)
|
||||
@assignments = @context.assignments.active.where(id: params[:quizzes], workflow_state: 'published')
|
||||
@assignments.each(&:unpublish!)
|
||||
|
||||
flash[:notice] = t('notices.quizzes_unpublished',
|
||||
{ :one => "1 quiz successfully unpublished!",
|
||||
:other => "%{count} quizzes successfully unpublished!" },
|
||||
:count => @assignments.length)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to named_context_url(@context, :context_quizzes_url) }
|
||||
format.json { render :json => {}, :status => :ok }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def set_default_tool_env!(context, hash)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class Quizzes::QuizzesController < ApplicationController
|
||||
include Api::V1::Quiz
|
||||
include Api::V1::QuizzesNext::Quiz
|
||||
include Api::V1::AssignmentOverride
|
||||
include KalturaHelper
|
||||
include ::Filters::Quizzes
|
||||
|
@ -65,31 +66,17 @@ class Quizzes::QuizzesController < ApplicationController
|
|||
|
||||
can_manage = @context.grants_right?(@current_user, session, :manage_assignments)
|
||||
|
||||
scope = @context.quizzes.active.preload(:assignment)
|
||||
|
||||
# students only get to see published quizzes, and they will fetch the
|
||||
# overrides later using the API:
|
||||
scope = scope.available unless @context.grants_right?(@current_user, session, :read_as_admin)
|
||||
|
||||
scope = DifferentiableAssignment.scope_filter(scope, @current_user, @context)
|
||||
|
||||
quizzes = scope.sort_by do |quiz|
|
||||
due_date = quiz.assignment ? quiz.assignment.due_at : quiz.lock_at
|
||||
quiz_options = Rails.cache.fetch(
|
||||
[
|
||||
due_date || CanvasSort::Last,
|
||||
Canvas::ICU.collation_key(quiz.title || CanvasSort::First)
|
||||
]
|
||||
end
|
||||
|
||||
quiz_options = Rails.cache.fetch([
|
||||
'quiz_user_permissions', @context.id, @current_user,
|
||||
quizzes.map(&:id), # invalidate on add/delete of quizzes
|
||||
quizzes.map(&:updated_at).sort.last # invalidate on modifications
|
||||
].cache_key) do
|
||||
'quiz_user_permissions', @context.id, @current_user,
|
||||
scoped_quizzes.map(&:id), # invalidate on add/delete of quizzes
|
||||
scoped_quizzes.map(&:updated_at).sort.last # invalidate on modifications
|
||||
].cache_key
|
||||
) do
|
||||
if can_manage
|
||||
Quizzes::Quiz.preload_can_unpublish(quizzes)
|
||||
Quizzes::Quiz.preload_can_unpublish(scoped_quizzes)
|
||||
end
|
||||
quizzes.each_with_object({}) do |quiz, quiz_user_permissions|
|
||||
scoped_quizzes.each_with_object({}) do |quiz, quiz_user_permissions|
|
||||
quiz_user_permissions[quiz.id] = {
|
||||
can_update: can_manage,
|
||||
can_unpublish: can_manage && quiz.can_unpublish?
|
||||
|
@ -97,9 +84,8 @@ class Quizzes::QuizzesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
assignment_quizzes = quizzes.select{ |q| q.quiz_type == QUIZ_TYPE_ASSIGNMENT }
|
||||
open_quizzes = quizzes.select{ |q| q.quiz_type == QUIZ_TYPE_PRACTICE }
|
||||
surveys = quizzes.select{ |q| QUIZ_TYPE_SURVEYS.include?(q.quiz_type) }
|
||||
practice_quizzes = scoped_quizzes.select{ |q| q.quiz_type == QUIZ_TYPE_PRACTICE }
|
||||
surveys = scoped_quizzes.select{ |q| QUIZ_TYPE_SURVEYS.include?(q.quiz_type) }
|
||||
serializer_options = [@context, @current_user, session, {
|
||||
permissions: quiz_options,
|
||||
skip_date_overrides: true,
|
||||
|
@ -113,8 +99,8 @@ class Quizzes::QuizzesController < ApplicationController
|
|||
|
||||
hash = {
|
||||
:QUIZZES => {
|
||||
assignment: quizzes_json(assignment_quizzes, *serializer_options),
|
||||
open: quizzes_json(open_quizzes, *serializer_options),
|
||||
assignment: assignment_quizzes_json(serializer_options),
|
||||
open: quizzes_json(practice_quizzes, *serializer_options),
|
||||
surveys: quizzes_json(surveys, *serializer_options),
|
||||
options: quiz_options
|
||||
},
|
||||
|
@ -1041,6 +1027,18 @@ class Quizzes::QuizzesController < ApplicationController
|
|||
quiz_lti_tool.url != 'http://void.url.inseng.net'
|
||||
end
|
||||
|
||||
def assignment_quizzes_json(serializer_options)
|
||||
old_quizzes = scoped_quizzes.select{ |q| q.quiz_type == QUIZ_TYPE_ASSIGNMENT }
|
||||
unless @context.root_account.feature_enabled?(:newquizzes_on_quiz_page)
|
||||
return quizzes_json(old_quizzes, *serializer_options)
|
||||
end
|
||||
new_quizzes = Assignments::ScopedToUser.new(@context, @current_user).scope.preload(:duplicate_of).select(&:quiz_lti?)
|
||||
quizzes_next_json(
|
||||
(old_quizzes + new_quizzes).sort_by(&:created_at),
|
||||
*serializer_options
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def get_quiz_params
|
||||
|
@ -1057,4 +1055,23 @@ class Quizzes::QuizzesController < ApplicationController
|
|||
def hide_quiz?
|
||||
!@submission.posted?
|
||||
end
|
||||
|
||||
def scoped_quizzes
|
||||
return @_quizzes if @_quizzes
|
||||
scope = @context.quizzes.active.preload(:assignment)
|
||||
|
||||
# students only get to see published quizzes, and they will fetch the
|
||||
# overrides later using the API:
|
||||
scope = scope.available unless @context.grants_right?(@current_user, session, :read_as_admin)
|
||||
|
||||
scope = DifferentiableAssignment.scope_filter(scope, @current_user, @context)
|
||||
|
||||
@_quizzes = scope.sort_by do |quiz|
|
||||
due_date = quiz.assignment ? quiz.assignment.due_at : quiz.lock_at
|
||||
[
|
||||
due_date || CanvasSort::Last,
|
||||
Canvas::ICU.collation_key(quiz.title || CanvasSort::First)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
#
|
||||
# Copyright (C) 2019 - present 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 QuizzesNext
|
||||
class QuizSerializer < Canvas::APISerializer
|
||||
include PermissionsSerializer
|
||||
|
||||
root :quiz
|
||||
|
||||
attributes :id, :title, :description, :quiz_type, :due_at, :lock_at, :unlock_at,
|
||||
:published, :points_possible, :can_update,
|
||||
:assignment_id, :assignment_group_id, :migration_id, :only_visible_to_overrides,
|
||||
:post_to_sis, :allowed_attempts, :permissions,
|
||||
:html_url, :mobile_url, :can_duplicate,
|
||||
:course_id, :original_course_id, :original_assignment_id,
|
||||
:workflow_state, :original_assignment_name
|
||||
|
||||
def_delegators :@controller
|
||||
|
||||
def quiz_type
|
||||
'quizzes.next'
|
||||
end
|
||||
|
||||
def published
|
||||
object.published?
|
||||
end
|
||||
|
||||
def assignment_id
|
||||
object.id
|
||||
end
|
||||
|
||||
def can_update
|
||||
object.grants_right?(current_user, :update)
|
||||
end
|
||||
|
||||
def html_url
|
||||
controller.send(:course_assignment_url, context, object)
|
||||
end
|
||||
|
||||
def mobile_url
|
||||
controller.send(:course_assignment_url, context, quiz, persist_headless: 1, force_user: 1)
|
||||
end
|
||||
|
||||
def can_duplicate
|
||||
object.can_duplicate?
|
||||
end
|
||||
|
||||
def course_id
|
||||
object.context.id
|
||||
end
|
||||
|
||||
def original_course_id
|
||||
object.duplicate_of&.context_id
|
||||
end
|
||||
|
||||
def original_assignment_id
|
||||
object.duplicate_of&.id
|
||||
end
|
||||
|
||||
def original_assignment_name
|
||||
object.duplicate_of&.title
|
||||
end
|
||||
|
||||
def stringify_ids?
|
||||
!!(accepts_jsonapi? || stringify_json_ids?)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -21,3 +21,4 @@
|
|||
@import "pages/shared/message_students";
|
||||
@import "components/ui.selectmenu";
|
||||
@import "components/conditional_release";
|
||||
@import "components/spinner";
|
||||
|
|
|
@ -9,6 +9,35 @@
|
|||
class="ig-row"
|
||||
{{/if}}
|
||||
>
|
||||
{{#if isDuplicating}}
|
||||
<div class="ig-row__layout">
|
||||
{{>spinner}} {{#t}}Making a copy of "{{original_assignment_name}}"{{/t}}
|
||||
</div>
|
||||
{{else}} {{#if failedToDuplicate}}
|
||||
<span aria-live="polite" role="alert" aria-atomic="true">{{#t}}Oops! Something went wrong with making a copy of "{{original_assignment_name}}"{{/t}}</span>
|
||||
<div class="duplicate-failed-actions">
|
||||
<button class="duplicate-failed-retry btn btn-primary">
|
||||
<span class="screenreader-only">{{#t}}Retry duplicating "{{original_assignment_name}}"{{/t}}</span>
|
||||
<span aria-hidden="true">{{#t}}Retry{{/t}}</span>
|
||||
</button>
|
||||
<button class="duplicate-failed-cancel btn">
|
||||
<span class="screenreader-only">{{#t}}Cancel duplicating "{{original_assignment_name}}"{{/t}}</span>
|
||||
<span aria-hidden="true">{{#t}}Cancel{{/t}}</span>
|
||||
</button>
|
||||
</div>
|
||||
{{else}} {{#if isImporting}}
|
||||
<div class="ig-row__layout">
|
||||
{{>spinner}} {{#t}}Importing "{{name}}"{{/t}}
|
||||
</div>
|
||||
{{else}} {{#if failedToImport}}
|
||||
<span aria-live="polite" role="alert" aria-atomic="true">{{#t}}Oops! Something went wrong importing "{{name}}"{{/t}}</span>
|
||||
<div class="import-failed-actions">
|
||||
<button class="import-failed-cancel btn">
|
||||
<span class="screenreader-only">{{#t}}Cancel importing "{{name}}"{{/t}}</span>
|
||||
<span aria-hidden="true">{{#t}}Cancel{{/t}}</span>
|
||||
</button>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="ig-row__layout">
|
||||
|
||||
<div class="ig-type-icon">
|
||||
|
@ -60,6 +89,16 @@
|
|||
<li role="presentation">
|
||||
<a href="{{edit_url}}" id="ui-id-{{id}}-2" class="icon-edit" tabindex="-1" role="menuitem" title='{{#t}}Edit Quiz{{/t}}'>{{#t}}Edit{{/t}}</a>
|
||||
</li>
|
||||
{{#if canDuplicate}}
|
||||
<li>
|
||||
<a
|
||||
class="duplicate_assignment icon-copy-course"
|
||||
id="assignment_{{id}}_settings_duplicate_item"
|
||||
aria-label="{{#t}}Duplicate Quiz {{name}}{{/t}}"
|
||||
data-focus-returns-to"assign_{{id}}_manage_link"
|
||||
>{{#t}}Duplicate{{/t}}</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if cyoe.isCyoeAble}}
|
||||
<li role="presentation">
|
||||
<a href="{{edit_url}}?return_to={{return_to}}#mastery-paths-editor" class="icon-mastery-path" tabindex="-1" role="menuitem" title="{{#t}}Edit Mastery Paths for {{title_label}}{{/t}}">{{#t}}Mastery Paths{{/t}}</a>
|
||||
|
@ -94,4 +133,5 @@
|
|||
{{/if}}
|
||||
|
||||
</div>
|
||||
{{/if}}{{/if}}{{/if}}{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -369,6 +369,9 @@ CanvasRails::Application.routes.draw do
|
|||
post 'quizzes/unpublish' => 'quizzes/quizzes#unpublish'
|
||||
post 'quizzes/:id/toggle_post_to_sis' => "quizzes/quizzes#toggle_post_to_sis"
|
||||
|
||||
post 'assignments/publish/quiz' => 'assignments#publish_quizzes'
|
||||
post 'assignments/unpublish/quiz' => 'assignments#unpublish_quizzes'
|
||||
|
||||
post 'quizzes/new' => 'quizzes/quizzes#new' # use POST instead of GET (not idempotent)
|
||||
resources :quizzes, controller: 'quizzes/quizzes', except: :new do
|
||||
get :managed_quiz_data
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
#
|
||||
# Copyright (C) 2011 - present 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 Api::V1::QuizzesNext::Quiz
|
||||
extend Api::V1::Quiz
|
||||
|
||||
def quizzes_next_json(quizzes, context, user, session, options={})
|
||||
quizzes.map do |quiz|
|
||||
quiz_json(quiz, context, user, session, options, klass(quiz))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def klass(quiz)
|
||||
return QuizzesNext::QuizSerializer if quiz.is_a?(Assignment)
|
||||
|
||||
return Quizzes::QuizSerializer if quiz.is_a?(Quizzes::Quiz)
|
||||
|
||||
raise ArgumentError, 'An invalid quiz object is passed to quizzes_next_json'
|
||||
end
|
||||
end
|
|
@ -1354,6 +1354,34 @@ describe AssignmentsApiController, type: :request do
|
|||
new_assignment = duplicated_assignments.where.not(id: failed_assignment.id).first
|
||||
expect(new_assignment.workflow_state).to eq('duplicating')
|
||||
end
|
||||
|
||||
context "when result_type is specified (Quizzes.Next serialization)" do
|
||||
before do
|
||||
@course.root_account.enable_feature!(:newquizzes_on_quiz_page)
|
||||
end
|
||||
|
||||
it "outputs quiz shell json using quizzes.next serializer" do
|
||||
url = "/api/v1/courses/#{@course.id}/assignments/#{assignment.id}/duplicate.json" \
|
||||
"?target_assignment_id=#{failed_assignment.id}&target_course_id=#{course_copied.id}" \
|
||||
"&result_type=Quiz"
|
||||
|
||||
json = api_call_as_user(
|
||||
@teacher, :post,
|
||||
url,
|
||||
{
|
||||
controller: "assignments_api",
|
||||
action: "duplicate",
|
||||
format: "json",
|
||||
course_id: @course.id.to_s,
|
||||
assignment_id: assignment.id.to_s,
|
||||
target_assignment_id: failed_assignment.id,
|
||||
target_course_id: course_copied.id,
|
||||
result_type: 'Quiz'
|
||||
}
|
||||
)
|
||||
expect(json['quiz_type']).to eq('quizzes.next')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -4504,6 +4532,25 @@ describe AssignmentsApiController, type: :request do
|
|||
expect(uri.query).to include('assignment_id=')
|
||||
end
|
||||
end
|
||||
|
||||
context "when result_type is specified (Quizzes.Next serialization)" do
|
||||
before do
|
||||
@course.root_account.enable_feature!(:newquizzes_on_quiz_page)
|
||||
end
|
||||
|
||||
it "outputs quiz shell json using quizzes.next serializer" do
|
||||
@assignment = @course.assignments.create!(:title => "Test Assignment",:description => "foo")
|
||||
json = api_call(:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}.json",
|
||||
{ :controller => "assignments_api", :action => "show",
|
||||
:format => "json", :course_id => @course.id.to_s,
|
||||
:id => @assignment.id.to_s,
|
||||
:all_dates => true,
|
||||
result_type: 'Quiz'},
|
||||
{:override_assignment_dates => 'false'})
|
||||
expect(json['quiz_type']).to eq('quizzes.next')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "draft state" do
|
||||
|
|
|
@ -177,6 +177,42 @@ test('checks for no multiple due dates from quiz overrides', () => {
|
|||
ok(!quiz.multipleDueDates())
|
||||
})
|
||||
|
||||
QUnit.module('Quiz.Next', {
|
||||
setup() {
|
||||
this.quiz = new Quiz({
|
||||
id: 7,
|
||||
html_url: 'http://localhost:3000/courses/1/assignments/7',
|
||||
assignment_id: 7,
|
||||
quiz_type: 'quizzes.next'
|
||||
})
|
||||
this.ajaxStub = sandbox.stub($, 'ajaxJSON')
|
||||
},
|
||||
teardown() {}
|
||||
})
|
||||
|
||||
test('#initialize model record id', function() {
|
||||
equal(this.quiz.id, 'assignment_7')
|
||||
})
|
||||
|
||||
test('#initialize should set url from html url', function() {
|
||||
equal(this.quiz.get('url'), 'http://localhost:3000/courses/1/assignments/7')
|
||||
})
|
||||
|
||||
test('#initialize should set edit_url from html url', function() {
|
||||
equal(this.quiz.get('edit_url'), 'http://localhost:3000/courses/1/assignments/7/edit')
|
||||
})
|
||||
|
||||
test('#initialize should set publish_url from html url', function() {
|
||||
equal(this.quiz.get('publish_url'), 'http://localhost:3000/courses/1/assignments/publish/quiz')
|
||||
})
|
||||
|
||||
test('#initialize should set unpublish_url from html url', function() {
|
||||
equal(
|
||||
this.quiz.get('unpublish_url'),
|
||||
'http://localhost:3000/courses/1/assignments/unpublish/quiz'
|
||||
)
|
||||
})
|
||||
|
||||
QUnit.module('Quiz#allDates')
|
||||
|
||||
test('gets the due dates from the assignment overrides', () => {
|
||||
|
@ -290,3 +326,69 @@ test('includes singleSectionDueDate', () => {
|
|||
const json = quiz.toView()
|
||||
equal(json.singleSectionDueDate, dueAt.toISOString())
|
||||
})
|
||||
|
||||
QUnit.module('Quiz#duplicate')
|
||||
|
||||
test('make ajax call with right url when duplicate is called', () => {
|
||||
const assignmentID = '200'
|
||||
const courseID = '123'
|
||||
const quiz = new Quiz({
|
||||
name: 'foo',
|
||||
id: assignmentID,
|
||||
course_id: courseID
|
||||
})
|
||||
const spy = sandbox.spy($, 'ajaxJSON')
|
||||
quiz.duplicate()
|
||||
ok(spy.withArgs(`/api/v1/courses/${courseID}/assignments/${assignmentID}/duplicate`).calledOnce)
|
||||
})
|
||||
|
||||
QUnit.module('Quiz#duplicate_failed')
|
||||
|
||||
test('make ajax call with right url when duplicate_failed is called', () => {
|
||||
const assignmentID = '200'
|
||||
const originalAssignmentID = '42'
|
||||
const courseID = '123'
|
||||
const originalCourseID = '234'
|
||||
const quiz = new Quiz({
|
||||
name: 'foo',
|
||||
id: assignmentID,
|
||||
original_assignment_id: originalAssignmentID,
|
||||
course_id: courseID,
|
||||
original_course_id: originalCourseID
|
||||
})
|
||||
const spy = sandbox.spy($, 'ajaxJSON')
|
||||
quiz.duplicate_failed()
|
||||
ok(
|
||||
spy.withArgs(
|
||||
`/api/v1/courses/${originalCourseID}/assignments/${originalAssignmentID}/duplicate?target_assignment_id=${assignmentID}&target_course_id=${courseID}`
|
||||
).calledOnce
|
||||
)
|
||||
})
|
||||
|
||||
QUnit.module('Assignment#pollUntilFinishedLoading', {
|
||||
setup() {
|
||||
this.clock = sinon.useFakeTimers()
|
||||
this.quiz = new Quiz({workflow_state: 'duplicating'})
|
||||
sandbox.stub(this.quiz, 'fetch').returns($.Deferred().resolve())
|
||||
},
|
||||
teardown() {
|
||||
this.clock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('polls for updates', function() {
|
||||
this.quiz.pollUntilFinishedLoading(4000)
|
||||
this.clock.tick(2000)
|
||||
notOk(this.quiz.fetch.called)
|
||||
this.clock.tick(3000)
|
||||
ok(this.quiz.fetch.called)
|
||||
})
|
||||
|
||||
test('stops polling when the quiz has finished duplicating', function() {
|
||||
this.quiz.pollUntilFinishedLoading(3000)
|
||||
this.quiz.set({workflow_state: 'unpublished'})
|
||||
this.clock.tick(3000)
|
||||
ok(this.quiz.fetch.calledOnce)
|
||||
this.clock.tick(3000)
|
||||
ok(this.quiz.fetch.calledOnce)
|
||||
})
|
||||
|
|
|
@ -432,3 +432,94 @@ test('renders direct share menu items when DIRECT_SHARE_ENABLED', () => {
|
|||
equal(view.$('.quiz-copy-to').length, 1)
|
||||
equal(view.$('.quiz-send-to').length, 1)
|
||||
})
|
||||
|
||||
test('can duplicate when a quiz can be duplicated', () => {
|
||||
const quiz = createQuiz({
|
||||
id: 1,
|
||||
title: 'Foo',
|
||||
can_duplicate: true,
|
||||
can_update: true
|
||||
})
|
||||
Object.assign(window.ENV, {current_user_roles: ['admin']})
|
||||
const view = createView(quiz, {})
|
||||
const json = view.toJSON()
|
||||
ok(json.canDuplicate)
|
||||
equal(view.$('.duplicate_assignment').length, 1)
|
||||
})
|
||||
|
||||
test('duplicate option is not available when a quiz can not be duplicated (old quizzes)', () => {
|
||||
const quiz = createQuiz({
|
||||
id: 1,
|
||||
title: 'Foo',
|
||||
can_update: true
|
||||
})
|
||||
Object.assign(window.ENV, {current_user_roles: ['admin']})
|
||||
const view = createView(quiz, {})
|
||||
const json = view.toJSON()
|
||||
ok(!json.canDuplicate)
|
||||
equal(view.$('.duplicate_assignment').length, 0)
|
||||
})
|
||||
|
||||
test('clicks on Retry button to trigger another duplicating request', () => {
|
||||
const quiz = createQuiz({
|
||||
id: 2,
|
||||
title: 'Foo Copy',
|
||||
original_assignment_name: 'Foo',
|
||||
workflow_state: 'failed_to_duplicate'
|
||||
})
|
||||
const dfd = $.Deferred()
|
||||
const view = createView(quiz)
|
||||
sandbox.stub(quiz, 'duplicate_failed').returns(dfd)
|
||||
view.$(`.duplicate-failed-retry`).simulate('click')
|
||||
ok(quiz.duplicate_failed.called)
|
||||
})
|
||||
|
||||
test('can duplicate when a user has permissons to manage assignments', () => {
|
||||
const quiz = createQuiz({
|
||||
id: 1,
|
||||
title: 'Foo',
|
||||
can_duplicate: true,
|
||||
can_update: true
|
||||
})
|
||||
Object.assign(window.ENV, {current_user_roles: ['teacher']})
|
||||
const view = createView(quiz, {canManage: true})
|
||||
const json = view.toJSON()
|
||||
ok(json.canDuplicate)
|
||||
equal(view.$('.duplicate_assignment').length, 1)
|
||||
})
|
||||
|
||||
test('cannot duplicate when user is not admin', () => {
|
||||
const quiz = createQuiz({
|
||||
id: 1,
|
||||
title: 'Foo',
|
||||
can_duplicate: true,
|
||||
can_update: true
|
||||
})
|
||||
Object.assign(window.ENV, {current_user_roles: ['user']})
|
||||
const view = createView(quiz, {})
|
||||
const json = view.toJSON()
|
||||
ok(!json.canDuplicate)
|
||||
equal(view.$('.duplicate_assignment').length, 0)
|
||||
})
|
||||
|
||||
test('displays duplicating message when assignment is duplicating', () => {
|
||||
const quiz = createQuiz({
|
||||
id: 2,
|
||||
title: 'Foo Copy',
|
||||
original_assignment_name: 'Foo',
|
||||
workflow_state: 'duplicating'
|
||||
})
|
||||
const view = createView(quiz)
|
||||
ok(view.$el.text().includes('Making a copy of "Foo"'))
|
||||
})
|
||||
|
||||
test('displays failed to duplicate message when assignment failed to duplicate', () => {
|
||||
const quiz = createQuiz({
|
||||
id: 2,
|
||||
title: 'Foo Copy',
|
||||
original_assignment_name: 'Foo',
|
||||
workflow_state: 'failed_to_duplicate'
|
||||
})
|
||||
const view = createView(quiz)
|
||||
ok(view.$el.text().includes('Something went wrong with making a copy of "Foo"'))
|
||||
})
|
||||
|
|
|
@ -1559,6 +1559,41 @@ describe AssignmentsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "POST 'publish'" do
|
||||
it "should require authorization" do
|
||||
post 'publish_quizzes', params: { course_id: @course.id, quizzes: [@assignment.id] }
|
||||
assert_unauthorized
|
||||
end
|
||||
|
||||
it "should publish unpublished assignments" do
|
||||
user_session(@teacher)
|
||||
@assignment = @course.assignments.build(title: 'New quiz!', workflow_state: 'unpublished')
|
||||
@assignment.save!
|
||||
|
||||
expect(@assignment).not_to be_published
|
||||
post 'publish_quizzes', params: { course_id: @course.id, quizzes: [@assignment.id] }
|
||||
|
||||
expect(@assignment.reload).to be_published
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST 'unpublish'" do
|
||||
it "should require authorization" do
|
||||
post 'unpublish_quizzes', params: { course_id: @course.id, quizzes: [@assignment.id] }
|
||||
assert_unauthorized
|
||||
end
|
||||
|
||||
it "should unpublish published quizzes" do
|
||||
user_session(@teacher)
|
||||
@assignment = @course.assignments.create(title: 'New quiz!', workflow_state: 'published')
|
||||
|
||||
expect(@assignment).to be_published
|
||||
post 'unpublish_quizzes', params: { course_id: @course.id, quizzes: [@assignment.id] }
|
||||
|
||||
expect(@assignment.reload).not_to be_published
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET list_google_docs" do
|
||||
it "passes errors through to Canvas::Errors" do
|
||||
user_session(@teacher)
|
||||
|
|
|
@ -272,6 +272,79 @@ describe Quizzes::QuizzesController do
|
|||
expect(assigns[:js_env][:FLAGS][:DIRECT_SHARE_ENABLED]).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when newquizzes_on_quiz_page FF is enabled' do
|
||||
let_once(:course_assignments) do
|
||||
group = @course.assignment_groups.create(:name => "some group")
|
||||
(0..3).map do |i|
|
||||
@course.assignments.create(
|
||||
title: "some assignment #{i}",
|
||||
assignment_group: group,
|
||||
due_at: Time.zone.now + 1.week,
|
||||
external_tool_tag_attributes: { content: tool },
|
||||
workflow_state: workflow_states[i]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
let_once(:course_quizzes) do
|
||||
[course_quiz, course_quiz(true)]
|
||||
end
|
||||
|
||||
let_once(:workflow_states) do
|
||||
[:unpublished, :published, :unpublished, :published]
|
||||
end
|
||||
|
||||
let_once(:tool) do
|
||||
@course.context_external_tools.create!(
|
||||
name: 'Quizzes.Next',
|
||||
consumer_key: 'test_key',
|
||||
shared_secret: 'test_secret',
|
||||
tool_id: 'Quizzes 2',
|
||||
url: 'http://example.com/launch'
|
||||
)
|
||||
end
|
||||
|
||||
before :once do
|
||||
@course.root_account.settings[:provision] = {'lti' => 'lti url'}
|
||||
@course.root_account.save!
|
||||
@course.root_account.enable_feature! :quizzes_next
|
||||
@course.enable_feature! :quizzes_next
|
||||
@course.root_account.enable_feature! :newquizzes_on_quiz_page
|
||||
# make the last two of course_assignments to be quiz_lti assignment
|
||||
(2..3).each { |i| course_assignments[i].quiz_lti! && course_assignments[i].save! }
|
||||
course_quizzes
|
||||
end
|
||||
|
||||
context "teacher interface" do
|
||||
it "includes all old quizzes and new quizzes" do
|
||||
user_session(@teacher)
|
||||
get 'index', params: { course_id: @course.id }
|
||||
expect(controller.js_env[:QUIZZES][:assignment]).not_to be_nil
|
||||
expect(controller.js_env[:QUIZZES][:assignment].count).to eq(4)
|
||||
expect(
|
||||
controller.js_env[:QUIZZES][:assignment].map{ |x| x[:id] }
|
||||
).to contain_exactly(
|
||||
course_quizzes[0].id,
|
||||
course_quizzes[1].id,
|
||||
course_assignments[2].id,
|
||||
course_assignments[3].id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "student interface" do
|
||||
it "includes published quizzes" do
|
||||
user_session(@student)
|
||||
get 'index', params: { course_id: @course.id }
|
||||
expect(controller.js_env[:QUIZZES][:assignment]).not_to be_nil
|
||||
expect(controller.js_env[:QUIZZES][:assignment].count).to eq(2)
|
||||
expect(
|
||||
controller.js_env[:QUIZZES][:assignment].map{ |x| x[:id] }
|
||||
).to contain_exactly(course_quizzes[1].id, course_assignments[3].id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST 'new'" do
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
#
|
||||
# Copyright (C) 2019 - present 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/>.
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe QuizzesNext::QuizSerializer do
|
||||
subject { quiz_serializer.as_json }
|
||||
|
||||
let(:original_context) do
|
||||
Account.default.courses.create
|
||||
end
|
||||
|
||||
let(:original_assignment) do
|
||||
group = original_context.assignment_groups.create(:name => "some group 1")
|
||||
original_context.assignments.create(
|
||||
title: 'some assignment 1',
|
||||
assignment_group: group,
|
||||
due_at: Time.zone.now + 1.week,
|
||||
workflow_state: 'published'
|
||||
)
|
||||
end
|
||||
|
||||
let(:context) do
|
||||
Account.default.courses.create
|
||||
end
|
||||
|
||||
let(:assignment) do
|
||||
group = context.assignment_groups.create(:name => "some group")
|
||||
context.assignments.create(
|
||||
title: 'some assignment',
|
||||
assignment_group: group,
|
||||
due_at: Time.zone.now + 1.week,
|
||||
workflow_state: 'published',
|
||||
duplicate_of: original_assignment
|
||||
)
|
||||
end
|
||||
let(:user) { User.create }
|
||||
let(:session) { double(:[] => nil) }
|
||||
let(:controller) do
|
||||
ActiveModel::FakeController.new(accepts_jsonapi: false, stringify_json_ids: false)
|
||||
end
|
||||
let(:quiz_serializer) do
|
||||
QuizzesNext::QuizSerializer.new(assignment, {
|
||||
controller: controller,
|
||||
scope: user,
|
||||
session: session,
|
||||
root: false
|
||||
})
|
||||
end
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:session).and_return session
|
||||
allow(controller).to receive(:context).and_return context
|
||||
allow(assignment).to receive(:grants_right?).at_least(:once).and_return true
|
||||
allow(context).to receive(:grants_right?).at_least(:once).and_return true
|
||||
end
|
||||
|
||||
[
|
||||
:id, :title, :description, :due_at, :lock_at, :unlock_at,
|
||||
:points_possible,
|
||||
:assignment_group_id, :migration_id, :only_visible_to_overrides,
|
||||
:post_to_sis, :allowed_attempts,
|
||||
:workflow_state
|
||||
].each do |attribute|
|
||||
it "serializes #{attribute}" do
|
||||
expect(subject[attribute]).to eq assignment.send(attribute)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#quiz_type" do
|
||||
it "serializes quiz_type" do
|
||||
expect(subject[:quiz_type]).to eq('quizzes.next')
|
||||
end
|
||||
end
|
||||
|
||||
describe "#published" do
|
||||
it "serializes published" do
|
||||
expect(subject[:published]).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#course_id" do
|
||||
it "serializes course_id" do
|
||||
expect(subject[:course_id]).to eq(context.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#assignment_id" do
|
||||
it "serializes assignment_id" do
|
||||
expect(subject[:assignment_id]).to eq(assignment.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#original_course_id" do
|
||||
it "serializes original_course_id" do
|
||||
expect(subject[:original_course_id]).to eq(original_context.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#original_assignment_id" do
|
||||
it "serializes original_assignment_id" do
|
||||
expect(subject[:original_assignment_id]).to eq(original_assignment.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#original_assignment_name" do
|
||||
it "serializes original_assignment_name" do
|
||||
expect(subject[:original_assignment_name]).to eq('some assignment 1')
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions" do
|
||||
it "serializes permissions" do
|
||||
expect(subject[:permissions]).to include({
|
||||
read: true,
|
||||
create: true,
|
||||
update: true,
|
||||
delete: true
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue