Planner API

Closes FALCOR-183
Closes FALCOR-184
Closes FALCOR-255
Closes FALCOR-186
Closes FALCOR-187
Closes FALCOR-188
Closes FALCOR-189
Closes FALCOR-147
Closes FALCOR-148

Test Plan:
* As a student in an account with the Student Planner
  feature flag enabled
* In a course with multiple upcoming due assignments
  in the current and following weeks
* Test that the following API endpoints provide you
  with the data described:

  get '/api/v1/planner/items'
    * Should return a list matching that of the todo
      list endpoint, while adding an additional key
      named `visible_in_planner`
  get '/api/v1/planner/overrides'
    * Should return a list of previously created
      PlannerOverrides
  get '/api/v1/planner/overrides/:override_id'
    * Should return the specific override for the
      passed in id
  put '/api/v1/planner/overrides/:override_id'
    * Should update an existing override's `visible`
      value to match what was passed in the request. No
      other values should be updated
  post '/api/v1/planner/overrides'
    * Should create a new PlannerOverride with the specified
      `plannable_type`, `plannable_id`, and `visible` values.
      `user_id` should match that of the user making the request
  delete '/api/v1/planner/overrides/:override_id'
    * Should set the PlannerOverride's `deleted_at` to when
      the request was made, as well as updating the `workflow_state`
      to `deleted`

Change-Id: I03890a525f8201a8df1d2f1290cdcd549ba548d7
Reviewed-on: https://gerrit.instructure.com/107495
Tested-by: Jenkins
Reviewed-by: Steven Burnett <sburnett@instructure.com>
QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com>
Product-Review: Dan Minkevitch <dan@instructure.com>
This commit is contained in:
Dan Minkevitch 2017-05-10 21:40:45 -07:00
parent e2ebcf2c4e
commit 1f4132e1ba
17 changed files with 859 additions and 3 deletions

View File

@ -0,0 +1,212 @@
#
# Copyright (C) 2011 - 2017 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 Planner override
#
# API for creating, accessing and updating planner override. PlannerOverrides are used
# to control the visibility of objects displayed on the Planner.
#
# @model PlannerOverride
# {
# "id": "PlannerOverride",
# "description": "User-controlled setting for whether an item should be displayed on the planner or not",
# "properties": {
# "id": {
# "description": "The ID of the planner override",
# "example": 234,
# "type": "integer"
# },
# "plannable_type": {
# "description": "The type of the associated object for the planner override",
# "example": "Assignment",
# "type": "string"
# },
# "plannable_id": {
# "description": "The id of the associated object for the planner override",
# "example": 1578941,
# "type": "integer"
# },
# "user_id": {
# "description": "The id of the associated user for the planner override",
# "example": 1578941,
# "type": "integer"
# },
# "workflow_state": {
# "description": "The current published state of the item, synced with the associated object",
# "example": "published",
# "type": "string"
# },
# "visible": {
# "description": "Controls whether or not the associated plannable item is displayed on the planner",
# "example": false,
# "type": "boolean"
# },
# "deleted_at": {
# "description": "The datetime of when the planner override was deleted, if applicable",
# "example": "2017-05-09T10:12:00Z",
# "type": "datetime"
# }
# }
# }
#
class PlannerOverridesController < ApplicationController
include Api::V1::PlannerItem
before_action :require_user
# @API List planner items
#
# Retrieve the list of objects to be shown on the planner for the current user
# with the associated planner override to override an item's visibility if set.
#
# @argument date [Date]
# Only return items since the given date.
# The value should be formatted as: yyyy-mm-dd or ISO 8601 YYYY-MM-DDTHH:MM:SSZ.
#
# @example_response
# [
# {
# 'type': 'grading', // an assignment that needs grading
# 'assignment': { .. assignment object .. },
# 'ignore': '.. url ..',
# 'ignore_permanently': '.. url ..',
# 'visible_in_planner': true
# 'html_url': '.. url ..',
# 'needs_grading_count': 3, // number of submissions that need grading
# 'context_type': 'course', // course|group
# 'course_id': 1,
# 'group_id': null,
# },
# {
# 'type' => 'submitting', // an assignment that needs submitting soon
# 'assignment' => { .. assignment object .. },
# 'ignore' => '.. url ..',
# 'ignore_permanently' => '.. url ..',
# 'visible_in_planner': true
# 'html_url': '.. url ..',
# 'context_type': 'course',
# 'course_id': 1,
# },
# {
# 'type' => 'submitting', // a quiz that needs submitting soon
# 'quiz' => { .. quiz object .. },
# 'ignore' => '.. url ..',
# 'ignore_permanently' => '.. url ..',
# 'visible_in_planner': true
# 'html_url': '.. url ..',
# 'context_type': 'course',
# 'course_id': 1,
# },
# ]
def items_index
render :json => assignments_for_user
end
# @API List planner overrides
#
# Retrieve a planner override for the current user
#
# @returns [PlannerOverride]
def index
render :json => PlannerOverride.for_user(@current_user)
end
# @API Show a planner override
#
# Retrieve a planner override for the current user
#
# @returns PlannerOverride
def show
planner_override = PlannerOverride.find(params[:override_id])
if planner_override.present?
render json: planner_override
else
not_found
end
end
# @API Update a planner override
#
# Update a planner override's visibilty for the current user
#
# @returns PlannerOverride
def update
planner_override = PlannerOverride.find(params[:override_id])
planner_override.visible = value_to_boolean(params[:visible])
if planner_override.save
render json: planner_override, status: :updated
else
render json: planner_override.errors, status: :bad_request
end
end
# @API Create a planner override
#
# Create a planner override for the current user
#
# @returns PlannerOverride
def create
planner_override = PlannerOverride.new(plannable_type: params[:plannable_type],
plannable_id: params[:plannable_id],
visible: value_to_boolean(params[:visible]),
user: @current_user)
if planner_override.save
render json: planner_override, status: :created
else
render json: planner_override.errors, status: :bad_request
end
end
# @API Delete a planner override
#
# Delete a planner override for the current user
#
# @returns PlannerOverride
def destroy
planner_override = PlannerOverride.find(params[:override_id])
if planner_override.destroy
render json: planner_override, status: :deleted
else
render json: planner_override.errors, status: :bad_request
end
end
private
def assignments_for_user
grading = @current_user.assignments_needing_grading(include_ignored: true).map { |a| planner_item_json(a, @current_user, session, 'grading') }
submitting = @current_user.assignments_needing_submitting(include_ignored: true, include_ungraded: true, limit: ToDoListPresenter::ASSIGNMENT_LIMIT).map { |a|
planner_item_json(a, @current_user, session, 'submitting')
}
submitting += @current_user.ungraded_quizzes_needing_submitting(include_ignored: true).map { |q| planner_item_json(q, @current_user, session, 'submitting') }
submitting.sort_by! { |j| (j[:assignment] || j[:quiz])[:due_at] || CanvasSort::Last }
moderation = @current_user.assignments_needing_moderation(include_ignored: true).map { |a|
planner_item_json(a, @current_user, session, 'moderation')
}
(grading + submitting + moderation)
end
def require_user
render_unauthorized_action if !@current_user || !@domain_root_account.feature_enabled?(:student_planner)
end
end

View File

@ -22,6 +22,7 @@ class Announcement < DiscussionTopic
has_a_broadcast_policy
include HasContentTags
include Plannable
sanitize_field :message, CanvasSanitize::SANITIZE

View File

@ -34,6 +34,7 @@ class Assignment < ActiveRecord::Base
include SearchTermHelper
include Canvas::DraftStateValidations
include TurnitinID
include Plannable
ALLOWED_GRADING_TYPES = %w(
pass_fail percent letter_grade gpa_scale points not_graded

View File

@ -33,6 +33,7 @@ class DiscussionTopic < ActiveRecord::Base
include ContextModuleItem
include SearchTermHelper
include Submittable
include Plannable
include MasterCourses::Restrictor
restrict_columns :content, [:title, :message]
restrict_columns :settings, [:delayed_post_at, :require_initial_post, :discussion_type,

View File

@ -0,0 +1,88 @@
#
# Copyright (C) 2017 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/>.
#
class PlannerOverride < ActiveRecord::Base
CONTENT_TYPES = %w(Announcement Assignment DiscussionTopic Quizzes::Quiz WikiPage).freeze
include Workflow
belongs_to :plannable, polymorphic: true
belongs_to :user
validates_inclusion_of :plannable_type, :allow_nil => false, :in => CONTENT_TYPES
validates_presence_of :plannable_id, :workflow_state, :user_id
scope :active, -> { where workflow_state: 'active' }
scope :visible, -> { where visible: true }
scope :hidden, -> { where.not visible }
scope :deleted, -> { where workflow_state: 'deleted' }
scope :not_deleted, -> { where.not deleted }
workflow do
state :active do
event :unpublish, :transitions_to => :unpublished
end
state :unpublished do
event :publish, :transitions_to => :active
end
state :deleted
end
alias_method :published?, :active?
def self.update_for(obj)
overrides = PlannerOverride.where(plannable_id: obj.id, plannable_type: obj.class.to_s)
overrides.update_all(workflow_state: plannable_workflow_state(obj)) if overrides.exists?
end
def self.for_user(user)
overrides = PlannerOverride.where(user_id: user)
end
def self.plannable_workflow_state(plannable)
if plannable.respond_to?(:published?)
if plannable.respond_to?(:deleted?) && plannable.deleted?
'deleted'
elsif plannable.published?
'active'
else
'unpublished'
end
else
if plannable.respond_to?(:workflow_state)
workflow_state = plannable.workflow_state.to_s
if ['active', 'available', 'published'].include?(workflow_state)
'active'
elsif ['unpublished', 'deleted'].include?(workflow_state)
workflow_state
end
else
nil
end
end
end
def plannable_workflow_state
PlannerOverride.plannable_workflow_state(self.plannable)
end
alias_method :destroy_permanently!, :destroy
def destroy
self.workflow_state = 'deleted'
self.deleted_at = Time.now.utc
self.save
end
end

View File

@ -28,6 +28,7 @@ class Quizzes::Quiz < ActiveRecord::Base
include ContextModuleItem
include DatesOverridable
include SearchTermHelper
include Plannable
include Canvas::DraftStateValidations
attr_readonly :context_id, :context_type

View File

@ -1462,7 +1462,7 @@ class User < ActiveRecord::Base
if opts[:contexts]
course_ids = Array(opts[:contexts]).map(&:id) & course_ids
end
opts = {limit: 15}.merge(opts.slice(:due_after, :due_before, :limit, :include_ungraded, :ungraded_quizzes))
opts = {limit: 15}.merge(opts.slice(:due_after, :due_before, :limit, :include_ungraded, :ungraded_quizzes, :include_ignored))
course_ids_cache_key = Digest::MD5.hexdigest(course_ids.sort.join('/'))
Rails.cache.fetch([self, "assignments_needing_#{purpose}", course_ids_cache_key, opts].cache_key, :expires_in => expires_in) do
@ -1470,10 +1470,12 @@ class User < ActiveRecord::Base
Shard.partition_by_shard(course_ids) do |shard_course_ids|
if opts[:ungraded_quizzes]
scope = Quizzes::Quiz.where(context_type: 'Course', context_id: shard_course_ids).
not_for_assignment.not_ignored_by(self, purpose)
not_for_assignment
scope = scope.not_ignored_by(self, purpose) unless opts[:include_ignored]
yield(scope, opts.merge(:shard_course_ids => shard_course_ids))
else
scope = Assignment.for_course(shard_course_ids).not_ignored_by(self, purpose)
scope = Assignment.for_course(shard_course_ids)
scope = scope.not_ignored_by(self, purpose) unless opts[:include_ignored]
yield(scope, opts.merge(:shard_course_ids => shard_course_ids))
end
end

View File

@ -31,6 +31,7 @@ class WikiPage < ActiveRecord::Base
include CopyAuthorizedLinks
include ContextModuleItem
include Submittable
include Plannable
include SearchTermHelper

View File

@ -1943,6 +1943,15 @@ CanvasRails::Application.routes.draw do
post 'courses/:id/late_policy', action: :create
patch 'courses/:id/late_policy', action: :update
end
scope(controller: :planner_overrides) do
get 'planner/items', action: :items_index, as: :planner_overrides
get 'planner/overrides', action: :index
get 'planner/overrides/:override_id', action: :show
put 'planner/overrides/:override_id', action: :update
post 'planner/overrides', action: :create
delete 'planner/overrides/:override_id', action: :destroy
end
end
# this is not a "normal" api endpoint in the sense that it is not documented or

View File

@ -25,6 +25,7 @@ class FixDeprecatedPolymorphicNames < ActiveRecord::Migration[4.2]
end
reflections.flatten!
reflections.group_by(&:klass).each do |(klass, klass_reflections)|
next unless klass.table_exists?
klass.find_ids_in_ranges(batch_size: 10000) do |min_id, max_id|
klass_reflections.each do |reflection|
klass.where(id: min_id..max_id,

View File

@ -0,0 +1,40 @@
#
# Copyright (C) 2017 - present 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/>.
class CreatePlannerOverrides < ActiveRecord::Migration[4.2]
tag :predeploy
def self.up
create_table :planner_overrides do |t|
t.string :plannable_type, null: false
t.integer :plannable_id, limit: 8, null: false
t.integer :user_id, limit: 8, null: false
t.string :workflow_state
t.boolean :visible, null: false, default: true
t.datetime :deleted_at
t.timestamps null: false
end
add_index :planner_overrides, [:plannable_type, :plannable_id, :user_id], unique: true, name: 'index_planner_overrides_on_plannable_and_user'
add_foreign_key :planner_overrides, :users
end
def self.down
drop_table :planner_overrides
end
end

View File

@ -0,0 +1,60 @@
#
# Copyright (C) 2011 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::PlannerItem
include Api::V1::Assignment
include Api::V1::Quiz
include Api::V1::Context
include Api::V1::DiscussionTopics
include Api::V1::WikiPage
def planner_item_json(item, user, session, todo_type)
context_data(item).merge({
:type => todo_type,
:ignore => api_v1_users_todo_ignore_url(item.asset_string, todo_type, :permanent => '0'),
:ignore_permanently => api_v1_users_todo_ignore_url(item.asset_string, todo_type, :permanent => '1'),
:visible_in_planner => item.visible_in_planner_for?(user),
:planner_override => item.planner_override_for(user)
}).tap do |hash|
case item
when Assignment, DiscussionTopic
assignment = item
hash[:assignment] = assignment_json(assignment, user, session, include_discussion_topic: true)
hash[:html_url] = todo_type == 'grading' ?
speed_grader_course_gradebook_url(assignment.context_id, :assignment_id => assignment.id) :
"#{course_assignment_url(assignment.context_id, assignment.id)}#submit"
if todo_type == 'grading'
hash['needs_grading_count'] = Assignments::NeedsGradingCountQuery.new(assignment, user).count
end
when Quizzes::Quiz
quiz = item
hash[:quiz] = quiz_json(quiz, quiz.context, user, session)
hash[:html_url] = course_quiz_url(quiz.context_id, quiz.id)
when WikiPage
wiki_page = item
hash[:wiki_page] = wiki_page_json(wiki_page, user, session)
hash[:html_url] = wiki_page.url
when Announcement
announcement = item
hash[:announcement] = discussion_topic_api_json(announcement, announcement.context, user, session)
hash[:html_url] = announcement.topic_pagination_url
end
end
end
end

70
lib/plannable.rb Normal file
View File

@ -0,0 +1,70 @@
#
# Copyright (C) 2017 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 Plannable
def self.included(base)
base.class_eval do
has_many :planner_overrides, as: :plannable
after_save :update_associated_planner_overrides
before_save :check_if_associated_planner_overrides_need_updating
end
end
def update_associated_planner_overrides_later
send_later(:update_associated_planner_overrides) if @associated_planner_items_need_updating != false
end
def update_associated_planner_overrides
PlannerOverride.update_for(self) if @associated_planner_items_need_updating
end
def check_if_associated_planner_overrides_need_updating
@associated_planner_items_need_updating = false
return if self.new_record?
return if self.respond_to?(:context_type) && !PlannerOverride::CONTENT_TYPES.include?(self.context_type)
@associated_planner_items_need_updating = true if self.respond_to?(:workflow_state_changed?) && self.workflow_state_changed? || self.workflow_state == 'deleted'
end
def visible_in_planner_for?(user)
return true unless planner_enabled?
self.planner_overrides.where(user_id: user,
visible: false,
workflow_state: 'active'
).blank?
end
def planner_override_for(user)
return nil unless planner_enabled?
self.planner_overrides.where(user_id: user).take
end
private
def planner_enabled?
root_account_for_model(self).feature_enabled?(:student_planner)
end
def root_account_for_model(klass)
case klass
when Assignment
klass.context.root_account
else
klass.course.root_account
end
end
end

View File

@ -0,0 +1,106 @@
#
# Copyright (C) 2017 - present 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_relative '../spec_helper'
describe PlannerOverridesController do
before :once do
course_with_teacher(active_all: true)
student_in_course(active_all: true)
@group = @course.assignment_groups.create(:name => "some group")
@assignment = course_assignment
@assignment2 = course_assignment
@planner_override = PlannerOverride.create!(plannable_id: @assignment.id,
plannable_type: "Assignment",
visible: true,
user_id: @student.id)
end
def course_assignment
assignment = @course.assignments.create(
:title => "some assignment #{@course.assignments.count}",
:assignment_group => @group,
:due_at => Time.zone.now + 1.week
)
assignment
end
context "unauthenticated" do
it "should return unauthorized" do
get :index
assert_unauthorized
post :create, :plannable_type => "Assignment",
:plannable_id => @assignment.id,
:visible => true
assert_unauthorized
end
end
context "authenticated" do
context "as student" do
before :each do
user_session(@student)
@course.root_account.enable_feature!(:student_planner)
end
describe "GET #items_index" do
it "returns http success" do
get :items_index
expect(response).to have_http_status(:success)
end
end
describe "GET #index" do
it "returns http success" do
get :index
expect(response).to have_http_status(:success)
end
end
describe "GET #show" do
it "returns http success" do
get :show, override_id: @planner_override.id
expect(response).to have_http_status(:success)
end
end
describe "PUT #update" do
it "returns http success" do
expect(@planner_override.visible).to be_truthy
put :update, override_id: @planner_override.id, visible: false
expect(@planner_override.reload.visible).to be_falsey
end
end
describe "POST #create" do
it "returns http success" do
post :create, plannable_type: "Assignment", plannable_id: @assignment2.id, visible: false
expect(response).to have_http_status(:created)
expect(PlannerOverride.where(user_id: @student.id).count).to be 2
end
end
describe "DELETE #destroy" do
it "returns http success" do
delete :destroy, override_id: @planner_override.id
expect(@planner_override.reload).to be_deleted
end
end
end
end
end

View File

@ -0,0 +1,36 @@
#
# Copyright (C) 2011 - present 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 Factories
def planner_override_model(opts={})
user = opts[:user] || @user || user_model
plannable = opts[:plannable] || assignment_model
visibility = opts.has_key?(:visible) ? opts[:visible] : true
attrs = { user_id: user.id,
plannable_type: plannable.class.to_s,
plannable_id: plannable.id,
visible: visibility }
@planner_override = PlannerOverride.create!(valid_planner_override_attributes.merge(attrs))
end
def valid_planner_override_attributes
{
:visible => true
}
end
end

View File

@ -0,0 +1,96 @@
#
# Copyright (C) 2017 - present 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_relative '../../../spec_helper'
describe Api::V1::PlannerItem do
class PlannerItemHarness
include Api::V1::PlannerItem
def api_v1_users_todo_ignore_url(*args); end
def assignment_json(*args); end
def speed_grader_course_gradebook_url(*args); end
def quiz_json(*args); end
def course_quiz_url(*args); end
def course_assignment_url(*args); end
def wiki_page_json(*args); end
def discussion_topic_api_json(*args); end
end
before :once do
course_factory
@course.root_account.enable_feature!(:student_planner)
teacher_in_course
student_in_course
for_course = { course: @course }
assignment_model for_course
discussion_topic_model for_course
quiz_model for_course
@teacher_override = planner_override_model(plannable: @assignment, user: @teacher)
@student_override = planner_override_model(plannable: @assignment, user: @student, visible: false)
end
describe '.planner_item_json' do
let(:api) { PlannerItemHarness.new }
let(:session) { Object.new }
before :once do
@teacher_hash = api.planner_item_json(@assignment, @teacher, session, 'submitting')
@student_hash = api.planner_item_json(@assignment, @student, session, 'submitting')
@hash = api.planner_item_json(@quiz, @student, session, 'submitting')
end
context 'with an existing planner override' do
it 'should return the planner visibility state' do
expect(@teacher_hash[:visible_in_planner]).to eq true
expect(@student_hash[:visible_in_planner]).to eq false
end
it 'should return the planner override id' do
expect(@teacher_hash[:planner_override].id).to eq @teacher_override.id
expect(@student_hash[:planner_override].id).to eq @student_override.id
end
end
context 'without an existing planner override' do
it 'should return true for `visible_in_planner`' do
expect(@hash[:visible_in_planner]).to eq true
end
it 'should have a nil planner_override value' do
expect(@hash[:planner_override]).to be_nil
end
end
describe 'object types' do
before :once do
@assignment_hash = api.planner_item_json(@assignment, @student, session, 'submitting')
@topic_hash = api.planner_item_json(@topic, @student, session, 'submitting')
@quiz_hash = api.planner_item_json(@quiz, @student, session, 'submitting')
end
it 'should include the respective jsons for the given object type' do
expect(@assignment_hash.has_key?(:assignment)).to be_truthy
expect(@topic_hash.has_key?(:assignment)).to be_truthy
expect(@quiz_hash.has_key?(:quiz)).to be_truthy
end
end
end
end

View File

@ -0,0 +1,131 @@
#
# Copyright (C) 2017 - present 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_relative '../spec_helper'
describe PlannerOverride do
before :once do
course_factory
student_in_course
teacher_in_course
assignment_model course: @course
@student_planner_override = PlannerOverride.create!(user_id: @student.id,
plannable_id: @assignment.id,
plannable_type: "Assignment",
visible: false)
@teacher_planner_override = PlannerOverride.create!(user_id: @teacher.id,
plannable_id: @assignment.id,
plannable_type: "Assignment",
visible: true)
end
describe "::plannable_workflow_state" do
context "respond_to?(:published?)" do
mock_asset = Class.new do
def initialize(opts={})
opts = {published: true, deleted: false}.merge(opts)
@published = opts[:published]
@deleted = opts[:deleted]
end
def published?; !!@published; end
def unpublished?; !@published; end
def deleted?; @deleted; end
end
it "returns 'deleted' for deleted assets" do
a = mock_asset.new(deleted: true)
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'deleted'
end
it "returns 'active' for published assets" do
a = mock_asset.new(published: true)
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'active'
end
it "returns 'unpublished' for unpublished assets" do
a = mock_asset.new(published: false)
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'unpublished'
end
end
context "respond_to?(:workflow_state)" do
mock_asset = Class.new do
attr_reader :workflow_state
def initialize(workflow_state)
@workflow_state = workflow_state
end
end
it "returns 'active' for 'active' workflow_state" do
a = mock_asset.new('active')
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'active'
end
it "returns 'active' for 'available' workflow_state" do
a = mock_asset.new('available')
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'active'
end
it "returns 'active' for 'published' workflow_state" do
a = mock_asset.new('published')
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'active'
end
it "returns 'unpublished' for 'unpublished' workflow_state" do
a = mock_asset.new('unpublished')
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'unpublished'
end
it "returns 'deleted' for 'deleted' workflow_state" do
a = mock_asset.new('deleted')
expect(PlannerOverride.plannable_workflow_state(a)).to eq 'deleted'
end
it "returns nil for other workflow_state" do
a = mock_asset.new('terrified')
expect(PlannerOverride.plannable_workflow_state(a)).to eq nil
end
end
end
describe "#for_user" do
it "should return all PlannerOverrides for specified user" do
student_overrides = PlannerOverride.for_user(@student)
expect(student_overrides.count).to eq 1
expect(student_overrides.first.user_id).to eq @student.id
teacher_overrides = PlannerOverride.for_user(@teacher)
expect(teacher_overrides.count).to eq 1
expect(teacher_overrides.first.user_id).to eq @teacher.id
end
end
describe "#update_for" do
it "should update the PlannerOverride for the given object" do
overrides = PlannerOverride.where(plannable_id: @assignment.id)
expect(overrides.all? { |o| o.workflow_state == 'active' }).to be_truthy
@assignment.destroy
PlannerOverride.update_for(@assignment.reload)
expect(overrides.reload.all? { |o| o.workflow_state == 'deleted' }).to be_truthy
end
end
end