Quiz Submissions API - Index

Endpoints introduced:

  - GET /api/v1/courses/<course_id>/quizzes/<quiz_id>/submissions

Refactoring/changes:

  - Creating a ZIP archive has been moved, and refactored, to the
    Submission API model because it was used by both QuizSubmission
    and Submission controllers
  - time_spent is a property now exposed for QuizSubmission objects
  - new API spec shared example group 'API tests':
    - new API spec helper 'assert_jsonapi_compliance!' that tests a
      given API response for compliance with the JSON-API format
      specification

fixes CNVS-8978, CNVS-8923

Change-Id: I6a18e35a2bcd0ebe357faff46c18f1bce54ddac0
Reviewed-on: https://gerrit.instructure.com/26073
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Derek DeVries <ddevries@instructure.com>
QA-Review: Myller de Araujo <myller@instructure.com>
Product-Review: Derek DeVries <ddevries@instructure.com>
This commit is contained in:
Ahmad Amireh 2013-11-07 19:46:59 +03:00 committed by Derek DeVries
parent 38bb3ca38a
commit 8609c2a8f3
11 changed files with 681 additions and 76 deletions

View File

@ -16,9 +16,200 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# @API Quiz Submissions
# @beta
#
# API for accessing quiz submissions
#
# @model QuizSubmission
# {
# "id": "QuizSubmission",
# "required": ["id", "quiz_id"],
# "properties": {
# "id": {
# "description": "The ID of the quiz submission.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "quiz_id": {
# "description": "The ID of the Quiz the quiz submission belongs to.",
# "example": 2,
# "type": "integer",
# "format": "int64"
# },
# "user_id": {
# "description": "The ID of the Student that made the quiz submission.",
# "example": 3,
# "type": "integer",
# "format": "int64"
# },
# "submission_id": {
# "description": "The ID of the Submission the quiz submission represents.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "started_at": {
# "description": "The time at which the student started the quiz submission.",
# "example": "2013-11-07T13:16:18Z",
# "type": "string",
# "format": "date-time"
# },
# "finished_at": {
# "description": "The time at which the student submitted the quiz submission.",
# "example": "2013-11-07T13:16:18Z",
# "type": "string",
# "format": "date-time"
# },
# "end_at": {
# "description": "The time at which the quiz submission will be overdue, and be flagged as a late submission.",
# "example": "2013-11-07T13:16:18Z",
# "type": "string",
# "format": "date-time"
# },
# "attempt": {
# "description": "For quizzes that allow multiple attempts, this field specifies the quiz submission attempt number.",
# "example": 3,
# "type": "integer",
# "format": "int64"
# },
# "extra_attempts": {
# "description": "Number of times the student was allowed to re-take the quiz over the multiple-attempt limit.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "extra_time": {
# "description": "Amount of extra time allowed for the quiz submission, in seconds.",
# "example": 60,
# "type": "integer",
# "format": "int64"
# },
# "time_spent": {
# "description": "Amount of time spent, in seconds.",
# "example": 300,
# "type": "integer",
# "format": "int64"
# },
# "score": {
# "description": "The score of the quiz submission, if graded.",
# "example": 3,
# "type": "integer",
# "format": "int64"
# },
# "score_before_regrade": {
# "description": "The original score of the quiz submission prior to any re-grading.",
# "example": 2,
# "type": "integer",
# "format": "int64"
# },
# "kept_score": {
# "description": "For quizzes that allow multiple attempts, this is the score that will be used, which might be the score of the latest, or the highest, quiz submission.",
# "example": 5,
# "type": "integer",
# "format": "int64"
# },
# "fudge_points": {
# "description": "Number of points the quiz submission's score was fudged by.",
# "example": 1,
# "type": "integer",
# "format": "int64"
# },
# "workflow_state": {
# "description": "The current state of the quiz submission. Possible values: ['untaken'|'pending_review'|'complete'|'settings_only'|'preview'].",
# "example": "untaken",
# "type": "string"
# }
# }
# }
#
class QuizSubmissionsApiController < ApplicationController
include Api::V1::Submission
include Api::V1::QuizSubmission
before_filter :require_user, :require_context
before_filter :require_user, :require_context, :require_quiz
# @API Get all quiz submissions.
# @beta
#
# Get a list of all submissions for this quiz.
#
# @argument include[] [String, "submission"|"quiz"|"user"]
# Associations to include with the quiz submission.
#
# <b>200 OK</b> response code is returned if the request was successful.
#
# @example_response
# {
# "quiz_submissions": [
# {
# "attempt": 6,
# "end_at": null,
# "extra_attempts": null,
# "extra_time": null,
# "finished_at": "2013-11-07T13:16:18Z",
# "fudge_points": null,
# "id": 8,
# "kept_score": 4,
# "quiz_id": 8,
# "quiz_points_possible": 6,
# "quiz_version": 13,
# "score": 0,
# "score_before_regrade": null,
# "started_at": "2013-10-24T05:21:22Z",
# "submission_id": 6,
# "user_id": 2,
# "workflow_state": "pending_review",
# "time_spent": 1238095,
# "html_url": "http://example.com/courses/1/quizzes/8/submissions/8"
# },
# {
# "attempt": 1,
# "end_at": "2013-10-31T05:59:59Z",
# "extra_attempts": null,
# "extra_time": null,
# "finished_at": "2013-10-29T05:04:42Z",
# "fudge_points": 0,
# "id": 9,
# "kept_score": 5,
# "quiz_id": 8,
# "quiz_points_possible": 6,
# "quiz_version": 13,
# "score": 5,
# "score_before_regrade": null,
# "started_at": "2013-10-29T05:04:32Z",
# "submission_id": 7,
# "user_id": 5,
# "workflow_state": "complete",
# "time_spent": 10,
# "html_url": "http://example.com/courses/1/quizzes/8/submissions/9"
# }
# ]
# }
def index
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
scope = @quiz.quiz_submissions.where(:user_id => visible_user_ids)
api_route = polymorphic_url([:api, :v1, @context, @quiz, :submissions])
quiz_submissions = Api.paginate(scope, self, api_route)
includes = Array(params[:include])
out = quiz_submissions_json(quiz_submissions, @quiz, @current_user, session, @context, includes)
render :json => out
end
end
private
def require_quiz
unless @quiz = @context.quizzes.find(params[:quiz_id])
raise ActiveRecord::RecordNotFound
end
end
def visible_user_ids(opts = {})
scope = @context.enrollments_visible_to(@current_user, opts)
scope.pluck(:user_id)
end
end

View File

@ -17,6 +17,7 @@
#
class QuizSubmissionsController < ApplicationController
include Api::V1::QuizSubmission
protect_from_forgery :except => [:create, :backup, :record_answer]
before_filter :require_context
batch_jobs_in_actions :only => [:update, :create], :batch => { :priority => Delayed::LOW_PRIORITY }
@ -24,7 +25,7 @@ class QuizSubmissionsController < ApplicationController
def index
@quiz = @context.quizzes.find(params[:quiz_id])
if params[:zip] && authorized_action(@quiz, @current_user, :grade)
submission_zip
generate_submission_zip(@quiz, @context)
else
redirect_to named_context_url(@context, :context_quiz_url, @quiz.id)
end
@ -182,42 +183,46 @@ class QuizSubmissionsController < ApplicationController
is_previewing? ? { :preview => 1 } : {}
end
# TODO: this is mostly copied and pasted from submission_controller.rb. pull
# out common code
def submission_zip
@attachments = @quiz.attachments.where(:display_name => 'submissions.zip', :workflow_state => ['to_be_zipped', 'zipping', 'zipped', 'errored', 'unattached'], :user_id => @current_user).order(:created_at).all
@attachment = @attachments.pop
@attachments.each{|a| a.destroy! }
if @attachment && (@attachment.created_at < 1.hour.ago || @attachment.created_at < (@quiz.quiz_submissions.map{|s| s.finished_at}.compact.max || @attachment.created_at))
@attachment.destroy!
@attachment = nil
end
if !@attachment
@attachment = @quiz.attachments.build(:display_name => 'submissions.zip')
@attachment.workflow_state = 'to_be_zipped'
@attachment.file_state = '0'
@attachment.user = @current_user
@attachment.save!
ContentZipper.send_later_enqueue_args(:process_attachment, { :priority => Delayed::LOW_PRIORITY, :max_attempts => 1 }, @attachment)
render :json => @attachment
else
respond_to do |format|
if @attachment.zipped?
if Attachment.s3_storage?
format.html { redirect_to @attachment.cacheable_s3_inline_url }
format.zip { redirect_to @attachment.cacheable_s3_inline_url }
else
cancel_cache_buster
format.html { send_file(@attachment.full_filename, :type => @attachment.content_type_with_encoding, :disposition => 'inline') }
format.zip { send_file(@attachment.full_filename, :type => @attachment.content_type_with_encoding, :disposition => 'inline') }
end
format.json { render :json => @attachment.as_json(:methods => :readable_size) }
def generate_submission_zip(quiz, context)
attachment = quiz_submission_zip(quiz)
respond_to do |format|
if attachment.zipped?
if Attachment.s3_storage?
format.html { redirect_to attachment.cacheable_s3_inline_url }
format.zip { redirect_to attachment.cacheable_s3_inline_url }
else
flash[:notice] = t('still_zipping', "File zipping still in process...")
format.html { redirect_to named_context_url(@context, :context_quiz_url, @quiz.id) }
format.zip { redirect_to named_context_url(@context, :context_quiz_url, @quiz.id) }
format.json { render :json => @attachment }
cancel_cache_buster
format.html do
send_file(attachment.full_filename, {
:type => attachment.content_type_with_encoding,
:disposition => 'inline'
})
end
format.zip do
send_file(attachment.full_filename, {
:type => attachment.content_type_with_encoding,
:disposition => 'inline'
})
end
end
format.json { render :json => attachment.as_json(:methods => :readable_size) }
else
flash[:notice] = t('still_zipping', "File zipping still in process...")
format.html do
redirect_to named_context_url(context, :context_quiz_url, quiz.id)
end
format.zip do
redirect_to named_context_url(context, :context_quiz_url, quiz.id)
end
format.json { render :json => attachment }
end
end
end

View File

@ -106,7 +106,7 @@ class SubmissionsController < ApplicationController
@assignment = @context.assignments.active.find(params[:assignment_id])
if authorized_action(@assignment, @current_user, :grade)
if params[:zip]
submission_zip
generate_submission_zip(@assignment, @context)
else
respond_to do |format|
format.html { redirect_to named_context_url(@context, :context_assignment_url, @assignment.id) }
@ -529,44 +529,6 @@ class SubmissionsController < ApplicationController
protected
def submission_zip
@attachments = @assignment.attachments.where(:display_name => 'submissions.zip', :workflow_state => ['to_be_zipped', 'zipping', 'zipped', 'errored', 'unattached'], :user_id => @current_user).order(:created_at).all
@attachment = @attachments.pop
@attachments.each{|a| a.destroy! }
if @attachment && (@attachment.created_at < 1.hour.ago || @attachment.created_at < (@assignment.submissions.map{|s| s.submitted_at}.compact.max || @attachment.created_at))
@attachment.destroy!
@attachment = nil
end
if !@attachment
@attachment = @assignment.attachments.build(:display_name => 'submissions.zip')
@attachment.workflow_state = 'to_be_zipped'
@attachment.file_state = '0'
@attachment.user = @current_user
@attachment.save!
ContentZipper.send_later_enqueue_args(:process_attachment, { :priority => Delayed::LOW_PRIORITY, :max_attempts => 1 }, @attachment)
render :json => @attachment
else
respond_to do |format|
if @attachment.zipped?
if Attachment.s3_storage?
format.html { redirect_to @attachment.cacheable_s3_inline_url }
format.zip { redirect_to @attachment.cacheable_s3_inline_url }
else
cancel_cache_buster
format.html { send_file(@attachment.full_filename, :type => @attachment.content_type_with_encoding, :disposition => 'inline') }
format.zip { send_file(@attachment.full_filename, :type => @attachment.content_type_with_encoding, :disposition => 'inline') }
end
format.json { render :json => @attachment.as_json(:methods => :readable_size) }
else
flash[:notice] = t('still_zipping', "File zipping still in process...")
format.html { redirect_to named_context_url(@context, :context_assignment_url, @assignment.id) }
format.zip { redirect_to named_context_url(@context, :context_assignment_url, @assignment.id) }
format.json { render :json => @attachment }
end
end
end
end
def update_student_entered_score(score)
if score.present? && score != "null"
@submission.student_entered_score = score.to_f.round(2)
@ -575,4 +537,46 @@ class SubmissionsController < ApplicationController
end
@submission.save
end
def generate_submission_zip(assignment, context)
attachment = submission_zip(assignment)
respond_to do |format|
if attachment.zipped?
if Attachment.s3_storage?
format.html { redirect_to attachment.cacheable_s3_inline_url }
format.zip { redirect_to attachment.cacheable_s3_inline_url }
else
cancel_cache_buster
format.html do
send_file(attachment.full_filename, {
:type => attachment.content_type_with_encoding,
:disposition => 'inline'
})
end
format.zip do
send_file(attachment.full_filename, {
:type => attachment.content_type_with_encoding,
:disposition => 'inline'
})
end
end
format.json { render :json => attachment.as_json(:methods => :readable_size) }
else
flash[:notice] = t('still_zipping', "File zipping still in process...")
format.html do
redirect_to named_context_url(context, :context_assignment_url, assignment.id)
end
format.zip do
redirect_to named_context_url(context, :context_assignment_url, assignment.id)
end
format.json { render :json => attachment }
end
end
end
end

View File

@ -637,6 +637,11 @@ class QuizSubmission < ActiveRecord::Base
(self.finished_at || self.started_at) - self.started_at rescue 0
end
def time_spent
return unless finished_at.present?
(finished_at - started_at + (extra_time||0)).round
end
def self.score_question(q, params)
params = params.with_indifferent_access
# TODO: undefined_if_blank - we need a better solution for the

View File

@ -1310,6 +1310,10 @@ routes.draw do
post 'courses/:course_id/quizzes/:quiz_id/quiz_submissions/self/files', :action => :create, :path_name => 'quiz_submission_files'
end
scope(:controller => :quiz_submissions_api) do
get 'courses/:course_id/quizzes/:quiz_id/quiz_submissions', :action => :index, :path_name => 'course_quiz_submissions'
end
scope(:controller => :outcome_groups_api) do
def og_routes(context)
prefix = (context == "global" ? context : "#{context}s/:#{context}_id")

View File

@ -0,0 +1,116 @@
#
# Copyright (C) 2011 - 2012 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::QuizSubmission
include Api::V1::Json
include Api::V1::Submission
include Api::V1::Quiz
include Api::V1::User
QUIZ_SUBMISSION_JSON_FIELDS = %w[
id
user_id
submission_id
quiz_id
quiz_points_possible
quiz_version
attempt
extra_attempts
extra_time
started_at
finished_at
end_at
fudge_points
kept_score
score
score_before_regrade
workflow_state
].freeze
QUIZ_SUBMISSION_JSON_FIELD_METHODS = %w[
time_spent
].freeze
def quiz_submission_json(qs, quiz, user, session, context = nil)
context ||= quiz.context
hash = api_json(qs, user, session, {
only: QUIZ_SUBMISSION_JSON_FIELDS,
methods: QUIZ_SUBMISSION_JSON_FIELD_METHODS.dup
})
hash.merge!({
html_url: polymorphic_url([ context, quiz, qs ]),
})
hash
end
# Render a set of Quiz Submission objects as JSON-API.
#
# @param [QuizSubmission|Array<QuizSubmission>] quiz_submissions
# The resource(s) to render.
#
# @param [Array<String>] includes
# Associations to include in the output for each Quiz Submission.
# Allowed associations are: "user", "quiz", and "submission"
#
# @return [Hash]
# A JSON-API complying construct representing the quiz submissions, and
# any associations requested.
def quiz_submissions_json(quiz_submissions, quiz, user, session, context = nil, includes = [])
hash = {}
hash[:quiz_submissions] = [ quiz_submissions ].flatten.map do |qs|
quiz_submission_json(qs, quiz, user, session, context)
end
if includes.include?('submission')
with_submissions = quiz_submissions.select { |qs| !!qs.submission }
hash[:submissions] = with_submissions.map do |qs|
submission_json(qs.submission, quiz.assignment, user, session, context)
end
end
if includes.include?('quiz')
hash[:quizzes] = [
quiz_json(quiz, context, user, session)
]
end
if includes.include?('user')
hash[:users] = quiz_submissions.map do |qs|
user_json(qs.user, user, session, ['avatar_url'], context, nil)
end
end
unless includes.empty?
hash[:meta] = {
primaryCollection: 'quiz_submissions'
}
end
hash
end
def quiz_submission_zip(quiz)
latest_submission = quiz.quiz_submissions.map { |s| s.finished_at }.compact.max
submission_zip(quiz, latest_submission)
end
end

View File

@ -149,5 +149,57 @@ module Api::V1::Submission
hash
end
# Create an attachment with a ZIP archive of an assignment's submissions.
# The attachment will be re-created if it's 1 hour old, or determined to be
# "stale". See the argument descriptions for testing the staleness of the attachment.
#
# @param [Assignment] assignment
# The assignment, or an object that implements its interface, for which the
# submissions will be zipped.
#
# @param [DateTime] updated_at
# A timestamp that marks the latest update to the assignment object which will
# be used to determine whether the attachment will be re-created.
#
# Note that this timestamp will be ignored if the attachment is 1 hour old.
#
# @return [Attachment] The attachment that contains the archive.
def submission_zip(assignment, updated_at = nil)
attachments = assignment.attachments.where({
display_name: 'submissions.zip',
workflow_state: %w[to_be_zipped zipping zipped errored unattached],
user_id: @current_user
}).order(:created_at).all
attachment = attachments.pop
attachments.each { |a| a.destroy! }
# Remove the earlier attachment and re-create it if it's "stale"
if attachment
created_at = attachment.created_at
updated_at ||= assignment.submissions.map { |s| s.submitted_at }.compact.max
if created_at < 1.hour.ago || (updated_at && created_at < updated_at)
attachment.destroy!
attachment = nil
end
end
if !attachment
attachment = assignment.attachments.build(:display_name => 'submissions.zip')
attachment.workflow_state = 'to_be_zipped'
attachment.file_state = '0'
attachment.user = @current_user
attachment.save!
ContentZipper.send_later_enqueue_args(:process_attachment, {
priority: Delayed::LOW_PRIORITY,
max_attempts: 1
}, attachment)
end
attachment
end
end

View File

@ -17,6 +17,7 @@
#
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
require File.expand_path(File.dirname(__FILE__) + '/helpers/api_specs')
class HashWithDupCheck < Hash
def []=(k,v)

View File

@ -0,0 +1,63 @@
shared_examples_for "API tests" do
# Assert the provided JSON hash complies with the JSON-API format specification.
#
# The following tests will be carried out:
#
# - all resource entries must be wrapped inside arrays, even if the set
# includes only a single resource entry
# - when associations are present, a "meta" entry should be present and
# it should indicate the primary set in the "primaryCollection" key
#
# @param [Hash] json
# The JSON construct to test.
#
# @param [String] primary_set
# Name of the primary resource the construct represents, i.e, the model
# the API endpoint represents, like 'quiz', 'assignment', or 'submission'.
#
# @param [Array<String>] associations
# An optional set of associated resources that should be included with
# the primary resource (e.g, a user, an assignment, a submission, etc.).
#
# @example Testing a Quiz API model:
# test_jsonapi_compliance!(json, 'quiz')
#
# @example Testing a Quiz API model with its assignment included:
# test_jsonapi_compliance!(json, 'quiz', [ 'assignment' ])
#
# @example A complying construct of a Quiz Submission with its Assignment:
#
# {
# "quiz_submissions": [{
# "id": 10,
# "assignment_id": 5
# }],
# "assignments": [{
# "id": 5
# }],
# "meta": {
# "primaryCollection": "quiz_submissions"
# }
# }
#
def assert_jsonapi_compliance!(json, primary_set, associations = [])
required_keys = [ primary_set ]
if associations.any?
required_keys.concat associations.map { |s| s.pluralize }
required_keys << 'meta'
end
json.size.should == required_keys.size
required_keys.each do |key|
json.has_key?(key).should be_true
json[key].is_a?(Array).should be_true unless key == 'meta'
end
if associations.any?
json['meta']['primaryCollection'].should == primary_set
end
end
end

View File

@ -19,5 +19,140 @@
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
describe QuizSubmissionsApiController, :type => :integration do
it_should_behave_like 'API tests'
before :each do
course_with_teacher_logged_in :active_all => true
@quiz = Quiz.create!(:title => 'quiz', :context => @course)
@quiz.quiz_data = [ multiple_choice_question_data ]
@quiz.generate_quiz_data
@quiz.published_at = Time.now
@quiz.workflow_state = 'available'
@quiz.save!
@assignment = @quiz.assignment
end
def enroll_student_and_submit
last_user = @user
student_in_course
student = @user
@user = last_user
quiz_submission = @quiz.generate_submission(student)
quiz_submission.submission_data = { "question_1" => "1658" }
quiz_submission.mark_completed
quiz_submission.grade_submission
quiz_submission.reload
[ student, quiz_submission ]
end
context 'index' do
def get_index(raw = false, data = {})
helper = method(raw ? :raw_api_call : :api_call)
helper.call(:get,
"/api/v1/courses/#{@course.id}/quizzes/#{@quiz.id}/quiz_submissions.json",
{ :controller => 'quiz_submissions_api', :action => 'index', :format => 'json',
:course_id => @course.id.to_s,
:quiz_id => @quiz.id.to_s
}, data)
end
it 'should return an empty list' do
json = get_index
json.has_key?('quiz_submissions').should be_true
json['quiz_submissions'].size.should == 0
end
it 'should list quiz submissions' do
enroll_student_and_submit
json = get_index
json['quiz_submissions'].size.should == 1
end
it 'should restrict access to itself' do
student_in_course
json = get_index(true)
response.status.to_i.should == 401
end
context 'quiz submission objects' do
before :each do
@student, @quiz_submission = *enroll_student_and_submit
end
it "should include its associated user" do
json = get_index(false, {
:include => [ 'user' ]
})
json.has_key?('users').should be_true
json['quiz_submissions'].size.should == 1
json['users'].size.should == 1
json['users'][0]['id'].should == json['quiz_submissions'][0]['user_id']
end
it "should include its associated quiz" do
json = get_index(false, {
:include => [ 'quiz' ]
})
json.has_key?('quizzes').should be_true
json['quiz_submissions'].size.should == 1
json['quizzes'].size.should == 1
json['quizzes'][0]['id'].should == json['quiz_submissions'][0]['quiz_id']
end
it "should include its associated submission" do
json = get_index(false, {
:include => [ 'submission' ]
})
json.has_key?('submissions').should be_true
json['quiz_submissions'].size.should == 1
json['submissions'].size.should == 1
json['submissions'][0]['id'].should == json['quiz_submissions'][0]['submission_id']
end
it "should include its associated user, quiz, and submission" do
json = get_index(false, {
:include => [ 'user', 'quiz', 'submission' ]
})
json.has_key?('users').should be_true
json.has_key?('quizzes').should be_true
json.has_key?('submissions').should be_true
end
context 'JSON-API compliance' do
it 'should conform to the JSON-API spec when returning the object' do
json = get_index(false)
assert_jsonapi_compliance!(json, 'quiz_submissions')
end
it 'should conform to the JSON-API spec when returning associated objects' do
includes = [ 'user', 'quiz', 'submission' ]
json = get_index(false, {
:include => includes
})
assert_jsonapi_compliance!(json, 'quiz_submissions', includes)
end
end
it "should include time spent" do
@quiz_submission.started_at = Time.now
@quiz_submission.finished_at = @quiz_submission.started_at + 5.minutes
@quiz_submission.save!
json = get_index
json.has_key?('quiz_submissions').should be_true
json['quiz_submissions'][0]['time_spent'].should == 5.minutes
end
end
end
end

View File

@ -66,6 +66,35 @@ describe QuizSubmission do
end
end
describe "#time_spent" do
it "should return nil if there's no finished_at" do
q = @quiz.quiz_submissions.new
q.finished_at = nil
q.time_spent.should be_nil
end
it "should return the correct time spent in seconds" do
anchor = Time.now
q = @quiz.quiz_submissions.new
q.started_at = anchor
q.finished_at = anchor + 1.hour
q.time_spent.should eql(1.hour.to_i)
end
it "should account for extra time" do
anchor = Time.now
q = @quiz.quiz_submissions.new
q.started_at = anchor
q.finished_at = anchor + 1.hour
q.extra_time = 5.minutes
q.time_spent.should eql((1.hour + 5.minutes).to_i)
end
end
describe "#update_scores" do
before(:each) do
student_in_course