live assessments api

fixes CNVS-12916

test plan
- as a teacher in a course, use the api to create a live
 assessment aligned with an outcome (see the api docs for how the
 endpoint works)
- create results for some students
- ensure that the results and the assessment can be read back using
 the index endpoints
- ensure that the assessment shows up in the web ui at
 /course/:course_id/outcomes/users/:student_id (click 'Show All
 Artifacts')

- try to create an assessment using the same key as an existing
 assessment
- ensure that the existing assessment is returned (check the id)

- fetch results specifying a user id to filter by
- ensure that only results for that user are returned

Change-Id: I2d09691f772658aea3ccdd36cff2df5835b1f2cd
Reviewed-on: https://gerrit.instructure.com/35092
Reviewed-by: Ethan Vizitei <evizitei@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Steven Shepherd <sshepherd@instructure.com>
Product-Review: Derrick Hathaway <derrick@instructure.com>
This commit is contained in:
Joel Hough 2014-05-16 18:48:33 -06:00
parent bb45484387
commit 4ec0d04bb9
17 changed files with 1063 additions and 4 deletions

View File

@ -0,0 +1,30 @@
#
# Copyright (C) 2014 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 Filters::LiveAssessments
protected
# be sure to have a valid context before calling this
def require_assessment
id = params.has_key?(:assessment_id) ? params[:assessment_id] : params[:id]
@assessment = LiveAssessments::Assessment.find(id)
reject! 'assessment does not belong to the given context' unless @assessment.context == @context
@assessment
end
end

View File

@ -0,0 +1,146 @@
#
# Copyright (C) 2014 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 LiveAssessments
# @API LiveAssessments
# @beta
# Manage live assessments
#
# @model Assessment
# {
# "id": "Assessment",
# "description": "A simple assessment that collects pass/fail results for a student",
# "properties": {
# "id": {
# "type": "string",
# "example": "42",
# "description": "A unique identifier for this live assessment"
# },
# "key": {
# "type": "string",
# "example": "2014-05-27,outcome_52",
# "description": "A client specified unique identifier for the assessment"
# },
# "title": {
# "type": "string",
# "example": "May 27th Reading Assessment",
# "description": "A human readable title for the assessment"
# }
# }
# }
class AssessmentsController < ApplicationController
before_filter :require_user
before_filter :require_context
# @API Create or find a live assessment
# @beta
#
# Creates or finds a live assessment with
#
# @example_request
# {
# "assessments": [{
# "key": "2014-05-27-Outcome-52",
# "title": "Tuesday's LiveAssessment",
# "links": {
# "outcome": "1"
# }
# }]
# }
#
# @example_response
# {
# "links": {
# "assessments.results": "http://example.com/courses/1/live_assessments/5/results"
# },
# "assessments": [Assessment]
# }
#
def create
return unless authorized_action(Assessment.new(context: @context), @current_user, :create)
reject! 'missing required key :assessments' unless params[:assessments].is_a?(Array)
@assessments = []
Assessment.transaction do
params[:assessments].each do |assessment_hash|
if assessment_hash[:links] && outcome_id = assessment_hash[:links][:outcome]
return unless authorized_action(@context, @current_user, :manage_outcomes)
@outcome = @context.linked_learning_outcomes.where(id: outcome_id).first
reject! 'outcome must be linked to the context' unless @outcome
end
reject! 'missing required key :title' if assessment_hash[:title].blank?
reject! 'missing required key :key' if assessment_hash[:key].blank?
assessment = Assessment.find_or_initialize_by_context_id_and_context_type_and_key(@context.id, @context.class.to_s, assessment_hash[:key])
assessment.title = assessment_hash[:title]
assessment.save!
if @outcome
criterion = @outcome.data && @outcome.data[:rubric_criterion]
mastery_score = criterion && criterion[:mastery_points] / criterion[:points_possible]
@outcome.align(assessment, @context, mastery_type: "none", mastery_score: mastery_score)
end
@assessments << assessment
end
end
render json: serialize_jsonapi(@assessments)
end
# @API List live assessments
# @beta
#
# Returns a list of live assessments.
#
# @example_response
# {
# "links": {
# "assessments.results": "http://example.com/courses/1/live_assessments/{assessments.id}/results"
# },
# "assessments": [Assessment]
# }
#
def index
return unless authorized_action(Assessment.new(context: @context), @current_user, :read)
@assessments = Assessment.for_context(@context)
@assessments = Api.paginate(@assessments, self, polymorphic_url([:api_v1, @context, :live_assessments]))
render json: serialize_jsonapi(@assessments)
end
protected
def serialize_jsonapi(assessments)
serialized = Canvas::APIArraySerializer.new(assessments, {
each_serializer: LiveAssessments::AssessmentSerializer,
controller: self,
scope: @current_user,
root: false,
include_root: false
}).as_json
{
links: {
'assessments.results' => polymorphic_url([:api_v1, @context]) + '/live_assessments/{assessments.id}/results'
},
assessments: serialized
}
end
end
end

View File

@ -0,0 +1,146 @@
#
# Copyright (C) 2014 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 LiveAssessments
# @API LiveAssessments
# @beta
# Manage live assessment results
#
# @model Result
# {
# "id": "Result",
# "description": "A pass/fail results for a student",
# "properties": {
# "id": {
# "type": "string",
# "example": "42",
# "description": "A unique identifier for this result"
# },
# "passed": {
# "type": "boolean",
# "example": true,
# "description": "Whether the user passed or not"
# },
# "assessed_at": {
# "type": "datetime",
# "example": "2014-13-05T00:01:57-06:00",
# "description": "When this result was recorded"
# },
# "links": {
# "example": "{\"user\"=>\"3\", \"assessor\"=>\"42\", \"assessment\"=>\"30\"}",
# "description": "Unique identifiers of objects associated with this result"
# }
# }
# }
class ResultsController < ApplicationController
include Filters::LiveAssessments
before_filter :require_user
before_filter :require_context
before_filter :require_assessment
# @API Create a live assessment results
# @beta
#
# @example_request
# {
# "results": [{
# "passed": false,
# "assessed_at": "2014-05-26T14:57:23-07:00",
# "links": [
# "user": "15"
# ]
# },{
# "passed": true,
# "assessed_at": "2014-05-26T13:05:40-07:00",
# "links": [
# "user": "16"
# ]
# }]
# }
#
# @example_response
# {
# "results": [Result]
# }
#
def create
return unless authorized_action(@assessment.results.new, @current_user, :create)
reject! 'missing required key :results' unless params[:results].is_a?(Array)
@results = []
result_hashes_by_user_id = params[:results].group_by {|result| result[:links] and result[:links][:user]}
Result.transaction do
result_hashes_by_user_id.each do |user_id, result_hashes|
reject! 'missing required key :user' unless user_id
@user = @context.users.where(id: user_id).first
reject! 'user must be in the context' unless @user
result_hashes.each do |result_hash|
result = @assessment.results.build(
user: @user,
assessor: @current_user,
passed: result_hash[:passed],
assessed_at: result_hash[:assessed_at]
)
result.save!
@results << result
end
end
end
@assessment.send_later_if_production(:generate_submissions_for, @results.map(&:user).uniq)
render json: serialize_jsonapi(@results)
end
# @API List live assessment results
# @beta
#
# Returns a list of live assessment results
#
# @argument user_id [Optional, Integer]
# If set, restrict results to those for this user
#
# @example_response
# {
# "results": [Result]
# }
#
def index
return unless authorized_action(@assessment.results.new, @current_user, :read)
@results = @assessment.results
@results = @results.for_user(params[:user_id]) if params[:user_id]
@results = Api.paginate(@results, self, polymorphic_url([:api_v1, @context, :live_assessment_results], assessment_id: @assessment.id))
render json: serialize_jsonapi(@results)
end
protected
def serialize_jsonapi(results)
serialized = Canvas::APIArraySerializer.new(results, {
each_serializer: LiveAssessments::ResultSerializer,
controller: self,
scope: @current_user,
root: false,
include_root: false
}).as_json
{ results: serialized }
end
end
end

View File

@ -29,7 +29,7 @@ class ContentTag < ActiveRecord::Base
belongs_to :content, :polymorphic => true
validates_inclusion_of :content_type, :allow_nil => true, :in => ['Attachment', 'Assignment', 'WikiPage',
'ContextModuleSubHeader', 'Quizzes::Quiz', 'ExternalUrl', 'LearningOutcome', 'DiscussionTopic',
'Rubric', 'ContextExternalTool', 'LearningOutcomeGroup', 'AssessmentQuestionBank']
'Rubric', 'ContextExternalTool', 'LearningOutcomeGroup', 'AssessmentQuestionBank', 'LiveAssessments::Assessment']
belongs_to :context, :polymorphic => true
validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'LearningOutcomeGroup',
'Assignment', 'Account', 'Quizzes::Quiz']

View File

@ -26,11 +26,11 @@ class LearningOutcomeResult < ActiveRecord::Base
belongs_to :learning_outcome
belongs_to :alignment, :class_name => 'ContentTag', :foreign_key => :content_tag_id
belongs_to :association_object, :polymorphic => true, :foreign_type => :association_type, :foreign_key => :association_id
validates_inclusion_of :association_type, :allow_nil => true, :in => ['Quizzes::Quiz', 'RubricAssociation', 'Assignment']
validates_inclusion_of :association_type, :allow_nil => true, :in => ['Quizzes::Quiz', 'RubricAssociation', 'Assignment', 'LiveAssessments::Assessment']
belongs_to :artifact, :polymorphic => true
validates_inclusion_of :artifact_type, :allow_nil => true, :in => ['Quizzes::QuizSubmission', 'RubricAssessment', 'Submission']
validates_inclusion_of :artifact_type, :allow_nil => true, :in => ['Quizzes::QuizSubmission', 'RubricAssessment', 'Submission', 'LiveAssessments::Submission']
belongs_to :associated_asset, :polymorphic => true
validates_inclusion_of :associated_asset_type, :allow_nil => true, :in => ['AssessmentQuestion', 'Quizzes::Quiz']
validates_inclusion_of :associated_asset_type, :allow_nil => true, :in => ['AssessmentQuestion', 'Quizzes::Quiz', 'LiveAssessments::Assessment']
belongs_to :context, :polymorphic => true
validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course']
simply_versioned

View File

@ -0,0 +1,66 @@
#
# Copyright (C) 2014 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 LiveAssessments
class Assessment < ActiveRecord::Base
attr_accessible :context, :key, :title
belongs_to :context, polymorphic: true
has_many :submissions, class_name: 'LiveAssessments::Submission'
has_many :results, class_name: 'LiveAssessments::Result'
has_many :learning_outcome_alignments, :as => :content, :class_name => 'ContentTag', :conditions => ['content_tags.tag_type = ? AND content_tags.workflow_state != ?', 'learning_outcome', 'deleted'], :include => :learning_outcome
validates_presence_of :context_id, :context_type, :key, :title
validates_length_of :title, maximum: maximum_string_length
validates_length_of :key, maximum: maximum_string_length
scope :for_context, lambda { |context| where(:context_id => context, :context_type => context.class.to_s) }
set_policy do
given { |user, session| self.cached_context_grants_right?(user, session, :manage_assignments) }
can :create and can :update
given { |user, session| self.cached_context_grants_right?(user, session, :view_all_grades) }
can :read
end
def generate_submissions_for(users)
# if we aren't aligned, we don't need submissions
return unless learning_outcome_alignments.any?
Assessment.transaction do
users.each do |user|
submission = submissions.find_or_initialize_by_user_id(user.id)
user_results = results.for_user(user).all
next unless user_results.any?
submission.possible = user_results.count
submission.score = user_results.count(&:passed)
submission.assessed_at = user_results.map(&:assessed_at).max
submission.save!
# it's likely that there is only one alignment per assessment, but we
# have to deal with any number of them
learning_outcome_alignments.each do |alignment|
submission.create_outcome_result(alignment)
end
end
end
end
end
end

View File

@ -0,0 +1,23 @@
#
# Copyright (C) 2014 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 LiveAssessments
def self.table_name_prefix
"live_assessments_"
end
end

View File

@ -0,0 +1,40 @@
#
# Copyright (C) 2014 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 LiveAssessments
class Result < ActiveRecord::Base
attr_accessible :user, :assessor, :passed, :assessed_at
belongs_to :assessor, class_name: 'User'
belongs_to :user
belongs_to :assessment, class_name: 'LiveAssessments::Assessment'
validates_presence_of :assessor_id, :assessment_id, :assessed_at
validates_inclusion_of :passed, :in => [true, false]
scope :for_user, lambda { |user| where(:user_id => user) }
set_policy do
given { |user, session| self.assessment.grants_right?(user, session, :update) }
can :create
given { |user, session| self.assessment.grants_right?(user, session, :read) }
can :read
end
end
end

View File

@ -0,0 +1,62 @@
#
# Copyright (C) 2014 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 LiveAssessments
class Submission < ActiveRecord::Base
attr_accessible :user, :assessment, :possible, :score, :assessed_at
belongs_to :user
belongs_to :assessment, class_name: 'LiveAssessments::Assessment'
validates_presence_of :user, :assessment
def create_outcome_result(alignment)
# we don't delete results right now
# when we do, we'll need to start cleaning up outcome results when all the results are deleted. bail until then.
return if possible == 0
outcome_result = alignment.learning_outcome_results.find_or_initialize_by_user_id(user.id)
outcome_result.title = "#{user.name}, #{assessment.title}"
outcome_result.context = assessment.context
outcome_result.associated_asset = assessment
outcome_result.artifact = self
outcome_result.assessed_at = assessed_at
outcome_result.score = score
outcome_result.possible = possible
outcome_result.percent = score.to_f / possible.to_f
if alignment.mastery_score
outcome_result.mastery = outcome_result.percent >= alignment.mastery_score
else
outcome_result.mastery = nil
end
# map actual magic marker result to outcome rubric criterion if we have one
# this is a hack. the rollups and gradebooks should handle explicit mastery
# this only works because we set mastery_score based on the rubric in the first place
criterion = alignment.learning_outcome.data && alignment.learning_outcome.data[:rubric_criterion]
if criterion
outcome_result.possible = criterion[:points_possible]
outcome_result.score = outcome_result.percent * outcome_result.possible
end
outcome_result.save!
end
end
end

View File

@ -0,0 +1,25 @@
#
# Copyright (C) 2014 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 LiveAssessments
class AssessmentSerializer < Canvas::APISerializer
root :assessment
attributes :id, :key, :title
end
end

View File

@ -0,0 +1,31 @@
#
# Copyright (C) 2014 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 LiveAssessments
class ResultSerializer < Canvas::APISerializer
root :result
attributes :id, :passed, :assessed_at
def_delegators :@object, :user
has_one :user, embed: :ids, embed_in_root: true
has_one :assessor, class_name: 'User', embed: :ids, embed_in_root: true
has_one :assessment, embed: :ids, embed_in_root: true
end
end

View File

@ -1431,6 +1431,22 @@ routes.draw do
get "polls/:poll_id/poll_sessions/:poll_session_id/poll_submissions/:id", :action => :show, :path_name => 'poll_submission'
end
scope(:controller => 'live_assessments/assessments') do
%w(course).each do |context|
prefix = "#{context}s/:#{context}_id"
get "#{prefix}/live_assessments", :action => :index, :path_name => "#{context}_live_assessments"
post "#{prefix}/live_assessments", :action => :create, :path_name => "#{context}_live_assessment_create"
end
end
scope(:controller => 'live_assessments/results') do
%w(course).each do |context|
prefix = "#{context}s/:#{context}_id"
get "#{prefix}/live_assessments/:assessment_id/results", :action => :index, :path_name => "#{context}_live_assessment_results"
post "#{prefix}/live_assessments/:assessment_id/results", :action => :create, :path_name => "#{context}_live_assessment_result_create"
end
end
scope(:controller => :outcome_groups_api) do
def og_routes(context)
prefix = (context == "global" ? context : "#{context}s/:#{context}_id")

View File

@ -0,0 +1,44 @@
class CreateLiveAssessments < ActiveRecord::Migration
tag :predeploy
def self.up
create_table :live_assessments_assessments do |t|
t.string :key, null: false
t.string :title, null: false
t.integer :context_id, limit: 8, null: false
t.string :context_type, null: false
t.timestamps
end
add_index :live_assessments_assessments, [:context_id, :context_type, :key], unique: true, name: 'index_live_assessments'
create_table :live_assessments_submissions do |t|
t.integer :user_id, limit: 8, null: false
t.integer :assessment_id, limit: 8, null: false
t.float :possible
t.float :score
t.datetime :assessed_at
t.timestamps
end
add_index :live_assessments_submissions, [:assessment_id, :user_id], unique: true
create_table :live_assessments_results do |t|
t.integer :user_id, limit: 8, null: false
t.integer :assessor_id, limit: 8, null: false
t.integer :assessment_id, limit: 8, null: false
t.boolean :passed, null: false
t.datetime :assessed_at, null: false
end
add_index :live_assessments_results, [:assessment_id, :user_id]
add_foreign_key :live_assessments_submissions, :live_assessments_assessments, column: :assessment_id
add_foreign_key :live_assessments_submissions, :users
add_foreign_key :live_assessments_results, :users, column: :assessor_id
add_foreign_key :live_assessments_results, :live_assessments_assessments, column: :assessment_id
end
def self.down
drop_table :live_assessments_results
drop_table :live_assessments_submissions
drop_table :live_assessments_assessments
end
end

View File

@ -0,0 +1,108 @@
#
# Copyright (C) 2014 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 LiveAssessments::AssessmentsController, type: :request do
let(:assessment_course) { course(active_all: true) }
let(:teacher) { assessment_course.teachers.first }
let(:student) { course_with_student(course: assessment_course).user }
let(:outcome) do
outcome = assessment_course.created_learning_outcomes.create!(:description => 'this is a test outcome', :short_description => 'test outcome')
assessment_course.root_outcome_group.add_outcome(outcome)
assessment_course.root_outcome_group.save!
assessment_course.reload
outcome
end
let(:unrelated_outcome) {course_with_teacher.course.created_learning_outcomes.create!(description: 'this outcome is in a different course', short_description: 'unrelated outcome')}
let(:assessment_hash) {{key: '2014-05-28-Outcome-52', title: 'a test assessment'}}
describe 'POST create' do
def create_assessments(params, opts={})
api_call_as_user(opts[:user] || teacher,
:post,
"/api/v1/courses/#{assessment_course.id}/live_assessments",
{ controller: 'live_assessments/assessments', action: 'create', format: 'json', course_id: assessment_course.id.to_s },
{ assessments: params }, {}, opts)
end
context "as a teacher" do
it "creates an assessment" do
create_assessments([assessment_hash])
data = json_parse
assessment = LiveAssessments::Assessment.find(data['assessments'][0]['id'])
assessment.key.should == assessment_hash[:key]
assessment.title.should == assessment_hash[:title]
end
it "aligns an assessment when given an outcome" do
create_assessments([assessment_hash.merge(links: {outcome: outcome.id})])
data = json_parse
assessment = LiveAssessments::Assessment.find(data['assessments'][0]['id'])
assessment.learning_outcome_alignments.count.should == 1
assessment.learning_outcome_alignments.first.learning_outcome.should == outcome
end
it "won't align an unrelated outcome" do
create_assessments([assessment_hash.merge(links: {outcome: unrelated_outcome.id})], expected_status: 400)
end
it 'returns an existing assessment with the same key' do
assessment = LiveAssessments::Assessment.create!(assessment_hash.merge(context: assessment_course))
create_assessments([assessment_hash])
data = json_parse
data['assessments'].count.should == 1
data['assessments'][0]['id'].should == assessment.id.to_s
end
end
context "as a student" do
it "is unauthorized" do
create_assessments([assessment_hash], user: student, expected_status: 401)
end
end
end
describe 'GET index' do
def index_assessments(opts={})
api_call_as_user(opts[:user] || teacher,
:get,
"/api/v1/courses/#{assessment_course.id}/live_assessments",
{ controller: 'live_assessments/assessments', action: 'index', format: 'json', course_id: assessment_course.id.to_s },
{}, {}, opts)
end
context 'as a teacher' do
it 'returns all the assessments for the context' do
LiveAssessments::Assessment.create!(assessment_hash.merge(context: assessment_course))
LiveAssessments::Assessment.create!(assessment_hash.merge(context: assessment_course, key: 'another assessment'))
index_assessments
data = json_parse
data['assessments'].count.should == 2
data['assessments'][0]['key'].should == assessment_hash[:key]
data['assessments'][1]['key'].should == 'another assessment'
end
end
context 'as a student' do
it 'is unauthorized' do
index_assessments(user: student, expected_status: 401)
end
end
end
end

View File

@ -0,0 +1,138 @@
#
# Copyright (C) 2014 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 LiveAssessments::ResultsController, type: :request do
let(:assessment_course) { course(active_all: true) }
let(:teacher) { assessment_course.teachers.first }
let(:student) { course_with_student(course: assessment_course).user }
let(:another_student) { course_with_student(course: assessment_course).user }
let(:outcome) do
outcome = assessment_course.created_learning_outcomes.create!(:description => 'this is a test outcome', :short_description => 'test outcome')
assessment_course.root_outcome_group.add_outcome(outcome)
assessment_course.root_outcome_group.save!
assessment_course.reload
outcome
end
let(:assessment) do
assessment = LiveAssessments::Assessment.create!(context: assessment_course, key: '2014-05-28-Outcome-1', title: 'an assessment')
outcome.align(assessment, assessment_course, mastery_type: 'none', mastery_score: 0.6)
assessment
end
let(:assessed_at) { Time.now - 1.day }
def result_hashes
[student, another_student].map do |s|
{passed: true, assessed_at: assessed_at, links: {user: s.id}}
end
end
describe 'POST create' do
def create_results(params, opts={})
api_call_as_user(opts[:user] || teacher,
:post,
"/api/v1/courses/#{assessment_course.id}/live_assessments/#{assessment.id}/results",
{ controller: 'live_assessments/results', action: 'create', format: 'json', course_id: assessment_course.id.to_s, assessment_id: assessment.id.to_s },
{ results: params }, {}, opts)
end
context "as a teacher" do
it "creates results" do
create_results(result_hashes)
data = json_parse
data['results'].count.should == 2
results = data['results'].map { |r| result = LiveAssessments::Result.find(r['id']) }
results.each do |r|
r.assessor.should == teacher
r.assessment.should == assessment
r.passed.should == true
r.assessed_at.to_i.should == assessed_at.to_i
end
results.map(&:user).should == [student, another_student]
end
it 'generates submissions' do
LiveAssessments::Assessment.any_instance.expects(:generate_submissions_for).with([student, another_student])
create_results(result_hashes)
end
it 'requires user to be in the context' do
create_results([result_hashes[0].merge(links: {user: user_model.id})], expected_status: 400)
end
end
context "as a student" do
it "is unauthorized" do
create_results([], user: student, expected_status: 401)
end
end
end
describe 'GET index' do
def index_results(params, opts={})
api_call_as_user(opts[:user] || teacher,
:get,
"/api/v1/courses/#{assessment_course.id}/live_assessments/#{assessment.id}/results",
{ controller: 'live_assessments/results', action: 'index', format: 'json', course_id: assessment_course.id.to_s, assessment_id: assessment.id.to_s },
params, {}, opts)
end
context 'as a teacher' do
it 'returns all the results for the assessment' do
results = [student, another_student].map do |s|
assessment.results.create!(user: s, assessor: teacher, passed: true, assessed_at: assessed_at)
end
index_results({})
data = json_parse
results = data['results']
results.count.should == 2
results.each do |r|
r['links']['assessor'].should == teacher.id.to_s
r['links']['assessment'].should == assessment.id.to_s
r['passed'].should == true
Time.parse(r['assessed_at']).to_i.should == assessed_at.to_i
end
results.map{|h|h['links']['user']}.should == [student.id.to_s, another_student.id.to_s]
end
it 'filters the results by user' do
results = [student, another_student].map do |s|
assessment.results.create!(user: s, assessor: teacher, passed: true, assessed_at: assessed_at)
end
index_results({user_id: another_student.id})
data = json_parse
results = data['results']
results.count.should == 1
results.each do |r|
r['links']['assessor'].should == teacher.id.to_s
r['links']['assessment'].should == assessment.id.to_s
r['passed'].should == true
Time.parse(r['assessed_at']).to_i.should == assessed_at.to_i
end
results.map{|h|h['links']['user']}.should == [another_student.id.to_s]
end
end
context 'as a student' do
it 'is unauthorized' do
index_results({}, user: student, expected_status: 401)
end
end
end
end

View File

@ -0,0 +1,91 @@
#
# Copyright (C) 2014 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__) + '/../../spec_helper.rb')
describe LiveAssessments::Assessment do
let(:assessment_context) { course(active_all: true) }
let(:assessment_user) { course_with_student(course: assessment_context, active_all: true).user }
let(:assessor) { assessment_context.teachers.first }
let(:another_assessment_user) { course_with_student(course: assessment_context, active_all: true).user }
let(:assessment) { LiveAssessments::Assessment.create!(context: assessment_context, key: 'test key', title: 'test title') }
let(:outcome) do
outcome = assessment_context.created_learning_outcomes.create!(:description => 'this is a test outcome', :short_description => 'test outcome')
assessment_context.root_outcome_group.add_outcome(outcome)
assessment_context.root_outcome_group.save!
assessment_context.reload
outcome
end
let(:another_outcome) do
outcome = assessment_context.created_learning_outcomes.create!(:description => 'this is another test outcome', :short_description => 'test outcome 2')
assessment_context.root_outcome_group.add_outcome(outcome)
assessment_context.root_outcome_group.save!
assessment_context.reload
outcome
end
describe '#generate_submissions_for' do
it "doesn't do anything without aligned outcomes" do
assessment.generate_submissions_for(assessment_user)
assessment.submissions.count.should == 0
end
it "doesn't create a submission for users with no results" do
outcome.align(assessment, assessment_context, mastery_type: 'none', mastery_score: 0.6)
assessment.results.create!(user: assessment_user, assessor: assessor, passed: true, assessed_at: Time.now)
assessment.generate_submissions_for([assessment_user, another_assessment_user])
assessment.submissions.count.should == 1
end
it "creates a submission for each given user" do
outcome.align(assessment, assessment_context, mastery_type: 'none', mastery_score: 0.6)
assessment.results.create!(user: assessment_user, assessor: assessor, passed: true, assessed_at: Time.now)
assessment.results.create!(user: another_assessment_user, assessor: assessor, passed: false, assessed_at: Time.now)
assessment.generate_submissions_for([assessment_user, another_assessment_user])
assessment.submissions.count.should == 2
assessment.submissions[0].possible.should == 1
assessment.submissions[0].score.should == 1
assessment.submissions[1].possible.should == 1
assessment.submissions[1].score.should == 0
end
it "updates existing submission" do
outcome.align(assessment, assessment_context, mastery_type: 'none', mastery_score: 0.6)
assessment.results.create!(user: assessment_user, assessor: assessor, passed: true, assessed_at: Time.now)
assessment.generate_submissions_for([assessment_user])
assessment.submissions.count.should == 1
submission = assessment.submissions.first
submission.possible.should == 1
submission.score.should == 1
assessment.results.create!(user: assessment_user, assessor: assessor, passed: false, assessed_at: Time.now)
assessment.generate_submissions_for([assessment_user])
assessment.submissions.count.should == 1
submission.reload.possible.should == 2
submission.score.should == 1
end
it "creates outcome results for each alignment" do
alignment1 = outcome.align(assessment, assessment_context, mastery_type: 'none', mastery_score: 0.6)
alignment2 = another_outcome.align(assessment, assessment_context, mastery_type: 'none', mastery_score: 0.5)
LiveAssessments::Submission.any_instance.expects(:create_outcome_result).with(alignment1)
LiveAssessments::Submission.any_instance.expects(:create_outcome_result).with(alignment2)
assessment.results.create!(user: assessment_user, assessor: assessor, passed: true, assessed_at: Time.now)
assessment.generate_submissions_for([assessment_user])
end
end
end

View File

@ -0,0 +1,93 @@
#
# Copyright (C) 2014 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__) + '/../../spec_helper.rb')
describe LiveAssessments::Submission do
let(:assessment_context) { course(active_all: true) }
let(:outcome) do
outcome = assessment_context.created_learning_outcomes.create!(:description => 'this is a test outcome', :short_description => 'test outcome')
assessment_context.root_outcome_group.add_outcome(outcome)
assessment_context.root_outcome_group.save!
assessment_context.reload
outcome
end
let(:alignment) { outcome.align(assessment, assessment_context, mastery_type: 'none') }
let(:assessment_user) { course_with_student(course: assessment_context, active_all: true).user }
let(:assessment) { LiveAssessments::Assessment.create(context: assessment_context, key: 'test key', title: 'test title') }
let(:submission) { LiveAssessments::Submission.create(user: assessment_user, assessment: assessment, possible: 10, score: 5, assessed_at: Time.now) }
describe '#create_outcome_result' do
it 'does not create a result when no points are possible' do
# we can probably create a meaningful result with no points
# possible, but we don't now so that's what we test
submission.possible = 0
submission.create_outcome_result(alignment)
result = alignment.learning_outcome_results.count.should == 0
end
it 'creates an outcome result' do
submission.create_outcome_result(alignment)
result = alignment.learning_outcome_results.first
result.should_not be_nil
result.title.should == "#{assessment_user.name}, #{assessment.title}"
result.context.should == assessment.context
result.artifact.should == submission
result.assessed_at.to_i.should == submission.assessed_at.to_i
result.score.should == submission.score
result.possible.should == submission.possible
result.percent.should == 0.5
result.mastery.should be_nil
end
it 'updates an existing outcome result' do
submission.create_outcome_result(alignment)
result = alignment.learning_outcome_results.first
result.percent.should == 0.5
submission.score = 80
submission.possible = 100
submission.create_outcome_result(alignment)
alignment.learning_outcome_results.count.should == 1
result.reload.percent.should == 0.8
end
it "scales the score to the outcome rubric criterion if present" do
outcome.data = {rubric_criterion: {mastery_points: 3, points_possible: 5}}
outcome.save!
submission.create_outcome_result(alignment)
result = alignment.learning_outcome_results.first
result.percent.should == 0.5
result.score.should == 2.5
end
context 'alignment has a mastery score' do
it 'sets mastery based on percent passed' do
alignment.mastery_score = 0.6
alignment.save!
submission.create_outcome_result(alignment)
result = alignment.learning_outcome_results.first
result.mastery.should be_false
submission.score = 80
submission.possible = 100
submission.create_outcome_result(alignment)
alignment.learning_outcome_results.count.should == 1
result.reload.mastery.should be_true
end
end
end
end