2011-02-01 09:57:29 +08:00
#
2012-02-02 08:21:01 +08:00
# Copyright (C) 2012 Instructure, Inc.
2011-02-01 09:57:29 +08:00
#
# 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 DiscussionTopic < ActiveRecord :: Base
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
include Workflow
include SendToStream
include HasContentTags
include CopyAuthorizedLinks
discussion topics materialized view api, refs #7567
This is a specialized, optimized view of the entire discussion,
including a nested view on all the entries and participants, and the
current user's unread entry list.
An upcoming commit will cache these views to the database, and generate
them asynchronously, rather than in-request.
test plan: No UI yet. GET /api/v1/courses/X/discussion_topics/Y/view ,
and verify the formatting of the response, including the nesting of
arbitrarily nested discussion entires (also only creatable via the api,
right now). verify that deleted entries are returned, but without a
user_id or summary and with a deleted flag.
Change-Id: Ib7332743f92cca40cc2a861973bf492b1f294a02
Reviewed-on: https://gerrit.instructure.com/9305
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-03-09 03:53:58 +08:00
include TextHelper
allow using an item in modules more than once
closes #8769
An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.
test plan:
For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.
Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.
Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.
Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.
Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.
Rename the item. Verify that all instances of it in modules get renamed.
Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
2012-06-19 06:18:43 +08:00
include ContextModuleItem
2012-02-14 04:09:41 +08:00
attr_accessible :title , :message , :user , :delayed_post_at , :assignment ,
:plaintext_message , :podcast_enabled , :podcast_has_student_posts ,
2012-05-08 04:14:56 +08:00
:require_initial_post , :threaded , :discussion_type , :context
2012-03-28 09:10:47 +08:00
module DiscussionTypes
SIDE_COMMENT = 'side_comment'
THREADED = 'threaded'
2012-05-23 05:13:06 +08:00
FLAT = 'flat'
2012-03-28 09:10:47 +08:00
TYPES = DiscussionTypes . constants . map { | c | DiscussionTypes . const_get ( c ) }
end
2011-02-01 09:57:29 +08:00
attr_readonly :context_id , :context_type , :user_id
2011-07-14 00:24:17 +08:00
2011-02-01 09:57:29 +08:00
has_many :discussion_entries , :order = > :created_at , :dependent = > :destroy
2012-03-02 07:49:24 +08:00
has_many :root_discussion_entries , :class_name = > 'DiscussionEntry' , :include = > [ :user ] , :conditions = > [ 'discussion_entries.parent_id IS NULL AND discussion_entries.workflow_state != ?' , 'deleted' ]
2011-02-01 09:57:29 +08:00
has_one :external_feed_entry , :as = > :asset
belongs_to :external_feed
belongs_to :context , :polymorphic = > true
belongs_to :cloned_item
belongs_to :attachment
belongs_to :assignment
belongs_to :editor , :class_name = > 'User'
belongs_to :old_assignment , :class_name = > 'Assignment'
2012-06-21 02:58:03 +08:00
belongs_to :root_topic , :class_name = > 'DiscussionTopic'
2011-02-01 09:57:29 +08:00
has_many :child_topics , :class_name = > 'DiscussionTopic' , :foreign_key = > :root_topic_id , :dependent = > :destroy
2012-02-14 04:09:41 +08:00
has_many :discussion_topic_participants , :dependent = > :destroy
2012-03-19 02:58:17 +08:00
has_many :discussion_entry_participants , :through = > :discussion_entries
2011-02-01 09:57:29 +08:00
belongs_to :user
2012-05-22 00:11:48 +08:00
validates_presence_of :context_id , :context_type
validates_inclusion_of :discussion_type , :in = > DiscussionTypes :: TYPES
2011-02-01 09:57:29 +08:00
validates_length_of :message , :maximum = > maximum_long_text_length , :allow_nil = > true , :allow_blank = > true
2011-04-02 00:59:20 +08:00
validates_length_of :title , :maximum = > maximum_string_length , :allow_nil = > true
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
sanitize_field :message , Instructure :: SanitizeField :: SANITIZE
copy_authorized_links ( :message ) { [ self . context , nil ] }
acts_as_list :scope = > :context
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
before_create :initialize_last_reply_at
before_save :default_values
before_save :set_schedule_delayed_post
after_save :update_assignment
after_save :update_subtopics
after_save :touch_context
after_save :schedule_delayed_post
2012-02-14 04:09:41 +08:00
after_create :create_participant
2012-03-24 05:11:05 +08:00
after_create :create_materialized_view
2012-02-14 04:09:41 +08:00
2012-03-28 09:10:47 +08:00
def threaded = ( v )
self . discussion_type = Canvas :: Plugin . value_to_boolean ( v ) ? DiscussionTypes :: THREADED : DiscussionTypes :: SIDE_COMMENT
end
def threaded?
self . discussion_type == DiscussionTypes :: THREADED
end
2012-04-28 06:36:42 +08:00
alias :threaded :threaded?
2012-03-28 09:10:47 +08:00
def discussion_type
read_attribute ( :discussion_type ) || DiscussionTypes :: SIDE_COMMENT
end
2011-02-01 09:57:29 +08:00
def default_values
self . context_code = " #{ self . context_type . underscore } _ #{ self . context_id } "
2011-06-16 04:39:32 +08:00
self . title || = t '#discussion_topic.default_title' , " No Title "
2012-03-28 09:10:47 +08:00
self . discussion_type = DiscussionTypes :: SIDE_COMMENT if ! read_attribute ( :discussion_type )
2011-02-01 09:57:29 +08:00
@content_changed = self . message_changed? || self . title_changed?
if self . assignment_id != self . assignment_id_was
@old_assignment_id = self . assignment_id_was
end
if self . assignment_id
self . assignment_id = nil unless ( self . assignment && self . assignment . context == self . context ) || ( self . root_topic && self . root_topic . assignment_id == self . assignment_id )
self . old_assignment_id = self . assignment_id if self . assignment_id
2011-09-22 04:31:36 +08:00
if self . assignment && self . assignment . submission_types == 'discussion_topic' && self . assignment . has_group_category?
2011-02-01 09:57:29 +08:00
self . subtopics_refreshed_at || = Time . parse ( " Jan 1 2000 " )
end
end
end
protected :default_values
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def set_schedule_delayed_post
@should_schedule_delayed_post = self . delayed_post_at_changed?
true
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def schedule_delayed_post
self . send_at ( self . delayed_post_at , :try_posting_delayed ) if @should_schedule_delayed_post
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def update_subtopics
2012-06-21 02:58:03 +08:00
if ! self . deleted? && self . assignment && self . assignment . submission_types == 'discussion_topic' && self . assignment . has_group_category?
2012-03-28 11:09:18 +08:00
send_later_if_production :refresh_subtopics
2011-02-01 09:57:29 +08:00
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def refresh_subtopics
2012-06-21 02:58:03 +08:00
return if self . deleted?
category = self . assignment . try ( :group_category )
return unless category && self . root_topic_id . blank?
category . groups . active . each do | group |
group . shard . activate do
DiscussionTopic . unique_constraint_retry do
topic = DiscussionTopic . scoped ( :conditions = > { :context_id = > group . id , :context_type = > 'Group' , :root_topic_id = > self . id } ) . first
topic || = group . discussion_topics . build { | dt | dt . root_topic = self }
topic . message = self . message
topic . title = " #{ self . title } - #{ group . name } "
topic . assignment_id = self . assignment_id
topic . user_id = self . user_id
topic . discussion_type = self . discussion_type
topic . save if topic . changed?
topic
end
end
2011-02-01 09:57:29 +08:00
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
attr_accessor :saved_by
def update_assignment
2012-06-21 02:58:03 +08:00
return if self . deleted?
allow using an item in modules more than once
closes #8769
An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.
test plan:
For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.
Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.
Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.
Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.
Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.
Rename the item. Verify that all instances of it in modules get renamed.
Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
2012-06-19 06:18:43 +08:00
if ! self . assignment_id && @old_assignment_id
self . context_module_tags . each { | tag | tag . confirm_valid_module_requirements }
2011-02-01 09:57:29 +08:00
end
if @old_assignment_id
2011-08-31 06:13:41 +08:00
Assignment . update_all ( { :workflow_state = > 'deleted' , :updated_at = > Time . now . utc } , { :id = > @old_assignment_id , :context_id = > self . context_id , :context_type = > self . context_type , :submission_types = > 'discussion_topic' } )
2011-02-01 09:57:29 +08:00
ContentTag . delete_for ( Assignment . find ( @old_assignment_id ) ) if @old_assignment_id
2012-04-28 03:46:03 +08:00
elsif self . assignment && @saved_by != :assignment && ! self . root_topic_id
2011-02-01 09:57:29 +08:00
self . assignment . title = self . title
self . assignment . description = self . message
self . assignment . submission_types = " discussion_topic "
self . assignment . saved_by = :discussion_topic
self . assignment . workflow_state = 'available' if self . assignment . deleted?
self . assignment . save
end
2011-11-10 01:33:13 +08:00
# make sure that if the topic has a new assignment (either by going from
# ungraded to graded, or from one assignment to another; we ignore the
# transition from graded to ungraded) we acknowledge that the users that
# have posted have contributed to the topic
2012-03-22 05:28:24 +08:00
if self . assignment_id && self . assignment_id_changed?
2011-11-10 01:33:13 +08:00
posters . each { | user | self . context_module_action ( user , :contributed ) }
end
2011-02-01 09:57:29 +08:00
end
protected :update_assignment
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def restore_old_assignment
return nil unless self . old_assignment && self . old_assignment . deleted?
2011-08-10 01:55:13 +08:00
self . old_assignment . workflow_state = 'available'
self . old_assignment . saved_by = :discussion_topic
self . old_assignment . save ( false )
2011-02-01 09:57:29 +08:00
self . old_assignment
end
def is_announcement ; false end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def root_topic?
2011-09-22 04:31:36 +08:00
! self . root_topic_id && self . assignment_id && self . assignment . has_group_category?
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def discussion_subentries
self . root_discussion_entries
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def discussion_subentry_count
discussion_subentries . count
end
def for_assignment?
2011-09-21 00:51:50 +08:00
self . assignment && self . assignment . submission_types =~ / discussion_topic /
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def for_group_assignment?
2011-09-22 04:31:36 +08:00
self . for_assignment? && self . context == self . assignment . context && self . assignment . has_group_category?
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def plaintext_message = ( val )
self . message = format_message ( strip_tags ( val ) ) . first
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def plaintext_message
truncate_html ( self . message , :max_length = > 250 )
end
2012-02-14 04:09:41 +08:00
def create_participant
self . discussion_topic_participants . create ( :user = > self . user , :workflow_state = > " read " , :unread_entry_count = > 0 ) if self . user
end
2012-03-10 00:34:40 +08:00
def update_materialized_view
2012-03-30 07:52:51 +08:00
# kick off building of the view
DiscussionTopic :: MaterializedView . for ( self ) . update_materialized_view
2012-03-10 00:34:40 +08:00
end
2012-02-14 04:09:41 +08:00
# If no join record exists, assume all discussion enrties are unread, and
# that a join record will be created the first time one is marked as read.
attr_accessor :current_user
def read_state ( current_user = nil )
current_user || = self . current_user
return " read " unless current_user #default for logged out user
uid = current_user . is_a? ( User ) ? current_user . id : current_user
discussion_topic_participants . find_by_user_id ( uid ) . try ( :workflow_state ) || " unread "
end
def read? ( current_user = nil )
read_state ( current_user ) == " read "
end
def unread? ( current_user = nil )
! read? ( current_user )
end
def change_read_state ( new_state , current_user = nil )
current_user || = self . current_user
return nil unless current_user
if new_state != self . read_state ( current_user )
self . update_or_create_participant ( :current_user = > current_user , :new_state = > new_state )
else
true
end
end
def change_all_read_state ( new_state , current_user = nil )
current_user || = self . current_user
return unless current_user
transaction do
new_count = ( new_state == 'unread' ? self . default_unread_count : 0 )
self . update_or_create_participant ( :current_user = > current_user , :new_state = > new_state , :new_count = > new_count )
2012-03-20 03:15:59 +08:00
entry_ids = self . discussion_entries . map ( & :id )
2012-02-14 04:09:41 +08:00
if entry_ids . present?
existing_entry_participants = DiscussionEntryParticipant . find ( :all , :conditions = > [ " user_id = ? AND discussion_entry_id IN (?) " ,
current_user . id , entry_ids ] )
existing_ids = existing_entry_participants . map ( & :id )
DiscussionEntryParticipant . update_all ( { :workflow_state = > new_state } , [ " id IN (?) " , existing_ids ] ) if existing_ids . present?
if new_state == " read "
new_entry_ids = entry_ids - existing_entry_participants . map ( & :discussion_entry_id )
connection . bulk_insert ( 'discussion_entry_participants' , new_entry_ids . map { | entry_id |
{
:discussion_entry_id = > entry_id ,
:user_id = > current_user . id ,
:workflow_state = > new_state
}
} )
end
end
end
end
def default_unread_count
2012-03-20 03:15:59 +08:00
self . discussion_entries . count
2012-02-14 04:09:41 +08:00
end
def unread_count ( current_user = nil )
current_user || = self . current_user
return 0 unless current_user # default for logged out users
uid = current_user . is_a? ( User ) ? current_user . id : current_user
2012-03-18 04:50:24 +08:00
topic_participant = discussion_topic_participants . find_by_user_id ( uid , :lock = > true )
2012-02-14 04:09:41 +08:00
topic_participant . try ( :unread_entry_count ) || self . default_unread_count
end
def update_or_create_participant ( opts = { } )
current_user = opts [ :current_user ] || self . current_user
return nil unless current_user
2012-03-18 01:15:45 +08:00
topic_participant = nil
2012-03-18 04:50:24 +08:00
DiscussionTopic . uncached do
DiscussionTopic . unique_constraint_retry do
topic_participant = self . discussion_topic_participants . find ( :first , :conditions = > [ 'user_id = ?' , current_user . id ] , :lock = > true )
topic_participant || = self . discussion_topic_participants . build ( :user = > current_user ,
:unread_entry_count = > self . unread_count ( current_user ) ,
:workflow_state = > " unread " )
topic_participant . workflow_state = opts [ :new_state ] if opts [ :new_state ]
topic_participant . unread_entry_count += opts [ :offset ] if opts [ :offset ] && opts [ :offset ] != 0
topic_participant . unread_entry_count = opts [ :new_count ] if opts [ :new_count ]
topic_participant . save
end
2012-03-18 01:15:45 +08:00
end
2012-02-14 04:09:41 +08:00
topic_participant
end
2011-02-01 09:57:29 +08:00
def self . search ( query )
2011-03-01 08:37:39 +08:00
find ( :all , :conditions = > wildcard ( 'title' , 'message' , query ) )
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
named_scope :recent , lambda {
{ :conditions = > [ 'discussion_topics.last_reply_at > ?' , 2 . weeks . ago ] , :order = > 'discussion_topics.last_reply_at DESC' }
}
named_scope :only_discussion_topics , lambda {
{ :conditions = > [ 'discussion_topics.type IS NULL' ] }
}
named_scope :for_subtopic_refreshing , lambda {
{ :conditions = > [ 'discussion_topics.subtopics_refreshed_at IS NOT NULL AND discussion_topics.subtopics_refreshed_at < discussion_topics.updated_at' ] , :order = > 'discussion_topics.subtopics_refreshed_at' }
}
named_scope :for_delayed_posting , lambda {
{ :conditions = > [ 'discussion_topics.workflow_state = ? AND discussion_topics.delayed_post_at < ?' , 'post_delayed' , Time . now . utc ] , :order = > 'discussion_topics.delayed_post_at' }
}
named_scope :active , :conditions = > [ 'discussion_topics.workflow_state != ?' , 'deleted' ]
named_scope :for_context_codes , lambda { | codes |
{ :conditions = > [ 'discussion_topics.context_code IN (?)' , codes ] }
}
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
named_scope :before , lambda { | date |
{ :conditions = > [ 'discussion_topics.created_at < ?' , date ] }
}
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def try_posting_delayed
if self . post_delayed? && Time . now > = self . delayed_post_at
self . delayed_post
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
workflow do
2011-08-30 05:10:26 +08:00
state :active do
event :lock , :transitions_to = > :locked do
2012-02-02 08:21:01 +08:00
raise " cannot lock before due date " if self . assignment . try ( :due_at ) && self . assignment . due_at > Time . now
2011-08-30 05:10:26 +08:00
end
end
2011-02-01 09:57:29 +08:00
state :post_delayed do
event :delayed_post , :transitions_to = > :active do
@delayed_just_posted = true
self . last_reply_at = Time . now
self . posted_at = Time . now
end
end
2011-08-30 05:10:26 +08:00
state :locked do
event :unlock , :transitions_to = > :active
end
2011-02-01 09:57:29 +08:00
state :deleted
end
2012-02-14 04:09:41 +08:00
2011-04-05 23:06:10 +08:00
def should_send_to_stream
2011-02-01 09:57:29 +08:00
if self . delayed_post_at && self . delayed_post_at > Time . now
2011-04-05 23:06:10 +08:00
false
2011-02-01 09:57:29 +08:00
elsif self . cloned_item_id
2011-04-05 23:06:10 +08:00
false
2011-09-22 04:31:36 +08:00
elsif self . assignment && self . root_topic_id && self . assignment . has_group_category?
2011-04-05 23:06:10 +08:00
false
2011-02-01 09:57:29 +08:00
elsif self . assignment && self . assignment . submission_types == 'discussion_topic' && ( ! self . assignment . due_at || self . assignment . due_at > 1 . week . from_now )
2011-04-05 23:06:10 +08:00
false
2012-05-23 05:13:06 +08:00
elsif self . context . is_a? ( CollectionItem )
# we'll only send notifications of entries to the streams, not creations of topics
false
2011-02-01 09:57:29 +08:00
else
2011-04-05 23:06:10 +08:00
true
end
end
2012-02-14 04:09:41 +08:00
2011-04-05 23:06:10 +08:00
on_create_send_to_streams do
if should_send_to_stream
2011-12-01 06:49:22 +08:00
self . active_participants
2011-02-01 09:57:29 +08:00
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
on_update_send_to_streams do
2011-08-04 02:41:22 +08:00
if should_send_to_stream && ( @delayed_just_posted || @content_changed || changed_state ( :active , :post_delayed ) )
2011-12-01 06:49:22 +08:00
self . active_participants
2011-02-01 09:57:29 +08:00
end
end
2012-02-14 04:09:41 +08:00
2011-11-04 05:55:38 +08:00
def require_initial_post?
self . require_initial_post || ( self . root_topic && self . root_topic . require_initial_post )
end
2012-02-14 04:09:41 +08:00
2011-11-04 05:55:38 +08:00
def user_ids_who_have_posted_and_admins
ids = DiscussionEntry . active . scoped ( :select = > " distinct user_id " ) . find_all_by_discussion_topic_id ( self . id ) . map ( & :user_id )
ids += self . context . admin_enrollments . scoped ( :select = > 'user_id' ) . map ( & :user_id ) if self . context . respond_to? ( :admin_enrollments )
ids
end
memoize :user_ids_who_have_posted_and_admins
2012-02-14 04:09:41 +08:00
2011-11-04 05:55:38 +08:00
def user_can_see_posts? ( user , session = nil )
return false unless user
2012-02-14 04:09:41 +08:00
! self . require_initial_post || self . grants_right? ( user , session , :update ) || user_ids_who_have_posted_and_admins . member? ( user . id )
2011-11-04 05:55:38 +08:00
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def reply_from ( opts )
user = opts [ :user ]
discussion topics materialized view api, refs #7567
This is a specialized, optimized view of the entire discussion,
including a nested view on all the entries and participants, and the
current user's unread entry list.
An upcoming commit will cache these views to the database, and generate
them asynchronously, rather than in-request.
test plan: No UI yet. GET /api/v1/courses/X/discussion_topics/Y/view ,
and verify the formatting of the response, including the nesting of
arbitrarily nested discussion entires (also only creatable via the api,
right now). verify that deleted entries are returned, but without a
user_id or summary and with a deleted flag.
Change-Id: Ib7332743f92cca40cc2a861973bf492b1f294a02
Reviewed-on: https://gerrit.instructure.com/9305
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-03-09 03:53:58 +08:00
if opts [ :text ]
message = opts [ :text ] . strip
message = format_message ( message ) . first
else
message = opts [ :html ] . strip
end
2011-02-01 09:57:29 +08:00
user = nil unless user && self . context . users . include? ( user )
if ! user
raise " Only context participants may reply to messages "
elsif ! message || message . empty?
raise " Message body cannot be blank "
else
DiscussionEntry . create! ( {
:message = > message ,
2011-04-07 02:46:06 +08:00
:discussion_topic = > self ,
:user = > user ,
2011-02-01 09:57:29 +08:00
} )
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
alias_method :destroy! , :destroy
def destroy
ContentTag . delete_for ( self )
self . workflow_state = 'deleted'
self . deleted_at = Time . now
self . save
2012-06-21 02:58:03 +08:00
if self . for_assignment? && self . root_topic_id . blank?
2011-02-01 09:57:29 +08:00
self . assignment . destroy unless self . assignment . deleted?
end
2012-06-21 02:58:03 +08:00
self . child_topics . each do | child |
child . destroy
end
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def restore
self . workflow_state = 'active'
self . save
if self . for_assignment?
self . assignment . restore ( :discussion_topic )
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def self . find_or_create_for_new_context ( new_context , old_context , old_id )
res = new_context . discussion_topics . active . find_by_cloned_item_id ( old_context . discussion_topics . find_by_id ( old_id ) . cloned_item_id || 0 ) rescue nil
res = nil if res && ! res . cloned_item_id
if ! res
old = old_context . discussion_topics . active . find_by_id ( old_id )
res = old . clone_for ( new_context ) if old
res . save if res
end
res
end
def unlink_from ( type )
@saved_by = type
if self . discussion_entries . empty?
2011-03-08 04:15:19 +08:00
self . assignment = nil
self . destroy
2011-02-01 09:57:29 +08:00
else
2011-03-08 04:15:19 +08:00
self . assignment = nil
2011-02-01 09:57:29 +08:00
self . save
end
self . child_topics . each { | t | t . unlink_from ( :assignment ) }
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def self . per_page
10
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def initialize_last_reply_at
2011-05-04 06:28:41 +08:00
self . posted_at || = Time . now
2011-02-01 09:57:29 +08:00
self . last_reply_at = Time . now
end
set_policy do
given { | user | self . user && self . user == user && ! self . locked? }
2011-07-14 00:24:17 +08:00
can :update and can :reply and can :read
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
given { | user | self . user && self . user == user }
2011-07-14 00:24:17 +08:00
can :read
2011-02-01 09:57:29 +08:00
given { | user | self . user && self . user == user and self . discussion_entries . active . empty? && ! self . locked? && ! self . root_topic_id }
2011-07-14 00:24:17 +08:00
can :delete
2012-02-14 04:09:41 +08:00
2012-01-14 05:25:35 +08:00
given { | user , session | ( self . active? || self . locked? ) && self . cached_context_grants_right? ( user , session , :read_forum ) } #
2011-07-14 00:24:17 +08:00
can :read
2012-02-14 04:09:41 +08:00
2011-08-30 05:10:26 +08:00
given { | user , session | self . active? && self . cached_context_grants_right? ( user , session , :post_to_forum ) } #students.include?(user) }
2011-07-14 00:24:17 +08:00
can :reply and can :read
2011-08-30 05:10:26 +08:00
given { | user , session | ( self . active? || self . locked? ) && self . cached_context_grants_right? ( user , session , :post_to_forum ) } #students.include?(user) }
can :read
2011-02-01 09:57:29 +08:00
given { | user , session | self . cached_context_grants_right? ( user , session , :post_to_forum ) and not self . is_announcement }
2011-07-14 00:24:17 +08:00
can :create
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
given { | user , session | self . context . respond_to? ( :allow_student_forum_attachments ) && self . context . allow_student_forum_attachments && self . cached_context_grants_right? ( user , session , :post_to_forum ) } # students.find_by_id(user) }
2011-07-14 00:24:17 +08:00
can :attach
2012-02-14 04:09:41 +08:00
2011-08-30 05:10:26 +08:00
given { | user , session | ! self . root_topic_id && self . cached_context_grants_right? ( user , session , :moderate_forum ) && ! self . locked? }
2011-07-14 00:24:17 +08:00
can :update and can :delete and can :create and can :read and can :attach
2011-08-30 05:10:26 +08:00
# Moderators can still modify content even in locked topics (*especially* unlocking them), but can't create new content
given { | user , session | ! self . root_topic_id && self . cached_context_grants_right? ( user , session , :moderate_forum ) }
can :update and can :delete and can :read
2011-02-01 09:57:29 +08:00
given { | user , session | self . root_topic && self . root_topic . grants_right? ( user , session , :update ) }
2011-07-14 00:24:17 +08:00
can :update
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
given { | user , session | self . root_topic && self . root_topic . grants_right? ( user , session , :delete ) }
2011-07-14 00:24:17 +08:00
can :delete
2012-05-23 05:13:06 +08:00
given { | user , session | self . context . respond_to? ( :collection ) && self . context . collection . grants_right? ( user , session , :read ) }
can :read
given { | user , session | self . context . respond_to? ( :collection ) && self . context . collection . grants_right? ( user , session , :comment ) }
can :reply
given { | user , session | self . context . respond_to? ( :collection ) && user == self . context . user }
can :read and can :update and can :delete and can :reply
2011-02-01 09:57:29 +08:00
end
def discussion_topic_id
self . id
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def discussion_topic
self
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def to_atom ( opts = { } )
2012-04-05 03:06:48 +08:00
author_name = self . user . present? ? self . user . name : t ( '#discussion_topic.atom_no_author' , " No Author " )
2011-06-09 06:29:03 +08:00
prefix = [ self . is_announcement ? t ( '#titles.announcement' , " Announcement " ) : t ( '#titles.discussion' , " Discussion " ) ]
prefix << self . context . name if opts [ :include_context ]
2011-02-01 09:57:29 +08:00
Atom :: Entry . new do | entry |
2011-06-09 06:29:03 +08:00
entry . title = [ before_label ( prefix . to_sentence ) , self . title ] . join ( " " )
2012-04-05 03:06:48 +08:00
entry . authors << Atom :: Person . new ( :name = > author_name )
2011-02-01 09:57:29 +08:00
entry . updated = self . updated_at
entry . published = self . created_at
entry . id = " tag: #{ HostUrl . default_host } , #{ self . created_at . strftime ( " %Y-%m-%d " ) } :/discussion_topics/ #{ self . feed_code } "
2012-02-14 04:09:41 +08:00
entry . links << Atom :: Link . new ( :rel = > 'alternate' ,
2011-02-01 09:57:29 +08:00
:href = > " http:// #{ HostUrl . context_host ( self . context ) } / #{ context_url_prefix } /discussion_topics/ #{ self . id } " )
entry . content = Atom :: Content :: Html . new ( self . message || " " )
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def context_prefix
context_url_prefix
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def context_module_action ( user , action , points = nil )
allow using an item in modules more than once
closes #8769
An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.
test plan:
For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.
Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.
Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.
Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.
Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.
Rename the item. Verify that all instances of it in modules get renamed.
Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
2012-06-19 06:18:43 +08:00
tags_to_update = self . context_module_tags . to_a
2011-11-10 01:33:13 +08:00
if self . for_assignment?
allow using an item in modules more than once
closes #8769
An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.
test plan:
For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.
Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.
Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.
Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.
Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.
Rename the item. Verify that all instances of it in modules get renamed.
Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
2012-06-19 06:18:43 +08:00
tags_to_update += self . assignment . context_module_tags
2011-11-10 01:33:13 +08:00
self . ensure_submission ( user ) if self . assignment . context . students . include? ( user ) && action == :contributed
end
allow using an item in modules more than once
closes #8769
An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.
test plan:
For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.
Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.
Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.
Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.
Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.
Rename the item. Verify that all instances of it in modules get renamed.
Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
2012-06-19 06:18:43 +08:00
tags_to_update . each { | tag | tag . context_module_action ( user , action , points ) }
2011-11-10 01:33:13 +08:00
end
def ensure_submission ( user )
submission = Submission . find_by_assignment_id_and_user_id ( self . assignment_id , user . id )
2012-03-22 05:28:24 +08:00
return if submission && submission . submission_type == 'discussion_topic' && submission . workflow_state != 'unsubmitted'
2011-11-10 01:33:13 +08:00
self . assignment . submit_homework ( user , :submission_type = > 'discussion_topic' )
2011-02-01 09:57:29 +08:00
end
has_a_broadcast_policy
set_broadcast_policy do | p |
p . dispatch :new_discussion_topic
2011-12-01 06:49:22 +08:00
p . to { active_participants - [ user ] }
2011-02-01 09:57:29 +08:00
p . whenever { | record |
record . context . available? and
( ( record . just_created and not record . post_delayed? ) || record . changed_state ( :active , :post_delayed ) )
}
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def delay_posting = ( val ) ; end
def set_assignment = ( val ) ; end
2012-02-14 04:09:41 +08:00
2012-03-09 01:14:57 +08:00
def participants ( include_observers = false )
2012-05-23 05:13:06 +08:00
participants = [ self . user ]
if self . context . is_a? ( CollectionItem )
participants += self . posters
else
participants += context . participants ( include_observers )
end
participants . compact . uniq
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2012-03-09 01:14:57 +08:00
def active_participants ( include_observers = false )
2012-05-23 05:13:06 +08:00
if self . context . respond_to? ( :available? ) && ! self . context . available? && self . context . respond_to? ( :participating_admins )
2011-12-01 06:49:22 +08:00
self . context . participating_admins
else
2012-03-09 01:14:57 +08:00
self . participants ( include_observers )
2011-12-01 06:49:22 +08:00
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def posters
2012-03-20 05:16:24 +08:00
user_ids = discussion_entries . map ( & :user_id ) . push ( self . user_id ) . uniq
2012-05-23 05:13:06 +08:00
context . respond_to? ( :participating_users ) ? context . participating_users ( user_ids ) : User . find ( user_ids )
2011-02-01 09:57:29 +08:00
end
def user_name
2012-06-28 05:40:03 +08:00
self . user ? self . user . name : nil
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def locked_for? ( user = nil , opts = { } )
return false if opts [ :check_policies ] && self . grants_right? ( user , nil , :update )
allow using an item in modules more than once
closes #8769
An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.
test plan:
For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.
Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.
Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.
Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.
Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.
Rename the item. Verify that all instances of it in modules get renamed.
Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
2012-06-19 06:18:43 +08:00
Rails . cache . fetch ( locked_cache_key ( user ) , :expires_in = > 1 . minute ) do
2011-02-01 09:57:29 +08:00
locked = false
if ( self . delayed_post_at && self . delayed_post_at > Time . now )
locked = { :asset_string = > self . asset_string , :unlock_at = > self . delayed_post_at }
elsif ( self . assignment && l = self . assignment . locked_for? ( user , opts ) )
locked = l
allow using an item in modules more than once
closes #8769
An item can be added to multiple modules, or even the same module more
than once. This is especially useful for attachment items, but is also
useful for allowing multiple paths through a course, with say an
assignment in two different modules and the user only has to complete
one of the two modules.
test plan:
For an item in only one module, verify that the module navigation still
appears if you go straight to that item's page, without going through
the modules page.
Add an item to more than one module. If you visit that item from the
modules page, you'll see the right nav depending on which instance of
the item you clicked on. If you visit the item directly without going
through the modules page, you'll see no nav.
Lock one instance of the item by adding a prerequisite, but leave the
other unlocked. You can still see the item as a student.
Lock all instances of the item with prerequisites. The item will now be
locked and you can't see it as a student.
Add completion requirements to the item, such as a minimum score on a
quiz. Make the requirements different -- 3 points in one instance and 5
in the other, for instance. Verify that if you get 3 points on the quiz,
one item is marked as completed but the other isn't, as expected.
Rename the item. Verify that all instances of it in modules get renamed.
Change-Id: I4f1b2f6f033062ec47ac34fe5eb973a950c17b0c
Reviewed-on: https://gerrit.instructure.com/11671
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
2012-06-19 06:18:43 +08:00
elsif self . could_be_locked && item = locked_by_module_item? ( user , opts [ :deep_check_if_needed ] )
locked = { :asset_string = > self . asset_string , :context_module = > item . context_module . attributes }
2011-02-01 09:57:29 +08:00
elsif ( self . root_topic && l = self . root_topic . locked_for? ( user , opts ) )
locked = l
end
locked
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
attr_accessor :clone_updated
attr_accessor :assignment_clone_updated
def clone_for ( context , dup = nil , options = { } )
options [ :migrate ] = true if options [ :migrate ] == nil
if ! self . cloned_item && ! self . new_record?
self . cloned_item || = ClonedItem . create ( :original_item = > self )
self . save!
end
existing = context . discussion_topics . active . find_by_id ( self . id )
existing || = context . discussion_topics . active . find_by_cloned_item_id ( self . cloned_item_id || 0 )
return existing if existing && ! options [ :overwrite ]
if context . merge_mapped_id ( self . assignment )
dup || = context . discussion_topics . find_by_assignment_id ( context . merge_mapped_id ( self . assignment ) )
end
dup || = DiscussionTopic . new
dup = existing if existing && options [ :overwrite ]
self . attributes . delete_if { | k , v | [ :id , :assignment_id , :attachment_id , :root_topic_id ] . include? ( k . to_sym ) } . each do | key , val |
dup . send ( " #{ key } = " , val )
end
dup . assignment_id = context . merge_mapped_id ( self . assignment )
if ! dup . assignment_id && self . assignment_id && self . assignment && ! options [ :cloning_for_assignment ]
new_assignment = self . assignment . clone_for ( context , nil , :cloning_for_topic = > true )
assignment_clone_updated = new_assignment . clone_updated
new_assignment . save_without_broadcasting!
context . map_merge ( self . assignment , new_assignment )
dup . assignment_id = new_assignment . id
end
if ! dup . attachment_id && self . attachment
attachment = self . attachment . clone_for ( context )
attachment . folder_id = nil
2012-07-19 05:51:43 +08:00
attachment . save_without_broadcasting!
2011-02-01 09:57:29 +08:00
context . map_merge ( self . attachment , attachment )
context . warn_merge_result ( " Added file \" #{ attachment . folder . full_name } / #{ attachment . display_name } \" which is needed for the topic \" #{ self . title } \" " )
dup . attachment_id = attachment . id
end
dup . context = context
dup . message = context . migrate_content_links ( self . message , self . context ) if options [ :migrate ]
dup . saved_by = :assignment if options [ :cloning_for_assignment ]
dup . save_without_broadcasting!
context . log_merge_result ( " Discussion \" #{ dup . title } \" created " )
if options [ :include_entries ]
self . discussion_entries . sort_by { | e | e . created_at } . each do | entry |
dup_entry = entry . clone_for ( context , nil , :migrate = > options [ :migrate ] )
2012-03-02 07:49:24 +08:00
dup_entry . parent_id = context . merge_mapped_id ( " discussion_entry_ #{ entry . parent_id } " )
2011-02-01 09:57:29 +08:00
dup_entry . discussion_topic_id = dup . id
dup_entry . save!
context . map_merge ( entry , dup_entry )
dup_entry
end
context . log_merge_result ( " Included #{ dup . discussion_entries . length } entries for the topic \" #{ dup . title } \" " )
end
context . may_have_links_to_migrate ( dup )
dup . updated_at = Time . now
dup . clone_updated = true
dup
end
def self . process_migration ( data , migration )
announcements = data [ 'announcements' ] ? data [ 'announcements' ] : [ ]
announcements . each do | event |
2012-04-03 06:38:05 +08:00
if migration . import_object? ( " announcements " , event [ 'migration_id' ] )
2011-05-04 04:46:53 +08:00
event [ :type ] = 'announcement'
2011-06-18 00:58:18 +08:00
begin
import_from_migration ( event , migration . context )
rescue
migration . add_warning ( " Couldn't import the announcement \" #{ event [ :title ] } \" " , $! )
end
2011-02-01 09:57:29 +08:00
end
end
topics = data [ 'discussion_topics' ] ? data [ 'discussion_topics' ] : [ ]
topic_entries_to_import = migration . to_import 'topic_entries'
topics . each do | topic |
2012-02-14 04:09:41 +08:00
context = Group . find_by_context_id_and_context_type_and_migration_id ( migration . context . id , migration . context . class . to_s , topic [ 'group_id' ] ) if topic [ 'group_id' ]
2011-02-01 09:57:29 +08:00
context || = migration . context
if context
2012-04-03 06:38:05 +08:00
if migration . import_object? ( " topics " , topic [ 'migration_id' ] )
2011-06-18 00:58:18 +08:00
begin
import_from_migration ( topic . merge ( { :topic_entries_to_import = > topic_entries_to_import } ) , context )
rescue
migration . add_warning ( " Couldn't import the topic \" #{ topic [ :title ] } \" " , $! )
end
2011-02-01 09:57:29 +08:00
end
end
end
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
def self . import_from_migration ( hash , context , item = nil )
hash = hash . with_indifferent_access
return nil if hash [ :migration_id ] && hash [ :topics_to_import ] && ! hash [ :topics_to_import ] [ hash [ :migration_id ] ]
hash [ :skip_replies ] = true if hash [ :migration_id ] && hash [ :topic_entries_to_import ] && ! hash [ :topic_entries_to_import ] [ hash [ :migration_id ] ]
item || = find_by_context_type_and_context_id_and_id ( context . class . to_s , context . id , hash [ :id ] )
item || = find_by_context_type_and_context_id_and_migration_id ( context . class . to_s , context . id , hash [ :migration_id ] ) if hash [ :migration_id ]
2011-04-16 10:48:44 +08:00
if hash [ :type ] =~ / announcement /i
item || = context . announcements . new
else
item || = context . discussion_topics . new
end
2011-02-01 09:57:29 +08:00
item . migration_id = hash [ :migration_id ]
item . title = hash [ :title ]
item . message = ImportedHtmlConverter . convert ( hash [ :description ] || hash [ :text ] , context )
2011-06-16 04:39:32 +08:00
item . message = t ( '#discussion_topic.empty_message' , " No message " ) if item . message . blank?
2011-09-14 01:51:33 +08:00
item . posted_at = Canvas :: Migration :: MigratorHelper . get_utc_time_from_timestamp ( hash [ :posted_at ] ) if hash [ :posted_at ]
item . delayed_post_at = Canvas :: Migration :: MigratorHelper . get_utc_time_from_timestamp ( hash [ :delayed_post_at ] ) if hash [ :delayed_post_at ]
item . delayed_post_at || = Canvas :: Migration :: MigratorHelper . get_utc_time_from_timestamp ( hash [ :start_date ] ) if hash [ :start_date ]
2011-04-16 10:48:44 +08:00
item . position = hash [ :position ] if hash [ :position ]
2012-05-03 02:46:11 +08:00
item . workflow_state = 'active' if item . deleted?
2011-04-16 10:48:44 +08:00
if hash [ :attachment_migration_id ]
2011-04-16 12:09:56 +08:00
item . attachment = context . attachments . find_by_migration_id ( hash [ :attachment_migration_id ] )
2011-04-16 10:48:44 +08:00
end
if hash [ :external_feed_migration_id ]
item . external_feed = context . external_feeds . find_by_migration_id ( hash [ :external_feed_migration_id ] )
2011-02-01 09:57:29 +08:00
end
if hash [ :attachment_ids ] && ! hash [ :attachment_ids ] . empty?
item . message += Attachment . attachment_list_from_migration ( context , hash [ :attachment_ids ] )
end
2011-04-16 10:48:44 +08:00
if hash [ :assignment ]
assignment = Assignment . import_from_migration ( hash [ :assignment ] , context )
item . assignment = assignment
elsif grading = hash [ :grading ]
2011-02-01 09:57:29 +08:00
assignment = Assignment . import_from_migration ( {
:grading = > grading ,
:migration_id = > hash [ :migration_id ] ,
:submission_format = > " discussion_topic " ,
:due_date = > hash [ :due_date ] || hash [ :grading ] [ :due_date ] ,
:title = > grading [ :title ]
} , context )
item . assignment = assignment
end
item . save_without_broadcasting!
context . migration_results << " " if hash [ :peer_rating_type ] && hash [ :peer_rating_types ] != " none " if context . respond_to? ( :migration_results )
context . migration_results << " " if hash [ :peer_rating_type ] && hash [ :peer_rating_types ] != " none " if context . respond_to? ( :migration_results )
hash [ :messages ] || = hash [ :posts ]
2011-04-16 10:48:44 +08:00
context . imported_migration_items << item if context . respond_to? ( :imported_migration_items ) && context . imported_migration_items
2011-02-01 09:57:29 +08:00
item
end
2012-02-14 04:09:41 +08:00
2011-02-10 01:26:42 +08:00
def self . podcast_elements ( messages , context )
2011-02-01 09:57:29 +08:00
attachment_ids = [ ]
media_object_ids = [ ]
2011-02-10 01:26:42 +08:00
messages_hash = { }
messages . each do | message |
txt = ( message . message || " " )
2011-02-01 09:57:29 +08:00
attachment_matches = txt . scan ( / \/ #{ context . class . to_s . pluralize . underscore } \/ #{ context . id } \/ files \/ ( \ d+) \/ download / )
attachment_ids += ( attachment_matches || [ ] ) . map { | m | m [ 0 ] }
media_object_matches = txt . scan ( / media_comment_([0-9a-z_]+) / )
media_object_ids += ( media_object_matches || [ ] ) . map { | m | m [ 0 ] }
( attachment_ids + media_object_ids ) . each do | id |
2011-02-10 01:26:42 +08:00
messages_hash [ id ] || = message
2011-02-01 09:57:29 +08:00
end
end
media_object_ids = media_object_ids . uniq . compact
attachment_ids = attachment_ids . uniq . compact
2011-05-03 13:56:39 +08:00
attachments = attachment_ids . empty? ? [ ] : context . attachments . active . find_all_by_id ( attachment_ids ) . compact
2011-02-01 09:57:29 +08:00
attachments = attachments . select { | a | a . content_type && a . content_type . match ( / (video|audio) / ) }
attachments . each do | attachment |
2011-02-10 01:26:42 +08:00
attachment . podcast_associated_asset = messages_hash [ attachment . id ]
2011-02-01 09:57:29 +08:00
end
2011-05-03 13:56:39 +08:00
media_objects = media_object_ids . empty? ? [ ] : MediaObject . find_all_by_media_id ( media_object_ids )
2011-02-01 09:57:29 +08:00
media_objects += media_object_ids . map { | id | MediaObject . new ( :media_id = > id ) }
media_objects = media_objects . once_per ( & :media_id )
media_objects = media_objects . map do | media_object |
if media_object . new_record?
media_object . context = context
2011-02-10 01:26:42 +08:00
media_object . user_id = messages_hash [ media_object . media_id ] . user_id rescue nil
2011-02-01 09:57:29 +08:00
media_object . root_account_id = context . root_account_id rescue nil
media_object . save
elsif media_object . deleted? || media_object . context != context
media_object = nil
end
2011-08-09 05:38:41 +08:00
if media_object . try ( :podcast_format_details )
media_object . podcast_associated_asset = messages_hash [ media_object . media_id ]
2011-02-01 09:57:29 +08:00
end
media_object
end
to_podcast ( attachments + media_objects . compact )
end
def self . to_podcast ( elements , opts = { } )
require 'rss/2.0'
elements . map do | elem |
2011-02-10 01:26:42 +08:00
asset = elem . podcast_associated_asset
next unless asset
2011-02-01 09:57:29 +08:00
item = RSS :: Rss :: Channel :: Item . new
2011-06-09 06:29:03 +08:00
item . title = before_label ( ( asset . title rescue " " ) ) + elem . name
2011-02-10 01:26:42 +08:00
link = nil
if asset . is_a? ( DiscussionTopic )
link = " http:// #{ HostUrl . context_host ( asset . context ) } / #{ asset . context_url_prefix } /discussion_topics/ #{ asset . id } "
elsif asset . is_a? ( DiscussionEntry )
link = " http:// #{ HostUrl . context_host ( asset . context ) } / #{ asset . context_url_prefix } /discussion_topics/ #{ asset . discussion_topic_id } "
end
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
item . link = link
item . guid = RSS :: Rss :: Channel :: Item :: Guid . new
item . pubDate = elem . updated_at . utc
2011-02-10 01:26:42 +08:00
item . description = asset ? asset . message : elem . name
2011-02-01 09:57:29 +08:00
item . enclosure
if elem . is_a? ( Attachment )
item . guid . content = link + " / #{ elem . uuid } "
item . enclosure = RSS :: Rss :: Channel :: Item :: Enclosure . new ( " http:// #{ HostUrl . context_host ( elem . context ) } / #{ elem . context_url_prefix } /files/ #{ elem . id } /download. #{ } ?verifier= #{ elem . uuid } " , elem . size , elem . content_type )
elsif elem . is_a? ( MediaObject )
item . guid . content = link + " / #{ elem . media_id } "
details = elem . podcast_format_details
content_type = 'video/mpeg'
content_type = 'audio/mpeg' if elem . media_type == 'audio'
size = details [ :size ] . to_i . kilobytes
item . enclosure = RSS :: Rss :: Channel :: Item :: Enclosure . new ( " http:// #{ HostUrl . context_host ( elem . context ) } / #{ elem . context_url_prefix } /media_download. #{ details [ :fileExt ] } ?entryId= #{ elem . media_id } &redirect=1 " , size , content_type )
end
item
end
end
discussion topics materialized view api, refs #7567
This is a specialized, optimized view of the entire discussion,
including a nested view on all the entries and participants, and the
current user's unread entry list.
An upcoming commit will cache these views to the database, and generate
them asynchronously, rather than in-request.
test plan: No UI yet. GET /api/v1/courses/X/discussion_topics/Y/view ,
and verify the formatting of the response, including the nesting of
arbitrarily nested discussion entires (also only creatable via the api,
right now). verify that deleted entries are returned, but without a
user_id or summary and with a deleted flag.
Change-Id: Ib7332743f92cca40cc2a861973bf492b1f294a02
Reviewed-on: https://gerrit.instructure.com/9305
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-03-09 03:53:58 +08:00
2012-03-24 04:47:02 +08:00
def initial_post_required? ( user , enrollment , session )
if require_initial_post?
# check if the user, or the user being observed can see the posts
if enrollment && enrollment . respond_to? ( :associated_user ) && enrollment . associated_user
return true if ! user_can_see_posts? ( enrollment . associated_user )
elsif ! user_can_see_posts? ( user , session )
return true
end
end
false
end
discussion topics materialized view api, refs #7567
This is a specialized, optimized view of the entire discussion,
including a nested view on all the entries and participants, and the
current user's unread entry list.
An upcoming commit will cache these views to the database, and generate
them asynchronously, rather than in-request.
test plan: No UI yet. GET /api/v1/courses/X/discussion_topics/Y/view ,
and verify the formatting of the response, including the nesting of
arbitrarily nested discussion entires (also only creatable via the api,
right now). verify that deleted entries are returned, but without a
user_id or summary and with a deleted flag.
Change-Id: Ib7332743f92cca40cc2a861973bf492b1f294a02
Reviewed-on: https://gerrit.instructure.com/9305
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-03-09 03:53:58 +08:00
# returns the materialized view of the discussion as structure, participant_ids, and entry_ids
# the view is already converted to a json string, the other two arrays of ids are ruby arrays
# see the description of the format in the discussion topics api documentation.
#
# returns nil if the view is not currently available, and kicks off a
# background job to build the view. this typically only takes a couple seconds.
#
# if a new message is posted, it won't appear in this view until the job to
# update it completes. so this view is eventually consistent.
2012-05-23 05:13:06 +08:00
#
# if the topic itself is not yet created, it will return blank data. this is for situations
# where we're creating topics on the first write - until that first write, we need to return
# blank data on reads.
2012-03-31 00:21:58 +08:00
def materialized_view ( opts = { } )
2012-05-23 05:13:06 +08:00
if self . new_record?
return " [] " , [ ] , [ ] , " [] "
else
DiscussionTopic :: MaterializedView . materialized_view_for ( self , opts )
end
discussion topics materialized view api, refs #7567
This is a specialized, optimized view of the entire discussion,
including a nested view on all the entries and participants, and the
current user's unread entry list.
An upcoming commit will cache these views to the database, and generate
them asynchronously, rather than in-request.
test plan: No UI yet. GET /api/v1/courses/X/discussion_topics/Y/view ,
and verify the formatting of the response, including the nesting of
arbitrarily nested discussion entires (also only creatable via the api,
right now). verify that deleted entries are returned, but without a
user_id or summary and with a deleted flag.
Change-Id: Ib7332743f92cca40cc2a861973bf492b1f294a02
Reviewed-on: https://gerrit.instructure.com/9305
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Simon Williams <simon@instructure.com>
2012-03-09 03:53:58 +08:00
end
2012-03-24 05:11:05 +08:00
# synchronously create/update the materialized view
def create_materialized_view
2012-03-30 07:52:51 +08:00
DiscussionTopic :: MaterializedView . for ( self ) . update_materialized_view_without_send_later
2012-03-24 05:11:05 +08:00
end
2011-02-01 09:57:29 +08:00
end