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:
parent
e2ebcf2c4e
commit
1f4132e1ba
|
@ -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
|
|
@ -22,6 +22,7 @@ class Announcement < DiscussionTopic
|
|||
|
||||
has_a_broadcast_policy
|
||||
include HasContentTags
|
||||
include Plannable
|
||||
|
||||
sanitize_field :message, CanvasSanitize::SANITIZE
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,6 +31,7 @@ class WikiPage < ActiveRecord::Base
|
|||
include CopyAuthorizedLinks
|
||||
include ContextModuleItem
|
||||
include Submittable
|
||||
include Plannable
|
||||
|
||||
include SearchTermHelper
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue