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:
Matthew Berns 2016-08-12 15:40:02 -05:00 committed by Matt Berns
parent d745c6a73d
commit 88805f4b1e
5 changed files with 551 additions and 0 deletions

View File

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

View File

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

49
lib/api/v1/rubric.rb Normal file
View File

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

View File

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

View File

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