Skip to content

Commit 4ec0d04

Browse files
committed
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>
1 parent bb45484 commit 4ec0d04

17 files changed

Lines changed: 1063 additions & 4 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#
2+
# Copyright (C) 2014 Instructure, Inc.
3+
#
4+
# This file is part of Canvas.
5+
#
6+
# Canvas is free software: you can redistribute it and/or modify it under
7+
# the terms of the GNU Affero General Public License as published by the Free
8+
# Software Foundation, version 3 of the License.
9+
#
10+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License along
16+
# with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
module Filters::LiveAssessments
20+
protected
21+
22+
# be sure to have a valid context before calling this
23+
def require_assessment
24+
id = params.has_key?(:assessment_id) ? params[:assessment_id] : params[:id]
25+
26+
@assessment = LiveAssessments::Assessment.find(id)
27+
reject! 'assessment does not belong to the given context' unless @assessment.context == @context
28+
@assessment
29+
end
30+
end
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#
2+
# Copyright (C) 2014 Instructure, Inc.
3+
#
4+
# This file is part of Canvas.
5+
#
6+
# Canvas is free software: you can redistribute it and/or modify it under
7+
# the terms of the GNU Affero General Public License as published by the Free
8+
# Software Foundation, version 3 of the License.
9+
#
10+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License along
16+
# with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
module LiveAssessments
20+
# @API LiveAssessments
21+
# @beta
22+
# Manage live assessments
23+
#
24+
# @model Assessment
25+
# {
26+
# "id": "Assessment",
27+
# "description": "A simple assessment that collects pass/fail results for a student",
28+
# "properties": {
29+
# "id": {
30+
# "type": "string",
31+
# "example": "42",
32+
# "description": "A unique identifier for this live assessment"
33+
# },
34+
# "key": {
35+
# "type": "string",
36+
# "example": "2014-05-27,outcome_52",
37+
# "description": "A client specified unique identifier for the assessment"
38+
# },
39+
# "title": {
40+
# "type": "string",
41+
# "example": "May 27th Reading Assessment",
42+
# "description": "A human readable title for the assessment"
43+
# }
44+
# }
45+
# }
46+
47+
class AssessmentsController < ApplicationController
48+
before_filter :require_user
49+
before_filter :require_context
50+
51+
# @API Create or find a live assessment
52+
# @beta
53+
#
54+
# Creates or finds a live assessment with
55+
#
56+
# @example_request
57+
# {
58+
# "assessments": [{
59+
# "key": "2014-05-27-Outcome-52",
60+
# "title": "Tuesday's LiveAssessment",
61+
# "links": {
62+
# "outcome": "1"
63+
# }
64+
# }]
65+
# }
66+
#
67+
# @example_response
68+
# {
69+
# "links": {
70+
# "assessments.results": "http://example.com/courses/1/live_assessments/5/results"
71+
# },
72+
# "assessments": [Assessment]
73+
# }
74+
#
75+
def create
76+
return unless authorized_action(Assessment.new(context: @context), @current_user, :create)
77+
reject! 'missing required key :assessments' unless params[:assessments].is_a?(Array)
78+
79+
@assessments = []
80+
81+
Assessment.transaction do
82+
params[:assessments].each do |assessment_hash|
83+
if assessment_hash[:links] && outcome_id = assessment_hash[:links][:outcome]
84+
return unless authorized_action(@context, @current_user, :manage_outcomes)
85+
@outcome = @context.linked_learning_outcomes.where(id: outcome_id).first
86+
reject! 'outcome must be linked to the context' unless @outcome
87+
end
88+
89+
reject! 'missing required key :title' if assessment_hash[:title].blank?
90+
reject! 'missing required key :key' if assessment_hash[:key].blank?
91+
assessment = Assessment.find_or_initialize_by_context_id_and_context_type_and_key(@context.id, @context.class.to_s, assessment_hash[:key])
92+
assessment.title = assessment_hash[:title]
93+
assessment.save!
94+
if @outcome
95+
criterion = @outcome.data && @outcome.data[:rubric_criterion]
96+
mastery_score = criterion && criterion[:mastery_points] / criterion[:points_possible]
97+
@outcome.align(assessment, @context, mastery_type: "none", mastery_score: mastery_score)
98+
end
99+
@assessments << assessment
100+
end
101+
end
102+
103+
render json: serialize_jsonapi(@assessments)
104+
end
105+
106+
# @API List live assessments
107+
# @beta
108+
#
109+
# Returns a list of live assessments.
110+
#
111+
# @example_response
112+
# {
113+
# "links": {
114+
# "assessments.results": "http://example.com/courses/1/live_assessments/{assessments.id}/results"
115+
# },
116+
# "assessments": [Assessment]
117+
# }
118+
#
119+
def index
120+
return unless authorized_action(Assessment.new(context: @context), @current_user, :read)
121+
122+
@assessments = Assessment.for_context(@context)
123+
@assessments = Api.paginate(@assessments, self, polymorphic_url([:api_v1, @context, :live_assessments]))
124+
125+
render json: serialize_jsonapi(@assessments)
126+
end
127+
128+
protected
129+
130+
def serialize_jsonapi(assessments)
131+
serialized = Canvas::APIArraySerializer.new(assessments, {
132+
each_serializer: LiveAssessments::AssessmentSerializer,
133+
controller: self,
134+
scope: @current_user,
135+
root: false,
136+
include_root: false
137+
}).as_json
138+
{
139+
links: {
140+
'assessments.results' => polymorphic_url([:api_v1, @context]) + '/live_assessments/{assessments.id}/results'
141+
},
142+
assessments: serialized
143+
}
144+
end
145+
end
146+
end
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#
2+
# Copyright (C) 2014 Instructure, Inc.
3+
#
4+
# This file is part of Canvas.
5+
#
6+
# Canvas is free software: you can redistribute it and/or modify it under
7+
# the terms of the GNU Affero General Public License as published by the Free
8+
# Software Foundation, version 3 of the License.
9+
#
10+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License along
16+
# with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
module LiveAssessments
20+
# @API LiveAssessments
21+
# @beta
22+
# Manage live assessment results
23+
#
24+
# @model Result
25+
# {
26+
# "id": "Result",
27+
# "description": "A pass/fail results for a student",
28+
# "properties": {
29+
# "id": {
30+
# "type": "string",
31+
# "example": "42",
32+
# "description": "A unique identifier for this result"
33+
# },
34+
# "passed": {
35+
# "type": "boolean",
36+
# "example": true,
37+
# "description": "Whether the user passed or not"
38+
# },
39+
# "assessed_at": {
40+
# "type": "datetime",
41+
# "example": "2014-13-05T00:01:57-06:00",
42+
# "description": "When this result was recorded"
43+
# },
44+
# "links": {
45+
# "example": "{\"user\"=>\"3\", \"assessor\"=>\"42\", \"assessment\"=>\"30\"}",
46+
# "description": "Unique identifiers of objects associated with this result"
47+
# }
48+
# }
49+
# }
50+
class ResultsController < ApplicationController
51+
include Filters::LiveAssessments
52+
53+
before_filter :require_user
54+
before_filter :require_context
55+
before_filter :require_assessment
56+
57+
# @API Create a live assessment results
58+
# @beta
59+
#
60+
# @example_request
61+
# {
62+
# "results": [{
63+
# "passed": false,
64+
# "assessed_at": "2014-05-26T14:57:23-07:00",
65+
# "links": [
66+
# "user": "15"
67+
# ]
68+
# },{
69+
# "passed": true,
70+
# "assessed_at": "2014-05-26T13:05:40-07:00",
71+
# "links": [
72+
# "user": "16"
73+
# ]
74+
# }]
75+
# }
76+
#
77+
# @example_response
78+
# {
79+
# "results": [Result]
80+
# }
81+
#
82+
def create
83+
return unless authorized_action(@assessment.results.new, @current_user, :create)
84+
reject! 'missing required key :results' unless params[:results].is_a?(Array)
85+
86+
@results = []
87+
result_hashes_by_user_id = params[:results].group_by {|result| result[:links] and result[:links][:user]}
88+
Result.transaction do
89+
result_hashes_by_user_id.each do |user_id, result_hashes|
90+
reject! 'missing required key :user' unless user_id
91+
@user = @context.users.where(id: user_id).first
92+
reject! 'user must be in the context' unless @user
93+
94+
result_hashes.each do |result_hash|
95+
result = @assessment.results.build(
96+
user: @user,
97+
assessor: @current_user,
98+
passed: result_hash[:passed],
99+
assessed_at: result_hash[:assessed_at]
100+
)
101+
result.save!
102+
@results << result
103+
end
104+
end
105+
end
106+
107+
@assessment.send_later_if_production(:generate_submissions_for, @results.map(&:user).uniq)
108+
render json: serialize_jsonapi(@results)
109+
end
110+
111+
# @API List live assessment results
112+
# @beta
113+
#
114+
# Returns a list of live assessment results
115+
#
116+
# @argument user_id [Optional, Integer]
117+
# If set, restrict results to those for this user
118+
#
119+
# @example_response
120+
# {
121+
# "results": [Result]
122+
# }
123+
#
124+
def index
125+
return unless authorized_action(@assessment.results.new, @current_user, :read)
126+
@results = @assessment.results
127+
@results = @results.for_user(params[:user_id]) if params[:user_id]
128+
@results = Api.paginate(@results, self, polymorphic_url([:api_v1, @context, :live_assessment_results], assessment_id: @assessment.id))
129+
130+
render json: serialize_jsonapi(@results)
131+
end
132+
133+
protected
134+
135+
def serialize_jsonapi(results)
136+
serialized = Canvas::APIArraySerializer.new(results, {
137+
each_serializer: LiveAssessments::ResultSerializer,
138+
controller: self,
139+
scope: @current_user,
140+
root: false,
141+
include_root: false
142+
}).as_json
143+
{ results: serialized }
144+
end
145+
end
146+
end

app/models/content_tag.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def initialize( alignment )
2929
belongs_to :content, :polymorphic => true
3030
validates_inclusion_of :content_type, :allow_nil => true, :in => ['Attachment', 'Assignment', 'WikiPage',
3131
'ContextModuleSubHeader', 'Quizzes::Quiz', 'ExternalUrl', 'LearningOutcome', 'DiscussionTopic',
32-
'Rubric', 'ContextExternalTool', 'LearningOutcomeGroup', 'AssessmentQuestionBank']
32+
'Rubric', 'ContextExternalTool', 'LearningOutcomeGroup', 'AssessmentQuestionBank', 'LiveAssessments::Assessment']
3333
belongs_to :context, :polymorphic => true
3434
validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course', 'LearningOutcomeGroup',
3535
'Assignment', 'Account', 'Quizzes::Quiz']

app/models/learning_outcome_result.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ class LearningOutcomeResult < ActiveRecord::Base
2626
belongs_to :learning_outcome
2727
belongs_to :alignment, :class_name => 'ContentTag', :foreign_key => :content_tag_id
2828
belongs_to :association_object, :polymorphic => true, :foreign_type => :association_type, :foreign_key => :association_id
29-
validates_inclusion_of :association_type, :allow_nil => true, :in => ['Quizzes::Quiz', 'RubricAssociation', 'Assignment']
29+
validates_inclusion_of :association_type, :allow_nil => true, :in => ['Quizzes::Quiz', 'RubricAssociation', 'Assignment', 'LiveAssessments::Assessment']
3030
belongs_to :artifact, :polymorphic => true
31-
validates_inclusion_of :artifact_type, :allow_nil => true, :in => ['Quizzes::QuizSubmission', 'RubricAssessment', 'Submission']
31+
validates_inclusion_of :artifact_type, :allow_nil => true, :in => ['Quizzes::QuizSubmission', 'RubricAssessment', 'Submission', 'LiveAssessments::Submission']
3232
belongs_to :associated_asset, :polymorphic => true
33-
validates_inclusion_of :associated_asset_type, :allow_nil => true, :in => ['AssessmentQuestion', 'Quizzes::Quiz']
33+
validates_inclusion_of :associated_asset_type, :allow_nil => true, :in => ['AssessmentQuestion', 'Quizzes::Quiz', 'LiveAssessments::Assessment']
3434
belongs_to :context, :polymorphic => true
3535
validates_inclusion_of :context_type, :allow_nil => true, :in => ['Course']
3636
simply_versioned

0 commit comments

Comments
 (0)