add allowed_attempts to assignments
Test Plan: allowed_attempts: * As a teacher in a course, set the "allowed_attempts" parameter to something for an assignment. Example curl: curl -H "Authorization: Bearer <token>" \ http://canvas-url.com/api/v1/courses/834/assignments/370 \ -X PUT \ -F "assignment[allowed_attempts]=10" * Perform a GET request on that assignment to validate that it was set * Validate that you can set allowed_attempts to values greater than 0 or -1 * Test a Course Copy and Course Import -- validate that the allowed_attempts come over when a course is copied AssignmentExtensions: * Validate that the submissions API now returns "extra_attempts" * Test setting extra_attempts with the new AssignmentExtensions API. Example curl: curl -H "Authorization: Bearer <api token>" \ http://canvas-url.com/api/v1/courses/14/assignments/1/extensions \ -X POST \ -F "assignment_extensions[][user_id]=1" \ -F "assignment_extensions[][extra_attempts]=10" * Validate that you must have permissions to create assignments in a course in order to use this API * Validate that extra_attempts can not be set to negative values refs PFS-11440 Change-Id: I43a5b97d9d03df47ed3c15c4c59714b99c190921 Reviewed-on: https://gerrit.instructure.com/172569 Tested-by: Jenkins QA-Review: Marisa Jense <mjense@instructure.com> Product-Review: Bryce Stevenson <bstevenson@instructure.com> Reviewed-by: Carl Kibler <ckibler@instructure.com>
This commit is contained in:
parent
ceeed5f1d2
commit
77020a4d3f
|
@ -0,0 +1,127 @@
|
|||
#
|
||||
# Copyright (C) 2018 - 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/>.
|
||||
|
||||
# @API Assignment Extensions
|
||||
#
|
||||
# API for setting extensions on student assignment submissions. These cannot be
|
||||
# set for discussion assignments or quizzes. For quizzes, use <a href="/doc/api/quiz_extensions.html">Quiz Extensions</a>
|
||||
# instead.
|
||||
#
|
||||
# @model AssignmentExtension
|
||||
# {
|
||||
# "id": "AssignmentExtension",
|
||||
# "required": ["assignment_id", "user_id"],
|
||||
# "properties": {
|
||||
# "assignment_id": {
|
||||
# "description": "The ID of the Assignment the extension belongs to.",
|
||||
# "example": 2,
|
||||
# "type": "integer",
|
||||
# "format": "int64"
|
||||
# },
|
||||
# "user_id": {
|
||||
# "description": "The ID of the Student that needs the assignment extension.",
|
||||
# "example": 3,
|
||||
# "type": "integer",
|
||||
# "format": "int64"
|
||||
# },
|
||||
# "extra_attempts": {
|
||||
# "description": "Number of times the student is allowed to re-submit the assignment",
|
||||
# "example": 2,
|
||||
# "type": "integer",
|
||||
# "format": "int64"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
class AssignmentExtensionsController < ApplicationController
|
||||
before_action :require_context, :require_user, :require_assignment
|
||||
|
||||
# @API Set extensions for student assignment submissions
|
||||
#
|
||||
# @argument assignment_extensions[][user_id] [Required, Integer]
|
||||
# The ID of the user we want to add assignment extensions for.
|
||||
#
|
||||
# @argument assignment_extensions[][extra_attempts] [Required, Integer]
|
||||
# Number of times the student is allowed to re-take the assignment over the
|
||||
# limit.
|
||||
#
|
||||
# <b>Responses</b>
|
||||
#
|
||||
# * <b>200 OK</b> if the request was successful
|
||||
# * <b>403 Forbidden</b> if you are not allowed to extend assignments for this course
|
||||
# * <b>400 Bad Request</b> if any of the extensions are invalid
|
||||
# @example_request
|
||||
# {
|
||||
# "assignment_extensions": [{
|
||||
# "user_id": 3,
|
||||
# "extra_attempts": 2
|
||||
# },{
|
||||
# "user_id": 2,
|
||||
# "extra_attempts": 2
|
||||
# }]
|
||||
# }
|
||||
#
|
||||
# @example_response
|
||||
# {
|
||||
# "assignment_extensions": [AssignmentExtension]
|
||||
# }
|
||||
#
|
||||
def create
|
||||
return unless authorized_action?(@assignment, @current_user, :create)
|
||||
|
||||
unless params[:assignment_extensions].is_a?(Array)
|
||||
reject! "missing required key :assignment_extensions", 400
|
||||
end
|
||||
|
||||
extensions_are_valid = params[:assignment_extensions].all? do |extension|
|
||||
extension.key?(:user_id) && extension.key?(:extra_attempts)
|
||||
end
|
||||
|
||||
unless extensions_are_valid
|
||||
reject! ":assignment_extensions must be in this format: { user_id: 1, extra_attempts: 2 }", 400
|
||||
end
|
||||
|
||||
# A hash where the key is the user id and the value is the extra attempts.
|
||||
# This allows us to avoid N+1 queries.
|
||||
assignment_extensions_map = {}
|
||||
params[:assignment_extensions].each { |ext| assignment_extensions_map[ext[:user_id].to_s] = ext[:extra_attempts] }
|
||||
|
||||
submissions = @context.submissions.where(user_id: assignment_extensions_map.keys, assignment_id: @assignment.id)
|
||||
submissions.each { |submission| submission.extra_attempts = assignment_extensions_map[submission.user_id.to_s] }
|
||||
|
||||
if submissions.all?(&:valid?) # Check if all are valid before saving
|
||||
submissions.each(&:save)
|
||||
assignment_extensions = submissions.map do |submission|
|
||||
{ user_id: submission.user.id, extra_attempts: submission.extra_attempts }
|
||||
end
|
||||
|
||||
render json: { assignment_extensions: assignment_extensions }
|
||||
else
|
||||
invalid_submissions = submissions.reject(&:valid?)
|
||||
errors = invalid_submissions.map do |submission|
|
||||
{ user_id: submission.user.id, errors: submission.errors.full_messages }
|
||||
end
|
||||
|
||||
render json: { errors: errors }, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_assignment
|
||||
@assignment = @context.active_assignments.find(params[:assignment_id])
|
||||
end
|
||||
end
|
|
@ -569,6 +569,11 @@
|
|||
# "description": "Boolean indicating if the assignment is moderated.",
|
||||
# "example": true,
|
||||
# "type": "boolean"
|
||||
# },
|
||||
# "allowed_attempts": {
|
||||
# "description": "The number of submission attempts a student can make for this assignment. -1 is considered unlimited.",
|
||||
# "example": 2,
|
||||
# "type": "integer"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
@ -945,6 +950,9 @@ class AssignmentsApiController < ApplicationController
|
|||
# @argument assignment[moderated_grading] [Boolean]
|
||||
# Whether this assignment is moderated.
|
||||
#
|
||||
# @argument assignment[allowed_attempts] [Integer]
|
||||
# The number of submission attempts allowed for this assignment. Set to -1 for unlimited attempts.
|
||||
#
|
||||
# @returns Assignment
|
||||
def create
|
||||
@assignment = @context.assignments.build
|
||||
|
@ -1101,6 +1109,10 @@ class AssignmentsApiController < ApplicationController
|
|||
# @argument assignment[moderated_grading] [Boolean]
|
||||
# Whether this assignment is moderated.
|
||||
#
|
||||
# @argument assignment[allowed_attempts] [Integer]
|
||||
# The number of submission attempts allowed for this assignment. Set to -1 or null for
|
||||
# unlimited attempts.
|
||||
#
|
||||
# @returns Assignment
|
||||
def update
|
||||
@assignment = @context.active_assignments.api_id(params[:id])
|
||||
|
|
|
@ -174,6 +174,11 @@
|
|||
# "pending_review"
|
||||
# ]
|
||||
# }
|
||||
# },
|
||||
# "extra_attempts": {
|
||||
# "description": "Extra submission attempts allowed for the given user and assignment.",
|
||||
# "example": 10,
|
||||
# "type": "number"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
|
|
@ -117,6 +117,7 @@ class Assignment < ActiveRecord::Base
|
|||
validate :assignment_name_length_ok?, :unless => :deleted?
|
||||
validates :lti_context_id, presence: true, uniqueness: true
|
||||
validates :grader_count, numericality: true
|
||||
validates :allowed_attempts, numericality: { greater_than: 0 }, unless: proc { |a| a.allowed_attempts == -1 }, allow_nil: true
|
||||
|
||||
with_options unless: :moderated_grading? do
|
||||
validates :graders_anonymous_to_graders, absence: true
|
||||
|
|
|
@ -195,6 +195,8 @@ module Importers
|
|||
item.points_possible = 0
|
||||
end
|
||||
|
||||
item.allowed_attempts = hash[:allowed_attempts] if hash[:allowed_attempts]
|
||||
|
||||
if !item.new_record? && item.is_child_content? && (item.editing_restricted?(:due_dates) || item.editing_restricted?(:availability_dates))
|
||||
# is a date-restricted master course item - clear their old overrides because we're mean
|
||||
item.assignment_overrides.where.not(:set_type => AssignmentOverride::SET_TYPE_NOOP).destroy_all
|
||||
|
|
|
@ -115,8 +115,10 @@ class Submission < ActiveRecord::Base
|
|||
validates_as_url :url
|
||||
validates :points_deducted, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :seconds_late_override, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :extra_attempts, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :late_policy_status, inclusion: ['none', 'missing', 'late'], allow_nil: true
|
||||
validate :ensure_grader_can_grade
|
||||
validate :extra_attempts_can_only_be_set_on_online_uploads
|
||||
|
||||
scope :active, -> { where("submissions.workflow_state <> 'deleted'") }
|
||||
scope :for_enrollments, -> (enrollments) { where(user_id: enrollments.select(:user_id)) }
|
||||
|
@ -1514,6 +1516,16 @@ class Submission < ActiveRecord::Base
|
|||
false
|
||||
end
|
||||
|
||||
def extra_attempts_can_only_be_set_on_online_uploads
|
||||
return true unless changes.key?("extra_attempts") && assignment
|
||||
allowed_submission_types = %w[online_upload online_url online_text_entry]
|
||||
return true if (assignment.submission_types.split(",") & allowed_submission_types).any?
|
||||
|
||||
error_msg = 'extra_attempts can only be set on submissions for an assignment with a type of online_upload, online_url, or online_text_entry'
|
||||
errors.add(:extra_attempts, error_msg)
|
||||
false
|
||||
end
|
||||
|
||||
def can_autograde?
|
||||
result = GRADE_STATUS_MESSAGES_MAP[can_autograde_symbolic_status]
|
||||
result ||= { status: false, message: I18n.t('Cannot autograde at this time') }
|
||||
|
|
|
@ -1093,6 +1093,10 @@ CanvasRails::Application.routes.draw do
|
|||
delete 'courses/:course_id/assignments/:id', action: :destroy, controller: :assignments
|
||||
end
|
||||
|
||||
scope(controller: 'assignment_extensions') do
|
||||
post "courses/:course_id/assignments/:assignment_id/extensions", action: :create, as: "course_assignment_extensions_create"
|
||||
end
|
||||
|
||||
scope(controller: :peer_reviews_api) do
|
||||
get 'courses/:course_id/assignments/:assignment_id/peer_reviews', action: :index
|
||||
get 'sections/:section_id/assignments/:assignment_id/peer_reviews', action: :index
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
#
|
||||
# Copyright (C) 2018 - 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/>.
|
||||
|
||||
class AddAllowedAttemptsToAssignments < ActiveRecord::Migration[5.1]
|
||||
tag :predeploy
|
||||
|
||||
def change
|
||||
add_column :assignments, :allowed_attempts, :integer
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
#
|
||||
# Copyright (C) 2018 - 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/>.
|
||||
|
||||
class AddExtraAttemptsToSubmissions < ActiveRecord::Migration[5.1]
|
||||
tag :predeploy
|
||||
|
||||
def change
|
||||
add_column :submissions, :extra_attempts, :integer
|
||||
end
|
||||
end
|
|
@ -60,6 +60,7 @@ module Api::V1::Assignment
|
|||
omit_from_final_grade
|
||||
anonymous_instructor_annotations
|
||||
anonymous_grading
|
||||
allowed_attempts
|
||||
)
|
||||
}.freeze
|
||||
|
||||
|
@ -141,6 +142,7 @@ module Api::V1::Assignment
|
|||
hash['has_submitted_submissions'] = assignment.has_submitted_submissions?
|
||||
hash['due_date_required'] = assignment.due_date_required?
|
||||
hash['max_name_length'] = assignment.max_name_length
|
||||
hash['allowed_attempts'] = -1 if assignment.allowed_attempts.nil?
|
||||
|
||||
unless opts[:exclude_response_fields].include?('in_closed_grading_period')
|
||||
hash['in_closed_grading_period'] = assignment.in_closed_grading_period?
|
||||
|
@ -423,6 +425,7 @@ module Api::V1::Assignment
|
|||
integration_id
|
||||
omit_from_final_grade
|
||||
anonymous_instructor_annotations
|
||||
allowed_attempts
|
||||
).freeze
|
||||
|
||||
API_ALLOWED_TURNITIN_SETTINGS = %w(
|
||||
|
|
|
@ -114,7 +114,7 @@ module Api::V1::Submission
|
|||
|
||||
SUBMISSION_JSON_FIELDS = %w(id user_id url score grade excused attempt submission_type submitted_at body
|
||||
assignment_id graded_at grade_matches_current_submission grader_id workflow_state late_policy_status
|
||||
points_deducted grading_period_id cached_due_date).freeze
|
||||
points_deducted grading_period_id cached_due_date extra_attempts).freeze
|
||||
SUBMISSION_JSON_METHODS = %w(late missing seconds_late entered_grade entered_score).freeze
|
||||
SUBMISSION_OTHER_FIELDS = %w(attachments discussion_entries).freeze
|
||||
|
||||
|
|
|
@ -246,7 +246,8 @@ module CC
|
|||
:omit_from_final_grade, :intra_group_peer_reviews, :only_visible_to_overrides, :post_to_sis,
|
||||
:moderated_grading, :grader_count, :grader_comments_visible_to_graders,
|
||||
:anonymous_grading, :graders_anonymous_to_graders, :grader_names_visible_to_final_grader,
|
||||
:anonymous_instructor_annotations
|
||||
:anonymous_instructor_annotations,
|
||||
:allowed_attempts
|
||||
]
|
||||
atts.each do |att|
|
||||
node.tag!(att, assignment.send(att)) if assignment.send(att) == false || !assignment.send(att).blank?
|
||||
|
|
|
@ -136,7 +136,9 @@ module CC::Importer::Standard
|
|||
val = get_float_val(meta_doc, f_type)
|
||||
assignment[f_type] = val unless val.nil?
|
||||
end
|
||||
assignment['position'] = get_int_val(meta_doc, 'position')
|
||||
['position', 'allowed_attempts'].each do |i_type|
|
||||
assignment[i_type] = get_int_val(meta_doc, i_type)
|
||||
end
|
||||
assignment['peer_review_count'] = get_int_val(meta_doc, 'peer_review_count')
|
||||
assignment["grader_count"] = get_int_val(meta_doc, "grader_count")
|
||||
if meta_doc.at_css("assignment_overrides override")
|
||||
|
|
|
@ -192,6 +192,7 @@
|
|||
<xs:element name="omit_from_final_grade" type="xs:boolean" minOccurs="0"/>
|
||||
<xs:element name="only_visible_to_overrides" type="xs:boolean" minOccurs="0"/>
|
||||
<xs:element name="post_to_sis" type="xs:boolean" minOccurs="0"/>
|
||||
<xs:element name="allowed_attempts" type="optional_integer" minOccurs="0"/>
|
||||
<xs:element name="similarity_detection_tool" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="resource_type_code" type="xs:string" use="required"/>
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
#
|
||||
# Copyright (C) 2018 - 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 File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
|
||||
|
||||
describe AssignmentExtensionsController, type: :request do
|
||||
before :once do
|
||||
course_factory
|
||||
@assignment = @course.assignments.create!(title: "assignment")
|
||||
@assignment.workflow_state = "available"
|
||||
@assignment.submission_types = "online_upload"
|
||||
@assignment.save!
|
||||
@student = student_in_course(course: @course, active_all: true).user
|
||||
end
|
||||
|
||||
describe "POST /api/v1/courses/:course_id/assignments/:assignment_id/extensions (create)" do
|
||||
def api_create_assignment_extensions(assignment_extension_params, opts={}, raw=false)
|
||||
api_method = raw ? :raw_api_call : :api_call
|
||||
|
||||
send(api_method,
|
||||
:post,
|
||||
"/api/v1/courses/#{@course.id}/assignments/#{@assignment.id}/extensions",
|
||||
{
|
||||
controller: "assignment_extensions",
|
||||
action: "create",
|
||||
format: "json",
|
||||
course_id: @course.id.to_s,
|
||||
assignment_id: @assignment.id.to_s
|
||||
},
|
||||
{
|
||||
assignment_extensions: assignment_extension_params
|
||||
},
|
||||
{
|
||||
"Accept" => "application/vnd.api+json"
|
||||
},
|
||||
opts)
|
||||
end
|
||||
|
||||
context "as a student" do
|
||||
it "should be unauthorized" do
|
||||
@user = @student
|
||||
assignment_extension_params = [
|
||||
{ user_id: @student.id, extra_attempts: 3 },
|
||||
]
|
||||
api_create_assignment_extensions(assignment_extension_params, {}, true)
|
||||
assert_status(401)
|
||||
end
|
||||
end
|
||||
|
||||
context "as a teacher" do
|
||||
before :once do
|
||||
@student2 = user_factory
|
||||
@course.enroll_user(@student2, "StudentEnrollment", {})
|
||||
@teacher = teacher_in_course(course: @course, active_all: true).user
|
||||
@user = @teacher
|
||||
end
|
||||
|
||||
it "should extend attempts for the existing submission" do
|
||||
submission = @student.submissions.find_by(assignment_id: @assignment.id)
|
||||
assignment_extension_params = [
|
||||
{ user_id: @student.id, extra_attempts: 3 },
|
||||
]
|
||||
|
||||
api_create_assignment_extensions(assignment_extension_params)
|
||||
expect(submission.reload.extra_attempts).to eq(3)
|
||||
end
|
||||
|
||||
it "should extend attempts for multiple students" do
|
||||
submission_1 = @student.submissions.find_by(assignment_id: @assignment.id)
|
||||
submission_2 = @student2.submissions.find_by(assignment_id: @assignment.id)
|
||||
assignment_extension_params = [
|
||||
{ user_id: @student.id, extra_attempts: 3 },
|
||||
{ user_id: @student2.id, extra_attempts: 10 },
|
||||
]
|
||||
|
||||
api_create_assignment_extensions(assignment_extension_params)
|
||||
expect(submission_1.reload.extra_attempts).to eq(3)
|
||||
expect(submission_2.reload.extra_attempts).to eq(10)
|
||||
end
|
||||
|
||||
it "should error out if any of the extensions were invalid, and the response should indicate which ones were incorrect" do
|
||||
submission_1 = @student.submissions.find_by(assignment_id: @assignment.id)
|
||||
submission_2 = @student2.submissions.find_by(assignment_id: @assignment.id)
|
||||
assignment_extension_params = [
|
||||
{ user_id: @student.id, extra_attempts: -10 },
|
||||
{ user_id: @student2.id, extra_attempts: 10 },
|
||||
]
|
||||
|
||||
response = api_create_assignment_extensions(assignment_extension_params)
|
||||
expect(submission_1.reload.extra_attempts).to be_nil
|
||||
expect(submission_2.reload.extra_attempts).to be_nil
|
||||
expect(response["errors"]).to eq([
|
||||
{ "user_id" => @student.id, "errors" => ["Extra attempts must be greater than or equal to 0"] }
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1325,7 +1325,8 @@ describe AssignmentsApiController, type: :request do
|
|||
'group_category_id' => group_category.id,
|
||||
'turnitin_enabled' => true,
|
||||
'vericite_enabled' => true,
|
||||
'grading_type' => 'points'
|
||||
'grading_type' => 'points',
|
||||
'allowed_attempts' => 2
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -1460,6 +1461,7 @@ describe AssignmentsApiController, type: :request do
|
|||
expect(@json['due_at']).to eq @assignment.due_at.iso8601
|
||||
expect(@json['html_url']).to eq course_assignment_url(@course,@assignment)
|
||||
expect(@json['needs_grading_count']).to eq 0
|
||||
expect(@json['allowed_attempts']).to eq 2
|
||||
|
||||
expect(Assignment.count).to eq 1
|
||||
end
|
||||
|
@ -2938,7 +2940,8 @@ describe AssignmentsApiController, type: :request do
|
|||
'grading_type' => 'letter_grade',
|
||||
'due_at' => '2011-01-01T00:00:00Z',
|
||||
'position' => 1,
|
||||
'muted' => true
|
||||
'muted' => true,
|
||||
'allowed_attempts' => 10
|
||||
})
|
||||
@assignment.reload
|
||||
end
|
||||
|
@ -3036,6 +3039,10 @@ describe AssignmentsApiController, type: :request do
|
|||
expect(@assignment.grading_standard_id).to eq @new_grading_standard.id
|
||||
expect(@json['grading_standard_id']).to eq @new_grading_standard.id
|
||||
end
|
||||
|
||||
it "updates the allowed_attempts" do
|
||||
expect(@json['allowed_attempts']).to eq 10
|
||||
end
|
||||
end
|
||||
|
||||
it "should process html content in description on update" do
|
||||
|
@ -4889,7 +4896,8 @@ describe AssignmentsApiController, type: :request do
|
|||
"points_deducted" => nil,
|
||||
"seconds_late" => 0,
|
||||
"preview_url" =>
|
||||
"http://www.example.com/courses/#{@observer_course.id}/assignments/#{@assignment.id}/submissions/#{@observed_student.id}?preview=1&version=0"
|
||||
"http://www.example.com/courses/#{@observer_course.id}/assignments/#{@assignment.id}/submissions/#{@observed_student.id}?preview=1&version=0",
|
||||
"extra_attempts" => nil
|
||||
}]
|
||||
end
|
||||
|
||||
|
@ -4927,7 +4935,8 @@ describe AssignmentsApiController, type: :request do
|
|||
"points_deducted" => nil,
|
||||
"seconds_late" => 0,
|
||||
"preview_url" =>
|
||||
"http://www.example.com/courses/#{@observer_course.id}/assignments/#{@assignment.id}/submissions/#{@observed_student.id}?preview=1&version=0"
|
||||
"http://www.example.com/courses/#{@observer_course.id}/assignments/#{@assignment.id}/submissions/#{@observed_student.id}?preview=1&version=0",
|
||||
"extra_attempts" => nil
|
||||
}]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -421,6 +421,7 @@ describe UsersController, type: :request do
|
|||
'seconds_late' => 0,
|
||||
'url' => nil,
|
||||
'user_id' => @sub.user_id,
|
||||
'extra_attempts' => nil,
|
||||
|
||||
'submission_comments' => [
|
||||
{
|
||||
|
@ -547,6 +548,7 @@ describe UsersController, type: :request do
|
|||
'seconds_late' => 0,
|
||||
'url' => nil,
|
||||
'user_id' => @sub.user_id,
|
||||
'extra_attempts' => nil,
|
||||
|
||||
'submission_comments' => [
|
||||
{
|
||||
|
|
|
@ -937,7 +937,8 @@ describe 'Submissions API', type: :request do
|
|||
"missing"=>false,
|
||||
"late_policy_status"=>nil,
|
||||
"seconds_late"=>0,
|
||||
"points_deducted"=>nil
|
||||
"points_deducted"=>nil,
|
||||
"extra_attempts"=>nil
|
||||
})
|
||||
|
||||
# can't access other students' submissions
|
||||
|
@ -1086,6 +1087,7 @@ describe 'Submissions API', type: :request do
|
|||
"cached_due_date" => nil,
|
||||
"preview_url" => "http://www.example.com/courses/#{@course.id}/assignments/#{a1.id}/submissions/#{student1.id}?preview=1&version=3",
|
||||
"grade_matches_current_submission"=>true,
|
||||
"extra_attempts" => nil,
|
||||
"attachments" =>
|
||||
[
|
||||
{ "content-type" => "application/loser",
|
||||
|
@ -1136,7 +1138,8 @@ describe 'Submissions API', type: :request do
|
|||
"missing"=>false,
|
||||
"late_policy_status"=>nil,
|
||||
"seconds_late"=>0,
|
||||
"points_deducted"=>nil},
|
||||
"points_deducted"=>nil,
|
||||
"extra_attempts"=>nil},
|
||||
{"id"=>sub1.id,
|
||||
"grade"=>nil,
|
||||
"entered_grade"=>nil,
|
||||
|
@ -1167,7 +1170,8 @@ describe 'Submissions API', type: :request do
|
|||
"missing"=>false,
|
||||
"late_policy_status"=>nil,
|
||||
"seconds_late"=>0,
|
||||
"points_deducted"=>nil},
|
||||
"points_deducted"=>nil,
|
||||
"extra_attempts"=>nil},
|
||||
{"id"=>sub1.id,
|
||||
"grade"=>"A-",
|
||||
"entered_grade"=>"A-",
|
||||
|
@ -1222,7 +1226,8 @@ describe 'Submissions API', type: :request do
|
|||
"missing"=>false,
|
||||
"late_policy_status"=>nil,
|
||||
"seconds_late"=>0,
|
||||
"points_deducted"=>nil}],
|
||||
"points_deducted"=>nil,
|
||||
"extra_attempts"=>nil}],
|
||||
"attempt"=>3,
|
||||
"url"=>nil,
|
||||
"submission_type"=>"online_text_entry",
|
||||
|
@ -1274,6 +1279,7 @@ describe 'Submissions API', type: :request do
|
|||
"preview_url" => "http://www.example.com/courses/#{@course.id}/assignments/#{a1.id}/submissions/#{student2.id}?preview=1&version=1",
|
||||
"grade_matches_current_submission"=>true,
|
||||
"submitted_at"=>"1970-01-01T04:00:00Z",
|
||||
"extra_attempts" => nil,
|
||||
"submission_history"=>
|
||||
[{"id"=>sub2.id,
|
||||
"grade"=>"F",
|
||||
|
@ -1325,7 +1331,8 @@ describe 'Submissions API', type: :request do
|
|||
"missing"=>false,
|
||||
"late_policy_status"=>nil,
|
||||
"seconds_late"=>0,
|
||||
"points_deducted"=>nil}],
|
||||
"points_deducted"=>nil,
|
||||
"extra_attempts"=>nil}],
|
||||
"attempt"=>1,
|
||||
"url"=>"http://www.instructure.com",
|
||||
"submission_type"=>"online_url",
|
||||
|
|
|
@ -83,6 +83,30 @@ describe "Api::V1::Assignment" do
|
|||
expect(json['planner_override']).to be_nil
|
||||
end
|
||||
|
||||
describe "the allowed_attempts attribute" do
|
||||
it "returns -1 if set to nil" do
|
||||
assignment.update_attribute(:allowed_attempts, nil)
|
||||
json = api.assignment_json(assignment, user, session, {override_dates: false})
|
||||
expect(json["allowed_attempts"]).to eq(-1)
|
||||
end
|
||||
|
||||
it "returns -1 if set to -1" do
|
||||
assignment.update_attribute(:allowed_attempts, -1)
|
||||
json = api.assignment_json(assignment, user, session, {override_dates: false})
|
||||
expect(json["allowed_attempts"]).to eq(-1)
|
||||
end
|
||||
|
||||
it "returns any other values as set in the databse" do
|
||||
assignment.update_attribute(:allowed_attempts, 1)
|
||||
json = api.assignment_json(assignment, user, session, {override_dates: false})
|
||||
expect(json["allowed_attempts"]).to eq(1)
|
||||
|
||||
assignment.update_attribute(:allowed_attempts, 2)
|
||||
json = api.assignment_json(assignment, user, session, {override_dates: false})
|
||||
expect(json["allowed_attempts"]).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "for an assignment" do
|
||||
it "provides a submissions download URL" do
|
||||
json = api.assignment_json(assignment, user, session)
|
||||
|
|
|
@ -6953,6 +6953,34 @@ describe Assignment do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'allowed_attempts validation' do
|
||||
before(:once) do
|
||||
assignment_model(course: @course)
|
||||
end
|
||||
|
||||
it { is_expected.to validate_numericality_of(:allowed_attempts).allow_nil }
|
||||
|
||||
it 'should allow -1' do
|
||||
@assignment.allowed_attempts = -1
|
||||
expect(@assignment).to be_valid
|
||||
end
|
||||
|
||||
it 'should disallow 0' do
|
||||
@assignment.allowed_attempts = 0
|
||||
expect(@assignment).to_not be_valid
|
||||
end
|
||||
|
||||
it 'should disallow values less than -1' do
|
||||
@assignment.allowed_attempts = -2
|
||||
expect(@assignment).to_not be_valid
|
||||
end
|
||||
|
||||
it 'should allow values greater than 0' do
|
||||
@assignment.allowed_attempts = 2
|
||||
expect(@assignment).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "after create callbacks" do
|
||||
subject(:event) { AnonymousOrModerationEvent.where(assignment: assignment).last }
|
||||
|
||||
|
|
|
@ -187,6 +187,7 @@ describe ContentMigration do
|
|||
@assignment.omit_from_final_grade = true
|
||||
@assignment.only_visible_to_overrides = true
|
||||
@assignment.post_to_sis = true
|
||||
@assignment.allowed_attempts = 10
|
||||
|
||||
@assignment.save!
|
||||
|
||||
|
@ -196,7 +197,7 @@ describe ContentMigration do
|
|||
attrs = [:turnitin_enabled, :vericite_enabled, :turnitin_settings, :peer_reviews,
|
||||
:automatic_peer_reviews, :anonymous_peer_reviews,
|
||||
:grade_group_students_individually, :allowed_extensions,
|
||||
:position, :peer_review_count, :omit_from_final_grade, :post_to_sis]
|
||||
:position, :peer_review_count, :omit_from_final_grade, :post_to_sis, :allowed_attempts]
|
||||
|
||||
run_course_copy
|
||||
|
||||
|
@ -212,6 +213,38 @@ describe ContentMigration do
|
|||
expect(new_assignment.only_visible_to_overrides).to be_falsey
|
||||
end
|
||||
|
||||
describe "allowed_attempts copying" do
|
||||
it "copies nil over properly" do
|
||||
assignment_model(course: @copy_from, points_possible: 40, submission_types: 'file_upload', grading_type: 'points')
|
||||
@assignment.allowed_attempts = nil
|
||||
@assignment.save!
|
||||
|
||||
run_course_copy
|
||||
new_assignment = @copy_to.assignments.where(migration_id: mig_id(@assignment)).last
|
||||
expect(new_assignment.allowed_attempts).to be_nil
|
||||
end
|
||||
|
||||
it "copies -1 over properly" do
|
||||
assignment_model(course: @copy_from, points_possible: 40, submission_types: 'file_upload', grading_type: 'points')
|
||||
@assignment.allowed_attempts = -1
|
||||
@assignment.save!
|
||||
|
||||
run_course_copy
|
||||
new_assignment = @copy_to.assignments.where(migration_id: mig_id(@assignment)).last
|
||||
expect(new_assignment.allowed_attempts).to eq(-1)
|
||||
end
|
||||
|
||||
it "copies values > 0 over properly" do
|
||||
assignment_model(course: @copy_from, points_possible: 40, submission_types: 'file_upload', grading_type: 'points')
|
||||
@assignment.allowed_attempts = 3
|
||||
@assignment.save!
|
||||
|
||||
run_course_copy
|
||||
new_assignment = @copy_to.assignments.where(migration_id: mig_id(@assignment)).last
|
||||
expect(new_assignment.allowed_attempts).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
it "should copy other feature-dependent assignment attributes" do
|
||||
assignment_model(:course => @copy_from)
|
||||
@assignment.moderated_grading = true
|
||||
|
|
|
@ -5805,4 +5805,28 @@ describe Submission do
|
|||
}.from(nil).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'extra_attempts validations' do
|
||||
it { is_expected.to validate_numericality_of(:extra_attempts).is_greater_than_or_equal_to(0).allow_nil }
|
||||
|
||||
describe '#extra_attempts_can_only_be_set_on_online_uploads' do
|
||||
it 'does not allowe extra_attempts to be set for non online upload submission types' do
|
||||
submission = @assignment.submissions.first
|
||||
|
||||
%w[online_upload online_url online_text_entry].each do |submission_type|
|
||||
submission.assignment.submission_types = submission_type
|
||||
submission.assignment.save!
|
||||
submission.extra_attempts = 10
|
||||
expect(submission).to be_valid
|
||||
end
|
||||
|
||||
%w[discussion_entry online_quiz].each do |submission_type|
|
||||
submission.assignment.submission_types = submission_type
|
||||
submission.assignment.save!
|
||||
submission.extra_attempts = 10
|
||||
expect(submission).to_not be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue