add new Rubric API endpoint
closes OUT-358 refs PFS-4925 test plan: - review new API documentation - create some rubrics in the context of an account - create some rubrics in the context of a course - attach the rubrics to some assignments - for at least one assignment, enable peer reviews and assign some students - as a teacher, assess the assignment - as a student, complete some peer reviews - go to the following endpoints api/v1/accounts/{account_id}/rubrics - should list all rubrics in the given account api/v1/courses/{course_id}/rubrics - should list all rubrics in the given course api/v1/accounts/{account_id}/rubrics/{rubric_id} - should list the specific account rubric api/v1/courses/{course_id}/rubrics/{rubric_id} - should list the specific course rubric api/v1/courses/{course_id}/rubrics/{rubric_id}?include=assessments - should include all assessments for the rubric api/v1/courses/{course_id}/rubrics/{rubric_id}?include=graded_assessments - should include only the assessment(s) used for grading the rubric api/v1/courses/{course_id}/rubrics/{rubric_id}?include=peer_assessments - should include only the peer_assessment(s) for the rubric - when getting assessments, add the following parameters &style=full should return the full data hash associated with returned assessments &style=comments_only should only return the comments from an assessment's data hash - when entering in invalid values for include or style, an error should be returned that provides you with the valid values for the respective parameters Change-Id: Ib46900d4c58e06d6fa2771614ba2efa11d3b5b6c Reviewed-on: https://gerrit.instructure.com/87702 Tested-by: Jenkins Reviewed-by: Michael Brewer-Davis <mbd@instructure.com> QA-Review: Alex Ortiz-Rosado <aortiz@instructure.com> Product-Review: Josh Simpson <jsimpson@instructure.com>
This commit is contained in:
parent
d745c6a73d
commit
88805f4b1e
|
@ -0,0 +1,206 @@
|
|||
#
|
||||
# Copyright (C) 2016 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 Rubrics
|
||||
# @beta
|
||||
#
|
||||
# API for accessing rubric information.
|
||||
#
|
||||
# @model Rubric
|
||||
# {
|
||||
# "id": "Rubric",
|
||||
# "description": "",
|
||||
# "properties": {
|
||||
# "id": {
|
||||
# "description": "the ID of the rubric",
|
||||
# "example": 1,
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "title": {
|
||||
# "description": "title of the rubric",
|
||||
# "example": "some title",
|
||||
# "type": "string"
|
||||
# },
|
||||
# "context_id": {
|
||||
# "description": "the context owning the rubric",
|
||||
# "example": 1,
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "context_type": {
|
||||
# "example": "Course",
|
||||
# "type": "string"
|
||||
# },
|
||||
# "points_possible": {
|
||||
# "example": "10.0",
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "reusable": {
|
||||
# "example": "false",
|
||||
# "type": "boolean"
|
||||
# },
|
||||
# "read_only": {
|
||||
# "example": "true",
|
||||
# "type": "boolean"
|
||||
# },
|
||||
# "free_form_criterion_comments": {
|
||||
# "description": "whether or not free-form comments are used",
|
||||
# "example": "true",
|
||||
# "type": "boolean"
|
||||
# },
|
||||
# "hide_score_total": {
|
||||
# "example": "true",
|
||||
# "type": "boolean"
|
||||
# },
|
||||
# "assessments": {
|
||||
# "description": "If an assessment type is included in the 'include' parameter, includes an array of rubric assessment objects for a given rubric, based on the assessment type requested. If the user does not request an assessment type this key will be absent.",
|
||||
# "type": "array",
|
||||
# "$ref": "RubricAssessment"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# @model RubricAssessment
|
||||
# {
|
||||
# "id": "RubricAssessment",
|
||||
# "description": "",
|
||||
# "properties": {
|
||||
# "id": {
|
||||
# "description": "the ID of the rubric",
|
||||
# "example": 1,
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "rubric_id": {
|
||||
# "description": "the rubric the assessment belongs to",
|
||||
# "example": 1,
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "rubric_association_id": {
|
||||
# "example": "2",
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "score": {
|
||||
# "example": "5.0",
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "artifact_type": {
|
||||
# "description": "the object of the assessment",
|
||||
# "example": "Submission",
|
||||
# "type": "string"
|
||||
# },
|
||||
# "artifact_id": {
|
||||
# "description": "the id of the object of the assessment",
|
||||
# "example": "3",
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "artifact_attempt": {
|
||||
# "description": "the current number of attempts made on the object of the assessment",
|
||||
# "example": "2",
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "assessment_type": {
|
||||
# "description": "the type of assessment. values will be either 'grading', 'peer_review', or 'provisional_grade'",
|
||||
# "example": "grading",
|
||||
# "type": "string"
|
||||
# },
|
||||
# "assessor_id": {
|
||||
# "description": "user id of the person who made the assessment",
|
||||
# "example": "6",
|
||||
# "type": "integer"
|
||||
# },
|
||||
# "data": {
|
||||
# "description": "(Optional) If 'full' is included in the 'style' parameter, returned assessments will have their full details contained in their data hash. If the user does not request a style, this key will be absent.",
|
||||
# "type": "array"
|
||||
# },
|
||||
# "comments": {
|
||||
# "description": "(Optional) If 'comments_only' is included in the 'style' parameter, returned assessments will include only the comments portion of their data hash. If the user does not request a style, this key will be absent.",
|
||||
# "type": "array"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
class RubricsApiController < ApplicationController
|
||||
include Api::V1::Rubric
|
||||
include Api::V1::RubricAssessment
|
||||
|
||||
before_filter :require_user
|
||||
before_filter :require_context
|
||||
before_filter :validate_args
|
||||
before_filter :find_rubric, only: [:show]
|
||||
|
||||
# @API List rubrics
|
||||
# Returns the paginated list of active rubrics for the current context.
|
||||
|
||||
def index
|
||||
return unless authorized_action(@context, @current_user, :manage_rubrics)
|
||||
rubrics = Api.paginate(@context.rubrics.active, self, api_v1_course_assignments_url(@context))
|
||||
render json: rubrics_json(rubrics, @current_user, session) unless performed?
|
||||
end
|
||||
|
||||
# @API Get a single rubric
|
||||
# Returns the rubric with the given id.
|
||||
# @argument include [String, "assessments"|"graded_assessments"|"peer_assessments"]
|
||||
# If included, the type of associated rubric assessments to return. If not included, assessments will be omitted.
|
||||
# @argument style [String, "full"|"comments_only"]
|
||||
# Applicable only if assessments are being returned. If included, returns either all criteria data associated with the assessment, or just the comments. If not included, both data and comments are omitted.
|
||||
# @returns Rubric
|
||||
|
||||
def show
|
||||
return unless authorized_action(@context, @current_user, :manage_rubrics)
|
||||
if !@context.errors.present?
|
||||
assessments = get_rubric_assessment(params[:include])
|
||||
render json: rubric_json(@rubric, @current_user, session,
|
||||
assessments: assessments, style: params[:style])
|
||||
else
|
||||
render json: @context.errors, status: :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_rubric
|
||||
@rubric = Rubric.find(params[:id])
|
||||
end
|
||||
|
||||
def get_rubric_assessment(type)
|
||||
case type
|
||||
when 'assessments'
|
||||
RubricAssessment.where(rubric_id: @rubric.id)
|
||||
when 'graded_assessments'
|
||||
RubricAssessment.where(rubric_id: @rubric.id, assessment_type: 'grading')
|
||||
when 'peer_assessments'
|
||||
RubricAssessment.where(rubric_id: @rubric.id, assessment_type: 'peer_review')
|
||||
end
|
||||
end
|
||||
|
||||
def validate_args
|
||||
errs = {}
|
||||
valid_assessment_args = ['assessments', 'graded_assessments', 'peer_assessments']
|
||||
valid_style_args = ['full', 'comments_only']
|
||||
if params[:include] && !valid_assessment_args.include?(params[:include])
|
||||
errs['include'] = "invalid assessment type requested. Must be one of the following: #{valid_assessment_args.join(", ")}"
|
||||
end
|
||||
if params[:style] && !valid_style_args.include?(params[:style])
|
||||
errs['style'] = "invalid style requested. Must be one of the following: #{valid_style_args.join(", ")}"
|
||||
end
|
||||
if params[:style] && !params[:include]
|
||||
errs['style'] = "invalid parameters. Style parameter passed without requesting assessments"
|
||||
end
|
||||
errs.each{|key, msg| @context.errors.add(key, msg, att_name: key)}
|
||||
end
|
||||
end
|
||||
|
|
@ -1853,6 +1853,13 @@ CanvasRails::Application.routes.draw do
|
|||
scope(controller: :announcements_api) do
|
||||
get 'announcements', action: :index, as: :announcements
|
||||
end
|
||||
|
||||
scope(controller: :rubrics_api) do
|
||||
get 'accounts/:account_id/rubrics', action: :index, as: :account_rubrics
|
||||
get 'accounts/:account_id/rubrics/:id', action: :show
|
||||
get 'courses/:course_id/rubrics', action: :index, as: :course_rubrics
|
||||
get 'courses/:course_id/rubrics/:id', action: :show
|
||||
end
|
||||
end
|
||||
|
||||
# this is not a "normal" api endpoint in the sense that it is not documented or
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
#
|
||||
# Copyright (C) 2016 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::Rubric
|
||||
include Api::V1::Json
|
||||
include Api::V1::RubricAssessment
|
||||
|
||||
API_ALLOWED_RUBRIC_OUTPUT_FIELDS = {
|
||||
only: %w(
|
||||
id
|
||||
title
|
||||
context_id
|
||||
context_type
|
||||
points_possible
|
||||
reusable
|
||||
public
|
||||
read_only
|
||||
free_form_criterion_comments
|
||||
hide_score_total
|
||||
)
|
||||
}.freeze
|
||||
|
||||
def rubrics_json(rubrics, user, session, opts = {})
|
||||
rubrics.map { |r| rubric_json(r, user, session, opts) }
|
||||
end
|
||||
|
||||
|
||||
def rubric_json(rubric, user, session, opts = {})
|
||||
json_attributes = API_ALLOWED_RUBRIC_OUTPUT_FIELDS
|
||||
hash = api_json(rubric, user, session, json_attributes)
|
||||
hash['assessments'] = rubric_assessments_json(opts[:assessments], user, session, opts) if opts[:assessments].present?
|
||||
hash
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
#
|
||||
# Copyright (C) 2016 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::RubricAssessment
|
||||
include Api::V1::Json
|
||||
|
||||
API_ALLOWED_RUBRIC_ASSESSMENT_OUTPUT_FIELDS = {
|
||||
only: %w(
|
||||
id
|
||||
rubric_id
|
||||
rubric_association_id
|
||||
score
|
||||
artifact_type
|
||||
artifact_id
|
||||
artifact_attempt
|
||||
assessment_type
|
||||
assessor_id
|
||||
)
|
||||
}.freeze
|
||||
|
||||
def rubric_assessments_json(rubric_assessments, user, session, opts = {})
|
||||
rubric_assessments.map { |ra| rubric_assessment_json(ra, user, session, opts) }
|
||||
end
|
||||
|
||||
|
||||
def rubric_assessment_json(rubric_assessment, user, session, opts = {})
|
||||
json_attributes = API_ALLOWED_RUBRIC_ASSESSMENT_OUTPUT_FIELDS
|
||||
hash = api_json(rubric_assessment, user, session, json_attributes)
|
||||
hash['data'] = rubric_assessment.data if opts[:style] == "full"
|
||||
hash['comments'] = rubric_assessment.data.map{|rad| rad[:comments]} if opts[:style] == "comments_only"
|
||||
hash
|
||||
end
|
||||
end
|
|
@ -0,0 +1,241 @@
|
|||
#
|
||||
# Copyright (C) 2016 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')
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../sharding_spec_helper')
|
||||
|
||||
describe "Rubrics API", type: :request do
|
||||
include Api::V1::Rubric
|
||||
|
||||
ALLOWED_RUBRIC_FIELDS = Api::V1::Rubric::API_ALLOWED_RUBRIC_OUTPUT_FIELDS[:only]
|
||||
|
||||
before :once do
|
||||
@account = Account.default
|
||||
end
|
||||
|
||||
def create_rubric(context, opts={})
|
||||
@rubric = Rubric.new(:context => context)
|
||||
@rubric.data = [rubric_data_hash(opts)]
|
||||
@rubric.save!
|
||||
end
|
||||
|
||||
def rubric_association_params_for_assignment(assign)
|
||||
HashWithIndifferentAccess.new({
|
||||
hide_score_total: "0",
|
||||
purpose: "grading",
|
||||
skip_updating_points_possible: false,
|
||||
update_if_existing: true,
|
||||
use_for_grading: "1",
|
||||
association_object: assign
|
||||
})
|
||||
end
|
||||
|
||||
def create_rubric_assessment(opts={})
|
||||
assessment_type = opts[:type] || "grading"
|
||||
assignment1 = assignment_model(context: @course)
|
||||
submission = assignment1.find_or_create_submission(@student)
|
||||
ra_params = rubric_association_params_for_assignment(submission.assignment)
|
||||
rubric_assoc = RubricAssociation.generate(@teacher, @rubric, @course, ra_params)
|
||||
rubric_assessment = RubricAssessment.create!({
|
||||
artifact: submission,
|
||||
assessment_type: assessment_type,
|
||||
assessor: @teacher,
|
||||
rubric: @rubric,
|
||||
user: submission.user,
|
||||
rubric_association: rubric_assoc,
|
||||
data: [{points: 3.0, description: "hello", comments: opts[:comments]}]
|
||||
})
|
||||
end
|
||||
|
||||
def rubric_data_hash(opts={})
|
||||
hash = {
|
||||
points: 3,
|
||||
description: "Criteria row",
|
||||
id: 1,
|
||||
ratings: [
|
||||
{
|
||||
points: 3,
|
||||
description: "Rockin'",
|
||||
criterion_id: 1,
|
||||
id: 2
|
||||
},
|
||||
{
|
||||
points: 0,
|
||||
description: "Lame",
|
||||
criterion_id: 2,
|
||||
id: 3
|
||||
}
|
||||
]
|
||||
}.merge(opts)
|
||||
hash
|
||||
end
|
||||
|
||||
def rubrics_api_call
|
||||
api_call(
|
||||
:get, "/api/v1/courses/#{@course.id}/rubrics",
|
||||
controller: 'rubrics_api',
|
||||
action: 'index',
|
||||
course_id: @course.id.to_s,
|
||||
format: 'json'
|
||||
)
|
||||
end
|
||||
|
||||
def rubric_api_call(params={})
|
||||
api_call(
|
||||
:get, "/api/v1/courses/#{@course.id}/rubrics/#{@rubric.id}",
|
||||
controller: 'rubrics_api',
|
||||
action: 'show',
|
||||
course_id: @course.id.to_s,
|
||||
id: @rubric.id.to_s,
|
||||
format: 'json',
|
||||
include: params[:include],
|
||||
style: params[:style]
|
||||
)
|
||||
end
|
||||
|
||||
def raw_rubric_call(params={})
|
||||
raw_api_call(:get, "/api/v1/courses/#{@course.id}/rubrics/#{@rubric.id}",
|
||||
{ controller: 'rubrics_api',
|
||||
action: 'show',
|
||||
format: 'json',
|
||||
course_id: @course.id.to_s,
|
||||
id: @rubric.id.to_s,
|
||||
include: params[:include],
|
||||
style: params[:style]
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
describe "index action" do
|
||||
before :once do
|
||||
course_with_teacher active_all: true
|
||||
create_rubric(@course)
|
||||
end
|
||||
|
||||
it "returns an array of all rubrics in an account" do
|
||||
create_rubric(@account)
|
||||
response = rubrics_api_call
|
||||
expect(response[0].keys.sort).to eq ALLOWED_RUBRIC_FIELDS.sort
|
||||
expect(response.length).to eq 1
|
||||
end
|
||||
|
||||
it "returns an array of all rubrics in a course" do
|
||||
create_rubric(@course)
|
||||
response = rubrics_api_call
|
||||
expect(response[0].keys.sort).to eq ALLOWED_RUBRIC_FIELDS.sort
|
||||
expect(response.length).to eq 2
|
||||
end
|
||||
|
||||
it "requires the user to have permission to manage rubrics" do
|
||||
@user = @student
|
||||
raw_rubric_call
|
||||
|
||||
assert_status(401)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "show action" do
|
||||
before :once do
|
||||
course_with_teacher active_all: true
|
||||
create_rubric(@course)
|
||||
end
|
||||
|
||||
it "returns a rubric" do
|
||||
response = rubric_api_call
|
||||
expect(response.keys.sort).to eq ALLOWED_RUBRIC_FIELDS.sort
|
||||
end
|
||||
|
||||
it "requires the user to have permission to manage rubrics" do
|
||||
@user = @student
|
||||
raw_rubric_call
|
||||
|
||||
assert_status(401)
|
||||
end
|
||||
|
||||
|
||||
context "include parameter" do
|
||||
before :once do
|
||||
course_with_student(user: @user, active_all: true)
|
||||
course_with_teacher active_all: true
|
||||
create_rubric(@course)
|
||||
['grading', 'peer_review'].each.with_index do |type, index|
|
||||
create_rubric_assessment({type: type, comments: "comment #{index}"})
|
||||
end
|
||||
end
|
||||
|
||||
it "does not returns rubric assessments by default" do
|
||||
response = rubric_api_call
|
||||
expect(response).not_to have_key "assessmensdts"
|
||||
end
|
||||
|
||||
it "returns rubric assessments when passed 'assessessments'" do
|
||||
response = rubric_api_call({include: "assessments"})
|
||||
expect(response).to have_key "assessments"
|
||||
expect(response["assessments"].length).to eq 2
|
||||
end
|
||||
|
||||
it "returns any rubric assessments used for grading when passed 'graded_assessessments'" do
|
||||
response = rubric_api_call({include: "graded_assessments"})
|
||||
expect(response["assessments"][0]["assessment_type"]).to eq "grading"
|
||||
expect(response["assessments"].length).to eq 1
|
||||
end
|
||||
|
||||
it "returns any peer review assessments when passed 'peer_assessessments'" do
|
||||
response = rubric_api_call({include: "peer_assessments"})
|
||||
expect(response["assessments"][0]["assessment_type"]).to eq "peer_review"
|
||||
expect(response["assessments"].length).to eq 1
|
||||
end
|
||||
|
||||
it "returns an error if passed an invalid argument" do
|
||||
raw_rubric_call({include: "cheez"})
|
||||
|
||||
expect(response).not_to be_success
|
||||
json = JSON.parse response.body
|
||||
expect(json["errors"]["include"].first["message"]).to eq "invalid assessment type requested. Must be one of the following: assessments, graded_assessments, peer_assessments"
|
||||
end
|
||||
|
||||
context "style argument" do
|
||||
it "returns all data when passed 'full'" do
|
||||
response = rubric_api_call({include: "assessments", style: "full"})
|
||||
expect(response["assessments"][0]).to have_key 'data'
|
||||
end
|
||||
|
||||
it "returns only comments when passed 'comments_only'" do
|
||||
response = rubric_api_call({include: "assessments", style: "comments_only"})
|
||||
expect(response["assessments"][0]).to have_key 'comments'
|
||||
end
|
||||
|
||||
it "returns an error if passed an invalid argument" do
|
||||
raw_rubric_call({include: "assessments", style: "BigMcLargeHuge"})
|
||||
|
||||
expect(response).not_to be_success
|
||||
json = JSON.parse response.body
|
||||
expect(json["errors"]["style"].first["message"]).to eq "invalid style requested. Must be one of the following: full, comments_only"
|
||||
end
|
||||
|
||||
it "returns an error if passed a style parameter without assessments" do
|
||||
raw_rubric_call({style: "full"})
|
||||
|
||||
expect(response).not_to be_success
|
||||
json = JSON.parse response.body
|
||||
expect(json["errors"]["style"].first["message"]).to eq "invalid parameters. Style parameter passed without requesting assessments"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue