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:
Nate Collings 2018-11-15 11:45:49 -07:00 committed by Nathan Collings
parent ceeed5f1d2
commit 77020a4d3f
22 changed files with 470 additions and 13 deletions

View File

@ -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

View File

@ -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])

View File

@ -174,6 +174,11 @@
# "pending_review"
# ]
# }
# },
# "extra_attempts": {
# "description": "Extra submission attempts allowed for the given user and assignment.",
# "example": 10,
# "type": "number"
# }
# }
# }

View File

@ -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

View File

@ -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

View File

@ -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') }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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?

View File

@ -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")

View File

@ -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"/>

View File

@ -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

View File

@ -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

View File

@ -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' => [
{

View File

@ -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",

View File

@ -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)

View File

@ -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 }

View File

@ -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

View File

@ -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