2020-10-27 00:46:40 +08:00
# frozen_string_literal: true
2017-04-28 10:30:08 +08:00
# Copyright (C) 2011 - present 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/>.
#
2015-04-09 01:21:08 +08:00
require " atom "
2011-02-01 09:57:29 +08:00
class DiscussionTopic < ActiveRecord :: Base
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
2014-02-05 04:53:04 +08:00
include HtmlTextHelper
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
2013-07-24 02:09:33 +08:00
include SearchTermHelper
2016-04-08 00:37:16 +08:00
include Submittable
2017-05-11 12:40:45 +08:00
include Plannable
2016-12-29 06:24:13 +08:00
include MasterCourses :: Restrictor
2017-06-20 03:27:39 +08:00
include DuplicatingObjects
2018-05-15 06:29:01 +08:00
include LockedFor
2017-06-20 03:27:39 +08:00
2016-12-29 06:24:13 +08:00
restrict_columns :content , [ :title , :message ]
2023-04-29 04:59:51 +08:00
restrict_columns :settings , % i [ require_initial_post
discussion_type
assignment_id
pinned
locked
allow_rating
only_graders_can_rate
sort_by_rating
group_category_id ]
2017-11-07 01:41:44 +08:00
restrict_columns :state , [ :workflow_state ]
2020-12-10 12:40:39 +08:00
restrict_columns :availability_dates , [ :delayed_post_at , :lock_at ]
2017-03-07 06:42:44 +08:00
restrict_assignment_columns
2012-02-14 04:09:41 +08:00
2017-06-09 10:52:02 +08:00
attr_accessor :user_has_posted , :saved_by , :total_root_discussion_entries
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
2021-06-10 04:31:10 +08:00
module Errors
class LockBeforeDueDate < StandardError ; end
end
2021-12-22 06:54:25 +08:00
attr_readonly :context_id , :context_type , :user_id , :anonymous_state , :is_anonymous_author
2011-07-14 00:24:17 +08:00
2018-07-28 05:27:54 +08:00
has_many :discussion_entries , - > { order ( :created_at ) } , dependent : :destroy , inverse_of : :discussion_topic
2021-09-21 00:10:45 +08:00
has_many :discussion_entry_drafts , dependent : :destroy , inverse_of : :discussion_topic
2023-04-29 04:59:51 +08:00
has_many :rated_discussion_entries ,
lambda {
order (
Arel . sql ( " COALESCE(parent_id, 0) " ) , Arel . sql ( " COALESCE(rating_sum, 0) DESC " ) , :created_at
)
} ,
class_name : " DiscussionEntry "
2015-11-20 08:14:32 +08:00
has_many :root_discussion_entries , - > { preload ( :user ) . where ( " discussion_entries.parent_id IS NULL AND discussion_entries.workflow_state<>'deleted' " ) } , class_name : " DiscussionEntry "
2011-02-01 09:57:29 +08:00
has_one :external_feed_entry , as : :asset
2021-03-25 01:51:18 +08:00
belongs_to :root_account , class_name : " Account "
2011-02-01 09:57:29 +08:00
belongs_to :external_feed
2016-02-17 05:51:39 +08:00
belongs_to :context , polymorphic : [ :course , :group ]
2011-02-01 09:57:29 +08:00
belongs_to :attachment
belongs_to :editor , class_name : " User "
2012-06-21 02:58:03 +08:00
belongs_to :root_topic , class_name : " DiscussionTopic "
2014-04-11 06:39:45 +08:00
belongs_to :group_category
2023-10-11 05:31:17 +08:00
has_many :checkpoint_assignments , through : :assignment
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
2023-04-29 04:59:51 +08:00
has_many :discussion_topic_section_visibilities ,
lambda {
where ( " discussion_topic_section_visibilities.workflow_state<>'deleted' " )
} ,
inverse_of : :discussion_topic ,
dependent : :destroy
2017-12-12 05:22:03 +08:00
has_many :course_sections , through : :discussion_topic_section_visibilities , dependent : :destroy
2011-02-01 09:57:29 +08:00
belongs_to :user
2023-06-26 22:44:42 +08:00
has_one :master_content_tag , class_name : " MasterCourses::MasterContentTag " , inverse_of : :discussion_topic
2014-04-11 03:03:43 +08:00
2018-01-24 02:57:20 +08:00
validates_associated :discussion_topic_section_visibilities
2021-11-16 07:03:48 +08:00
validates :context_id , :context_type , presence : true
validates :discussion_type , inclusion : { in : DiscussionTypes :: TYPES }
2021-11-25 09:15:01 +08:00
validates :message , length : { maximum : maximum_long_text_length , allow_blank : true }
2021-11-16 07:03:48 +08:00
validates :title , length : { maximum : maximum_string_length , allow_nil : true }
2013-11-25 19:58:38 +08:00
validate :validate_draft_state_change , if : :workflow_state_changed?
2017-12-08 07:51:21 +08:00
validate :section_specific_topics_must_have_sections
2018-01-16 05:29:39 +08:00
validate :only_course_topics_can_be_section_specific
Disallows graded or group section specific discussions
Graded or group discussions can be assigned to people that are not part
of a given section, which could cause all sorts of headaches if that was
set with section specific announcements. Instead, this makes graded or
group discussions mutually exclusive with section specific discussions,
both for the API in the backend and for selecting the options in the
frontend.
Fixes COMMS-714
Test Plan:
- Make sure the section specific discussions feature flag is enabled
- Attempt to create a graded section specific discussion and a group
section specific discussion through the api and notice that it
returns a 4xx error. Here are some tips you could use for doing
this:
* Create a course with some groups and sections. Make note of the
course id, group ids, and section ids.
* Install httpie if it is not already
* Create an access token for your teacher/admin by going to
http://127.0.0.1:3000/profile/settings and clicking the
'New Access Token' button.
* In a terminal, save this token so that it can be easily used by
multiple tests bellow:
export TOKEN=<token_string>
* Use the following commands to test the API, substituting the
ids for the ones you made a note of above. Make sure they return
a 4xx error and have a related error message:
http POST :3000/api/v1/courses/1/discussion_topics Authorization:"Bearer $TOKEN" is_announcement:=false specific_sections="4,5" group_category_id="4"
echo '{"is_announcement": false, "specific_sections": "4,5", "set_assignment": "1", "assignment": {"points_possible": 1, "grading_type": "points"}}' | http POST :3000/api/v1/courses/1/discussion_topics Authorization:"Bearer $TOKEN"
- From create new discussions page, notice that if you change the
section specific stuff to not be `all sections`, the graded and
group options become disabled.
- From create new discussions page, notice that if you check the
graded or groups checkbox, the section specific field becomes
disabled.
- Notice that if you load an announcement that already has section
specific, graded, or grouped saved, it will probably have the
mutually exclusive fields disabled
- Notice that everything properly says that it is disabled when using
a screen reader. Also note that there is some funkyness with
selecting elements with the autocomplete component, that is already
known and being worked on by the instui team, and can be safely
ignored while QAing this.
- Make sure that you are not allowed to create a discussion if nothing
is selected for the section specific input, and that it properly
pops up an error message saying that 'You must input a section'
Change-Id: Ic63e733e7411116eb3fd637c81ad5cb1a63813ed
Reviewed-on: https://gerrit.instructure.com/139319
Reviewed-by: Venk Natarajan <vnatarajan@instructure.com>
Reviewed-by: Felix Milea-Ciobanu <fmileaciobanu@instructure.com>
QA-Review: Steven Burnett <sburnett@instructure.com>
Product-Review: Landon Gilbert-Bland <lbland@instructure.com>
Tested-by: Jenkins
2018-01-27 00:28:18 +08:00
validate :assignments_cannot_be_section_specific
validate :course_group_discussion_cannot_be_section_specific
2012-02-14 04:09:41 +08:00
2014-01-29 05:29:09 +08:00
sanitize_field :message , CanvasSanitize :: SANITIZE
2011-02-01 09:57:29 +08:00
copy_authorized_links ( :message ) { [ context , nil ] }
2013-11-26 06:33:50 +08:00
acts_as_list scope : { context : self , pinned : true }
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
before_create :initialize_last_reply_at
2020-05-09 04:53:05 +08:00
before_create :set_root_account_id
2011-02-01 09:57:29 +08:00
before_save :default_values
2013-05-03 07:39:26 +08:00
before_save :set_schedule_delayed_transitions
2011-02-01 09:57:29 +08:00
after_save :update_assignment
after_save :update_subtopics
after_save :touch_context
2013-05-03 07:39:26 +08:00
after_save :schedule_delayed_transitions
2014-10-15 18:38:00 +08:00
after_save :update_materialized_view_if_changed
2018-12-14 00:55:47 +08:00
after_save :recalculate_progressions_if_sections_changed
2019-04-18 04:17:00 +08:00
after_save :sync_attachment_with_publish_state
2021-03-13 03:46:01 +08:00
after_update :clear_non_applicable_stream_items
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
2017-12-08 07:51:21 +08:00
def section_specific_topics_must_have_sections
2018-01-11 22:09:21 +08:00
if ! deleted? && is_section_specific && discussion_topic_section_visibilities . none? ( & :active? )
2017-12-08 07:51:21 +08:00
errors . add ( :is_section_specific , t ( " Section specific topics must have sections " ) )
else
true
end
end
2018-01-16 05:29:39 +08:00
def only_course_topics_can_be_section_specific
if is_section_specific && ! ( context . is_a? Course )
2018-01-23 07:32:49 +08:00
errors . add ( :is_section_specific , t ( " Only course announcements and discussions can be section-specific " ) )
2018-01-16 05:29:39 +08:00
else
true
end
end
Disallows graded or group section specific discussions
Graded or group discussions can be assigned to people that are not part
of a given section, which could cause all sorts of headaches if that was
set with section specific announcements. Instead, this makes graded or
group discussions mutually exclusive with section specific discussions,
both for the API in the backend and for selecting the options in the
frontend.
Fixes COMMS-714
Test Plan:
- Make sure the section specific discussions feature flag is enabled
- Attempt to create a graded section specific discussion and a group
section specific discussion through the api and notice that it
returns a 4xx error. Here are some tips you could use for doing
this:
* Create a course with some groups and sections. Make note of the
course id, group ids, and section ids.
* Install httpie if it is not already
* Create an access token for your teacher/admin by going to
http://127.0.0.1:3000/profile/settings and clicking the
'New Access Token' button.
* In a terminal, save this token so that it can be easily used by
multiple tests bellow:
export TOKEN=<token_string>
* Use the following commands to test the API, substituting the
ids for the ones you made a note of above. Make sure they return
a 4xx error and have a related error message:
http POST :3000/api/v1/courses/1/discussion_topics Authorization:"Bearer $TOKEN" is_announcement:=false specific_sections="4,5" group_category_id="4"
echo '{"is_announcement": false, "specific_sections": "4,5", "set_assignment": "1", "assignment": {"points_possible": 1, "grading_type": "points"}}' | http POST :3000/api/v1/courses/1/discussion_topics Authorization:"Bearer $TOKEN"
- From create new discussions page, notice that if you change the
section specific stuff to not be `all sections`, the graded and
group options become disabled.
- From create new discussions page, notice that if you check the
graded or groups checkbox, the section specific field becomes
disabled.
- Notice that if you load an announcement that already has section
specific, graded, or grouped saved, it will probably have the
mutually exclusive fields disabled
- Notice that everything properly says that it is disabled when using
a screen reader. Also note that there is some funkyness with
selecting elements with the autocomplete component, that is already
known and being worked on by the instui team, and can be safely
ignored while QAing this.
- Make sure that you are not allowed to create a discussion if nothing
is selected for the section specific input, and that it properly
pops up an error message saying that 'You must input a section'
Change-Id: Ic63e733e7411116eb3fd637c81ad5cb1a63813ed
Reviewed-on: https://gerrit.instructure.com/139319
Reviewed-by: Venk Natarajan <vnatarajan@instructure.com>
Reviewed-by: Felix Milea-Ciobanu <fmileaciobanu@instructure.com>
QA-Review: Steven Burnett <sburnett@instructure.com>
Product-Review: Landon Gilbert-Bland <lbland@instructure.com>
Tested-by: Jenkins
2018-01-27 00:28:18 +08:00
def assignments_cannot_be_section_specific
if is_section_specific && assignment
errors . add ( :is_section_specific , t ( " Discussion assignments cannot be section-specific " ) )
else
true
end
end
def course_group_discussion_cannot_be_section_specific
if is_section_specific && has_group_category?
errors . add ( :is_section_specific , t ( " Discussions with groups cannot be section-specific " ) )
else
true
end
end
2021-04-28 07:59:15 +08:00
def sections_for ( user )
return unless is_section_specific?
2021-09-23 00:20:17 +08:00
2022-03-17 02:46:51 +08:00
unlocked_teacher = context . enrollments . active . instructor
2023-06-02 06:06:09 +08:00
. where ( limit_privileges_to_course_section : false , user : )
2022-03-17 02:46:51 +08:00
if unlocked_teacher . count > 0
CourseSection . where ( id : DiscussionTopicSectionVisibility . active
. where ( discussion_topic_id : id )
. select ( " discussion_topic_section_visibilities.course_section_id " ) )
else
CourseSection . where ( id : DiscussionTopicSectionVisibility . active . where ( discussion_topic_id : id )
2023-04-26 07:04:05 +08:00
. where ( Enrollment . active_or_pending
2022-03-17 02:46:51 +08:00
. where ( user_id : user )
2023-04-26 07:04:05 +08:00
. where ( " enrollments.course_section_id = discussion_topic_section_visibilities.course_section_id " )
. arel . exists )
2022-03-17 02:46:51 +08:00
. select ( " discussion_topic_section_visibilities.course_section_id " ) )
end
2021-04-28 07:59:15 +08:00
end
def address_book_context_for ( user )
if is_section_specific?
sections_for ( user )
else
context
end
end
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?
2021-07-29 09:08:00 +08:00
discussion_type == DiscussionTypes :: THREADED || context . feature_enabled? ( " react_discussions_post " )
2012-03-28 09:10:47 +08:00
end
2012-04-28 06:36:42 +08:00
alias_method :threaded , :threaded?
2012-03-28 09:10:47 +08:00
def discussion_type
read_attribute ( :discussion_type ) || DiscussionTypes :: SIDE_COMMENT
end
2013-11-25 19:58:38 +08:00
def validate_draft_state_change
old_draft_state , new_draft_state = changes [ " workflow_state " ]
return if old_draft_state == new_draft_state
2021-09-23 00:20:17 +08:00
2013-11-25 19:58:38 +08:00
if new_draft_state == " unpublished " && ! can_unpublish?
errors . add :workflow_state , I18n . t ( " # discussion_topics.error_draft_state_with_posts " ,
" This topic cannot be set to draft state because it contains posts. " )
end
end
2011-02-01 09:57:29 +08:00
def default_values
self . context_code = " #{ context_type . underscore } _ #{ context_id } "
2020-12-23 03:53:28 +08:00
if title . blank?
self . title = t ( " # discussion_topic.default_title " , " No Title " )
end
2021-07-29 07:24:23 +08:00
d_type = read_attribute ( :discussion_type )
d_type || = context . feature_enabled? ( " react_discussions_post " ) ? DiscussionTypes :: THREADED : DiscussionTypes :: SIDE_COMMENT
self . discussion_type = d_type
2011-02-01 09:57:29 +08:00
@content_changed = message_changed? || title_changed?
2021-02-17 05:03:25 +08:00
2016-04-08 00:37:16 +08:00
default_submission_values
2021-02-17 05:03:25 +08:00
2014-04-11 06:39:45 +08:00
if has_group_category?
2018-04-02 02:57:22 +08:00
self . subtopics_refreshed_at || = Time . zone . parse ( " Jan 1 2000 " )
2011-02-01 09:57:29 +08:00
end
2021-10-07 06:17:12 +08:00
self . lock_at = CanvasTime . fancy_midnight ( lock_at & . in_time_zone ( context . time_zone ) )
2017-03-29 00:04:33 +08:00
% i [
2023-04-29 04:59:51 +08:00
could_be_locked
podcast_enabled
podcast_has_student_posts
require_initial_post
pinned
locked
allow_rating
only_graders_can_rate
sort_by_rating
2017-03-29 00:04:33 +08:00
] . each { | attr | self [ attr ] = false if self [ attr ] . nil? }
2011-02-01 09:57:29 +08:00
end
protected :default_values
2012-02-14 04:09:41 +08:00
2014-04-11 06:39:45 +08:00
def has_group_category?
2014-11-05 05:40:08 +08:00
! ! group_category_id
2014-04-11 06:39:45 +08:00
end
2013-05-03 07:39:26 +08:00
def set_schedule_delayed_transitions
2020-03-04 06:04:35 +08:00
@delayed_post_at_changed = delayed_post_at_changed?
if delayed_post_at? && @delayed_post_at_changed
2015-11-06 03:49:19 +08:00
@should_schedule_delayed_post = true
self . workflow_state = " post_delayed " if [ :migration , :after_migration ] . include? ( saved_by ) && delayed_post_at > Time . now
end
if lock_at && lock_at_changed?
@should_schedule_lock_at = true
self . locked = false if [ :migration , :after_migration ] . include? ( saved_by ) && lock_at > Time . now
end
2011-02-01 09:57:29 +08:00
true
end
2012-02-14 04:09:41 +08:00
2014-10-15 18:38:00 +08:00
def update_materialized_view_if_changed
2018-03-22 05:17:34 +08:00
if saved_change_to_sort_by_rating?
2014-10-15 18:38:00 +08:00
update_materialized_view
end
end
2018-12-14 00:55:47 +08:00
attr_writer :sections_changed
2021-09-23 00:20:17 +08:00
2018-12-14 00:55:47 +08:00
def recalculate_progressions_if_sections_changed
# either changed sections or undid section specificness
return unless is_section_specific? ? @sections_changed : is_section_specific_before_last_save
2021-09-23 00:20:17 +08:00
2018-12-14 00:55:47 +08:00
self . class . connection . after_transaction_commit do
if context_module_tags . preload ( :context_module ) . exists?
2019-10-01 21:17:20 +08:00
context_module_tags . map ( & :context_module ) . uniq . each do | cm |
cm . invalidate_progressions
cm . touch
end
2018-12-14 00:55:47 +08:00
end
end
end
2013-05-03 07:39:26 +08:00
def schedule_delayed_transitions
2015-07-07 21:12:37 +08:00
return if saved_by == :migration
2021-09-23 00:20:17 +08:00
2020-12-10 12:40:39 +08:00
bp = true if @importing_migration & . migration_type == " master_course_import "
delay ( run_at : delayed_post_at ) . update_based_on_date ( for_blueprint : bp ) if @should_schedule_delayed_post
delay ( run_at : lock_at ) . update_based_on_date ( for_blueprint : bp ) if @should_schedule_lock_at
2013-11-25 15:56:28 +08:00
# need to clear these in case we do a save whilst saving (e.g.
# Announcement#respect_context_lock_rules), so as to avoid the dreaded
# double delayed job ಠ_ಠ
@should_schedule_delayed_post = nil
@should_schedule_lock_at = nil
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2019-04-18 04:17:00 +08:00
def sync_attachment_with_publish_state
if ( saved_change_to_workflow_state? || saved_change_to_locked? || saved_change_to_attachment_id? ) &&
attachment && ! attachment . hidden? # if it's already hidden leave alone
locked = ! ! ( unpublished? || not_available_yet? || not_available_anymore? )
attachment . update_attribute ( :locked , locked )
end
end
2011-02-01 09:57:29 +08:00
def update_subtopics
2018-03-22 05:17:34 +08:00
if ! deleted? && ( has_group_category? || ! ! group_category_id_before_last_save )
2019-06-03 21:04:17 +08:00
delay_if_production ( singleton : " refresh_subtopics_ #{ global_id } " ) . 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
2016-03-24 22:02:45 +08:00
sub_topics = [ ]
category = group_category
if category && root_topic_id . blank? && ! deleted?
2019-06-03 21:04:17 +08:00
category . groups . active . order ( :id ) . each do | group |
2016-03-24 22:02:45 +08:00
sub_topics << ensure_child_topic_for ( group )
2015-11-18 04:18:01 +08:00
end
end
2016-04-06 03:50:28 +08:00
2016-03-24 22:02:45 +08:00
shard . activate do
# delete any lingering child topics
DiscussionTopic . where ( root_topic_id : self ) . where . not ( id : sub_topics ) . update_all ( workflow_state : " deleted " )
2016-03-09 00:26:58 +08:00
end
end
def ensure_child_topic_for ( group )
group . shard . activate do
DiscussionTopic . unique_constraint_retry do
topic = DiscussionTopic . where ( context_id : group , context_type : " Group " , root_topic_id : self ) . first
topic || = group . discussion_topics . build { | dt | dt . root_topic = self }
topic . message = message
2016-03-24 22:02:45 +08:00
topic . title = CanvasTextHelper . truncate_text ( " #{ title } - #{ group . name } " , { max_length : 250 } ) # because of course people do this
2016-03-09 00:26:58 +08:00
topic . assignment_id = assignment_id
topic . attachment_id = attachment_id
topic . group_category_id = group_category_id
topic . user_id = user_id
topic . discussion_type = discussion_type
topic . workflow_state = workflow_state
topic . allow_rating = allow_rating
topic . only_graders_can_rate = only_graders_can_rate
topic . sort_by_rating = sort_by_rating
topic . save if topic . changed?
topic
2012-06-21 02:58:03 +08:00
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
def update_assignment
2012-06-21 02:58:03 +08:00
return if deleted?
2021-09-23 00:20:17 +08:00
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 ! assignment_id && @old_assignment_id
2022-04-19 07:29:19 +08:00
context_module_tags . find_each do | cmt |
cmt . confirm_valid_module_requirements
cmt . update_course_pace_module_items
end
2011-02-01 09:57:29 +08:00
end
if @old_assignment_id
2023-06-02 06:06:09 +08:00
Assignment . where ( id : @old_assignment_id , context_id : , context_type : , submission_types : " discussion_topic " ) . update_all ( workflow_state : " deleted " , updated_at : Time . now . utc )
2019-02-28 06:17:47 +08:00
old_assignment = Assignment . find ( @old_assignment_id )
ContentTag . delete_for ( old_assignment )
# prevent future syncs from recreating the deleted assignment
if is_child_content?
old_assignment . submission_types = " none "
2021-08-18 07:10:55 +08:00
own_tag = MasterCourses :: ChildContentTag . where ( content : self ) . take
2019-06-13 00:36:08 +08:00
own_tag & . child_subscription & . create_content_tag_for! ( old_assignment , downstream_changes : [ " workflow_state " ] )
2019-02-28 06:17:47 +08:00
end
2012-04-28 03:46:03 +08:00
elsif assignment && @saved_by != :assignment && ! root_topic_id
2016-04-08 00:37:16 +08:00
deleted_assignment = assignment . deleted?
sync_assignment
assignment . workflow_state = " published " if is_announcement && deleted_assignment
2011-02-01 09:57:29 +08:00
assignment . description = message
2018-03-22 05:17:34 +08:00
if saved_change_to_group_category_id?
2015-12-17 00:35:49 +08:00
assignment . validate_assignment_overrides ( force_override_destroy : true )
end
2011-02-01 09:57:29 +08:00
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
2022-04-19 07:29:19 +08:00
# have posted have contributed to the topic and that course paces are up
# to date
2018-03-22 05:17:34 +08:00
if assignment_id && saved_change_to_assignment_id?
2017-01-28 04:53:25 +08:00
recalculate_context_module_actions!
2022-04-19 07:29:19 +08:00
context_module_tags . find_each ( & :update_course_pace_module_items )
2011-11-10 01:33:13 +08:00
end
2011-02-01 09:57:29 +08:00
end
protected :update_assignment
2012-02-14 04:09:41 +08:00
2017-01-28 04:53:25 +08:00
def recalculate_context_module_actions!
posters . each { | user | context_module_action ( user , :contributed ) }
end
2011-02-01 09:57:29 +08:00
def is_announcement
false
end
2012-02-14 04:09:41 +08:00
2021-02-17 05:03:25 +08:00
def homeroom_announcement? ( _context )
false
end
2011-02-01 09:57:29 +08:00
def root_topic?
2014-04-11 06:39:45 +08:00
! root_topic_id && has_group_category?
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2012-08-28 05:50:54 +08:00
# only the root level entries
2011-02-01 09:57:29 +08:00
def discussion_subentries
root_discussion_entries
end
2012-02-14 04:09:41 +08:00
2012-08-28 05:50:54 +08:00
# count of all active discussion_entries
2011-02-01 09:57:29 +08:00
def discussion_subentry_count
2012-08-28 05:50:54 +08:00
discussion_entries . active . count
2011-02-01 09:57:29 +08:00
end
2014-04-11 06:39:45 +08:00
def for_group_discussion?
has_group_category? && root_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 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 ( message , max_length : 250 )
end
2012-02-14 04:09:41 +08:00
def create_participant
2023-06-02 06:06:09 +08:00
discussion_topic_participants . create ( user : , workflow_state : " read " , unread_entry_count : 0 , subscribed : ! subscription_hold ( user , nil ) ) if user
2012-02-14 04:09:41 +08:00
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
2018-03-28 03:48:47 +08:00
self . class . connection . after_transaction_commit do
DiscussionTopic :: MaterializedView . for ( self ) . update_materialized_view ( xlog_location : self . class . current_xlog_location )
end
2012-03-10 00:34:40 +08:00
end
2014-12-31 05:38:29 +08:00
def group_category_deleted_with_entries?
group_category . try ( :deleted_at? ) && ! can_group?
end
2017-06-20 03:27:39 +08:00
def get_potentially_conflicting_titles ( title_base )
2023-06-02 06:06:09 +08:00
DiscussionTopic . active . where ( context_type : , context_id : )
2018-02-22 00:49:16 +08:00
. starting_with_title ( title_base ) . pluck ( " title " ) . to_set
2017-06-20 03:27:39 +08:00
end
# This is a guess of what to copy over.
def duplicate_base_model ( title , opts )
DiscussionTopic . new ( {
2023-06-02 06:06:09 +08:00
title : ,
message : ,
context_id : ,
context_type : ,
2017-06-20 03:27:39 +08:00
user_id : opts [ :user ] ? opts [ :user ] . id : user_id ,
2023-06-02 06:06:09 +08:00
type : ,
2017-06-20 03:27:39 +08:00
workflow_state : " unpublished " ,
2023-06-02 06:06:09 +08:00
could_be_locked : ,
context_code : ,
podcast_enabled : ,
require_initial_post : ,
podcast_has_student_posts : ,
discussion_type : ,
delayed_post_at : ,
lock_at : ,
pinned : ,
locked : ,
group_category_id : ,
allow_rating : ,
only_graders_can_rate : ,
sort_by_rating : ,
todo_date : ,
is_section_specific : ,
anonymous_state :
2017-06-20 03:27:39 +08:00
} )
end
# Presumes that self has no parents
# Does not duplicate the child topics; the hooks take care of that for us.
def duplicate ( opts = { } )
# Don't clone a new record
return self if new_record?
2021-09-23 00:20:17 +08:00
2017-06-20 03:27:39 +08:00
default_opts = {
duplicate_assignment : true ,
copy_title : nil ,
user : nil
}
opts_with_default = default_opts . merge ( opts )
2017-10-09 23:04:57 +08:00
copy_title =
opts_with_default [ :copy_title ] || get_copy_title ( self , t ( " Copy " ) , title )
2017-06-20 03:27:39 +08:00
result = duplicate_base_model ( copy_title , opts_with_default )
2018-03-13 05:08:20 +08:00
# Start with a position guaranteed to not conflict with existing ones.
# Clients are encouraged to set the correct position later on and do
# an insert_at upon save.
if pinned
result . position = context . discussion_topics . active . where ( pinned : true ) . maximum ( :position ) + 1
end
2017-06-20 03:27:39 +08:00
if assignment && opts_with_default [ :duplicate_assignment ]
result . assignment = assignment . duplicate ( {
duplicate_discussion_topic : false ,
copy_title : result . title
} )
end
2018-03-01 03:00:30 +08:00
result . discussion_topic_section_visibilities = [ ]
if is_section_specific
original_visibilities = discussion_topic_section_visibilities . active
original_visibilities . each do | visibility |
new_visibility = DiscussionTopicSectionVisibility . new (
discussion_topic : result ,
course_section : visibility . course_section
)
result . discussion_topic_section_visibilities << new_visibility
end
end
2017-06-20 03:27:39 +08:00
# For some reason, the relation doesn't take care of this for us. Don't understand why.
# Without this line, *two* discussion topic duplicates appear when a save is performed.
result . assignment & . discussion_topic = result
result
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
2021-09-23 00:20:17 +08:00
2012-02-14 04:09:41 +08:00
def read_state ( current_user = nil )
current_user || = self . current_user
return " read " unless current_user # default for logged out user
2021-09-23 00:20:17 +08:00
2012-02-14 04:09:41 +08:00
uid = current_user . is_a? ( User ) ? current_user . id : current_user
2021-11-04 05:36:34 +08:00
ws = if discussion_topic_participants . loaded?
discussion_topic_participants . detect { | dtp | dtp . user_id == uid } & . workflow_state
else
discussion_topic_participants . where ( user_id : uid ) . pick ( :workflow_state )
end
ws || " unread "
2012-02-14 04:09:41 +08:00
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
2021-09-23 00:20:17 +08:00
2013-07-19 23:06:41 +08:00
context_module_action ( current_user , :read ) if new_state == " read "
2013-11-25 19:58:38 +08:00
2012-09-26 00:44:35 +08:00
return true if new_state == read_state ( current_user )
new dashboard design
the new dashboard design categorizes recent activity into buckets that can be
expanded/collapsed, and inidividual messages can be dismissed. the categories
are announcements, conversations, discussions and assignments. this redesign
applies to the homepage dashboard, the group home page, and the course homepage
when "recent activity dashboard" is selected as the course home page type.o
the motiviation is that the dashboard should capture and present in one place
important information happening in all the user's courses or groups, and allow
for jumping into this information to see more details:
- announcements/discussions should show on the dashboard when they are created,
or when there are root replies to recent announcements
- conversations should show on the dashboard when there is new activity
- assignments should show on the dashboard when they are created, or when
changes are made at least a couple hours after being created
the presence of a dashboard item means there is activity for that item that may
be of interest to the user. additionally, the dashboard items will show
read/unread state (excluding assignments) for items which the user has not yet
viewed.
additionally, global messages such as course inivitations, account level
announcements, and new user messages have been restyled, but will keep their
place above the recent activity widget on the dashboard.
test plan:
- visit many exising user's dashboards and make sure they are functional in the
new style.
- visit canvas as a brand new user (no enrollments), a new user enrolled in
a new course and make sure the dashboard is restyled and the messaging makes
sense.
- make an account level announcement and make sure it shows up on user's
dashboards.
- create all different types of conversations: single, group, bulk private,
from submission comment, add user to convo, etc. and make sure the
appropriate dashboard items appear and make sense
- create discussions and announcements, reply to them at the root level and at
the sub entry level (sub entries will not make new dashboard items), test
from both a read and unread user's perspective, making sure dashboard items are
correct. (note that read/unread state will not be correct for existing items
before this code is applied, but should be correct for future items moving
forward)
- dismiss dashboard items and account announcements, make sure they stay
dismissed.
- test creating assignments, waiting > 2 hours, and updating due dates or other
assignment details. make sure items appear. note that unread state will not
exist for assignment notifications.
closes #10783
refs #11038
refs #11039
Change-Id: I276a8cb1fae4c8a46425d0a368455e15a0c470c5
Reviewed-on: https://gerrit.instructure.com/14540
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
2012-10-05 05:49:54 +08:00
StreamItem . update_read_state_for_asset ( self , new_state , current_user . id )
2023-06-02 06:06:09 +08:00
update_or_create_participant ( current_user : , new_state : )
2012-02-14 04:09:41 +08:00
end
2013-05-07 03:38:50 +08:00
def change_all_read_state ( new_state , current_user = nil , opts = { } )
2012-02-14 04:09:41 +08:00
current_user || = self . current_user
return unless current_user
2014-01-09 07:32:14 +08:00
update_fields = { workflow_state : new_state }
2013-05-07 03:38:50 +08:00
update_fields [ :forced_read_state ] = opts [ :forced ] if opts . key? ( :forced )
2012-02-14 04:09:41 +08:00
transaction do
2014-01-09 07:32:14 +08:00
update_stream_item_state ( current_user , new_state )
update_participants_read_state ( current_user , new_state , update_fields )
end
end
def update_stream_item_state ( current_user , new_state )
context_module_action ( current_user , :read ) if new_state == " read "
StreamItem . update_read_state_for_asset ( self , new_state , current_user . id )
end
protected :update_stream_item_state
def update_participants_read_state ( current_user , new_state , update_fields )
2021-08-30 21:44:27 +08:00
# if workflow_state is unread, and force_read_state is not provided then
# mark everything as unread but use the defaults, or allow other entries to
# be implicitly unread, but still update any existing records.
if new_state == " unread " && ! update_fields . key? ( :forced_read_state )
DiscussionEntryParticipant . where ( discussion_entry_id : discussion_entries . select ( :id ) , user : current_user )
. where . not ( workflow_state : new_state )
. in_batches . update_all ( update_fields )
else
2023-04-29 04:59:51 +08:00
DiscussionEntryParticipant . upsert_for_topic ( self ,
current_user ,
2023-06-02 06:06:09 +08:00
new_state : ,
2021-08-30 21:44:27 +08:00
forced : update_fields [ :forced_read_state ] )
end
2023-06-02 06:06:09 +08:00
update_or_create_participant ( current_user : ,
new_state : ,
2014-01-09 07:32:14 +08:00
new_count : ( new_state == " unread " ) ? default_unread_count : 0 )
end
protected :update_participants_read_state
2012-02-14 04:09:41 +08:00
def default_unread_count
2013-05-01 00:58:07 +08:00
discussion_entries . active . count
2012-02-14 04:09:41 +08:00
end
2015-12-17 05:50:14 +08:00
# Do not use the lock options unless you truly need
# the lock, for instance to update the count.
# Careless use has caused database transaction deadlocks
2018-06-19 09:08:06 +08:00
def unread_count ( current_user = nil , lock : false , opts : { } )
2012-02-14 04:09:41 +08:00
current_user || = self . current_user
return 0 unless current_user # default for logged out users
2015-12-17 05:50:14 +08:00
2020-10-06 06:42:27 +08:00
environment = lock ? :primary : :secondary
GuardRail . activate ( environment ) do
2018-06-19 09:08:06 +08:00
topic_participant = if opts [ :use_preload ] && association ( :discussion_topic_participants ) . loaded?
discussion_topic_participants . find { | dtp | dtp . user_id == current_user . id }
else
discussion_topic_participants . where ( user_id : current_user ) . select ( :unread_entry_count ) . lock ( lock ) . take
end
2017-07-27 09:34:30 +08:00
topic_participant & . unread_entry_count || default_unread_count
2013-08-27 02:33:36 +08:00
end
2012-02-14 04:09:41 +08:00
end
2013-07-19 06:57:10 +08:00
# Cases where you CAN'T subscribe:
# - initial post is required and you haven't made one
# - it's an announcement
# - this is a root level graded group discussion and you aren't in any of the groups
# - this is group level discussion and you aren't in the group
2021-10-22 00:18:16 +08:00
def subscription_hold ( user , session )
2014-09-16 23:46:59 +08:00
return nil unless user
2021-09-23 00:20:17 +08:00
2017-05-04 05:40:51 +08:00
if initial_post_required? ( user , session )
2013-07-19 06:57:10 +08:00
:initial_post_required
elsif root_topic? && ! child_topic_for ( user )
:not_in_group_set
elsif context . is_a? ( Group ) && ! context . has_member? ( user )
:not_in_group
end
end
2018-06-19 09:08:06 +08:00
def subscribed? ( current_user = nil , opts : { } )
2013-06-12 03:55:33 +08:00
current_user || = self . current_user
return false unless current_user # default for logged out user
2013-07-06 04:51:07 +08:00
if root_topic?
participant = DiscussionTopicParticipant . where ( user_id : current_user . id ,
2018-06-19 09:08:06 +08:00
discussion_topic_id : child_topics . pluck ( :id ) ) . take
2013-07-06 04:51:07 +08:00
end
2018-06-19 09:08:06 +08:00
participant || = if opts [ :use_preload ] && association ( :discussion_topic_participants ) . loaded?
discussion_topic_participants . find { | dtp | dtp . user_id == current_user . id }
else
discussion_topic_participants . where ( user_id : current_user ) . take
end
2013-07-12 05:20:34 +08:00
if participant
if participant . subscribed . nil?
# if there is no explicit subscription, assume the author and posters
# are subscribed, everyone else is not subscribed
2021-10-22 00:18:16 +08:00
( current_user == user || participant . discussion_topic . posters . include? ( current_user ) ) && ! participant . discussion_topic . subscription_hold ( current_user , nil )
2013-07-12 05:20:34 +08:00
else
participant . subscribed
end
2013-07-06 04:51:07 +08:00
else
2021-10-22 00:18:16 +08:00
current_user == user && ! subscription_hold ( current_user , nil )
2013-07-06 04:51:07 +08:00
end
2013-06-12 03:55:33 +08:00
end
2013-07-06 04:51:07 +08:00
2013-06-12 03:55:33 +08:00
def subscribe ( current_user = nil )
change_subscribed_state ( true , current_user )
end
def unsubscribe ( current_user = nil )
change_subscribed_state ( false , current_user )
end
def change_subscribed_state ( new_state , current_user = nil )
current_user || = self . current_user
return unless current_user
return true if subscribed? ( current_user ) == new_state
2013-07-06 04:51:07 +08:00
if root_topic?
2021-05-25 08:12:47 +08:00
return if change_child_topic_subscribed_state ( new_state , current_user )
ctss = DiscussionTopicParticipant . new
ctss . errors . add ( :discussion_topic_id , I18n . t ( " no child topic found " ) )
ctss
2013-07-06 04:51:07 +08:00
else
2023-06-02 06:06:09 +08:00
update_or_create_participant ( current_user : , subscribed : new_state )
2013-07-06 04:51:07 +08:00
end
end
2013-07-19 06:57:10 +08:00
def child_topic_for ( user )
2021-06-09 02:18:03 +08:00
return unless context . is_a? ( Course )
2013-07-19 06:57:10 +08:00
group_ids = user . group_memberships . active . pluck ( :group_id ) &
2013-07-06 04:51:07 +08:00
context . groups . active . pluck ( :id )
2016-10-29 02:39:46 +08:00
child_topics . active . where ( context_id : group_ids , context_type : " Group " ) . first
2013-07-19 06:57:10 +08:00
end
def change_child_topic_subscribed_state ( new_state , current_user )
topic = child_topic_for ( current_user )
2023-06-02 06:06:09 +08:00
topic & . update_or_create_participant ( current_user : , subscribed : new_state )
2013-06-12 03:55:33 +08:00
end
2013-07-06 04:51:07 +08:00
protected :change_child_topic_subscribed_state
2012-02-14 04:09:41 +08:00
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
2020-10-06 06:42:27 +08:00
GuardRail . activate ( :primary ) do
2013-08-27 02:33:36 +08:00
DiscussionTopic . uncached do
DiscussionTopic . unique_constraint_retry do
topic_participant = discussion_topic_participants . where ( user_id : current_user ) . lock . first
topic_participant || = discussion_topic_participants . build ( user : current_user ,
2015-12-17 05:50:14 +08:00
unread_entry_count : unread_count ( current_user , lock : true ) ,
2013-08-27 02:33:36 +08:00
workflow_state : " unread " ,
2021-10-22 00:18:16 +08:00
subscribed : current_user == user && ! subscription_hold ( current_user , nil ) )
2013-08-27 02:33:36 +08:00
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 . subscribed = opts [ :subscribed ] if opts . key? ( :subscribed )
topic_participant . save
end
2012-03-18 04:50:24 +08:00
end
2012-03-18 01:15:45 +08:00
end
2012-02-14 04:09:41 +08:00
topic_participant
end
2017-05-26 10:34:01 +08:00
scope :not_ignored_by , lambda { | user , purpose |
2023-06-02 06:06:09 +08:00
where . not ( Ignore . where ( asset_type : " DiscussionTopic " , user_id : user , purpose : )
2023-04-26 07:04:05 +08:00
. where ( " asset_id=discussion_topics.id " ) . arel . exists )
2021-11-23 23:20:14 +08:00
}
2017-05-26 10:34:01 +08:00
2017-05-27 08:52:50 +08:00
scope :todo_date_between , lambda { | starting , ending |
where ( " (discussion_topics.type = 'Announcement' AND posted_at BETWEEN :start_at and :end_at)
2023-04-29 04:59:51 +08:00
OR todo_date BETWEEN :start_at and :end_at " ,
{ start_at : starting , end_at : ending } )
2021-11-23 23:20:14 +08:00
}
2017-05-27 08:52:50 +08:00
scope :for_courses_and_groups , lambda { | course_ids , group_ids |
2017-05-26 10:34:01 +08:00
where ( " (discussion_topics.context_type = 'Course'
AND discussion_topics . context_id IN ( ?) )
OR ( discussion_topics . context_type = 'Group'
2023-04-29 04:59:51 +08:00
AND discussion_topics . context_id IN ( ?) ) " ,
course_ids ,
group_ids )
2021-11-23 23:20:14 +08:00
}
2017-05-26 10:34:01 +08:00
2017-12-12 02:07:32 +08:00
class QueryError < StandardError
attr_accessor :status_code
def initialize ( message = nil , status_code = nil )
super ( message )
self . status_code = status_code
end
end
# Retrieves all the *course* (as oppposed to group) discussion topics that apply
# to the given sections. Group topics will not be returned. TODO: figure out
# a good way to deal with group topics here.
#
# Takes in an array of section objects, and it is required that they all belong
# to the same course. At least one section must be provided.
scope :in_sections , lambda { | course_sections |
course_ids = course_sections . pluck ( :course_id ) . uniq
if course_ids . length != 1
raise QueryError , I18n . t ( " Searching for announcements in sections must span exactly one course " )
end
2021-11-18 06:53:37 +08:00
2017-12-12 02:07:32 +08:00
course_id = course_ids . first
joins ( " LEFT OUTER JOIN #{ DiscussionTopicSectionVisibility . quoted_table_name }
AS discussion_section_visibilities ON discussion_topics . is_section_specific = true AND
discussion_section_visibilities . discussion_topic_id = discussion_topics . id " )
. where ( " discussion_topics.context_type = 'Course' AND
2023-04-29 04:59:51 +08:00
discussion_topics . context_id = :course_id " ,
2023-06-02 06:06:09 +08:00
{ course_id : } )
2017-12-12 02:07:32 +08:00
. where ( " discussion_section_visibilities.id IS null OR
( discussion_section_visibilities . workflow_state = 'active' AND
discussion_section_visibilities . course_section_id IN ( :course_sections ) ) " ,
{ course_sections : course_sections . pluck ( :id ) } ) . distinct
2021-11-23 23:20:14 +08:00
}
2017-12-12 02:07:32 +08:00
2018-08-31 01:20:33 +08:00
scope :visible_to_student_sections , lambda { | student |
2023-04-26 07:04:05 +08:00
visibility_scope = DiscussionTopicSectionVisibility
. active
. where ( " discussion_topic_section_visibilities.discussion_topic_id = discussion_topics.id " )
. where (
Enrollment . active_or_pending . where ( user_id : student )
. where ( " enrollments.course_section_id = discussion_topic_section_visibilities.course_section_id " )
. arel . exists
)
merge (
DiscussionTopic . where . not ( discussion_topics : { context_type : " Course " } )
. or ( DiscussionTopic . where ( discussion_topics : { is_section_specific : false } ) )
. or ( DiscussionTopic . where ( visibility_scope . arel . exists ) )
)
2018-08-31 01:20:33 +08:00
}
2014-07-02 03:38:26 +08:00
scope :recent , - > { where ( " discussion_topics.last_reply_at>? " , 2 . weeks . ago ) . order ( " discussion_topics.last_reply_at DESC " ) }
scope :only_discussion_topics , - > { where ( type : nil ) }
scope :for_subtopic_refreshing , - > { where ( " discussion_topics.subtopics_refreshed_at IS NOT NULL AND discussion_topics.subtopics_refreshed_at<discussion_topics.updated_at " ) . order ( " discussion_topics.subtopics_refreshed_at " ) }
scope :active , - > { where ( " discussion_topics.workflow_state<>'deleted' " ) }
scope :for_context_codes , - > ( codes ) { where ( context_code : codes ) }
2012-02-14 04:09:41 +08:00
2013-03-21 03:38:19 +08:00
scope :before , - > ( date ) { where ( " discussion_topics.created_at<? " , date ) }
2012-02-14 04:09:41 +08:00
2014-07-02 03:38:26 +08:00
scope :by_position , - > { order ( " discussion_topics.position ASC, discussion_topics.created_at DESC, discussion_topics.id DESC " ) }
scope :by_position_legacy , - > { order ( " discussion_topics.position DESC, discussion_topics.created_at DESC, discussion_topics.id DESC " ) }
scope :by_last_reply_at , - > { order ( " discussion_topics.last_reply_at DESC, discussion_topics.created_at DESC, discussion_topics.id DESC " ) }
2012-08-25 08:32:57 +08:00
2020-09-03 01:59:33 +08:00
scope :by_posted_at , lambda {
2021-11-13 03:01:16 +08:00
order ( Arel . sql ( << ~ SQL . squish ) )
2020-12-10 12:40:39 +08:00
COALESCE ( discussion_topics . delayed_post_at , discussion_topics . posted_at , discussion_topics . created_at ) DESC ,
discussion_topics . created_at DESC ,
discussion_topics . id DESC
2015-05-07 03:40:48 +08:00
SQL
}
2015-04-02 07:47:52 +08:00
2017-06-09 10:52:02 +08:00
scope :read_for , lambda { | user |
eager_load ( :discussion_topic_participants )
. where ( " discussion_topic_participants.id IS NOT NULL
AND ( discussion_topic_participants . user_id = :user
AND discussion_topic_participants . workflow_state = 'read' ) " ,
2023-06-02 06:06:09 +08:00
user : )
2017-06-09 10:52:02 +08:00
}
scope :unread_for , lambda { | user |
2017-07-13 00:42:02 +08:00
joins ( sanitize_sql ( [ " LEFT OUTER JOIN #{ DiscussionTopicParticipant . quoted_table_name } ON
discussion_topic_participants . discussion_topic_id = discussion_topics . id AND
2023-04-29 04:59:51 +08:00
discussion_topic_participants . user_id = ?" ,
user . id ] ) )
2017-07-13 00:42:02 +08:00
. where ( " discussion_topic_participants IS NULL
OR discussion_topic_participants . workflow_state < > 'read'
OR discussion_topic_participants . unread_entry_count > 0 " )
2017-06-09 10:52:02 +08:00
}
scope :published , - > { where ( " discussion_topics.workflow_state = 'active' " ) }
2017-06-20 03:27:39 +08:00
# TODO: this scope is appearing in a few models now with identical code.
# Can this be extracted somewhere?
scope :starting_with_title , lambda { | title |
where ( " title ILIKE ? " , " #{ title } % " )
}
2013-11-25 19:58:38 +08:00
alias_attribute :available_from , :delayed_post_at
alias_attribute :unlock_at , :delayed_post_at
alias_attribute :available_until , :lock_at
2013-07-02 06:27:46 +08:00
def should_lock_yet
2013-11-25 19:58:38 +08:00
# not assignment or vdd aware! only use this to check the topic's own field!
# you should be checking other lock statuses in addition to this one
lock_at && lock_at < Time . now . utc
2013-05-03 07:39:26 +08:00
end
2013-11-25 19:58:38 +08:00
alias_method :not_available_anymore? , :should_lock_yet
2013-05-03 07:39:26 +08:00
2013-07-02 06:27:46 +08:00
def should_not_post_yet
2013-11-25 19:58:38 +08:00
# not assignment or vdd aware! only use this to check the topic's own field!
# you should be checking other lock statuses in addition to this one
delayed_post_at && delayed_post_at > Time . now . utc
2013-05-03 07:39:26 +08:00
end
2013-11-25 19:58:38 +08:00
alias_method :not_available_yet? , :should_not_post_yet
2013-05-03 07:39:26 +08:00
2013-07-02 06:27:46 +08:00
# There may be delayed jobs that expect to call this to update the topic, so be sure to alias
# the old method name if you change it
2020-12-10 12:40:39 +08:00
# Also: if this method is scheduled by a blueprint sync, ensure it isn't counted as a manual downstream change
def update_based_on_date ( for_blueprint : false )
skip_downstream_changes! if for_blueprint
2013-11-25 15:56:28 +08:00
transaction do
reload lock : true # would call lock!, except, oops, workflow overwrote it :P
lock if should_lock_yet
delayed_post unless should_not_post_yet
end
2011-02-01 09:57:29 +08:00
end
2013-07-02 06:27:46 +08:00
alias_method :try_posting_delayed , :update_based_on_date
alias_method :auto_update_workflow , :update_based_on_date
2012-02-14 04:09:41 +08:00
2011-02-01 09:57:29 +08:00
workflow do
2013-07-02 06:27:46 +08:00
state :active
2013-11-25 19:58:38 +08:00
state :unpublished
2011-02-01 09:57:29 +08:00
state :post_delayed do
event :delayed_post , transitions_to : :active do
self . last_reply_at = Time . now
self . posted_at = Time . now
end
2013-11-25 19:58:38 +08:00
# with draft state, this means published. without, unpublished. so we really do support both events
2011-02-01 09:57:29 +08:00
end
state :deleted
end
2012-02-14 04:09:41 +08:00
2013-11-25 19:58:38 +08:00
def active?
# using state instead of workflow_state so this works with new records
2014-10-30 20:49:55 +08:00
state == :active || ( ! is_announcement && state == :post_delayed )
2013-11-25 19:58:38 +08:00
end
def publish
2023-08-04 07:49:26 +08:00
# follows the logic of setting post_delayed in other places of this file
self . workflow_state = ( delayed_post_at && delayed_post_at > Time . now ) ? " post_delayed " : " active "
2013-11-25 19:58:38 +08:00
self . last_reply_at = Time . now
self . posted_at = Time . now
end
def publish!
publish
save!
end
def unpublish
self . workflow_state = " unpublished "
end
def unpublish!
unpublish
save!
end
2014-10-14 23:08:28 +08:00
def can_lock?
! ( assignment . try ( :due_at ) && assignment . due_at > Time . now )
end
2017-01-10 04:38:54 +08:00
def comments_disabled?
! ! ( is_a? ( Announcement ) &&
context . is_a? ( Course ) &&
2017-02-03 01:07:24 +08:00
context . lock_all_announcements )
2016-12-23 22:55:37 +08:00
end
2013-07-09 06:58:54 +08:00
def lock ( opts = { } )
2021-06-10 04:31:10 +08:00
raise Errors :: LockBeforeDueDate unless can_lock?
2013-07-02 06:27:46 +08:00
self . locked = true
2013-07-09 06:58:54 +08:00
save! unless opts [ :without_save ]
2013-07-02 06:27:46 +08:00
end
alias_method :lock! , :lock
2013-07-09 06:58:54 +08:00
def unlock ( opts = { } )
2013-07-02 06:27:46 +08:00
self . locked = false
self . workflow_state = " active " if workflow_state == " locked "
2013-07-09 06:58:54 +08:00
save! unless opts [ :without_save ]
2013-07-02 06:27:46 +08:00
end
alias_method :unlock! , :unlock
2013-07-09 06:58:54 +08:00
def published?
2013-11-25 19:58:38 +08:00
return false if workflow_state == " unpublished "
2014-10-30 20:49:55 +08:00
return false if workflow_state == " post_delayed " && is_announcement
2021-09-23 00:20:17 +08:00
2013-11-25 19:58:38 +08:00
true
2013-07-09 06:58:54 +08:00
end
2014-11-27 04:18:15 +08:00
def can_unpublish? ( opts = { } )
2015-06-27 00:32:45 +08:00
return @can_unpublish unless @can_unpublish . nil?
@can_unpublish = if assignment
! assignment . has_student_submissions?
else
2015-09-17 02:23:31 +08:00
student_ids = opts [ :student_ids ] || context . all_real_student_enrollments . select ( :user_id )
2015-06-27 00:32:45 +08:00
if for_group_discussion?
2015-12-17 01:55:11 +08:00
! DiscussionEntry . active . joins ( :discussion_topic ) . merge ( child_topics ) . where ( user_id : student_ids ) . exists?
2015-06-27 00:32:45 +08:00
else
! discussion_entries . active . where ( user_id : student_ids ) . exists?
end
end
end
attr_writer :can_unpublish
def self . preload_can_unpublish ( context , topics , assmnt_ids_with_subs = nil )
return unless topics . any?
2021-09-23 00:20:17 +08:00
2021-11-15 23:09:24 +08:00
assmnt_ids_with_subs || = Assignment . assignment_ids_with_submissions ( topics . filter_map ( & :assignment_id ) )
2015-06-27 00:32:45 +08:00
2015-09-17 02:23:31 +08:00
student_ids = context . all_real_student_enrollments . select ( :user_id )
topic_ids_with_entries = DiscussionEntry . active . where ( discussion_topic_id : topics )
2017-03-11 04:01:31 +08:00
. where ( user_id : student_ids ) . distinct . pluck ( :discussion_topic_id )
2021-11-16 23:48:21 +08:00
topic_ids_with_entries += DiscussionTopic . where . not ( root_topic_id : nil )
2017-03-11 04:01:31 +08:00
. where ( id : topic_ids_with_entries ) . distinct . pluck ( :root_topic_id )
2015-06-27 00:32:45 +08:00
topics . each do | topic |
topic . can_unpublish = if topic . assignment_id
! assmnt_ids_with_subs . include? ( topic . assignment_id )
2014-11-27 04:18:15 +08:00
else
2015-06-27 00:32:45 +08:00
! topic_ids_with_entries . include? ( topic . id )
2014-11-27 04:18:15 +08:00
end
2013-11-25 19:58:38 +08:00
end
2013-10-03 06:37:45 +08:00
end
2015-09-17 02:23:31 +08:00
def can_group? ( opts = { } )
can_unpublish? ( opts )
2014-04-11 06:39:45 +08:00
end
2011-04-05 23:06:10 +08:00
def should_send_to_stream
2021-11-04 01:53:13 +08:00
published? &&
! not_available_yet? &&
! cloned_item_id &&
! ( root_topic_id && has_group_category? ) &&
! in_unpublished_module? &&
! locked_by_module?
2011-04-05 23:06:10 +08:00
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
2015-12-09 07:33:00 +08:00
active_participants_with_visibility
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
2015-05-19 00:15:35 +08:00
check_state = is_announcement ? " post_delayed " : " unpublished "
2018-03-22 05:17:34 +08:00
became_active = workflow_state_before_last_save == check_state && workflow_state == " active "
2015-05-19 00:15:35 +08:00
if should_send_to_stream && ( @content_changed || became_active )
2015-12-09 07:33:00 +08:00
active_participants_with_visibility
2011-02-01 09:57:29 +08:00
end
end
2012-02-14 04:09:41 +08:00
2020-06-04 03:35:29 +08:00
# This is manually called for module publishing
def send_items_to_stream
if should_send_to_stream
queue_create_stream_items
end
end
def in_unpublished_module?
return true if ContentTag . where ( content_type : " DiscussionTopic " , content_id : self , workflow_state : " unpublished " ) . exists?
2021-09-23 00:20:17 +08:00
2020-06-04 03:35:29 +08:00
ContextModule . joins ( :content_tags ) . where ( content_tags : { content_type : " DiscussionTopic " , content_id : self } , workflow_state : " unpublished " ) . exists?
end
2020-08-05 05:53:13 +08:00
def locked_by_module?
return false unless context_module_tags . any?
2021-09-23 00:20:17 +08:00
2020-08-05 05:53:13 +08:00
ContentTag . where ( content_type : " DiscussionTopic " , content_id : self , workflow_state : " active " ) . all? { | tag | tag . context_module . unlock_at & . future? }
end
2021-03-13 03:46:01 +08:00
def should_clear_all_stream_items?
2021-09-27 23:58:39 +08:00
( ! published? && saved_change_to_attribute? ( :workflow_state ) ) ||
( is_announcement && not_available_yet? && saved_change_to_attribute? ( :delayed_post_at ) )
2020-04-16 03:10:10 +08:00
end
2021-03-13 03:46:01 +08:00
def clear_non_applicable_stream_items
return clear_stream_items if should_clear_all_stream_items?
2020-03-04 06:04:35 +08:00
2021-03-13 03:46:01 +08:00
section = is_section_specific? ? @sections_changed : is_section_specific_before_last_save
lock = locked_by_module?
2020-04-16 03:10:10 +08:00
2021-03-13 03:46:01 +08:00
if lock || section
delay_if_production . partially_clear_stream_items ( locked_by_module : lock , section_specific : section )
2020-03-04 06:04:35 +08:00
end
end
2020-02-20 07:01:51 +08:00
2021-03-13 03:46:01 +08:00
def partially_clear_stream_items ( locked_by_module : false , section_specific : false )
remaining_participants = participants if section_specific
2020-08-05 05:53:13 +08:00
user_ids = [ ]
2021-03-13 03:46:01 +08:00
stream_item & . stream_item_instances & . shard ( stream_item ) & . find_each do | item |
2021-11-04 01:53:13 +08:00
if ( locked_by_module && locked_by_module_item? ( item . user ) ) ||
( section_specific && remaining_participants . none? { | p | p . id == item . user_id } )
2021-03-13 03:46:01 +08:00
destroy_item_and_track ( item , user_ids )
2020-08-05 05:53:13 +08:00
end
end
clear_stream_item_cache_for ( user_ids )
end
2021-03-13 03:46:01 +08:00
def destroy_item_and_track ( item , user_ids )
user_ids . push ( item . user_id )
item . destroy
end
2020-03-04 06:04:35 +08:00
def clear_stream_item_cache_for ( user_ids )
2020-02-20 07:01:51 +08:00
if stream_item && user_ids . any?
StreamItemCache . delay_if_production ( priority : Delayed :: LOW_PRIORITY )
. invalidate_all_recent_stream_items (
user_ids ,
stream_item . context_type ,
stream_item . context_id
)
end
end
2011-11-04 05:55:38 +08:00
def require_initial_post?
require_initial_post || 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
2023-02-22 07:28:43 +08:00
ids = discussion_entries . active . select ( :user_id ) . pluck ( :user_id )
ids = ids . uniq
2017-04-21 22:22:14 +08:00
ids += course . admin_enrollments . active . pluck ( :user_id ) if course . is_a? ( Course )
2011-11-04 05:55:38 +08:00
ids
end
2012-02-14 04:09:41 +08:00
2017-05-04 05:40:51 +08:00
def user_can_see_posts? ( user , session = nil , associated_user_ids = [ ] )
2011-11-04 05:55:38 +08:00
return false unless user
2021-09-23 00:20:17 +08:00
2017-10-10 03:19:17 +08:00
! require_initial_post? || grants_right? ( user , session , :read_as_admin ) ||
2023-06-02 06:06:09 +08:00
( [ user . id ] + associated_user_ids ) . intersect? ( user_ids_who_have_posted_and_admins )
2011-11-04 05:55:38 +08:00
end
2012-02-14 04:09:41 +08:00
2022-03-09 03:15:37 +08:00
def locked_announcement?
is_a? ( Announcement ) && locked?
end
2011-02-01 09:57:29 +08:00
def reply_from ( opts )
2015-08-01 06:02:10 +08:00
raise IncomingMail :: Errors :: ReplyToDeletedDiscussion if deleted?
2014-04-03 06:59:39 +08:00
raise IncomingMail :: Errors :: UnknownAddress if context . root_account . deleted?
2021-09-23 00:20:17 +08:00
2011-02-01 09:57:29 +08:00
user = opts [ :user ]
2012-09-25 04:01:19 +08:00
if opts [ :html ]
message = opts [ :html ] . strip
else
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
message = opts [ :text ] . strip
message = format_message ( message ) . first
end
2011-02-01 09:57:29 +08:00
user = nil unless user && context . users . include? ( user )
if ! user
2021-05-13 03:57:34 +08:00
raise IncomingMail :: Errors :: InvalidParticipant
2012-09-25 04:01:19 +08:00
elsif ! grants_right? ( user , :read )
nil
2011-02-01 09:57:29 +08:00
else
2017-09-20 20:20:47 +08:00
shard . activate do
2023-06-02 06:06:09 +08:00
entry = discussion_entries . new ( message : , user : )
2022-03-09 03:15:37 +08:00
if entry . grants_right? ( user , :create ) && ! comments_disabled? && ! locked_announcement?
2017-09-20 20:20:47 +08:00
entry . save!
entry
2021-11-20 22:33:31 +08:00
else
2017-09-20 20:20:47 +08:00
raise IncomingMail :: Errors :: ReplyToLockedTopic
end
2012-09-25 04:01:19 +08:00
end
2011-02-01 09:57:29 +08:00
end
end
2012-02-14 04:09:41 +08:00
2016-01-06 02:31:25 +08:00
alias_method :destroy_permanently! , :destroy
2011-02-01 09:57:29 +08:00
def destroy
ContentTag . delete_for ( self )
self . workflow_state = " deleted "
2013-10-30 05:34:26 +08:00
self . deleted_at = Time . now . utc
2017-12-08 07:51:21 +08:00
discussion_topic_section_visibilities & . update_all ( workflow_state : " deleted " )
2011-02-01 09:57:29 +08:00
save
2012-06-21 02:58:03 +08:00
if for_assignment? && root_topic_id . blank? && ! assignment . deleted?
2022-10-05 23:39:17 +08:00
assignment . skip_downstream_changes! if @skip_downstream_changes
2011-02-01 09:57:29 +08:00
assignment . destroy
end
2012-06-21 02:58:03 +08:00
child_topics . each ( & :destroy )
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2014-03-21 06:43:45 +08:00
def restore ( from = nil )
2023-04-26 08:32:46 +08:00
unless restorable?
errors . add ( :deleted_at , I18n . t ( " Cannot undelete a child topic when the root course topic is also deleted. Please undelete the root course topic instead. " ) )
return false
end
2018-04-17 10:01:52 +08:00
if is_section_specific?
DiscussionTopicSectionVisibility . where ( discussion_topic_id : id ) . to_a . uniq ( & :course_section_id ) . each do | dtsv |
dtsv . workflow_state = " active "
dtsv . save
end
end
2022-03-17 09:40:00 +08:00
discussion_topic_section_visibilities . reload
2016-03-12 00:25:45 +08:00
self . workflow_state = can_unpublish? ? " unpublished " : " active "
2011-02-01 09:57:29 +08:00
save
2013-04-03 01:43:12 +08:00
2014-03-21 06:43:45 +08:00
if from != :assignment && for_assignment? && root_topic_id . blank?
2011-02-01 09:57:29 +08:00
assignment . restore ( :discussion_topic )
end
2013-04-03 01:43:12 +08:00
2016-04-08 00:37:16 +08:00
child_topics . each ( & :restore )
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2023-04-26 08:32:46 +08:00
def restorable?
# Not restorable if the root topic context is a course and
# root topic is deleted.
! ( root_topic & . context_type == " Course " && root_topic & . deleted? )
end
2016-04-08 00:37:16 +08:00
def unlink! ( type )
2011-02-01 09:57:29 +08:00
@saved_by = type
2011-03-08 04:15:19 +08:00
self . assignment = nil
2011-02-01 09:57:29 +08:00
if discussion_entries . empty?
2011-03-08 04:15:19 +08:00
destroy
2011-02-01 09:57:29 +08:00
else
save
end
2016-04-08 00:37:16 +08:00
child_topics . each { | t | t . unlink! ( :assignment ) }
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 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
2017-01-31 00:42:40 +08:00
unless [ :migration , :after_migration ] . include? ( saved_by )
self . posted_at || = Time . now . utc
self . last_reply_at || = Time . now . utc
end
2011-02-01 09:57:29 +08:00
end
set_policy do
2021-10-03 06:08:17 +08:00
# Users may have can :read, but should not have access to all the data
# because the topic is locked_for?(user)
2016-04-27 07:01:36 +08:00
given { | user | visible_for? ( user ) }
2011-07-14 00:24:17 +08:00
can :read
2011-02-01 09:57:29 +08:00
2015-03-25 07:16:40 +08:00
given { | user | grants_right? ( user , :read ) }
can :read_replies
2017-01-23 22:38:26 +08:00
given { | user | self . user && self . user == user && visible_for? ( user ) && ! locked_for? ( user , check_policies : true ) && can_participate_in_course? ( user ) }
2013-02-08 04:09:05 +08:00
can :reply
2015-05-30 02:03:49 +08:00
given { | user | self . user && self . user == user && available_for? ( user ) && context . user_can_manage_own_discussion_posts? ( user ) && context . grants_right? ( user , :participate_as_student ) }
2013-02-08 04:09:05 +08:00
can :update
2015-05-30 02:03:49 +08:00
given { | user | self . user && self . user == user and discussion_entries . active . empty? && available_for? ( user ) && ! root_topic_id && context . user_can_manage_own_discussion_posts? ( user ) && context . grants_right? ( user , :participate_as_student ) }
2011-07-14 00:24:17 +08:00
can :delete
2012-02-14 04:09:41 +08:00
2015-02-07 05:48:50 +08:00
given do | user , session |
! locked_for? ( user , check_policies : true ) &&
2017-01-23 22:38:26 +08:00
context . grants_right? ( user , session , :post_to_forum ) && visible_for? ( user ) && can_participate_in_course? ( user )
2021-11-23 23:20:14 +08:00
end
2017-01-23 22:38:26 +08:00
can :reply
2011-08-30 05:10:26 +08:00
2021-04-08 21:46:06 +08:00
given { | user , session | user_can_create ( user , session ) }
2011-07-14 00:24:17 +08:00
can :create
2012-02-14 04:09:41 +08:00
2021-05-13 23:44:08 +08:00
given { | user , session | user_can_create ( user , session ) && user_can_duplicate ( user , session ) }
2021-04-08 21:46:06 +08:00
can :duplicate
2018-08-06 22:04:14 +08:00
given { | user , session | context . respond_to? ( :allow_student_forum_attachments ) && context . allow_student_forum_attachments && context . grants_any_right? ( user , session , :create_forum , :post_to_forum ) }
2011-07-14 00:24:17 +08:00
can :attach
2012-02-14 04:09:41 +08:00
2021-10-20 02:03:18 +08:00
given { course . student_reporting? }
can :student_reporting
2016-04-06 03:50:28 +08:00
given { | user , session | ! root_topic_id && context . grants_all_rights? ( user , session , :read_forum , :moderate_forum ) && available_for? ( user ) }
2017-10-10 03:19:17 +08:00
can :update and can :read_as_admin and can :delete and can :create and can :read and can :attach
2011-08-30 05:10:26 +08:00
2013-11-25 19:58:38 +08:00
# Moderators can still modify content even in unavailable topics (*especially* unlocking them), but can't create new content
2016-04-06 03:50:28 +08:00
given { | user , session | ! root_topic_id && context . grants_all_rights? ( user , session , :read_forum , :moderate_forum ) }
2017-10-10 03:19:17 +08:00
can :update and can :read_as_admin and can :delete and can :read and can :attach
given { | user , session | root_topic & . grants_right? ( user , session , :read_as_admin ) }
can :read_as_admin
2011-08-30 05:10:26 +08:00
2011-02-01 09:57:29 +08:00
given { | user , session | 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
2013-11-25 19:58:38 +08:00
given { | user , session | root_topic & . grants_right? ( user , session , :read ) }
can :read
2014-10-15 18:38:00 +08:00
2021-04-19 23:56:19 +08:00
given { | user , session | context . grants_all_rights? ( user , session , :moderate_forum , :read_forum ) }
can :moderate_forum
2014-10-15 18:38:00 +08:00
given do | user , session |
allow_rating && ( ! only_graders_can_rate ||
2020-05-21 00:50:12 +08:00
course . grants_right? ( user , session , :manage_grades ) )
2014-10-15 18:38:00 +08:00
end
can :rate
2011-02-01 09:57:29 +08:00
end
2013-09-18 06:24:57 +08:00
def self . context_allows_user_to_create? ( context , user , session )
2023-06-02 06:06:09 +08:00
new ( context : ) . grants_right? ( user , session , :create )
2013-09-18 06:24:57 +08:00
end
2013-01-09 07:02:16 +08:00
def context_allows_user_to_create? ( user )
return true unless context . respond_to? ( :allow_student_discussion_topics )
2019-11-05 07:09:28 +08:00
return true if context . grants_right? ( user , :read_as_admin )
2021-09-23 00:20:17 +08:00
2013-01-09 07:02:16 +08:00
context . allow_student_discussion_topics
end
2021-04-08 21:46:06 +08:00
def user_can_create ( user , session )
! is_announcement &&
context . grants_right? ( user , session , :create_forum ) &&
context_allows_user_to_create? ( user )
end
2021-05-13 23:44:08 +08:00
def user_can_duplicate ( user , session )
context . is_a? ( Group ) ||
course . user_is_instructor? ( user ) ||
context . grants_right? ( user , session , :read_as_admin )
end
2011-02-01 09:57:29 +08:00
def discussion_topic_id
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 = user . present? ? user . name : t ( " # discussion_topic.atom_no_author " , " No Author " )
2011-06-09 06:29:03 +08:00
prefix = [ is_announcement ? t ( " # titles.announcement " , " Announcement " ) : t ( " # titles.discussion " , " Discussion " ) ]
prefix << 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 ) , 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 = updated_at
entry . published = created_at
entry . id = " tag: #{ HostUrl . default_host } , #{ created_at . strftime ( " %Y-%m-%d " ) } :/discussion_topics/ #{ 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 ( context ) } / #{ context_url_prefix } /discussion_topics/ #{ id } " )
entry . content = Atom :: Content :: Html . new ( 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 )
2015-02-26 07:18:56 +08:00
return root_topic . context_module_action ( user , action , points ) if root_topic
2021-09-23 00:20:17 +08:00
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 = context_module_tags . to_a
2011-11-10 01:33:13 +08:00
if 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 += assignment . context_module_tags
2019-02-08 22:27:00 +08:00
if context . grants_right? ( user , :participate_as_student ) && assignment . visible_to_user? ( user ) && [ :contributed , :deleted ] . include? ( action )
only_update = ( action == :deleted ) # if we're deleting an entry, don't make a submission if it wasn't there already
ensure_submission ( user , only_update )
end
end
unless action == :deleted
tags_to_update . each { | tag | tag . context_module_action ( user , action , points ) }
2011-11-10 01:33:13 +08:00
end
end
2019-02-08 22:27:00 +08:00
def ensure_submission ( user , only_update = false )
2020-02-01 01:02:12 +08:00
topic = ( root_topic? && child_topic_for ( user ) ) || self
2019-02-28 22:21:31 +08:00
2023-10-14 02:28:07 +08:00
submissions = [ ]
all_entries_for_user = topic . discussion_entries . all_for_user ( user )
if topic . root_account & . feature_enabled? ( :discussion_checkpoints ) && checkpoints?
reply_to_topic_submitted_at = topic . discussion_entries . top_level_for_user ( user ) . minimum ( :created_at )
if reply_to_topic_submitted_at . present?
2023-10-17 23:20:13 +08:00
reply_to_topic_submission = ensure_particular_submission ( reply_to_topic_checkpoint , user , reply_to_topic_submitted_at , only_update : )
submissions << reply_to_topic_submission if reply_to_topic_submission . present?
2023-10-14 02:28:07 +08:00
end
2023-10-17 23:20:13 +08:00
reply_to_entry_submitted_at = topic . discussion_entries . non_top_level_for_user ( user ) . minimum ( :created_at )
if reply_to_entry_submitted_at . present?
reply_to_entry_submission = ensure_particular_submission ( reply_to_entry_checkpoint , user , reply_to_entry_submitted_at , only_update : )
submissions << reply_to_entry_submission if reply_to_entry_submission . present?
end
2023-10-14 02:28:07 +08:00
else
submitted_at = all_entries_for_user . minimum ( :created_at )
submission = ensure_particular_submission ( assignment , user , submitted_at , only_update : )
submissions << submission if submission . present?
end
return unless submissions . any?
attachment_ids = all_entries_for_user . where . not ( attachment_id : nil ) . pluck ( :attachment_id ) . sort . map ( & :to_s ) . join ( " , " )
submissions . each do | s |
s . attachment_ids = attachment_ids
s . save! if s . changed?
end
end
def ensure_particular_submission ( assignment , user , submitted_at , only_update : false )
submission = Submission . active . where ( assignment_id : assignment . id , user_id : user ) . first
2019-02-08 22:27:00 +08:00
unless only_update || ( submission && submission . submission_type == " discussion_topic " && submission . workflow_state != " unsubmitted " )
2023-04-29 04:59:51 +08:00
submission = assignment . submit_homework ( user ,
submission_type : " discussion_topic " ,
2023-10-14 02:28:07 +08:00
submitted_at : )
2016-02-08 22:40:58 +08:00
end
2021-09-23 00:20:17 +08:00
2023-10-14 02:28:07 +08:00
submission
2011-02-01 09:57:29 +08:00
end
2015-10-16 22:29:59 +08:00
def send_notification_for_context?
notification_context =
if context . is_a? ( Group ) && context . context . is_a? ( Course )
context . context # we need to go deeper
else
context
end
2017-05-16 02:41:00 +08:00
notification_context . available?
2015-10-16 22:29:59 +08:00
end
2020-02-28 10:32:37 +08:00
def course_broadcast_data
context & . broadcast_data
end
2011-02-01 09:57:29 +08:00
has_a_broadcast_policy
set_broadcast_policy do | p |
p . dispatch :new_discussion_topic
2017-09-01 00:00:33 +08:00
p . to { users_with_permissions ( active_participants_with_visibility ) }
2011-02-01 09:57:29 +08:00
p . whenever do | record |
2015-10-16 22:29:59 +08:00
record . send_notification_for_context? and
2014-10-30 20:49:55 +08:00
( ( record . just_created && record . active? ) || record . changed_state ( :active , record . is_announcement ? :post_delayed : :unpublished ) )
2021-11-23 23:20:14 +08:00
end
2020-02-28 10:32:37 +08:00
p . data { course_broadcast_data }
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 delay_posting = ( val ) ; end
2015-04-28 01:47:25 +08:00
2011-02-01 09:57:29 +08:00
def set_assignment = ( val ) ; end
2012-02-14 04:09:41 +08:00
2018-03-21 01:03:11 +08:00
# From the given list of users, return those that are permitted to see the section
# of the topic. If the topic is not section specific this just returns the
# original list.
def users_with_section_visibility ( users )
return users unless is_section_specific? && context . is_a? ( Course )
2021-09-23 00:20:17 +08:00
2018-03-21 01:03:11 +08:00
non_nil_users = users . compact
section_ids = DiscussionTopicSectionVisibility . active . where ( discussion_topic_id : id )
. pluck ( :course_section_id )
2018-03-24 01:08:57 +08:00
user_ids = non_nil_users . pluck ( :id )
2018-03-21 01:03:11 +08:00
# Context is known to be a course here
2022-10-04 20:54:46 +08:00
users_in_sections = context . enrollments . active_or_pending
2018-03-24 01:08:57 +08:00
. where ( user_id : user_ids , course_section_id : section_ids ) . pluck ( :user_id ) . to_set
2022-10-04 20:54:46 +08:00
unlocked_teachers = context . enrollments . active_or_pending . instructor
2018-03-24 01:08:57 +08:00
. where ( limit_privileges_to_course_section : false , user_id : user_ids )
. pluck ( :user_id ) . to_set
2018-03-21 01:03:11 +08:00
permitted_user_ids = users_in_sections . union ( unlocked_teachers )
non_nil_users . select { | u | permitted_user_ids . include? ( u . id ) }
end
2012-03-09 01:14:57 +08:00
def participants ( include_observers = false )
2023-06-02 06:06:09 +08:00
participants = context . participants ( include_observers : , by_date : true )
2018-03-21 01:03:11 +08:00
participants_in_section = users_with_section_visibility ( participants . compact )
2023-04-05 03:39:02 +08:00
if user && ! participants_in_section . to_set ( & :id ) . include? ( user . id )
2018-03-21 01:03:11 +08:00
participants_in_section += [ user ]
end
participants_in_section
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2018-05-12 05:48:26 +08:00
def visible_to_admins_only?
2021-09-27 23:58:39 +08:00
( context . respond_to? ( :available? ) && ! context . available? ) ||
2018-05-12 05:48:26 +08:00
unpublished? || not_available_yet? || not_available_anymore?
end
2012-03-09 01:14:57 +08:00
def active_participants ( include_observers = false )
2018-05-12 05:48:26 +08:00
if visible_to_admins_only? && context . respond_to? ( :participating_admins )
2011-12-01 06:49:22 +08:00
context . participating_admins
else
2012-03-09 01:14:57 +08:00
participants ( include_observers )
2011-12-01 06:49:22 +08:00
end
end
2012-02-14 04:09:41 +08:00
add instructor / ta to group announcement
test plan:
1. Create course with a teacher, three students (a,b,c),
a ta, a designer, a role inherited from teacher, and
a role inherited from ta.
2. Create a group in the course that includes students
a and b.
3. As student a, post an announcement in the group.
4. Verify that the following receive the announcement:
b, ta, teacher, role inherited from teacher, and
role inherited from ta all receive the announcement.
5. Verify that the following do not receive the
announcement: c, and designer.
Additionally, verify that when a teacher has selected
to never receive announcement notifications that they
do not receive them from group announcements.
Change-Id: Idf23cb942f79002d778aaa6ae51526e3b18c1731
Fixes: PFS-5748
Reviewed-on: https://gerrit.instructure.com/94187
Reviewed-by: Tyson Brown <tbrown@instructure.com>
Reviewed-by: Dan Minkevitch <dan@instructure.com>
QA-Review: Jahnavi Yetukuri <jyetukuri@instructure.com>
Product-Review: Chris Ward <cward@instructure.com>
Tested-by: Jenkins
2016-11-02 06:14:09 +08:00
def active_participants_include_tas_and_teachers ( include_observers = false )
participants = active_participants ( include_observers )
if context . is_a? ( Group ) && ! context . course . nil?
2017-03-14 01:46:02 +08:00
participants += context . course . participating_instructors_by_date
add instructor / ta to group announcement
test plan:
1. Create course with a teacher, three students (a,b,c),
a ta, a designer, a role inherited from teacher, and
a role inherited from ta.
2. Create a group in the course that includes students
a and b.
3. As student a, post an announcement in the group.
4. Verify that the following receive the announcement:
b, ta, teacher, role inherited from teacher, and
role inherited from ta all receive the announcement.
5. Verify that the following do not receive the
announcement: c, and designer.
Additionally, verify that when a teacher has selected
to never receive announcement notifications that they
do not receive them from group announcements.
Change-Id: Idf23cb942f79002d778aaa6ae51526e3b18c1731
Fixes: PFS-5748
Reviewed-on: https://gerrit.instructure.com/94187
Reviewed-by: Tyson Brown <tbrown@instructure.com>
Reviewed-by: Dan Minkevitch <dan@instructure.com>
QA-Review: Jahnavi Yetukuri <jyetukuri@instructure.com>
Product-Review: Chris Ward <cward@instructure.com>
Tested-by: Jenkins
2016-11-02 06:14:09 +08:00
participants = participants . compact . uniq
end
participants
end
2016-04-06 03:50:28 +08:00
def users_with_permissions ( users )
2016-10-20 04:47:54 +08:00
permission = is_announcement ? :read_announcements : :read_forum
2018-03-16 01:45:52 +08:00
course = self . course
unless course . is_a? ( Course )
return users . select do | u |
is_announcement ? context . grants_right? ( u , :read_announcements ) : context . grants_right? ( u , :read_forum )
end
2016-10-20 04:47:54 +08:00
end
2018-03-16 01:45:52 +08:00
readers = self . course . filter_users_by_permission ( users , permission )
2018-03-21 01:03:11 +08:00
users_with_section_visibility ( readers )
2016-04-06 03:50:28 +08:00
end
2015-02-28 01:34:13 +08:00
def course
@course || = context . is_a? ( Group ) ? context . context : context
end
2017-01-08 10:26:44 +08:00
def group
@group || = context . is_a? ( Group ) ? context : nil
end
2014-09-11 05:12:12 +08:00
def active_participants_with_visibility
2021-12-09 09:24:21 +08:00
return active_participants_include_tas_and_teachers unless for_assignment?
2021-09-23 00:20:17 +08:00
2016-08-13 00:22:19 +08:00
users_with_visibility = assignment . students_with_visibility . pluck ( :id )
2015-02-28 01:34:13 +08:00
admin_ids = course . participating_admins . pluck ( :id )
users_with_visibility . concat ( admin_ids )
2014-09-11 05:12:12 +08:00
# observers will not be returned, which is okay for the functions current use cases (but potentially not others)
2021-12-09 09:24:21 +08:00
active_participants_include_tas_and_teachers . select { | p | users_with_visibility . include? ( p . id ) }
2014-09-11 05:12:12 +08:00
end
2013-06-12 03:55:33 +08:00
def participating_users ( user_ids )
context . respond_to? ( :participating_users ) ? context . participating_users ( user_ids ) : User . find ( user_ids )
end
2013-07-12 05:20:34 +08:00
2013-06-12 03:55:33 +08:00
def subscribers
2013-11-25 19:58:38 +08:00
# this duplicates some logic from #subscribed? so we don't have to call
2013-07-12 05:20:34 +08:00
# #posters for each legacy subscriber.
sub_ids = discussion_topic_participants . where ( subscribed : true ) . pluck ( :user_id )
legacy_sub_ids = discussion_topic_participants . where ( subscribed : nil ) . pluck ( :user_id )
poster_ids = posters . map ( & :id )
2013-07-17 03:45:18 +08:00
legacy_sub_ids & = poster_ids
2013-07-12 05:20:34 +08:00
sub_ids += legacy_sub_ids
2014-09-11 05:12:12 +08:00
2016-01-14 22:42:51 +08:00
subscribed_users = participating_users ( sub_ids ) . to_a
2014-09-11 05:12:12 +08:00
2021-05-04 04:57:06 +08:00
filter_message_users ( subscribed_users )
end
def filter_message_users ( users )
2016-02-26 23:44:19 +08:00
if for_assignment?
2016-08-13 00:22:19 +08:00
students_with_visibility = assignment . students_with_visibility . pluck ( :id )
2015-02-28 01:34:13 +08:00
admin_ids = course . participating_admins . pluck ( :id )
observer_ids = course . participating_observers . pluck ( :id )
observed_students = ObserverEnrollment . observed_student_ids_by_observer_id ( course , observer_ids )
2014-09-11 05:12:12 +08:00
2021-05-04 04:57:06 +08:00
users . select! do | user |
2014-09-11 05:12:12 +08:00
students_with_visibility . include? ( user . id ) || admin_ids . include? ( user . id ) ||
2021-05-04 04:57:06 +08:00
# an observer with no students or one with students who have visibility
2023-06-02 06:06:09 +08:00
( observed_students [ user . id ] && ( observed_students [ user . id ] == [ ] || observed_students [ user . id ] . intersect? ( students_with_visibility ) ) )
2021-05-04 04:57:06 +08:00
end
2014-09-11 05:12:12 +08:00
end
2021-05-04 04:57:06 +08:00
users
2013-06-12 03:55:33 +08:00
end
2013-07-12 05:20:34 +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 ( user_id ) . uniq
2013-06-12 03:55:33 +08:00
participating_users ( user_ids )
2011-02-01 09:57:29 +08:00
end
def user_name
2023-04-05 05:33:38 +08:00
user & . name
2011-02-01 09:57:29 +08:00
end
2012-02-14 04:09:41 +08:00
2013-11-25 19:58:38 +08:00
def available_from_for ( user )
if assignment
assignment . overridden_for ( user ) . unlock_at
else
available_from
end
end
def available_for? ( user , opts = { } )
return false unless published?
2014-10-30 20:49:55 +08:00
return false if is_announcement && locked?
2021-09-23 00:20:17 +08:00
2013-11-25 19:58:38 +08:00
! locked_for? ( user , opts )
end
2013-08-06 22:58:44 +08:00
# Public: Determine if the given user can view this discussion topic.
#
# user - The user attempting to view the topic (default: nil).
#
# Returns a boolean.
2015-03-27 02:28:11 +08:00
def visible_for? ( user = nil )
2018-08-04 01:30:00 +08:00
RequestCache . cache ( " discussion_visible_for " , self , is_announcement , user ) do
2015-08-18 22:57:12 +08:00
# user is the topic's author
2018-08-07 23:18:44 +08:00
next true if user && user . id == user_id
2013-08-06 22:58:44 +08:00
2021-09-01 05:24:32 +08:00
next false unless context
2016-07-02 00:25:45 +08:00
next false unless is_announcement ? context . grants_right? ( user , :read_announcements ) : context . grants_right? ( user , :read_forum )
2016-04-06 03:50:28 +08:00
2018-03-22 05:09:55 +08:00
# Don't have visibilites for any of the specific sections in a section specific topic
if context . is_a? ( Course ) && try ( :is_section_specific )
section_visibilities = context . course_section_visibility ( user )
next false if section_visibilities == :none
2021-09-23 00:20:17 +08:00
2018-03-22 05:09:55 +08:00
if section_visibilities != :all
course_specific_sections = course_sections . pluck ( :id )
2023-06-02 06:06:09 +08:00
next false unless section_visibilities . intersect? ( course_specific_sections )
2018-03-22 05:09:55 +08:00
end
end
2015-08-18 22:57:12 +08:00
# user is an admin in the context (teacher/ta/designer) OR
# user is an account admin with appropriate permission
2016-04-27 07:01:36 +08:00
next true if context . grants_any_right? ( user , :manage , :read_course_content )
2013-08-06 22:58:44 +08:00
2017-12-20 21:26:17 +08:00
# assignment exists and isn't assigned to user (differentiated assignments)
2015-08-18 22:57:12 +08:00
if for_assignment? && ! assignment . visible_to_user? ( user )
2016-04-27 07:01:36 +08:00
next false
2015-11-20 08:18:59 +08:00
end
2015-08-18 22:57:12 +08:00
# topic is not published
if ! published?
2016-04-27 07:01:36 +08:00
next false
2021-09-28 06:37:43 +08:00
elsif is_announcement && ( unlock_at = available_from_for ( user ) )
2015-08-18 22:57:12 +08:00
# unlock date exists and has passed
2016-04-27 07:01:36 +08:00
next unlock_at < Time . now . utc
2015-08-18 22:57:12 +08:00
# everything else
else
2016-04-27 07:01:36 +08:00
next true
2015-08-18 22:57:12 +08:00
end
2013-04-26 05:38:04 +08:00
end
end
2017-01-23 22:38:26 +08:00
def can_participate_in_course? ( user )
if group & . deleted?
false
elsif course . is_a? ( Course )
# this probably isn't a perfect way to determine this but I can't think of a better one
course . enrollments . for_user ( user ) . active_by_date . exists? || course . grants_right? ( user , :read_as_admin )
else
true
end
2016-11-23 06:14:47 +08:00
end
2021-10-03 06:08:17 +08:00
# Determine if the discussion topic is locked for a user. The topic is locked
# if the delayed_post_at is in the future or the assignment is locked.
# This does not determine the visibility of the topic to the user,
# only that they are unable to reply and unable to see the message.
# Generally you want to call :locked_for?(user, check_policies: true), which
# will call this method.
2018-05-15 06:29:01 +08:00
def low_level_locked_for? ( user , opts = { } )
2017-10-10 03:19:17 +08:00
return false if opts [ :check_policies ] && grants_right? ( user , :read_as_admin )
2015-02-07 05:48:50 +08:00
2019-05-07 04:24:47 +08:00
RequestCache . cache ( locked_request_cache_key ( user ) ) do
2011-02-01 09:57:29 +08:00
locked = false
if delayed_post_at && delayed_post_at > Time . now
2018-05-15 06:29:01 +08:00
locked = { object : self , unlock_at : delayed_post_at }
2013-05-03 07:39:26 +08:00
elsif lock_at && lock_at < Time . now
2023-06-02 06:06:09 +08:00
locked = { object : self , lock_at : , can_view : true }
2021-09-28 06:37:43 +08:00
elsif ! opts [ :skip_assignment ] && ( l = assignment & . low_level_locked_for? ( user , opts ) )
2011-02-01 09:57:29 +08:00
locked = l
2021-09-28 06:37:43 +08:00
elsif could_be_locked && ( item = locked_by_module_item? ( user , opts ) )
2018-05-15 06:29:01 +08:00
locked = { object : self , module : item . context_module }
2016-04-27 07:01:36 +08:00
elsif locked? # nothing more specific, it's just locked
2018-05-15 06:29:01 +08:00
locked = { object : self , can_view : true }
2021-11-04 01:53:13 +08:00
elsif ( l = root_topic & . low_level_locked_for? ( user , opts ) ) # rubocop:disable Lint/DuplicateBranch
2011-02-01 09:57:29 +08:00
locked = l
end
locked
end
end
2012-02-14 04:09:41 +08:00
2016-10-04 06:23:33 +08:00
def self . reject_context_module_locked_topics ( topics , user )
progressions = ContextModuleProgression
. joins ( context_module : :content_tags )
. where ( {
:user = > user ,
" content_tags.content_type " = > " DiscussionTopic " ,
" content_tags.content_id " = > topics ,
} )
. select ( " context_module_progressions.* " )
. distinct_on ( " context_module_progressions.id " )
. preload ( :user )
progressions = progressions . index_by ( & :context_module_id )
topics . reject do | topic |
topic . locked_by_module_item? ( user , {
deep_check_if_needed : true ,
user_context_module_progressions : progressions ,
} )
end
end
2015-03-20 04:33:21 +08:00
def entries_for_feed ( user , podcast_feed = false )
return [ ] unless user_can_see_posts? ( user )
return [ ] if locked_for? ( user , check_policies : true )
entries = discussion_entries . active
if podcast_feed && ! podcast_has_student_posts && context . is_a? ( Course )
entries = entries . where ( user_id : context . admins )
end
entries
end
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 ( %r{ / #{ context . class . to_s . pluralize . underscore } / #{ context . id } /files/( \ d+)/download } )
2023-04-13 04:20:50 +08:00
attachment_ids += ( attachment_matches || [ ] ) . pluck ( 0 )
2023-04-05 03:34:23 +08:00
media_object_matches = txt . scan ( / media_comment_([ \ w-]+) / ) + txt . scan ( / data-media-id="([ \ w-]+)" / )
2023-04-13 04:20:50 +08:00
media_object_ids += ( media_object_matches || [ ] ) . pluck ( 0 ) . uniq
2011-02-01 09:57:29 +08:00
( 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
2015-02-25 05:08:10 +08:00
2011-02-01 09:57:29 +08:00
media_object_ids = media_object_ids . uniq . compact
attachment_ids = attachment_ids . uniq . compact
2014-09-12 03:44:34 +08:00
attachments = attachment_ids . empty? ? [ ] : context . attachments . active . find_all_by_id ( attachment_ids )
2011-02-01 09:57:29 +08:00
attachments = attachments . select { | a | a . content_type & . match ( / (video|audio) / ) }
attachments . each do | attachment |
2015-02-25 05:08:10 +08:00
attachment . podcast_associated_asset = messages_hash [ attachment . id . to_s ]
2011-02-01 09:57:29 +08:00
end
2021-11-15 23:09:24 +08:00
media_object_ids -= attachments . filter_map ( & :media_entry_id ) # don't include media objects if the file is already included
2015-02-25 05:08:10 +08:00
2014-09-12 03:44:34 +08:00
media_objects = media_object_ids . empty? ? [ ] : MediaObject . where ( media_id : media_object_ids ) . to_a
2014-02-12 23:57:25 +08:00
media_objects = media_objects . uniq ( & :media_id )
2011-02-01 09:57:29 +08:00
media_objects = media_objects . map do | media_object |
2020-09-10 02:29:38 +08:00
if media_object . media_id == " maybe " || media_object . deleted? || ( media_object . context_type != " User " && media_object . context != context )
2011-02-01 09:57:29 +08:00
media_object = nil
end
2015-02-25 05:08:10 +08:00
if media_object & . podcast_format_details
2011-08-09 05:38:41 +08:00
media_object . podcast_associated_asset = messages_hash [ media_object . media_id ]
2011-02-01 09:57:29 +08:00
end
media_object
end
2015-02-25 05:08:10 +08:00
2011-02-01 09:57:29 +08:00
to_podcast ( attachments + media_objects . compact )
end
2021-10-22 00:18:16 +08:00
def self . to_podcast ( elements )
2011-02-01 09:57:29 +08:00
require " rss/2.0 "
2021-11-15 23:09:24 +08:00
elements . filter_map do | elem |
2011-02-10 01:26:42 +08:00
asset = elem . podcast_associated_asset
next unless asset
2021-09-23 00:20:17 +08:00
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
case asset
when DiscussionTopic
link = " http:// #{ HostUrl . context_host ( asset . context ) } / #{ asset . context_url_prefix } /discussion_topics/ #{ asset . id } "
when DiscussionEntry
2015-12-18 07:03:37 +08:00
link = " http:// #{ HostUrl . context_host ( asset . context ) } / #{ asset . context_url_prefix } /discussion_topics/ #{ asset . discussion_topic_id } # entry- #{ asset . id } "
2011-02-10 01:26:42 +08:00
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
case elem
when Attachment
item . guid . content = link + " / #{ elem . uuid } "
2023-04-05 04:51:35 +08:00
url = " http:// #{ HostUrl . context_host ( elem . context ) } / #{ elem . context_url_prefix } " \
2021-09-27 23:26:51 +08:00
" /files/ #{ elem . id } /download #{ elem . extension } ?verifier= #{ elem . uuid } "
2015-04-14 06:13:33 +08:00
item . enclosure = RSS :: Rss :: Channel :: Item :: Enclosure . new ( url , elem . size , elem . content_type )
2011-02-01 09:57:29 +08:00
when 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
2015-02-25 05:08:10 +08:00
ext = details [ :extension ] || details [ :fileExt ]
2023-04-05 04:51:35 +08:00
url = " http:// #{ HostUrl . context_host ( elem . context ) } / #{ elem . context_url_prefix } " \
2021-09-27 23:26:51 +08:00
" /media_download. #{ ext } ?type= #{ ext } &entryId= #{ elem . media_id } &redirect=1 "
2015-04-14 06:13:33 +08:00
item . enclosure = RSS :: Rss :: Channel :: Item :: Enclosure . new ( url , size , content_type )
2011-02-01 09:57:29 +08:00
end
item
2021-11-15 23:09:24 +08:00
end
2011-02-01 09:57:29 +08:00
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
2017-05-04 05:40:51 +08:00
def initial_post_required? ( user , session = nil )
2012-03-24 04:47:02 +08:00
if require_initial_post?
2017-05-04 05:40:51 +08:00
associated_user_ids = user . observer_enrollments . active . where ( course_id : course ) . pluck ( :associated_user_id ) . compact
return ! user_can_see_posts? ( user , session , associated_user_ids )
2012-03-24 04:47:02 +08:00
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 new_record?
2014-01-11 08:44:27 +08:00
[ " [] " , [ ] , [ ] , [ ] ]
2012-05-23 05:13:06 +08:00
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
2020-10-23 04:58:48 +08:00
DiscussionTopic :: MaterializedView . for ( self ) . update_materialized_view ( synchronous : true , use_master : true )
2012-03-24 05:11:05 +08:00
end
fix default grading scheme for letter grade assignment
For letter grade assignments and discussion topics, the default grading
scheme is now 1. the course grading scheme, if it is using one, or 2.
the Canvas default grading scheme. Previously, the default grading
scheme for letter grade assignments and discussion topics was always
the Canvas default grading scheme.
closes GRADE-914
refs CNVS-15228
Test Plan 1: Assignments
1. Set up a course that is NOT using a grading scheme.
2. Create a new assignment, and verify the default Grading Scheme when
choosing 'Letter Grade' is the Canvas Default (A -> F).
3. Go to the New Gradebook and verify you can enter grades in the
submission cells and Grade Details Tray input according to the
Canvas default Grading Scheme rules (i.e. you can enter 'A' or 'B' or
'C', etc.)
4. Go to edit the assignment, and verify when you click 'Letter Grade'
the Canvas Default Grading Scheme is shown.
5. Create a new custom grading scheme at courses/:id/grading_standards.
I'll create one that looks like 'Hi' -> 100 to 94%, 'Bye' -> < 94 to
0%.
6. At the course settings page, check the box next to 'Enable course
grading scheme' and select the custom scheme created in step 5.
Click 'Update Course Details' at the bottom of the page.
7. Create a new assignment, and verify the default Grading Scheme when
choosing 'Letter Grade' is the custom scheme.
8. Go to the New Gradebook and verify you can enter grades in the
submission cells and Grade Details Tray input according to the
custom grading scheme rules (i.e. you can enter 'Hi' or 'Bye', but
entering 'A' or 'B' shows an invalid input error).
9. Go to edit the assignment, and verify when you click 'Letter Grade'
the custom grading scheme is shown.
Test Plan 2: Graded Discussion Topics
1. Set up a course that is NOT using a grading scheme.
2. Create a new graded discussion topic, and verify the default Grading
Scheme when choosing 'Letter Grade' is the Canvas Default (A -> F).
3. Go to the New Gradebook and verify you can enter grades in the
submission cells and Grade Details Tray input according to the
Canvas default Grading Scheme rules (i.e. you can enter 'A' or 'B' or
'C', etc.)
4. Go to edit the discussion topic, and verify when you click
'Letter Grade' the Canvas Default Grading Scheme is shown.
5. Create a new custom grading scheme at courses/:id/grading_standards.
I'll create one that looks like 'Hi' -> 100 to 94%, 'Bye' -> < 94 to
0%.
6. At the course settings page, check the box next to 'Enable course
grading scheme' and select the custom scheme created in step 5.
Click 'Update Course Details' at the bottom of the page.
7. Create a new graded discussion topic, and verify the default Grading
Scheme when choosing 'Letter Grade' is the custom scheme.
8. Go to the New Gradebook and verify you can enter grades in the
submission cells and Grade Details Tray input according to the
custom grading scheme rules (i.e. you can enter 'Hi' or 'Bye', but
entering 'A' or 'B' shows an invalid input error).
9. Go to edit the discussion topic, and verify when you click
'Letter Grade' the custom grading scheme is shown.
Change-Id: I0b7a7bd1f18a506be83a923a421a45e96d129c96
Reviewed-on: https://gerrit.instructure.com/142105
Reviewed-by: Jeremy Neander <jneander@instructure.com>
Reviewed-by: Shahbaz Javeed <sjaveed@instructure.com>
Tested-by: Jenkins
QA-Review: Indira Pai <ipai@instructure.com>
Product-Review: Matt Goodwin <mattg@instructure.com>
2018-02-28 07:06:54 +08:00
def grading_standard_or_default
grading_standard_context = assignment || context
if grading_standard_context . present?
grading_standard_context . grading_standard_or_default
else
GradingStandard . default_instance
end
end
2020-05-09 04:53:05 +08:00
def set_root_account_id
self . root_account_id || = context & . root_account_id
end
2021-11-30 07:42:27 +08:00
def anonymous?
! anonymous_state . nil?
end
2023-10-14 02:28:07 +08:00
def checkpoints?
checkpoint_assignments . any?
end
def reply_to_topic_checkpoint
checkpoint_assignments . find_by ( checkpoint_label : CheckpointLabels :: REPLY_TO_TOPIC )
end
def reply_to_entry_checkpoint
checkpoint_assignments . find_by ( checkpoint_label : CheckpointLabels :: REPLY_TO_ENTRY )
end
def create_checkpoints ( reply_to_topic_points : , reply_to_entry_points : )
return false if checkpoints?
return false unless context . is_a? ( Course )
parent = context . assignments . create! ( checkpointed : true , checkpoint_label : CheckpointLabels :: PARENT )
parent . checkpoint_assignments . create! ( context : , checkpoint_label : CheckpointLabels :: REPLY_TO_TOPIC , points_possible : reply_to_topic_points )
parent . checkpoint_assignments . create! ( context : , checkpoint_label : CheckpointLabels :: REPLY_TO_ENTRY , points_possible : reply_to_entry_points )
self . assignment = parent
save
end
2011-02-01 09:57:29 +08:00
end