1198 lines
48 KiB
Ruby
1198 lines
48 KiB
Ruby
#
|
|
# 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/>.
|
|
|
|
require 'atom'
|
|
|
|
# @API Discussion Topics
|
|
#
|
|
# API for accessing and participating in discussion topics in groups and courses.
|
|
#
|
|
# @model FileAttachment
|
|
# {
|
|
# "id": "FileAttachment",
|
|
# "description": "A file attachment",
|
|
# "properties": {
|
|
# "content-type": {
|
|
# "example": "unknown/unknown",
|
|
# "type": "string"
|
|
# },
|
|
# "url": {
|
|
# "example": "http://www.example.com/courses/1/files/1/download",
|
|
# "type": "string"
|
|
# },
|
|
# "filename": {
|
|
# "example": "content.txt",
|
|
# "type": "string"
|
|
# },
|
|
# "display_name": {
|
|
# "example": "content.txt",
|
|
# "type": "string"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# @model DiscussionTopic
|
|
# {
|
|
# "id": "DiscussionTopic",
|
|
# "description": "A discussion topic",
|
|
# "properties": {
|
|
# "id": {
|
|
# "description": "The ID of this topic.",
|
|
# "example": 1,
|
|
# "type": "integer"
|
|
# },
|
|
# "title": {
|
|
# "description": "The topic title.",
|
|
# "example": "Topic 1",
|
|
# "type": "string"
|
|
# },
|
|
# "message": {
|
|
# "description": "The HTML content of the message body.",
|
|
# "example": "<p>content here</p>",
|
|
# "type": "string"
|
|
# },
|
|
# "html_url": {
|
|
# "description": "The URL to the discussion topic in canvas.",
|
|
# "example": "https://<canvas>/courses/1/discussion_topics/2",
|
|
# "type": "string"
|
|
# },
|
|
# "posted_at": {
|
|
# "description": "The datetime the topic was posted. If it is null it hasn't been posted yet. (see delayed_post_at)",
|
|
# "example": "2037-07-21T13:29:31Z",
|
|
# "type": "datetime"
|
|
# },
|
|
# "last_reply_at": {
|
|
# "description": "The datetime for when the last reply was in the topic.",
|
|
# "example": "2037-07-28T19:38:31Z",
|
|
# "type": "datetime"
|
|
# },
|
|
# "require_initial_post": {
|
|
# "description": "If true then a user may not respond to other replies until that user has made an initial reply. Defaults to false.",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "user_can_see_posts": {
|
|
# "description": "Whether or not posts in this topic are visible to the user.",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "discussion_subentry_count": {
|
|
# "description": "The count of entries in the topic.",
|
|
# "example": 0,
|
|
# "type": "integer"
|
|
# },
|
|
# "read_state": {
|
|
# "description": "The read_state of the topic for the current user, 'read' or 'unread'.",
|
|
# "example": "read",
|
|
# "type": "string",
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "read",
|
|
# "unread"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "unread_count": {
|
|
# "description": "The count of unread entries of this topic for the current user.",
|
|
# "example": 0,
|
|
# "type": "integer"
|
|
# },
|
|
# "subscribed": {
|
|
# "description": "Whether or not the current user is subscribed to this topic.",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "subscription_hold": {
|
|
# "description": "(Optional) Why the user cannot subscribe to this topic. Only one reason will be returned even if multiple apply. Can be one of: 'initial_post_required': The user must post a reply first; 'not_in_group_set': The user is not in the group set for this graded group discussion; 'not_in_group': The user is not in this topic's group; 'topic_is_announcement': This topic is an announcement",
|
|
# "example": "not_in_group_set",
|
|
# "type": "string",
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "initial_post_required",
|
|
# "not_in_group_set",
|
|
# "not_in_group",
|
|
# "topic_is_announcement"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "assignment_id": {
|
|
# "description": "The unique identifier of the assignment if the topic is for grading, otherwise null.",
|
|
# "type": "integer"
|
|
# },
|
|
# "delayed_post_at": {
|
|
# "description": "The datetime to publish the topic (if not right away).",
|
|
# "type": "datetime"
|
|
# },
|
|
# "published": {
|
|
# "description": "Whether this discussion topic is published (true) or draft state (false)",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "lock_at": {
|
|
# "description": "The datetime to lock the topic (if ever).",
|
|
# "type": "datetime"
|
|
# },
|
|
# "locked": {
|
|
# "description": "Whether or not the discussion is 'closed for comments'.",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "pinned": {
|
|
# "description": "Whether or not the discussion has been 'pinned' by an instructor",
|
|
# "example": false,
|
|
# "type": "boolean"
|
|
# },
|
|
# "locked_for_user": {
|
|
# "description": "Whether or not this is locked for the user.",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "lock_info": {
|
|
# "description": "(Optional) Information for the user about the lock. Present when locked_for_user is true.",
|
|
# "$ref": "LockInfo"
|
|
# },
|
|
# "lock_explanation": {
|
|
# "description": "(Optional) An explanation of why this is locked for the user. Present when locked_for_user is true.",
|
|
# "example": "This discussion is locked until September 1 at 12:00am",
|
|
# "type": "string"
|
|
# },
|
|
# "user_name": {
|
|
# "description": "The username of the topic creator.",
|
|
# "example": "User Name",
|
|
# "type": "string"
|
|
# },
|
|
# "topic_children": {
|
|
# "description": "An array of topic_ids for the group discussions the user is a part of.",
|
|
# "example": [5, 7, 10],
|
|
# "type": "array",
|
|
# "items": { "type": "integer"}
|
|
# },
|
|
# "root_topic_id": {
|
|
# "description": "If the topic is for grading and a group assignment this will point to the original topic in the course.",
|
|
# "type": "integer"
|
|
# },
|
|
# "podcast_url": {
|
|
# "description": "If the topic is a podcast topic this is the feed url for the current user.",
|
|
# "example": "/feeds/topics/1/enrollment_1XAcepje4u228rt4mi7Z1oFbRpn3RAkTzuXIGOPe.rss",
|
|
# "type": "string"
|
|
# },
|
|
# "discussion_type": {
|
|
# "description": "The type of discussion. Values are 'side_comment', for discussions that only allow one level of nested comments, and 'threaded' for fully threaded discussions.",
|
|
# "example": "side_comment",
|
|
# "type": "string",
|
|
# "allowableValues": {
|
|
# "values": [
|
|
# "side_comment",
|
|
# "threaded"
|
|
# ]
|
|
# }
|
|
# },
|
|
# "group_category_id": {
|
|
# "description": "The unique identifier of the group category if the topic is a group discussion, otherwise null.",
|
|
# "type": "integer"
|
|
# },
|
|
# "attachments": {
|
|
# "description": "Array of file attachments.",
|
|
# "type": "array",
|
|
# "items": { "$ref": "FileAttachment" }
|
|
# },
|
|
# "permissions": {
|
|
# "description": "The current user's permissions on this topic.",
|
|
# "example": {"attach": true},
|
|
# "type": "object",
|
|
# "key": { "type": "string" },
|
|
# "value": { "type": "boolean" }
|
|
# },
|
|
# "allow_rating": {
|
|
# "description": "Whether or not users can rate entries in this topic.",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "only_graders_can_rate": {
|
|
# "description": "Whether or not grade permissions are required to rate entries.",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# },
|
|
# "sort_by_rating": {
|
|
# "description": "Whether or not entries should be sorted by rating.",
|
|
# "example": true,
|
|
# "type": "boolean"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
class DiscussionTopicsController < ApplicationController
|
|
before_action :require_context_and_read_access, :except => :public_feed
|
|
before_action :rich_content_service_config
|
|
|
|
include Api::V1::DiscussionTopics
|
|
include Api::V1::Assignment
|
|
include Api::V1::AssignmentOverride
|
|
include KalturaHelper
|
|
include SubmittableHelper
|
|
|
|
# @API List discussion topics
|
|
#
|
|
# Returns the paginated list of discussion topics for this course or group.
|
|
#
|
|
# @argument include[] [String, "all_dates"]
|
|
# If "all_dates" is passed, all dates associated with graded discussions'
|
|
# assignments will be included.
|
|
#
|
|
# @argument order_by [String, "position"|"recent_activity"]
|
|
# Determines the order of the discussion topic list. Defaults to "position".
|
|
#
|
|
# @argument scope [String, "locked"|"unlocked"|"pinned"|"unpinned"]
|
|
# Only return discussion topics in the given state(s). Defaults to including
|
|
# all topics. Filtering is done after pagination, so pages
|
|
# may be smaller than requested if topics are filtered.
|
|
# Can pass multiple states as comma separated string.
|
|
#
|
|
# @argument only_announcements [Boolean]
|
|
# Return announcements instead of discussion topics. Defaults to false
|
|
#
|
|
# @argument search_term [String]
|
|
# The partial title of the discussion topics to match and return.
|
|
#
|
|
# @argument exclude_context_module_locked_topics [Boolean]
|
|
# For students, exclude topics that are locked by module progression.
|
|
# Defaults to false.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
# @returns [DiscussionTopic]
|
|
def index
|
|
include_params = Array(params[:include])
|
|
|
|
if params[:only_announcements]
|
|
return unless authorized_action(@context.announcements.temp_record, @current_user, :read)
|
|
else
|
|
return unless authorized_action(@context.discussion_topics.temp_record, @current_user, :read)
|
|
end
|
|
|
|
return child_topic if is_child_topic?
|
|
|
|
scope = if params[:only_announcements]
|
|
@context.active_announcements
|
|
else
|
|
@context.active_discussion_topics.only_discussion_topics
|
|
end
|
|
|
|
# Specify the shard context, because downstream we use `union` which isn't
|
|
# cross-shard compatible.
|
|
@context.shard.activate do
|
|
scope = DiscussionTopic::ScopedToUser.new(@context, @current_user, scope).scope
|
|
end
|
|
|
|
scope = if params[:order_by] == 'recent_activity'
|
|
scope.by_last_reply_at
|
|
elsif params[:only_announcements]
|
|
scope.by_posted_at
|
|
else
|
|
scope.by_position_legacy
|
|
end
|
|
|
|
scope = DiscussionTopic.search_by_attribute(scope, :title, params[:search_term])
|
|
|
|
states = params[:scope].split(',').map{|s| s.strip} if params[:scope]
|
|
if states.present?
|
|
if (states.include?('pinned') && states.include?('unpinned')) ||
|
|
(states.include?('locked') && states.include?('unlocked'))
|
|
render json: {errors: {scope: "scope is contradictory"}}, :status => :bad_request
|
|
return
|
|
end
|
|
|
|
if states.include?('pinned')
|
|
scope = scope.where(:pinned => true)
|
|
elsif states.include?('unpinned')
|
|
scope = scope.where("discussion_topics.pinned IS NOT TRUE")
|
|
end
|
|
end
|
|
|
|
@topics = Api.paginate(scope, self, topic_pagination_url)
|
|
|
|
if params[:exclude_context_module_locked_topics]
|
|
@topics = DiscussionTopic.reject_context_module_locked_topics(@topics, @current_user)
|
|
end
|
|
|
|
if states.present?
|
|
@topics.reject! { |t| t.locked_for?(@current_user) } if states.include?('unlocked')
|
|
@topics.select! { |t| t.locked_for?(@current_user) } if states.include?('locked')
|
|
end
|
|
@topics.each { |topic| topic.current_user = @current_user }
|
|
|
|
respond_to do |format|
|
|
format.html do
|
|
log_asset_access([ "topics", @context ], 'topics', 'other')
|
|
|
|
@active_tab = 'discussions'
|
|
add_crumb(t('#crumbs.discussions', 'Discussions'),
|
|
named_context_url(@context, :context_discussion_topics_url))
|
|
|
|
locked_topics, open_topics = @topics.partition do |topic|
|
|
locked = topic.locked? || topic.locked_for?(@current_user)
|
|
locked.is_a?(Hash) ? locked[:can_view] : locked
|
|
end
|
|
|
|
hash = {
|
|
USER_SETTINGS_URL: api_v1_user_settings_url(@current_user),
|
|
openTopics: open_topics,
|
|
lockedTopics: locked_topics,
|
|
newTopicURL: named_context_url(@context, :new_context_discussion_topic_url),
|
|
permissions: {
|
|
create: @context.discussion_topics.temp_record.grants_right?(@current_user, session, :create),
|
|
moderate: user_can_moderate,
|
|
change_settings: user_can_edit_course_settings?,
|
|
manage_content: @context.grants_right?(@current_user, session, :manage_content),
|
|
publish: user_can_moderate
|
|
},
|
|
discussion_topic_menu_tools: external_tools_display_hashes(:discussion_topic_menu),
|
|
}
|
|
if @context.is_a?(Course) && @context.grants_right?(@current_user, session, :read) && !@js_env[:COURSE_ID].present?
|
|
hash[:COURSE_ID] = @context.id.to_s
|
|
end
|
|
conditional_release_js_env(includes: :active_rules)
|
|
append_sis_data(hash)
|
|
js_env(hash)
|
|
set_tutorial_js_env
|
|
|
|
if user_can_edit_course_settings?
|
|
js_env(SETTINGS_URL: named_context_url(@context, :api_v1_context_settings_url))
|
|
end
|
|
end
|
|
format.json do
|
|
if @context.grants_right?(@current_user, session, :moderate_forum)
|
|
mc_status = setup_master_course_restrictions(@topics, @context)
|
|
end
|
|
|
|
render json: discussion_topics_api_json(@topics, @context, @current_user, session,
|
|
user_can_moderate: user_can_moderate,
|
|
plain_messages: value_to_boolean(params[:plain_messages]),
|
|
exclude_assignment_description: value_to_boolean(params[:exclude_assignment_descriptions]),
|
|
include_all_dates: include_params.include?('all_dates'),
|
|
master_course_status: mc_status
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def is_child_topic?
|
|
root_topic_id = params[:root_discussion_topic_id]
|
|
|
|
root_topic_id && @context.respond_to?(:context) &&
|
|
@context.context && @context.context.discussion_topics.find(root_topic_id)
|
|
end
|
|
|
|
def new
|
|
@topic = @context.send(params[:is_announcement] ? :announcements : :discussion_topics).new
|
|
add_discussion_or_announcement_crumb
|
|
add_crumb t :create_new_crumb, "Create new"
|
|
edit
|
|
end
|
|
|
|
def edit
|
|
@topic ||= @context.all_discussion_topics.find(params[:id])
|
|
if authorized_action(@topic, @current_user, (@topic.new_record? ? :create : :update))
|
|
hash = {
|
|
URL_ROOT: named_context_url(@context, :api_v1_context_discussion_topics_url),
|
|
PERMISSIONS: {
|
|
CAN_CREATE_ASSIGNMENT: @context.respond_to?(:assignments) && @context.assignments.temp_record.grants_right?(@current_user, session, :create),
|
|
CAN_ATTACH: @topic.grants_right?(@current_user, session, :attach),
|
|
CAN_MODERATE: user_can_moderate
|
|
}
|
|
}
|
|
|
|
unless @topic.new_record?
|
|
add_discussion_or_announcement_crumb
|
|
add_crumb(@topic.title, named_context_url(@context, :context_discussion_topic_url, @topic.id))
|
|
add_crumb t :edit_crumb, "Edit"
|
|
hash[:ATTRIBUTES] = discussion_topic_api_json(@topic, @context, @current_user, session, override_dates: false)
|
|
end
|
|
(hash[:ATTRIBUTES] ||= {})[:is_announcement] = @topic.is_announcement
|
|
hash[:ATTRIBUTES][:can_group] = @topic.can_group?
|
|
handle_assignment_edit_params(hash[:ATTRIBUTES])
|
|
|
|
categories = @context.respond_to?(:group_categories) ? @context.group_categories : []
|
|
# if discussion has entries and is attached to a deleted group category,
|
|
# add that category to the ENV list so it will be shown on the edit page.
|
|
if @topic.group_category_deleted_with_entries?
|
|
categories << @topic.group_category
|
|
end
|
|
|
|
if @topic.assignment.present?
|
|
hash[:ATTRIBUTES][:assignment][:assignment_overrides] =
|
|
(assignment_overrides_json(
|
|
@topic.assignment.overrides_for(@current_user, ensure_set_not_empty: true)
|
|
))
|
|
hash[:ATTRIBUTES][:assignment][:has_student_submissions] = @topic.assignment.has_student_submissions?
|
|
end
|
|
|
|
sections = @context.respond_to?(:course_sections) ? @context.course_sections.active : []
|
|
|
|
js_hash = {
|
|
CONTEXT_ACTION_SOURCE: :discussion_topic,
|
|
CONTEXT_ID: @context.id,
|
|
DISCUSSION_TOPIC: hash,
|
|
GROUP_CATEGORIES: categories.
|
|
reject(&:student_organized?).
|
|
map { |category| { id: category.id, name: category.name } },
|
|
HAS_GRADING_PERIODS: @context.grading_periods?,
|
|
SECTION_LIST: sections.map { |section| { id: section.id, name: section.name } }
|
|
}
|
|
|
|
post_to_sis = Assignment.sis_grade_export_enabled?(@context)
|
|
js_hash[:POST_TO_SIS] = post_to_sis
|
|
if post_to_sis && @topic.new_record?
|
|
js_hash[:POST_TO_SIS_DEFAULT] = @context.account.sis_default_grade_export[:value]
|
|
end
|
|
if @context.root_account.feature_enabled?(:student_planner)
|
|
js_hash[:STUDENT_PLANNER_ENABLED] = @context.grants_any_right?(@current_user, session, :manage)
|
|
end
|
|
|
|
js_hash[:MAX_NAME_LENGTH_REQUIRED_FOR_ACCOUNT] = AssignmentUtil.name_length_required_for_account?(@context)
|
|
js_hash[:MAX_NAME_LENGTH] = AssignmentUtil.assignment_max_name_length(@context)
|
|
js_hash[:DUE_DATE_REQUIRED_FOR_ACCOUNT] = AssignmentUtil.due_date_required_for_account?(@context)
|
|
js_hash[:SIS_NAME] = AssignmentUtil.post_to_sis_friendly_name(@context)
|
|
|
|
if @context.is_a?(Course)
|
|
js_hash['SECTION_LIST'] = sections.map { |section|
|
|
{
|
|
id: section.id,
|
|
name: section.name,
|
|
start_at: section.start_at,
|
|
end_at: section.end_at,
|
|
override_course_and_term_dates: section.restrict_enrollments_to_section_dates
|
|
}
|
|
}
|
|
js_hash['VALID_DATE_RANGE'] = CourseDateRange.new(@context)
|
|
end
|
|
js_hash[:CANCEL_TO] = cancel_redirect_url
|
|
append_sis_data(js_hash)
|
|
|
|
if @context.grading_periods?
|
|
gp_context = @context.is_a?(Group) ? @context.context : @context
|
|
js_hash[:active_grading_periods] = GradingPeriod.json_for(gp_context, @current_user)
|
|
end
|
|
if context.is_a?(Course)
|
|
js_hash[:allow_self_signup] = true # for group creation
|
|
js_hash[:group_user_type] = 'student'
|
|
end
|
|
js_env(js_hash)
|
|
|
|
set_master_course_js_env_data(@topic, @context)
|
|
conditional_release_js_env(@topic.assignment)
|
|
|
|
render :edit
|
|
end
|
|
end
|
|
|
|
def show
|
|
parent_id = params[:parent_id]
|
|
@topic = @context.all_discussion_topics.find(params[:id])
|
|
@presenter = DiscussionTopicPresenter.new(@topic, @current_user)
|
|
@assignment = if @topic.for_assignment?
|
|
AssignmentOverrideApplicator.assignment_overridden_for(@topic.assignment, @current_user)
|
|
else
|
|
nil
|
|
end
|
|
@context.require_assignment_group rescue nil
|
|
add_discussion_or_announcement_crumb
|
|
add_crumb(@topic.title, named_context_url(@context, :context_discussion_topic_url, @topic.id))
|
|
if @topic.deleted?
|
|
flash[:notice] = t :deleted_topic_notice, "That topic has been deleted"
|
|
redirect_to named_context_url(@context, :context_discussion_topics_url)
|
|
return
|
|
end
|
|
|
|
unless @topic.grants_right?(@current_user, session, :read)
|
|
return render_unauthorized_action unless @current_user
|
|
respond_to do |format|
|
|
flash[:error] = t 'You do not have access to the requested discussion.'
|
|
format.html { redirect_to named_context_url(@context, :context_discussion_topics_url) }
|
|
end
|
|
else
|
|
@headers = !params[:headless]
|
|
# we still need the lock info even if the current user policies unlock the topic. check the policies manually later if you need to override the lockout.
|
|
@locked = @topic.locked_for?(@current_user, :check_policies => false, :deep_check_if_needed => true)
|
|
@unlock_at = @topic.available_from_for(@current_user)
|
|
@topic.change_read_state('read', @current_user) unless @locked.is_a?(Hash) && !@locked[:can_view]
|
|
if @topic.for_group_discussion?
|
|
|
|
group_scope = @topic.group_category.groups.active
|
|
if @topic.for_assignment? && @topic.assignment.only_visible_to_overrides?
|
|
@groups = group_scope.where(:id => @topic.assignment.assignment_overrides.active.where(:set_type => "Group").pluck(:set_id)).to_a
|
|
if @groups.empty?
|
|
@groups = group_scope.to_a # revert to default if we're not using Group overrides
|
|
end
|
|
else
|
|
@groups = group_scope.to_a
|
|
end
|
|
@groups.select!{ |g| g.grants_any_right?(@current_user, session, :post_to_forum, :read_as_admin) }
|
|
@groups.sort_by!(&:id)
|
|
|
|
topics = @topic.child_topics.to_a
|
|
topics = topics.select{|t| @groups.include?(t.context) } unless @topic.grants_right?(@current_user, session, :update)
|
|
@group_topics = @groups.map do |group|
|
|
{:group => group, :topic => topics.find{|t| t.context == group} }
|
|
end
|
|
end
|
|
|
|
@initial_post_required = @topic.initial_post_required?(@current_user, session)
|
|
|
|
@padless = true
|
|
|
|
log_asset_access(@topic, 'topics', 'topics')
|
|
respond_to do |format|
|
|
if topics && topics.length == 1 && !@topic.grants_right?(@current_user, session, :update)
|
|
format.html { redirect_to named_context_url(topics[0].context, :context_discussion_topics_url, :root_discussion_topic_id => @topic.id) }
|
|
else
|
|
format.html do
|
|
|
|
@discussion_topic_menu_tools = external_tools_display_hashes(:discussion_topic_menu)
|
|
@context_module_tag = ContextModuleItem.find_tag_with_preferred([@topic, @topic.root_topic, @topic.assignment], params[:module_item_id])
|
|
@sequence_asset = @context_module_tag.try(:content)
|
|
|
|
api_url = lambda do |endpoint, *params|
|
|
endpoint = "api_v1_context_discussion_#{endpoint}_url"
|
|
named_context_url(@context, endpoint, @topic, *params)
|
|
end
|
|
|
|
env_hash = {
|
|
:APP_URL => named_context_url(@context, :context_discussion_topic_url, @topic),
|
|
:TOPIC => {
|
|
:ID => @topic.id,
|
|
:IS_SUBSCRIBED => @topic.subscribed?(@current_user),
|
|
:IS_PUBLISHED => @topic.published?,
|
|
:CAN_UNPUBLISH => @topic.can_unpublish?,
|
|
},
|
|
:PERMISSIONS => {
|
|
# Can reply
|
|
:CAN_REPLY => @topic.grants_right?(@current_user, session, :reply),
|
|
# Can attach files on replies
|
|
:CAN_ATTACH => @topic.grants_right?(@current_user, session, :attach),
|
|
:CAN_RATE => @topic.grants_right?(@current_user, session, :rate),
|
|
:CAN_READ_REPLIES => @topic.grants_right?(@current_user, :read_replies),
|
|
# Can moderate their own topics
|
|
:CAN_MANAGE_OWN => @context.user_can_manage_own_discussion_posts?(@current_user) &&
|
|
!@topic.locked_for?(@current_user, :check_policies => true),
|
|
# Can moderate any topic
|
|
:MODERATE => user_can_moderate
|
|
},
|
|
:ROOT_URL => api_url.call('topic_view'),
|
|
:ENTRY_ROOT_URL => api_url.call('topic_entry_list'),
|
|
:REPLY_URL => api_url.call('add_reply', ':entry_id'),
|
|
:ROOT_REPLY_URL => api_url.call('add_entry'),
|
|
:DELETE_URL => api_url.call('delete_reply', ':id'),
|
|
:UPDATE_URL => api_url.call('update_reply', ':id'),
|
|
:MARK_READ_URL => api_url.call('topic_discussion_entry_mark_read', ':id'),
|
|
:MARK_UNREAD_URL => api_url.call('topic_discussion_entry_mark_unread', ':id'),
|
|
:RATE_URL => api_url.call('topic_discussion_entry_rate', ':id'),
|
|
:MARK_ALL_READ_URL => api_url.call('topic_mark_all_read'),
|
|
:MARK_ALL_UNREAD_URL => api_url.call('topic_mark_all_unread'),
|
|
:MANUAL_MARK_AS_READ => @current_user.try(:manual_mark_as_read?),
|
|
:CAN_SUBSCRIBE => !@topic.subscription_hold(@current_user, @context_enrollment, session),
|
|
:CURRENT_USER => user_display_json(@current_user),
|
|
:INITIAL_POST_REQUIRED => @initial_post_required,
|
|
:THREADED => @topic.threaded?,
|
|
:ALLOW_RATING => @topic.allow_rating,
|
|
:SORT_BY_RATING => @topic.sort_by_rating,
|
|
:TODO_DATE => @topic.todo_date
|
|
}
|
|
if params[:hide_student_names]
|
|
env_hash[:HIDE_STUDENT_NAMES] = true
|
|
env_hash[:STUDENT_ID] = params[:student_id]
|
|
end
|
|
if @sequence_asset
|
|
env_hash[:SEQUENCE] = {
|
|
:ASSET_TYPE => @sequence_asset.is_a?(Assignment) ? 'Assignment' : 'Discussion',
|
|
:ASSET_ID => @sequence_asset.id,
|
|
:COURSE_ID => @sequence_asset.context.id,
|
|
}
|
|
end
|
|
if @topic.for_assignment? &&
|
|
@topic.assignment.grants_right?(@current_user, session, :grade) && @presenter.allows_speed_grader?
|
|
env_hash[:SPEEDGRADER_URL_TEMPLATE] = named_context_url(@topic.assignment.context,
|
|
:speed_grader_context_gradebook_url,
|
|
:assignment_id => @topic.assignment.id,
|
|
:anchor => {:student_id => ":student_id"}.to_json)
|
|
end
|
|
|
|
js_hash = {:DISCUSSION => env_hash}
|
|
js_hash[:COURSE_ID] = @context.id if @context.is_a?(Course)
|
|
js_hash[:CONTEXT_ACTION_SOURCE] = :discussion_topic
|
|
js_hash[:STUDENT_CONTEXT_CARDS_ENABLED] = @context.is_a?(Course) &&
|
|
@domain_root_account.feature_enabled?(:student_context_cards) &&
|
|
@context.grants_right?(@current_user, session, :manage)
|
|
|
|
append_sis_data(js_hash)
|
|
js_env(js_hash)
|
|
set_master_course_js_env_data(@topic, @context)
|
|
conditional_release_js_env(@topic.assignment, includes: [:rule])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# @API Create a new discussion topic
|
|
#
|
|
# Create an new discussion topic for the course or group.
|
|
#
|
|
# @argument title [String]
|
|
#
|
|
# @argument message [String]
|
|
#
|
|
# @argument discussion_type [String, "side_comment"|"threaded"]
|
|
# The type of discussion. Defaults to side_comment if not value is given. Accepted values are 'side_comment', for discussions that only allow one level of nested comments, and 'threaded' for fully threaded discussions.
|
|
#
|
|
# @argument published [Boolean]
|
|
# Whether this topic is published (true) or draft state (false). Only
|
|
# teachers and TAs have the ability to create draft state topics.
|
|
#
|
|
# @argument delayed_post_at [DateTime]
|
|
# If a timestamp is given, the topic will not be published until that time.
|
|
#
|
|
# @argument lock_at [DateTime]
|
|
# If a timestamp is given, the topic will be scheduled to lock at the
|
|
# provided timestamp. If the timestamp is in the past, the topic will be
|
|
# locked.
|
|
#
|
|
# @argument podcast_enabled [Boolean]
|
|
# If true, the topic will have an associated podcast feed.
|
|
#
|
|
# @argument podcast_has_student_posts [Boolean]
|
|
# If true, the podcast will include posts from students as well. Implies
|
|
# podcast_enabled.
|
|
#
|
|
# @argument require_initial_post [Boolean]
|
|
# If true then a user may not respond to other replies until that user has
|
|
# made an initial reply. Defaults to false.
|
|
#
|
|
# @argument assignment [Assignment]
|
|
# To create an assignment discussion, pass the assignment parameters as a
|
|
# sub-object. See the {api:AssignmentsApiController#create Create an Assignment API}
|
|
# for the available parameters. The name parameter will be ignored, as it's
|
|
# taken from the discussion title. If you want to make a discussion that was
|
|
# an assignment NOT an assignment, pass set_assignment = false as part of
|
|
# the assignment object
|
|
#
|
|
# @argument is_announcement [Boolean]
|
|
# If true, this topic is an announcement. It will appear in the
|
|
# announcement's section rather than the discussions section. This requires
|
|
# announcment-posting permissions.
|
|
#
|
|
# @argument pinned [Boolean]
|
|
# If true, this topic will be listed in the "Pinned Discussion" section
|
|
#
|
|
# @argument position_after [String]
|
|
# By default, discussions are sorted chronologically by creation date, you
|
|
# can pass the id of another topic to have this one show up after the other
|
|
# when they are listed.
|
|
#
|
|
# @argument group_category_id [Integer]
|
|
# If present, the topic will become a group discussion assigned
|
|
# to the group.
|
|
#
|
|
# @argument allow_rating [Boolean]
|
|
# If true, users will be allowed to rate entries.
|
|
#
|
|
# @argument only_graders_can_rate [Boolean]
|
|
# If true, only graders will be allowed to rate entries.
|
|
#
|
|
# @argument sort_by_rating [Boolean]
|
|
# If true, entries will be sorted by rating.
|
|
#
|
|
# @argument attachment [File]
|
|
# A multipart/form-data form-field-style attachment.
|
|
# Attachments larger than 1 kilobyte are subject to quota restrictions.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics \
|
|
# -F title='my topic' \
|
|
# -F message='initial message' \
|
|
# -F podcast_enabled=1 \
|
|
# -H 'Authorization: Bearer <token>'
|
|
# -F 'attachment=@<filename>' \
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics \
|
|
# -F title='my assignment topic' \
|
|
# -F message='initial message' \
|
|
# -F assignment[points_possible]=15 \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
def create
|
|
process_discussion_topic(!!:is_new)
|
|
end
|
|
|
|
# @API Update a topic
|
|
#
|
|
# Update an existing discussion topic for the course or group.
|
|
#
|
|
# @argument title [String]
|
|
#
|
|
# @argument message [String]
|
|
#
|
|
# @argument discussion_type [String, "side_comment"|"threaded"]
|
|
# The type of discussion. Defaults to side_comment if not value is given. Accepted values are 'side_comment', for discussions that only allow one level of nested comments, and 'threaded' for fully threaded discussions.
|
|
#
|
|
# @argument published [Boolean]
|
|
# Whether this topic is published (true) or draft state (false). Only
|
|
# teachers and TAs have the ability to create draft state topics.
|
|
#
|
|
# @argument delayed_post_at [DateTime]
|
|
# If a timestamp is given, the topic will not be published until that time.
|
|
#
|
|
# @argument lock_at [DateTime]
|
|
# If a timestamp is given, the topic will be scheduled to lock at the
|
|
# provided timestamp. If the timestamp is in the past, the topic will be
|
|
# locked.
|
|
#
|
|
# @argument podcast_enabled [Boolean]
|
|
# If true, the topic will have an associated podcast feed.
|
|
#
|
|
# @argument podcast_has_student_posts [Boolean]
|
|
# If true, the podcast will include posts from students as well. Implies
|
|
# podcast_enabled.
|
|
#
|
|
# @argument require_initial_post [Boolean]
|
|
# If true then a user may not respond to other replies until that user has
|
|
# made an initial reply. Defaults to false.
|
|
#
|
|
# @argument assignment [Assignment]
|
|
# To create an assignment discussion, pass the assignment parameters as a
|
|
# sub-object. See the {api:AssignmentsApiController#create Create an Assignment API}
|
|
# for the available parameters. The name parameter will be ignored, as it's
|
|
# taken from the discussion title. If you want to make a discussion that was
|
|
# an assignment NOT an assignment, pass set_assignment = false as part of
|
|
# the assignment object
|
|
#
|
|
# @argument is_announcement [Boolean]
|
|
# If true, this topic is an announcement. It will appear in the
|
|
# announcement's section rather than the discussions section. This requires
|
|
# announcment-posting permissions.
|
|
#
|
|
# @argument pinned [Boolean]
|
|
# If true, this topic will be listed in the "Pinned Discussion" section
|
|
#
|
|
# @argument position_after [String]
|
|
# By default, discussions are sorted chronologically by creation date, you
|
|
# can pass the id of another topic to have this one show up after the other
|
|
# when they are listed.
|
|
#
|
|
# @argument group_category_id [Integer]
|
|
# If present, the topic will become a group discussion assigned
|
|
# to the group.
|
|
#
|
|
# @argument allow_rating [Boolean]
|
|
# If true, users will be allowed to rate entries.
|
|
#
|
|
# @argument only_graders_can_rate [Boolean]
|
|
# If true, only graders will be allowed to rate entries.
|
|
#
|
|
# @argument sort_by_rating [Boolean]
|
|
# If true, entries will be sorted by rating.
|
|
#
|
|
# @example_request
|
|
# curl https://<canvas>/api/v1/courses/<course_id>/discussion_topics/<topic_id> \
|
|
# -F title='This will be positioned after Topic #1234' \
|
|
# -F position_after=1234 \
|
|
# -H 'Authorization: Bearer <token>'
|
|
#
|
|
def update
|
|
process_discussion_topic(!:is_new)
|
|
end
|
|
|
|
# @API Delete a topic
|
|
#
|
|
# Deletes the discussion topic. This will also delete the assignment, if it's
|
|
# an assignment discussion.
|
|
#
|
|
# @example_request
|
|
# curl -X DELETE https://<canvas>/api/v1/courses/<course_id>/discussion_topics/<topic_id> \
|
|
# -H 'Authorization: Bearer <token>'
|
|
def destroy
|
|
@topic = @context.all_discussion_topics.find(params[:id] || params[:topic_id])
|
|
if authorized_action(@topic, @current_user, :delete)
|
|
return render_unauthorized_action if editing_restricted?(@topic)
|
|
@topic.destroy
|
|
respond_to do |format|
|
|
format.html {
|
|
flash[:notice] = t :topic_deleted_notice, "%{topic_title} deleted successfully", :topic_title => @topic.title
|
|
redirect_to named_context_url(@context, @topic.is_announcement ? :context_announcements_url : :context_discussion_topics_url)
|
|
}
|
|
format.json { render :json => @topic.as_json(:include => {:user => {:only => :name} } ), :status => :ok }
|
|
end
|
|
end
|
|
end
|
|
|
|
def public_feed
|
|
return unless get_feed_context
|
|
|
|
feed = Atom::Feed.new do |f|
|
|
f.title = t :discussion_feed_title, "%{title} Discussion Feed", :title => @context.name
|
|
f.links << Atom::Link.new(:href => polymorphic_url([@context, :discussion_topics]), :rel => 'self')
|
|
f.updated = Time.now
|
|
f.id = polymorphic_url([@context, :discussion_topics])
|
|
end
|
|
@entries = []
|
|
@entries.concat @context.discussion_topics.
|
|
select{|dt| dt.visible_for?(@current_user) }
|
|
@entries.concat @context.discussion_entries.active
|
|
@entries = @entries.sort_by{|e| e.updated_at}
|
|
@entries.each do |entry|
|
|
feed.entries << entry.to_atom
|
|
end
|
|
respond_to do |format|
|
|
format.atom { render :plain => feed.to_xml }
|
|
end
|
|
end
|
|
|
|
# @API Reorder pinned topics
|
|
#
|
|
# Puts the pinned discussion topics in the specified order.
|
|
# All pinned topics should be included.
|
|
#
|
|
# @argument order[] [Required, Integer]
|
|
# The ids of the pinned discussion topics in the desired order.
|
|
# (For example, "order=104,102,103".)
|
|
#
|
|
def reorder
|
|
if authorized_action(@context.discussion_topics.temp_record, @current_user, :update)
|
|
order = Api.value_to_array(params[:order])
|
|
reject! "order parameter required" unless order && order.length > 0
|
|
topics = pinned_topics.where(id: order)
|
|
reject! "topics not found" unless topics.length == order.length
|
|
topics[0].update_order(order)
|
|
new_order = pinned_topics.by_position.pluck(:id).map(&:to_s)
|
|
render :json => {:reorder => true, :order => new_order}, :status => :ok
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def rich_content_service_config
|
|
rce_js_env(:highrisk)
|
|
end
|
|
|
|
def cancel_redirect_url
|
|
topic_type = @topic.is_announcement ? :announcements : :discussion_topics
|
|
@topic.new_record? ? polymorphic_url([@context, topic_type]) : polymorphic_url([@context, @topic])
|
|
end
|
|
|
|
def pinned_topics
|
|
@context.active_discussion_topics.only_discussion_topics.where(pinned: true)
|
|
end
|
|
|
|
def add_discussion_or_announcement_crumb
|
|
if @topic.is_a? Announcement
|
|
@active_tab = "announcements"
|
|
add_crumb t('#crumbs.announcements', "Announcements"), named_context_url(@context, :context_announcements_url)
|
|
else
|
|
@active_tab = "discussions"
|
|
add_crumb t('#crumbs.discussions', "Discussions"), named_context_url(@context, :context_discussion_topics_url)
|
|
end
|
|
end
|
|
|
|
def user_can_moderate
|
|
@user_can_moderate = @context.grants_right?(@current_user, session, :moderate_forum) if @user_can_moderate.nil?
|
|
@user_can_moderate
|
|
end
|
|
|
|
API_ALLOWED_TOPIC_FIELDS = %w(title message discussion_type delayed_post_at lock_at podcast_enabled
|
|
podcast_has_student_posts require_initial_post pinned todo_date
|
|
group_category_id allow_rating only_graders_can_rate sort_by_rating).freeze
|
|
|
|
API_ALLOWED_TOPIC_FIELDS_FOR_GROUP = %w(title message discussion_type podcast_enabled pinned todo_date
|
|
allow_rating only_graders_can_rate sort_by_rating).freeze
|
|
|
|
|
|
def process_discussion_topic(is_new = false)
|
|
@errors = {}
|
|
model_type = value_to_boolean(params[:is_announcement]) && @context.announcements.temp_record.grants_right?(@current_user, session, :create) ? :announcements : :discussion_topics
|
|
if is_new
|
|
@topic = @context.send(model_type).build
|
|
prior_version = @topic.dup
|
|
else
|
|
@topic = @context.send(model_type).active.find(params[:id] || params[:topic_id])
|
|
prior_version = DiscussionTopic.find(@topic.id)
|
|
end
|
|
|
|
return unless authorized_action(@topic, @current_user, (is_new ? :create : :update))
|
|
|
|
allowed_fields = @context.is_a?(Group) ? API_ALLOWED_TOPIC_FIELDS_FOR_GROUP : API_ALLOWED_TOPIC_FIELDS
|
|
discussion_topic_hash = params.permit(*allowed_fields)
|
|
|
|
process_podcast_parameters(discussion_topic_hash)
|
|
|
|
if is_new
|
|
@topic.user = @current_user
|
|
elsif discussion_topic_hash.except(*%w{pinned}).present? # don't update editor if the only thing that changed was the pinned status
|
|
@topic.editor = @current_user
|
|
end
|
|
@topic.current_user = @current_user
|
|
@topic.content_being_saved_by(@current_user)
|
|
|
|
if discussion_topic_hash.has_key?(:message)
|
|
discussion_topic_hash[:message] = process_incoming_html_content(discussion_topic_hash[:message])
|
|
end
|
|
|
|
unless process_future_date_parameters(discussion_topic_hash)
|
|
process_lock_parameters(discussion_topic_hash)
|
|
end
|
|
|
|
process_published_parameters(discussion_topic_hash)
|
|
if is_new && @topic.published? && params[:assignment]
|
|
@topic.unpublish
|
|
@topic.root_topic.try(:unpublish)
|
|
publish_later = true
|
|
end
|
|
|
|
process_group_parameters(discussion_topic_hash)
|
|
process_pin_parameters(discussion_topic_hash)
|
|
process_todo_parameters(discussion_topic_hash)
|
|
|
|
if @errors.present?
|
|
render :json => {errors: @errors}, :status => :bad_request
|
|
else
|
|
@topic.skip_broadcasts = true
|
|
DiscussionTopic.transaction do
|
|
@topic.update_attributes(discussion_topic_hash)
|
|
@topic.root_topic.try(:save)
|
|
end
|
|
if !@topic.errors.any? && !@topic.root_topic.try(:errors).try(:any?)
|
|
log_asset_access(@topic, 'topics', 'topics', 'participate')
|
|
|
|
apply_positioning_parameters
|
|
apply_attachment_parameters
|
|
unless @topic.root_topic_id?
|
|
apply_assignment_parameters(params[:assignment], @topic)
|
|
end
|
|
|
|
if publish_later
|
|
@topic.publish!
|
|
@topic.root_topic.try(:publish!)
|
|
end
|
|
|
|
@topic = DiscussionTopic.find(@topic.id)
|
|
@topic.broadcast_notifications(prior_version)
|
|
|
|
render :json => discussion_topic_api_json(@topic, @context, @current_user, session)
|
|
else
|
|
errors = @topic.errors.as_json[:errors]
|
|
errors.merge!(@topic.root_topic.errors.as_json[:errors]) if @topic.root_topic
|
|
errors['published'] = errors.delete(:workflow_state) if errors.has_key?(:workflow_state)
|
|
render :json => {errors: errors}, :status => :bad_request
|
|
end
|
|
end
|
|
end
|
|
|
|
def process_podcast_parameters(discussion_topic_hash)
|
|
discussion_topic_hash[:podcast_enabled] = true if value_to_boolean(discussion_topic_hash[:podcast_has_student_posts])
|
|
|
|
unless user_can_moderate
|
|
discussion_topic_hash.delete :podcast_enabled
|
|
discussion_topic_hash.delete :podcast_has_student_posts
|
|
end
|
|
end
|
|
|
|
def process_todo_parameters(discussion_topic_hash)
|
|
unless @topic.context.root_account.feature_enabled?(:student_planner)
|
|
discussion_topic_hash.delete(:todo_date)
|
|
return
|
|
end
|
|
remove_assign = ['false', false, '0'].include?(params.dig(:assignment, :set_assignment))
|
|
if params[:assignment] && !remove_assign && !params[:todo_date]
|
|
@topic.todo_date = nil
|
|
return
|
|
end
|
|
return unless params[:todo_date]
|
|
if !authorized_action(@topic.context, @current_user, :manage)
|
|
@errors[:todo_date] = t(:error_todo_date_unauthorized,
|
|
"You do not have permission to add this topic to the student to-do list.")
|
|
elsif (@topic.assignment || params[:assignment]) && !remove_assign
|
|
@errors[:todo_date] = t(:error_todo_date_assignment, 'Date cannot be added if discussion topic is graded')
|
|
end
|
|
end
|
|
|
|
# Internal: detetermines if the delayed_post_at or lock_at dates were changed
|
|
# and applies changes to the topic if they were.
|
|
#
|
|
# Returns true if dates were changed and the topic was updated, false otherwise.
|
|
def process_future_date_parameters(discussion_topic_hash)
|
|
# Set the delayed_post_at and lock_at if provided. This will be used to determine if the values have changed
|
|
# in order to know if we should rely on this data to update the workflow state
|
|
@topic.delayed_post_at = discussion_topic_hash[:delayed_post_at] if params.has_key? :delayed_post_at
|
|
@topic.lock_at = discussion_topic_hash[:lock_at] if params.has_key? :lock_at
|
|
|
|
if @topic.delayed_post_at_changed? || @topic.lock_at_changed?
|
|
@topic.workflow_state = @topic.should_not_post_yet ? 'post_delayed' : 'active'
|
|
if @topic.should_lock_yet
|
|
@topic.lock(without_save: true)
|
|
else
|
|
@topic.unlock(without_save: true)
|
|
end
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def process_lock_parameters(discussion_topic_hash)
|
|
# Handle locking/unlocking (overrides workflow state if provided). It appears that the locked param as a hash
|
|
# is from old code and is not being used. Verification requested.
|
|
if params.has_key?(:locked) && !params[:locked].is_a?(Hash)
|
|
should_lock = value_to_boolean(params[:locked])
|
|
if should_lock != @topic.locked?
|
|
if should_lock
|
|
@topic.lock(without_save: true)
|
|
else
|
|
discussion_topic_hash[:lock_at] = nil
|
|
@topic.unlock(without_save: true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def process_published_parameters(discussion_topic_hash)
|
|
if params.has_key?(:published)
|
|
should_publish = value_to_boolean(params[:published])
|
|
if should_publish != @topic.published?
|
|
if should_publish
|
|
@topic.publish
|
|
@topic.root_topic.try(:publish)
|
|
elsif user_can_moderate
|
|
@topic.unpublish
|
|
@topic.root_topic.try(:unpublish)
|
|
else
|
|
@errors[:published] = t(:error_draft_state_unauthorized, "You do not have permission to set this topic to draft state.")
|
|
end
|
|
end
|
|
elsif @topic.new_record? && !@topic.is_announcement && user_can_moderate
|
|
@topic.unpublish
|
|
end
|
|
end
|
|
|
|
def process_group_parameters(discussion_topic_hash)
|
|
if params[:assignment] && params[:assignment].has_key?(:group_category_id)
|
|
id = params[:assignment].delete(:group_category_id)
|
|
discussion_topic_hash[:group_category_id] ||= id
|
|
end
|
|
return unless discussion_topic_hash.has_key?(:group_category_id)
|
|
return if discussion_topic_hash[:group_category_id].nil? && @topic.group_category_id.nil?
|
|
return if discussion_topic_hash[:group_category_id].to_i == @topic.group_category_id
|
|
if @topic.is_announcement
|
|
@errors[:group] = t(:error_group_announcement, "You cannot use grouped discussion on an announcement.")
|
|
return
|
|
end
|
|
if !@topic.can_group?
|
|
@errors[:group] = t(:error_group_change, "You cannot change grouping on a discussion with replies.")
|
|
end
|
|
if discussion_topic_hash[:group_category_id]
|
|
discussion_topic_hash[:group_category] = @context.group_categories.find(discussion_topic_hash[:group_category_id])
|
|
else
|
|
discussion_topic_hash[:group_category] = nil
|
|
end
|
|
end
|
|
|
|
# TODO: upgrade acts_as_list after rails3
|
|
# check_scope will probably handle this
|
|
def process_pin_parameters(discussion_topic_hash)
|
|
return unless params.key?(:pinned)
|
|
pinned = value_to_boolean(params[:pinned])
|
|
return unless pinned != @topic.pinned?
|
|
@topic.pinned = pinned
|
|
@topic.position = nil
|
|
@topic.add_to_list_bottom
|
|
end
|
|
|
|
def apply_positioning_parameters
|
|
if params[:position_after] && user_can_moderate
|
|
other_topic = @context.discussion_topics.active.find(params[:position_after])
|
|
@topic.insert_at(other_topic.position)
|
|
end
|
|
|
|
if params[:position_at] && user_can_moderate
|
|
@topic.insert_at(params[:position_at].to_i)
|
|
end
|
|
end
|
|
|
|
def apply_attachment_parameters
|
|
# handle creating/removing attachment
|
|
if @topic.grants_right?(@current_user, session, :attach)
|
|
attachment = params[:attachment] &&
|
|
params[:attachment].size > 0 &&
|
|
params[:attachment]
|
|
|
|
return if attachment && attachment.size > 1.kilobytes &&
|
|
quota_exceeded(@context, named_context_url(@context, :context_discussion_topics_url))
|
|
|
|
if (params.has_key?(:remove_attachment) || attachment) && @topic.attachment
|
|
@topic.transaction do
|
|
att = @topic.attachment
|
|
@topic.attachment = nil
|
|
@topic.save! if !@topic.new_record?
|
|
att.destroy
|
|
end
|
|
end
|
|
|
|
if attachment
|
|
@attachment = @context.attachments.create!(:uploaded_data => attachment)
|
|
@topic.attachment = @attachment
|
|
@topic.save
|
|
end
|
|
end
|
|
end
|
|
|
|
def child_topic
|
|
if params[:headless]
|
|
extra_params = {
|
|
:headless => 1,
|
|
:hide_student_names => params[:hide_student_names],
|
|
:student_id => params[:student_id]
|
|
}
|
|
end
|
|
|
|
@root_topic = @context.context.discussion_topics.find(params[:root_discussion_topic_id])
|
|
@topic = @root_topic.ensure_child_topic_for(@context)
|
|
redirect_to named_context_url(@context, :context_discussion_topic_url, @topic.id, extra_params)
|
|
end
|
|
|
|
def user_can_edit_course_settings?
|
|
@context.is_a?(Course) && @context.grants_right?(@current_user, session, :update)
|
|
end
|
|
|
|
def handle_assignment_edit_params(hash)
|
|
hash[:title] = params[:title] if params[:title]
|
|
if params.slice(*[:due_at, :points_possible, :assignment_group_id]).present?
|
|
if hash[:assignment].nil? && @context.respond_to?(:assignments) && @context.assignments.temp_record.grants_right?(@current_user, session, :create)
|
|
hash[:assignment] ||= {}
|
|
end
|
|
|
|
if !hash[:assignment].nil?
|
|
if params[:due_at]
|
|
hash[:assignment][:due_at] = params[:due_at].empty? || params[:due_at] == "null" ? nil : params[:due_at]
|
|
end
|
|
hash[:assignment][:points_possible] = params[:points_possible] if params[:points_possible]
|
|
hash[:assignment][:assignment_group_id] = params[:assignment_group_id] if params[:assignment_group_id]
|
|
end
|
|
end
|
|
end
|
|
end
|