Class: Topic
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- Topic
- Extended by:
- Forwardable
- Includes:
- HasCustomFields, LimitedEdit, RateLimiter::OnCreateRecord, Searchable, Trashable
- Defined in:
- app/models/topic.rb
Defined Under Namespace
Classes: NotAllowed, UserExists
Constant Summary collapse
- EXTERNAL_ID_MAX_LENGTH =
50
- PRIVATE_MESSAGES_SQL_USER =
<<~SQL SELECT topic_id FROM topic_allowed_users WHERE user_id = :user_id SQL
- PRIVATE_MESSAGES_SQL_GROUP =
<<~SQL SELECT tg.topic_id FROM topic_allowed_groups tg JOIN group_users gu ON gu.user_id = :user_id AND gu.group_id = tg.group_id SQL
- MAX_SIMILAR_BODY_LENGTH =
200
- TIME_TO_FIRST_RESPONSE_SQL =
<<-SQL SELECT AVG(t.hours)::float AS "hours", t.created_at AS "date" FROM ( SELECT t.id, t.created_at::date AS created_at, EXTRACT(EPOCH FROM MIN(p.created_at) - t.created_at)::float / 3600.0 AS "hours" FROM topics t LEFT JOIN posts p ON p.topic_id = t.id /*where*/ GROUP BY t.id ) t GROUP BY t.created_at ORDER BY t.created_at SQL
- TIME_TO_FIRST_RESPONSE_TOTAL_SQL =
<<-SQL SELECT AVG(t.hours)::float AS "hours" FROM ( SELECT t.id, EXTRACT(EPOCH FROM MIN(p.created_at) - t.created_at)::float / 3600.0 AS "hours" FROM topics t LEFT JOIN posts p ON p.topic_id = t.id /*where*/ GROUP BY t.id ) t SQL
- WITH_NO_RESPONSE_SQL =
<<-SQL SELECT COUNT(*) as count, tt.created_at AS "date" FROM ( SELECT t.id, t.created_at::date AS created_at, MIN(p.post_number) first_reply FROM topics t LEFT JOIN posts p ON p.topic_id = t.id AND p.user_id != t.user_id AND p.deleted_at IS NULL AND p.post_type = #{Post.types[:regular]} /*where*/ GROUP BY t.id ) tt WHERE tt.first_reply IS NULL OR tt.first_reply < 2 GROUP BY tt.created_at ORDER BY tt.created_at SQL
- WITH_NO_RESPONSE_TOTAL_SQL =
<<-SQL SELECT COUNT(*) as count FROM ( SELECT t.id, MIN(p.post_number) first_reply FROM topics t LEFT JOIN posts p ON p.topic_id = t.id AND p.user_id != t.user_id AND p.deleted_at IS NULL AND p.post_type = #{Post.types[:regular]} /*where*/ GROUP BY t.id ) tt WHERE tt.first_reply IS NULL OR tt.first_reply < 2 SQL
Constants included from Searchable
Constants included from HasCustomFields
HasCustomFields::CUSTOM_FIELDS_MAX_ITEMS, HasCustomFields::DEFAULT_FIELD_DESCRIPTOR
Instance Attribute Summary collapse
-
#advance_draft ⇒ Object
Returns the value of attribute advance_draft.
-
#allowed_group_ids ⇒ Object
Returns the value of attribute allowed_group_ids.
-
#allowed_user_ids ⇒ Object
Returns the value of attribute allowed_user_ids.
-
#category_user_data ⇒ Object
Returns the value of attribute category_user_data.
-
#dismissed ⇒ Object
Returns the value of attribute dismissed.
-
#ignore_category_auto_close ⇒ Object
Returns the value of attribute ignore_category_auto_close.
-
#import_mode ⇒ Object
set to true to optimize creation and save for imports.
-
#include_last_poster ⇒ Object
Returns the value of attribute include_last_poster.
-
#includes_destination_category ⇒ Object
Returns the value of attribute includes_destination_category.
-
#participant_groups ⇒ Object
Returns the value of attribute participant_groups.
-
#participants ⇒ Object
Returns the value of attribute participants.
-
#posters ⇒ Object
TODO: can replace with posters_summary once we remove old list code.
-
#skip_callbacks ⇒ Object
Returns the value of attribute skip_callbacks.
-
#tags_changed ⇒ Object
Returns the value of attribute tags_changed.
-
#topic_list ⇒ Object
Returns the value of attribute topic_list.
-
#user_data ⇒ Object
When we want to temporarily attach some data to a forum topic (usually before serialization).
Attributes included from HasCustomFields
Class Method Summary collapse
- .count_exceeds_minimum? ⇒ Boolean
- .editable_custom_fields(guardian) ⇒ Object
- .ensure_consistency! ⇒ Object
- .fancy_title(title) ⇒ Object
- .find_by_slug(slug) ⇒ Object
-
.for_digest(user, since, opts = nil) ⇒ Object
Returns hot topics since a date for display in email digest.
- .has_flag_scope ⇒ Object
- .listable_count_per_day(start_date, end_date, category_id = nil, include_subcategories = false, group_ids = nil) ⇒ Object
- .max_fancy_title_length ⇒ Object
-
.next_post_number(topic_id, opts = {}) ⇒ Object
Atomically creates the next post number.
- .private_message_topics_count_per_day(start_date, end_date, topic_subtype) ⇒ Object
- .publish_stats_to_clients!(topic_id, type, opts = {}) ⇒ Object
- .recent(max = 10) ⇒ Object
- .relative_url(id, slug, post_number = nil) ⇒ Object
- .reset_all_highest! ⇒ Object
-
.reset_highest(topic_id) ⇒ Object
If a post is deleted we have to update our highest post counters and last post information.
- .share_thumbnail_size ⇒ Object
- .similar_to(title, raw, user = nil) ⇒ Object
- .thumbnail_sizes ⇒ Object
- .time_to_first_response(sql, opts = nil) ⇒ Object
- .time_to_first_response_per_day(start_date, end_date, opts = {}) ⇒ Object
- .time_to_first_response_total(opts = nil) ⇒ Object
- .top_viewed(max = 10) ⇒ Object
- .url(id, slug, post_number = nil) ⇒ Object
- .visibility_reasons ⇒ Object
- .visible_post_types(viewed_by = nil, include_moderator_actions: true) ⇒ Object
- .with_no_response_per_day(start_date, end_date, category_id = nil, include_subcategories = nil) ⇒ Object
- .with_no_response_total(opts = {}) ⇒ Object
Instance Method Summary collapse
- #access_topic_via_group ⇒ Object
- #acting_user ⇒ Object
- #acting_user=(u) ⇒ Object
- #add_moderator_post(user, text, opts = nil) ⇒ Object
- #add_small_action(user, action_code, who = nil, opts = {}) ⇒ Object
- #advance_draft_sequence ⇒ Object
- #age_in_minutes ⇒ Object
-
#all_allowed_users ⇒ Object
all users (in groups or directly targeted) that are going to get the pm.
- #auto_close_threshold_reached? ⇒ Boolean
- #banner ⇒ Object
- #best_post ⇒ Object
- #cannot_permanently_delete_reason(user) ⇒ Object
- #category_allows_unlimited_owner_edits_on_first_post? ⇒ Boolean
- #change_category_to_id(category_id) ⇒ Object
- #changed_to_category(new_category) ⇒ Object
- #clear_pin_for(user) ⇒ Object
- #convert_to_private_message(user) ⇒ Object
- #convert_to_public_topic(user, category_id: nil) ⇒ Object
- #create_invite_notification!(target_user, notification_type, invited_by, post_number: 1) ⇒ Object
- #delete_topic_timer(status_type, by_user: Discourse.system_user) ⇒ Object
- #draft_key ⇒ Object
- #email_already_exists_for?(invite) ⇒ Boolean
- #ensure_topic_has_a_category ⇒ Object
- #expandable_first_post? ⇒ Boolean
- #fancy_title ⇒ Object
- #featured_link_root_domain ⇒ Object
- #featured_users ⇒ Object
- #filtered_topic_thumbnails(extra_sizes: []) ⇒ Object
- #first_smtp_enabled_group ⇒ Object
- #generate_thumbnails!(extra_sizes: []) ⇒ Object
- #grant_permission_to_user(lower_email) ⇒ Object
- #group_pm? ⇒ Boolean
- #has_flags? ⇒ Boolean
- #has_topic_embed? ⇒ Boolean
- #image_url(enqueue_if_missing: false) ⇒ Object
- #incoming_email_addresses(group: nil, received_before: Time.zone.now) ⇒ Object
- #inherit_auto_close_from_category(timer_type: :close) ⇒ Object
- #inherit_slow_mode_from_category ⇒ Object
- #initialize_default_values ⇒ Object
- #invite(invited_by, username_or_email, group_ids = nil, custom_message = nil) ⇒ Object
- #invite_group(user, group, should_notify: true) ⇒ Object
- #is_category_topic? ⇒ Boolean
- #is_official_warning? ⇒ Boolean
-
#last_post_url ⇒ Object
NOTE: These are probably better off somewhere else.
- #limit_private_messages_per_day ⇒ Object
-
#limit_topics_per_day ⇒ Object
Additional rate limits on topics: per day and private messages per day.
- #make_banner!(user, bannered_until = nil) ⇒ Object
- #max_post_number ⇒ Object
- #message_archived?(user) ⇒ Boolean
- #move_posts(moved_by, post_ids, opts) ⇒ Object
- #muted?(user) ⇒ Boolean
- #notifier ⇒ Object
- #open? ⇒ Boolean
- #participant_groups_summary(options = {}) ⇒ Object
- #participants_summary(options = {}) ⇒ Object
- #pm_with_non_human_user? ⇒ Boolean
- #post_numbers ⇒ Object
-
#posters_summary(options = {}) ⇒ Object
avatar lookup in options.
- #private_message? ⇒ Boolean
- #public_topic_timer ⇒ Object
- #rate_limit_topic_invitation(invited_by) ⇒ Object
- #re_pin_for(user) ⇒ Object
- #reached_recipients_limit? ⇒ Boolean
- #read_restricted_category? ⇒ Boolean
- #recover!(recovered_by = nil) ⇒ Object
- #regular? ⇒ Boolean
- #relative_url(post_number = nil) ⇒ Object
- #reload(options = nil) ⇒ Object
- #remove_allowed_group(removed_by, name) ⇒ Object
- #remove_allowed_user(removed_by, username) ⇒ Object
- #remove_banner!(user) ⇒ Object
- #reset_bumped_at(post_id = nil) ⇒ Object
- #secure_audience_publish_messages ⇒ Object
- #secure_group_ids ⇒ Object
-
#set_or_create_timer(status_type, time, by_user: nil, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id, duration_minutes: nil, silent: nil) ⇒ Object
Valid arguments for the time: * An integer, which is the number of hours from now to update the topic’s status.
- #shared_draft? ⇒ Boolean
- #slow_mode_topic_timer ⇒ Object
-
#slug ⇒ Object
Even if the slug column in the database is null, topic.slug will return something:.
- #slug_for_topic(title) ⇒ Object
- #slugless_url(post_number = nil) ⇒ Object
- #thumbnail_info(enqueue_if_missing: false, extra_sizes: []) ⇒ Object
- #thumbnail_job_redis_key(sizes) ⇒ Object
- #title=(t) ⇒ Object
- #trash!(trashed_by = nil) ⇒ Object
- #update_action_counts ⇒ Object
- #update_category_topic_count_by(num) ⇒ Object
- #update_excerpt(excerpt) ⇒ Object
- #update_pinned(status, global = false, pinned_until = nil) ⇒ Object
-
#update_statistics ⇒ Object
Updates the denormalized statistics of a topic including featured posters.
- #update_status(status, enabled, user, opts = {}) ⇒ Object
- #url(post_number = nil) ⇒ Object
- #visible_tags(guardian) ⇒ Object
Methods included from LimitedEdit
Methods included from Trashable
Methods included from HasCustomFields
#clear_custom_fields, #create_singular, #custom_field_preloaded?, #custom_fields, #custom_fields=, #custom_fields_clean?, #custom_fields_fk, #custom_fields_preloaded?, #on_custom_fields_change, #save_custom_fields, #set_preloaded_custom_fields, #upsert_custom_fields
Methods included from RateLimiter::OnCreateRecord
#default_rate_limiter, #disable_rate_limits!, included
Instance Attribute Details
#advance_draft ⇒ Object
Returns the value of attribute advance_draft.
395 396 397 |
# File 'app/models/topic.rb', line 395 def advance_draft @advance_draft end |
#allowed_group_ids ⇒ Object
Returns the value of attribute allowed_group_ids.
32 33 34 |
# File 'app/models/topic.rb', line 32 def allowed_group_ids @allowed_group_ids end |
#allowed_user_ids ⇒ Object
Returns the value of attribute allowed_user_ids.
32 33 34 |
# File 'app/models/topic.rb', line 32 def allowed_user_ids @allowed_user_ids end |
#category_user_data ⇒ Object
Returns the value of attribute category_user_data.
319 320 321 |
# File 'app/models/topic.rb', line 319 def category_user_data @category_user_data end |
#dismissed ⇒ Object
Returns the value of attribute dismissed.
320 321 322 |
# File 'app/models/topic.rb', line 320 def dismissed @dismissed end |
#ignore_category_auto_close ⇒ Object
Returns the value of attribute ignore_category_auto_close.
393 394 395 |
# File 'app/models/topic.rb', line 393 def ignore_category_auto_close @ignore_category_auto_close end |
#import_mode ⇒ Object
set to true to optimize creation and save for imports
327 328 329 |
# File 'app/models/topic.rb', line 327 def import_mode @import_mode end |
#include_last_poster ⇒ Object
Returns the value of attribute include_last_poster.
326 327 328 |
# File 'app/models/topic.rb', line 326 def include_last_poster @include_last_poster end |
#includes_destination_category ⇒ Object
Returns the value of attribute includes_destination_category.
32 33 34 |
# File 'app/models/topic.rb', line 32 def includes_destination_category @includes_destination_category end |
#participant_groups ⇒ Object
Returns the value of attribute participant_groups.
324 325 326 |
# File 'app/models/topic.rb', line 324 def participant_groups @participant_groups end |
#participants ⇒ Object
Returns the value of attribute participants.
323 324 325 |
# File 'app/models/topic.rb', line 323 def participants @participants end |
#posters ⇒ Object
TODO: can replace with posters_summary once we remove old list code
322 323 324 |
# File 'app/models/topic.rb', line 322 def posters @posters end |
#skip_callbacks ⇒ Object
Returns the value of attribute skip_callbacks.
394 395 396 |
# File 'app/models/topic.rb', line 394 def skip_callbacks @skip_callbacks end |
#tags_changed ⇒ Object
Returns the value of attribute tags_changed.
32 33 34 |
# File 'app/models/topic.rb', line 32 def @tags_changed end |
#topic_list ⇒ Object
Returns the value of attribute topic_list.
325 326 327 |
# File 'app/models/topic.rb', line 325 def topic_list @topic_list end |
#user_data ⇒ Object
When we want to temporarily attach some data to a forum topic (usually before serialization)
318 319 320 |
# File 'app/models/topic.rb', line 318 def user_data @user_data end |
Class Method Details
.count_exceeds_minimum? ⇒ Boolean
481 482 483 |
# File 'app/models/topic.rb', line 481 def self.count_exceeds_minimum? count > SiteSetting.minimum_topics_similar end |
.editable_custom_fields(guardian) ⇒ Object
2120 2121 2122 2123 2124 2125 |
# File 'app/models/topic.rb', line 2120 def self.editable_custom_fields(guardian) fields = [] fields.push(*DiscoursePluginRegistry.public_editable_topic_custom_fields) fields.push(*DiscoursePluginRegistry.staff_editable_topic_custom_fields) if guardian.is_staff? fields end |
.ensure_consistency! ⇒ Object
1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 |
# File 'app/models/topic.rb', line 1511 def self.ensure_consistency! # unpin topics that might have been missed Topic.where("pinned_until < ?", Time.now).update_all( pinned_at: nil, pinned_globally: false, pinned_until: nil, ) Topic .where("bannered_until < ?", Time.now) .find_each { |topic| topic.(Discourse.system_user) } end |
.fancy_title(title) ⇒ Object
529 530 531 532 533 |
# File 'app/models/topic.rb', line 529 def self.fancy_title(title) return unless escaped = ERB::Util.html_escape(title) fancy_title = Emoji.unicode_unescape(HtmlPrettify.render(escaped)) fancy_title.length > Topic.max_fancy_title_length ? escaped : fancy_title end |
.find_by_slug(slug) ⇒ Object
1421 1422 1423 1424 1425 1426 1427 1428 |
# File 'app/models/topic.rb', line 1421 def self.find_by_slug(slug) if SiteSetting.slug_generation_method != "encoded" Topic.find_by(slug: slug.downcase) else encoded_slug = CGI.escape(slug) Topic.find_by(slug: encoded_slug) end end |
.for_digest(user, since, opts = nil) ⇒ Object
Returns hot topics since a date for display in email digest.
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 |
# File 'app/models/topic.rb', line 557 def self.for_digest(user, since, opts = nil) opts ||= {} period = ListController.best_period_for(since) topics = Topic .visible .secured(Guardian.new(user)) .joins( "LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{user.id.to_i}", ) .joins( "LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id.to_i}", ) .joins("LEFT OUTER JOIN users ON users.id = topics.user_id") .where(closed: false, archived: false) .where( "COALESCE(topic_users.notification_level, 1) <> ?", TopicUser.notification_levels[:muted], ) .created_since(since) .where("topics.created_at < ?", (SiteSetting.editing_grace_period || 0).seconds.ago) .listable_topics .includes(:category) unless opts[:include_tl0] || user.user_option.try(:include_tl0_in_digests) topics = topics.where("COALESCE(users.trust_level, 0) > 0") end if !!opts[:top_order] topics = topics.joins("LEFT OUTER JOIN top_topics ON top_topics.topic_id = topics.id").order(<<~SQL) COALESCE(topic_users.notification_level, 1) DESC, COALESCE(category_users.notification_level, 1) DESC, COALESCE(top_topics.#{TopTopic.score_column_for_period(period)}, 0) DESC, topics.bumped_at DESC SQL end topics = topics.limit(opts[:limit]) if opts[:limit] # Remove category topics topics = topics.where.not(id: Category.select(:topic_id).where.not(topic_id: nil)) # Remove suppressed categories if SiteSetting.digest_suppress_categories.present? topics = topics.where.not(category_id: SiteSetting.digest_suppress_categories.split("|").map(&:to_i)) end # Remove suppressed tags if SiteSetting..present? tag_ids = Tag.where_name(SiteSetting..split("|")).pluck(:id) topics = topics.where.not(id: TopicTag.where(tag_id: tag_ids).select(:topic_id)) if tag_ids.present? end # Remove muted and shared draft categories remove_category_ids = CategoryUser.where( user_id: user.id, notification_level: CategoryUser.notification_levels[:muted], ).pluck(:category_id) remove_category_ids << SiteSetting.shared_drafts_category if SiteSetting.shared_drafts_enabled? if remove_category_ids.present? remove_category_ids.uniq! topics = topics.where( "topic_users.notification_level != ? OR topics.category_id NOT IN (?)", TopicUser.notification_levels[:muted], remove_category_ids, ) end # Remove muted tags muted_tag_ids = TagUser.lookup(user, :muted).pluck(:tag_id) unless muted_tag_ids.empty? # If multiple tags per topic, include topics with tags that aren't muted, # and don't forget untagged topics. topics = topics.where( "EXISTS ( SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id AND tag_id NOT IN (?) ) OR NOT EXISTS (SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id)", muted_tag_ids, ) end topics end |
.has_flag_scope ⇒ Object
493 494 495 |
# File 'app/models/topic.rb', line 493 def self.has_flag_scope ReviewableFlaggedPost.pending_and_default_visible end |
.listable_count_per_day(start_date, end_date, category_id = nil, include_subcategories = false, group_ids = nil) ⇒ Object
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 |
# File 'app/models/topic.rb', line 667 def self.listable_count_per_day( start_date, end_date, category_id = nil, include_subcategories = false, group_ids = nil ) result = listable_topics.where( "topics.created_at >= ? AND topics.created_at <= ?", start_date, end_date, ) result = result.group("date(topics.created_at)").order("date(topics.created_at)") result = result.where( category_id: include_subcategories ? Category.subcategory_ids(category_id) : category_id, ) if category_id if group_ids result = result .joins("INNER JOIN users ON users.id = topics.user_id") .joins("INNER JOIN group_users ON group_users.user_id = users.id") .where("group_users.group_id IN (?)", group_ids) end result.count end |
.max_fancy_title_length ⇒ Object
34 35 36 |
# File 'app/models/topic.rb', line 34 def self.max_fancy_title_length 400 end |
.next_post_number(topic_id, opts = {}) ⇒ Object
Atomically creates the next post number
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 |
# File 'app/models/topic.rb', line 819 def self.next_post_number(topic_id, opts = {}) highest = DB .query_single( "SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", topic_id, ) .first .to_i if opts[:whisper] result = DB.query_single(<<~SQL, highest, topic_id) UPDATE topics SET highest_staff_post_number = ? + 1 WHERE id = ? RETURNING highest_staff_post_number SQL result.first.to_i else reply_sql = opts[:reply] ? ", reply_count = reply_count + 1" : "" posts_sql = opts[:post] ? ", posts_count = posts_count + 1" : "" result = DB.query_single(<<~SQL, highest: highest, topic_id: topic_id) UPDATE topics SET highest_staff_post_number = :highest + 1, highest_post_number = :highest + 1 #{reply_sql} #{posts_sql} WHERE id = :topic_id RETURNING highest_post_number SQL result.first.to_i end end |
.private_message_topics_count_per_day(start_date, end_date, topic_subtype) ⇒ Object
1902 1903 1904 1905 1906 1907 1908 1909 |
# File 'app/models/topic.rb', line 1902 def self.(start_date, end_date, topic_subtype) .with_subtype(topic_subtype) .where("topics.created_at >= ? AND topics.created_at <= ?", start_date, end_date) .group("date(topics.created_at)") .order("date(topics.created_at)") .count end |
.publish_stats_to_clients!(topic_id, type, opts = {}) ⇒ Object
2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 |
# File 'app/models/topic.rb', line 2085 def self.publish_stats_to_clients!(topic_id, type, opts = {}) topic = Topic.find_by(id: topic_id) return if topic.blank? case type when :liked, :unliked stats = { like_count: topic.like_count } when :created, :destroyed, :deleted, :recovered stats = { posts_count: topic.posts_count, last_posted_at: topic.last_posted_at.as_json, last_poster: BasicUserSerializer.new(topic.last_poster, root: false).as_json, } else stats = nil end if stats secure_audience = topic. if secure_audience[:user_ids] != [] && secure_audience[:group_ids] != [] = stats.merge({ id: topic_id, updated_at: Time.now, type: :stats }) MessageBus.publish("/topic/#{topic_id}", , opts.merge(secure_audience)) end end end |
.recent(max = 10) ⇒ Object
477 478 479 |
# File 'app/models/topic.rb', line 477 def self.recent(max = 10) Topic.listable_topics.visible.secured.order("created_at desc").limit(max) end |
.relative_url(id, slug, post_number = nil) ⇒ Object
1453 1454 1455 1456 1457 1458 1459 |
# File 'app/models/topic.rb', line 1453 def self.relative_url(id, slug, post_number = nil) url = +"#{Discourse.base_path}/t/" url << "#{slug}/" if slug.present? url << id.to_s url << "/#{post_number}" if post_number.to_i > 1 url end |
.reset_all_highest! ⇒ Object
856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 |
# File 'app/models/topic.rb', line 856 def self.reset_all_highest! DB.exec <<~SQL WITH X as ( SELECT topic_id, COALESCE(MAX(post_number), 0) highest_post_number FROM posts WHERE deleted_at IS NULL GROUP BY topic_id ), Y as ( SELECT topic_id, coalesce(MAX(post_number), 0) highest_post_number, count(*) posts_count, max(created_at) last_posted_at FROM posts WHERE deleted_at IS NULL AND post_type <> 4 GROUP BY topic_id ), Z as ( SELECT topic_id, SUM(COALESCE(posts.word_count, 0)) word_count FROM posts WHERE deleted_at IS NULL AND post_type <> 4 GROUP BY topic_id ) UPDATE topics SET highest_staff_post_number = X.highest_post_number, highest_post_number = Y.highest_post_number, last_posted_at = Y.last_posted_at, posts_count = Y.posts_count, word_count = Z.word_count FROM X, Y, Z WHERE topics.archetype <> 'private_message' AND X.topic_id = topics.id AND Y.topic_id = topics.id AND Z.topic_id = topics.id AND ( topics.highest_staff_post_number <> X.highest_post_number OR topics.highest_post_number <> Y.highest_post_number OR topics.last_posted_at <> Y.last_posted_at OR topics.posts_count <> Y.posts_count OR topics.word_count <> Z.word_count ) SQL DB.exec <<~SQL WITH X as ( SELECT topic_id, COALESCE(MAX(post_number), 0) highest_post_number FROM posts WHERE deleted_at IS NULL GROUP BY topic_id ), Y as ( SELECT topic_id, coalesce(MAX(post_number), 0) highest_post_number, count(*) posts_count, max(created_at) last_posted_at FROM posts WHERE deleted_at IS NULL AND post_type <> 3 AND post_type <> 4 GROUP BY topic_id ), Z as ( SELECT topic_id, SUM(COALESCE(posts.word_count, 0)) word_count FROM posts WHERE deleted_at IS NULL AND post_type <> 3 AND post_type <> 4 GROUP BY topic_id ) UPDATE topics SET highest_staff_post_number = X.highest_post_number, highest_post_number = Y.highest_post_number, last_posted_at = Y.last_posted_at, posts_count = Y.posts_count, word_count = Z.word_count FROM X, Y, Z WHERE topics.archetype = 'private_message' AND X.topic_id = topics.id AND Y.topic_id = topics.id AND Z.topic_id = topics.id AND ( topics.highest_staff_post_number <> X.highest_post_number OR topics.highest_post_number <> Y.highest_post_number OR topics.last_posted_at <> Y.last_posted_at OR topics.posts_count <> Y.posts_count OR topics.word_count <> Z.word_count ) SQL end |
.reset_highest(topic_id) ⇒ Object
If a post is deleted we have to update our highest post counters and last post information
951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 |
# File 'app/models/topic.rb', line 951 def self.reset_highest(topic_id) archetype = Topic.where(id: topic_id).pick(:archetype) # ignore small_action replies for private messages post_type = archetype == Archetype. ? " AND post_type <> #{Post.types[:small_action]}" : "" result = DB.query_single(<<~SQL, topic_id: topic_id) UPDATE topics SET highest_staff_post_number = ( SELECT COALESCE(MAX(post_number), 0) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL ), highest_post_number = ( SELECT COALESCE(MAX(post_number), 0) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 #{post_type} ), posts_count = ( SELECT count(*) FROM posts WHERE deleted_at IS NULL AND topic_id = :topic_id AND post_type <> 4 #{post_type} ), word_count = ( SELECT SUM(COALESCE(posts.word_count, 0)) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 #{post_type} ), last_posted_at = ( SELECT MAX(created_at) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 #{post_type} ), last_post_user_id = COALESCE(( SELECT user_id FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 #{post_type} ORDER BY created_at desc LIMIT 1 ), last_post_user_id) WHERE id = :topic_id RETURNING highest_post_number SQL highest_post_number = result.first.to_i # Update the forum topic user records DB.exec(<<~SQL, highest: highest_post_number, topic_id: topic_id) UPDATE topic_users SET last_read_post_number = CASE WHEN last_read_post_number > :highest THEN :highest ELSE last_read_post_number END WHERE topic_id = :topic_id SQL end |
.share_thumbnail_size ⇒ Object
38 39 40 |
# File 'app/models/topic.rb', line 38 def self.share_thumbnail_size [1024, 1024] end |
.similar_to(title, raw, user = nil) ⇒ Object
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 |
# File 'app/models/topic.rb', line 711 def self.similar_to(title, raw, user = nil) return [] if SiteSetting.max_similar_results == 0 return [] if title.blank? raw = raw.presence || "" search_data = Search.prepare_data(title.strip) return [] if search_data.blank? tsquery = Search.set_tsquery_weight_filter(search_data, "A") if raw.present? cooked = SearchIndexer::HtmlScrubber.scrub(PrettyText.cook(raw[0...MAX_SIMILAR_BODY_LENGTH].strip)) prepared_data = cooked.present? && Search.prepare_data(cooked) if prepared_data.present? raw_tsquery = Search.set_tsquery_weight_filter(prepared_data, "B") tsquery = "#{tsquery} & #{raw_tsquery}" end end tsquery = Search.to_tsquery(term: tsquery, joiner: "|") guardian = Guardian.new(user) excluded_category_ids_sql = Category .secured(guardian) .where(search_priority: Searchable::PRIORITIES[:ignore]) .select(:id) .to_sql excluded_category_ids_sql = <<~SQL if user #{excluded_category_ids_sql} UNION #{CategoryUser.muted_category_ids_query(user, include_direct: true).select("categories.id").to_sql} SQL candidates = Topic .visible .listable_topics .secured(guardian) .joins("JOIN topic_search_data s ON topics.id = s.topic_id") .joins("LEFT JOIN categories c ON topics.id = c.topic_id") .where("search_data @@ #{tsquery}") .where("c.topic_id IS NULL") .where("topics.category_id NOT IN (#{excluded_category_ids_sql})") .order("ts_rank(search_data, #{tsquery}) DESC") .limit(SiteSetting.max_similar_results * 3) candidate_ids = candidates.pluck(:id) return [] if candidate_ids.blank? similars = Topic .joins("JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1") .where("topics.id IN (?)", candidate_ids) .order("similarity DESC") .limit(SiteSetting.max_similar_results) if raw.present? similars.select( DB.sql_fragment( "topics.*, similarity(topics.title, :title) + similarity(p.raw, :raw) AS similarity, p.cooked AS blurb", title: title, raw: raw, ), ).where( "similarity(topics.title, :title) + similarity(p.raw, :raw) > 0.2", title: title, raw: raw, ) else similars.select( DB.sql_fragment( "topics.*, similarity(topics.title, :title) AS similarity, p.cooked AS blurb", title: title, ), ).where("similarity(topics.title, :title) > 0.2", title: title) end end |
.thumbnail_sizes ⇒ Object
42 43 44 |
# File 'app/models/topic.rb', line 42 def self.thumbnail_sizes [self.share_thumbnail_size] + DiscoursePluginRegistry.topic_thumbnail_sizes end |
.time_to_first_response(sql, opts = nil) ⇒ Object
1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 |
# File 'app/models/topic.rb', line 1771 def self.time_to_first_response(sql, opts = nil) opts ||= {} builder = DB.build(sql) builder.where("t.created_at >= :start_date", start_date: opts[:start_date]) if opts[:start_date] builder.where("t.created_at < :end_date", end_date: opts[:end_date]) if opts[:end_date] if opts[:category_id] if opts[:include_subcategories] builder.where("t.category_id IN (?)", Category.subcategory_ids(opts[:category_id])) else builder.where("t.category_id = ?", opts[:category_id]) end end builder.where("t.archetype <> '#{Archetype.}'") builder.where("t.deleted_at IS NULL") builder.where("p.deleted_at IS NULL") builder.where("p.post_number > 1") builder.where("p.user_id != t.user_id") builder.where("p.user_id in (:user_ids)", user_ids: opts[:user_ids]) if opts[:user_ids] builder.where("p.post_type = :post_type", post_type: Post.types[:regular]) builder.where("EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0") builder.query_hash end |
.time_to_first_response_per_day(start_date, end_date, opts = {}) ⇒ Object
1794 1795 1796 1797 1798 1799 |
# File 'app/models/topic.rb', line 1794 def self.time_to_first_response_per_day(start_date, end_date, opts = {}) time_to_first_response( TIME_TO_FIRST_RESPONSE_SQL, opts.merge(start_date: start_date, end_date: end_date), ) end |
.time_to_first_response_total(opts = nil) ⇒ Object
1801 1802 1803 1804 |
# File 'app/models/topic.rb', line 1801 def self.time_to_first_response_total(opts = nil) total = time_to_first_response(TIME_TO_FIRST_RESPONSE_TOTAL_SQL, opts) total.first["hours"].to_f.round(2) end |
.top_viewed(max = 10) ⇒ Object
473 474 475 |
# File 'app/models/topic.rb', line 473 def self.top_viewed(max = 10) Topic.listable_topics.visible.secured.order("views desc").limit(max) end |
.url(id, slug, post_number = nil) ⇒ Object
1443 1444 1445 1446 1447 |
# File 'app/models/topic.rb', line 1443 def self.url(id, slug, post_number = nil) url = +"#{Discourse.base_url}/t/#{slug}/#{id}" url << "/#{post_number}" if post_number.to_i > 1 url end |
.visibility_reasons ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'app/models/topic.rb', line 46 def self.visibility_reasons @visible_reasons ||= Enum.new( op_flag_threshold_reached: 0, op_unhidden: 1, embedded_topic: 2, manually_unlisted: 3, manually_relisted: 4, bulk_action: 5, unknown: 99, ) end |
.visible_post_types(viewed_by = nil, include_moderator_actions: true) ⇒ Object
465 466 467 468 469 470 471 |
# File 'app/models/topic.rb', line 465 def self.visible_post_types(viewed_by = nil, include_moderator_actions: true) types = Post.types result = [types[:regular]] result += [types[:moderator_action], types[:small_action]] if include_moderator_actions result << types[:whisper] if viewed_by&.whisperer? result end |
.with_no_response_per_day(start_date, end_date, category_id = nil, include_subcategories = nil) ⇒ Object
1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 |
# File 'app/models/topic.rb', line 1820 def self.with_no_response_per_day( start_date, end_date, category_id = nil, include_subcategories = nil ) builder = DB.build(WITH_NO_RESPONSE_SQL) builder.where("t.created_at >= :start_date", start_date: start_date) if start_date builder.where("t.created_at < :end_date", end_date: end_date) if end_date if category_id if include_subcategories builder.where("t.category_id IN (?)", Category.subcategory_ids(category_id)) else builder.where("t.category_id = ?", category_id) end end builder.where("t.archetype <> '#{Archetype.}'") builder.where("t.deleted_at IS NULL") builder.query_hash end |
.with_no_response_total(opts = {}) ⇒ Object
1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 |
# File 'app/models/topic.rb', line 1853 def self.with_no_response_total(opts = {}) builder = DB.build(WITH_NO_RESPONSE_TOTAL_SQL) if opts[:category_id] if opts[:include_subcategories] builder.where("t.category_id IN (?)", Category.subcategory_ids(opts[:category_id])) else builder.where("t.category_id = ?", opts[:category_id]) end end builder.where("t.archetype <> '#{Archetype.}'") builder.where("t.deleted_at IS NULL") builder.query_single.first.to_i end |
Instance Method Details
#access_topic_via_group ⇒ Object
1962 1963 1964 1965 1966 1967 1968 1969 |
# File 'app/models/topic.rb', line 1962 def access_topic_via_group Group .joins(:category_groups) .where("category_groups.category_id = ?", self.category_id) .where("groups.public_admission OR groups.allow_membership_requests") .order(:allow_membership_requests) .first end |
#acting_user ⇒ Object
1693 1694 1695 |
# File 'app/models/topic.rb', line 1693 def acting_user @acting_user || user end |
#acting_user=(u) ⇒ Object
1697 1698 1699 |
# File 'app/models/topic.rb', line 1697 def acting_user=(u) @acting_user = u end |
#add_moderator_post(user, text, opts = nil) ⇒ Object
1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 |
# File 'app/models/topic.rb', line 1098 def add_moderator_post(user, text, opts = nil) opts ||= {} new_post = nil creator = PostCreator.new( user, raw: text, post_type: opts[:post_type] || Post.types[:moderator_action], action_code: opts[:action_code], no_bump: opts[:bump].blank?, topic_id: self.id, silent: opts[:silent], skip_validations: true, custom_fields: opts[:custom_fields], import_mode: opts[:import_mode], ) if (new_post = creator.create) && new_post.present? increment!(:moderator_posts_count) if new_post.persisted? # If we are moving posts, we want to insert the moderator post where the previous posts were # in the stream, not at the end. if opts[:post_number].present? new_post.update!(post_number: opts[:post_number], sort_order: opts[:post_number]) end # Grab any links that are present TopicLink.extract_from(new_post) QuotedPost.extract_from(new_post) end new_post end |
#add_small_action(user, action_code, who = nil, opts = {}) ⇒ Object
1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/topic.rb', line 1085 def add_small_action(user, action_code, who = nil, opts = {}) custom_fields = {} custom_fields["action_code_who"] = who if who.present? opts = opts.merge( post_type: Post.types[:small_action], action_code: action_code, custom_fields: custom_fields, ) add_moderator_post(user, nil, opts) end |
#advance_draft_sequence ⇒ Object
451 452 453 454 455 456 457 |
# File 'app/models/topic.rb', line 451 def advance_draft_sequence if self. DraftSequence.next!(user, Draft::NEW_PRIVATE_MESSAGE) else DraftSequence.next!(user, Draft::NEW_TOPIC) end end |
#age_in_minutes ⇒ Object
663 664 665 |
# File 'app/models/topic.rb', line 663 def age_in_minutes ((Time.zone.now - created_at) / 1.minute).round end |
#all_allowed_users ⇒ Object
all users (in groups or directly targeted) that are going to get the pm
506 507 508 509 510 511 512 |
# File 'app/models/topic.rb', line 506 def all_allowed_users moderators_sql = " UNION #{User.moderators.to_sql}" if && (has_flags? || is_official_warning?) User.from( "(#{allowed_users.to_sql} UNION #{allowed_group_users.to_sql}#{moderators_sql}) as users", ) end |
#auto_close_threshold_reached? ⇒ Boolean
1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 |
# File 'app/models/topic.rb', line 1935 def auto_close_threshold_reached? return if user&.staff? scores = ReviewableScore .pending .joins(:reviewable) .where("reviewable_scores.score >= ?", Reviewable.min_score_for_priority) .where("reviewables.topic_id = ?", self.id) .pluck( "COUNT(DISTINCT reviewable_scores.user_id), COALESCE(SUM(reviewable_scores.score), 0.0)", ) .first scores[0] >= SiteSetting.num_flaggers_to_close_topic && scores[1] >= Reviewable.score_to_auto_close_topic end |
#banner ⇒ Object
1387 1388 1389 1390 1391 |
# File 'app/models/topic.rb', line 1387 def post = self.ordered_posts.first { html: post.cooked, key: self.id, url: self.url } end |
#best_post ⇒ Object
485 486 487 488 489 490 491 |
# File 'app/models/topic.rb', line 485 def best_post posts .where(post_type: Post.types[:regular], user_deleted: false) .order("score desc nulls last") .limit(1) .first end |
#cannot_permanently_delete_reason(user) ⇒ Object
2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 |
# File 'app/models/topic.rb', line 2046 def cannot_permanently_delete_reason(user) all_posts_count = Post .with_deleted .where(topic_id: self.id) .where( post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]], ) .count if posts_count > 0 || all_posts_count > 1 I18n.t("post.cannot_permanently_delete.many_posts") elsif self.deleted_by_id == user&.id && self.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago time_left = RateLimiter.time_left( Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i, ) I18n.t("post.cannot_permanently_delete.wait_or_different_admin", time_left: time_left) end end |
#category_allows_unlimited_owner_edits_on_first_post? ⇒ Boolean
1689 1690 1691 |
# File 'app/models/topic.rb', line 1689 def category_allows_unlimited_owner_edits_on_first_post? category && category.allow_unlimited_owner_edits_on_first_post? end |
#change_category_to_id(category_id) ⇒ Object
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 |
# File 'app/models/topic.rb', line 1131 def change_category_to_id(category_id) return false if new_category_id = category_id.to_i # if the category name is blank, reset the attribute new_category_id = SiteSetting.uncategorized_category_id if new_category_id == 0 return true if self.category_id == new_category_id cat = Category.find_by(id: new_category_id) return false unless cat reviewables.update_all(category_id: new_category_id) changed_to_category(cat) end |
#changed_to_category(new_category) ⇒ Object
1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 |
# File 'app/models/topic.rb', line 1022 def changed_to_category(new_category) return true if new_category.blank? || Category.exists?(topic_id: id) if new_category.id == SiteSetting.uncategorized_category_id && !SiteSetting.allow_uncategorized_topics return false end Topic.transaction do old_category = category if self.category_id != new_category.id self.update(category_id: new_category.id) if old_category Category.where(id: old_category.id).update_all("topic_count = topic_count - 1") count = if old_category.read_restricted && !new_category.read_restricted 1 elsif !old_category.read_restricted && new_category.read_restricted -1 end Tag.update_counters(self., { public_topic_count: count }) if count end # when a topic changes category we may have to start watching it # if we happen to have read state for it CategoryUser.auto_watch(category_id: new_category.id, topic_id: self.id) CategoryUser.auto_track(category_id: new_category.id, topic_id: self.id) if !SiteSetting.disable_category_edit_notifications && (post = self.ordered_posts.first) notified_user_ids = [post.user_id, post.last_editor_id].uniq DB.after_commit do Jobs.enqueue( :notify_category_change, post_id: post.id, notified_user_ids: notified_user_ids, ) end end # when a topic changes category we may need to make uploads # linked to posts secure/not secure depending on whether the # category is private. this is only done if the category # has actually changed to avoid noise. DB.after_commit { Jobs.enqueue(:update_topic_upload_security, topic_id: self.id) } end Category.where(id: new_category.id).update_all("topic_count = topic_count + 1") if Topic.update_featured_topics != false CategoryFeaturedTopic.feature_topics_for(old_category) unless @import_mode unless @import_mode || old_category.try(:id) == new_category.id CategoryFeaturedTopic.feature_topics_for(new_category) end end end true end |
#clear_pin_for(user) ⇒ Object
1469 1470 1471 1472 |
# File 'app/models/topic.rb', line 1469 def clear_pin_for(user) return if user.blank? TopicUser.change(user.id, id, cleared_pinned_at: Time.now) end |
#convert_to_private_message(user) ⇒ Object
1871 1872 1873 |
# File 'app/models/topic.rb', line 1871 def (user) TopicConverter.new(self, user). end |
#convert_to_public_topic(user, category_id: nil) ⇒ Object
1867 1868 1869 |
# File 'app/models/topic.rb', line 1867 def convert_to_public_topic(user, category_id: nil) TopicConverter.new(self, user).convert_to_public_topic(category_id) end |
#create_invite_notification!(target_user, notification_type, invited_by, post_number: 1) ⇒ Object
2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 |
# File 'app/models/topic.rb', line 2009 def create_invite_notification!(target_user, notification_type, invited_by, post_number: 1) if UserCommScreener.new( acting_user: invited_by, target_user_ids: target_user.id, ).ignoring_or_muting_actor?(target_user.id) raise NotAllowed.new(I18n.t("not_accepting_pms", username: target_user.username)) end target_user.notifications.create!( notification_type: notification_type, topic_id: self.id, post_number: post_number, data: { topic_title: self.title, display_username: invited_by.username, original_user_id: user.id, original_username: user.username, }.to_json, ) end |
#delete_topic_timer(status_type, by_user: Discourse.system_user) ⇒ Object
1573 1574 1575 1576 1577 1578 1579 |
# File 'app/models/topic.rb', line 1573 def delete_topic_timer(status_type, by_user: Discourse.system_user) = { status_type: status_type } .merge!(user: by_user) unless TopicTimer.public_types[status_type] self.topic_timers.find_by()&.trash!(by_user) @public_topic_timer = nil nil end |
#draft_key ⇒ Object
1499 1500 1501 |
# File 'app/models/topic.rb', line 1499 def draft_key "#{Draft::EXISTING_TOPIC}#{id}" end |
#email_already_exists_for?(invite) ⇒ Boolean
1279 1280 1281 |
# File 'app/models/topic.rb', line 1279 def email_already_exists_for?(invite) invite.email_already_exists && end |
#ensure_topic_has_a_category ⇒ Object
459 460 461 462 463 |
# File 'app/models/topic.rb', line 459 def ensure_topic_has_a_category if category_id.nil? && (archetype.nil? || self.regular?) self.category_id = category&.id || SiteSetting.uncategorized_category_id end end |
#expandable_first_post? ⇒ Boolean
1710 1711 1712 |
# File 'app/models/topic.rb', line 1710 def SiteSetting. && end |
#fancy_title ⇒ Object
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 |
# File 'app/models/topic.rb', line 535 def fancy_title return ERB::Util.html_escape(title) unless SiteSetting.title_fancy_entities? unless fancy_title = read_attribute(:fancy_title) fancy_title = Topic.fancy_title(title) write_attribute(:fancy_title, fancy_title) if !new_record? && !Discourse.readonly_mode? # make sure data is set in table, this also allows us to change algorithm # by simply nulling this column DB.exec( "UPDATE topics SET fancy_title = :fancy_title where id = :id", id: self.id, fancy_title: fancy_title, ) end end fancy_title end |
#featured_link_root_domain ⇒ Object
1898 1899 1900 |
# File 'app/models/topic.rb', line 1898 def featured_link_root_domain MiniSuffix.domain(UrlHelper.encode_and_parse(self.featured_link).hostname) end |
#featured_users ⇒ Object
148 149 150 |
# File 'app/models/topic.rb', line 148 def featured_users @featured_users ||= TopicFeaturedUsers.new(self) end |
#filtered_topic_thumbnails(extra_sizes: []) ⇒ Object
67 68 69 70 71 72 73 74 75 |
# File 'app/models/topic.rb', line 67 def filtered_topic_thumbnails(extra_sizes: []) return nil unless original = image_upload return nil unless original.read_attribute(:width) && original.read_attribute(:height) thumbnail_sizes = Topic.thumbnail_sizes + extra_sizes topic_thumbnails.filter do |record| thumbnail_sizes.include?([record.max_width, record.max_height]) end end |
#first_smtp_enabled_group ⇒ Object
2067 2068 2069 |
# File 'app/models/topic.rb', line 2067 def first_smtp_enabled_group self.allowed_groups.where(smtp_enabled: true).first end |
#generate_thumbnails!(extra_sizes: []) ⇒ Object
117 118 119 120 121 122 123 124 125 126 127 |
# File 'app/models/topic.rb', line 117 def generate_thumbnails!(extra_sizes: []) return nil unless SiteSetting.create_thumbnails return nil unless original = image_upload return nil if original.filesize >= SiteSetting.max_image_size_kb.kilobytes return nil unless original.width && original.height extra_sizes = [] unless extra_sizes.kind_of?(Array) (Topic.thumbnail_sizes + extra_sizes).each do |dim| TopicThumbnail.find_or_create_for!(original, max_width: dim[0], max_height: dim[1]) end end |
#grant_permission_to_user(lower_email) ⇒ Object
1283 1284 1285 1286 1287 1288 |
# File 'app/models/topic.rb', line 1283 def (lower_email) user = User.find_by_email(lower_email) unless topic_allowed_users.exists?(user_id: user.id) topic_allowed_users.create!(user_id: user.id) end end |
#group_pm? ⇒ Boolean
2112 2113 2114 |
# File 'app/models/topic.rb', line 2112 def group_pm? && all_allowed_users.count > 2 end |
#has_flags? ⇒ Boolean
497 498 499 |
# File 'app/models/topic.rb', line 497 def has_flags? self.class.has_flag_scope.exists?(topic_id: self.id) end |
#has_topic_embed? ⇒ Boolean
1706 1707 1708 |
# File 'app/models/topic.rb', line 1706 def TopicEmbed.where(topic_id: id).exists? end |
#image_url(enqueue_if_missing: false) ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'app/models/topic.rb', line 129 def image_url(enqueue_if_missing: false) thumbnail = topic_thumbnails.detect do |record| record.max_width == Topic.share_thumbnail_size[0] && record.max_height == Topic.share_thumbnail_size[1] end if thumbnail.nil? && image_upload && SiteSetting.create_thumbnails && image_upload.filesize < SiteSetting.max_image_size_kb.kilobytes && image_upload.read_attribute(:width) && image_upload.read_attribute(:height) && enqueue_if_missing && Discourse.redis.set(thumbnail_job_redis_key([]), 1, nx: true, ex: 1.minute) Jobs.enqueue(:generate_topic_thumbnails, { topic_id: id }) end raw_url = thumbnail&.optimized_image&.url || image_upload&.url UrlHelper.cook_url(raw_url, secure: image_upload&.secure?, local: true) if raw_url end |
#incoming_email_addresses(group: nil, received_before: Time.zone.now) ⇒ Object
1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 |
# File 'app/models/topic.rb', line 1971 def incoming_email_addresses(group: nil, received_before: Time.zone.now) email_addresses = Set.new self .incoming_email .where("created_at <= ?", received_before) .each do |incoming_email| to_addresses = incoming_email.to_addresses_split cc_addresses = incoming_email.cc_addresses_split combined_addresses = [to_addresses, cc_addresses].flatten # We only care about the emails addressed to the group or CC'd to the # group if the group is present. If combined addresses is empty we do # not need to do this check, and instead can proceed on to adding the # from address. # # Will not include [email protected] if the only IncomingEmail # is: # # from: [email protected] # to: [email protected] # # Because we don't care about the from addresses and also the to address # is not the email_username, which will be something like [email protected]. if group.present? && combined_addresses.any? next if combined_addresses.none? { |address| address =~ group.email_username_regex } end email_addresses.add(incoming_email.from_address) email_addresses.merge(combined_addresses) end email_addresses.subtract([nil, ""]) email_addresses.delete(group.email_username) if group.present? email_addresses.to_a end |
#inherit_auto_close_from_category(timer_type: :close) ⇒ Object
1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 |
# File 'app/models/topic.rb', line 1529 def inherit_auto_close_from_category(timer_type: :close) auto_close_hours = self.category&.auto_close_hours if self.open? && !@ignore_category_auto_close && auto_close_hours.present? && public_topic_timer&.execute_at.blank? based_on_last_post = self.category.auto_close_based_on_last_post duration_minutes = based_on_last_post ? auto_close_hours * 60 : nil # the timer time can be a timestamp or an integer based # on the number of hours auto_close_time = auto_close_hours if !based_on_last_post # set auto close to the original time it should have been # when the topic was first created. start_time = self.created_at || Time.zone.now auto_close_time = start_time + auto_close_hours.hours # if we have already passed the original close time then # we should not recreate the auto-close timer for the topic return if auto_close_time < Time.zone.now # timestamp must be a string for set_or_create_timer auto_close_time = auto_close_time.to_s end self.set_or_create_timer( TopicTimer.types[timer_type], auto_close_time, by_user: Discourse.system_user, based_on_last_post: based_on_last_post, duration_minutes: duration_minutes, ) end end |
#inherit_slow_mode_from_category ⇒ Object
1523 1524 1525 1526 1527 |
# File 'app/models/topic.rb', line 1523 def inherit_slow_mode_from_category if self.category&.default_slow_mode_seconds self.slow_mode_seconds = self.category&.default_slow_mode_seconds end end |
#initialize_default_values ⇒ Object
446 447 448 449 |
# File 'app/models/topic.rb', line 446 def initialize_default_values self.bumped_at ||= Time.now self.last_post_user_id ||= user_id end |
#invite(invited_by, username_or_email, group_ids = nil, custom_message = nil) ⇒ Object
1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 |
# File 'app/models/topic.rb', line 1230 def invite(invited_by, username_or_email, group_ids = nil, = nil) guardian = Guardian.new(invited_by) if target_user = User.find_by_username_or_email(username_or_email) if topic_allowed_users.exists?(user_id: target_user.id) raise UserExists.new(I18n.t("topic_invite.user_exists")) end comm_screener = UserCommScreener.new(acting_user: invited_by, target_user_ids: target_user.id) if comm_screener.ignoring_or_muting_actor?(target_user.id) raise NotAllowed.new(I18n.t("not_accepting_pms", username: target_user.username)) end if TopicUser.where( topic: self, user: target_user, notification_level: TopicUser.notification_levels[:muted], ).exists? raise NotAllowed.new(I18n.t("topic_invite.muted_topic")) end if comm_screener.disallowing_pms_from_actor?(target_user.id) raise NotAllowed.new(I18n.t("topic_invite.receiver_does_not_allow_pm")) end if UserCommScreener.new( acting_user: target_user, target_user_ids: invited_by.id, ).disallowing_pms_from_actor?(invited_by.id) raise NotAllowed.new(I18n.t("topic_invite.sender_does_not_allow_pm")) end if !!(invited_by, target_user, guardian) else !!invite_to_topic(invited_by, target_user, group_ids, guardian) end elsif username_or_email =~ /\A.+@.+\z/ && guardian.can_invite_via_email?(self) !!Invite.generate( invited_by, email: username_or_email, topic: self, group_ids: group_ids, custom_message: , invite_to_topic: true, ) end end |
#invite_group(user, group, should_notify: true) ⇒ Object
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 |
# File 'app/models/topic.rb', line 1189 def invite_group(user, group, should_notify: true) TopicAllowedGroup.create!(topic_id: self.id, group_id: group.id) self.allowed_groups.reload last_post = self.posts.order("post_number desc").where("not hidden AND posts.deleted_at IS NULL").first if last_post add_small_action(user, "invited_group", group.name) if should_notify Jobs.enqueue(:post_alert, post_id: last_post.id) Jobs.enqueue(:group_pm_alert, user_id: user.id, group_id: group.id, post_id: last_post.id) end end # If the group invited includes the OP of the topic as one of is members, # we cannot strip the topic_allowed_user record since it will be more # complicated to recover the topic_allowed_user record for the OP if the # group is removed. allowed_user_where_clause = <<~SQL users.id IN ( SELECT topic_allowed_users.user_id FROM topic_allowed_users INNER JOIN group_users ON group_users.user_id = topic_allowed_users.user_id INNER JOIN topic_allowed_groups ON topic_allowed_groups.group_id = group_users.group_id WHERE topic_allowed_groups.group_id = :group_id AND topic_allowed_users.topic_id = :topic_id AND topic_allowed_users.user_id != :op_user_id ) SQL User .where( [ allowed_user_where_clause, { group_id: group.id, topic_id: self.id, op_user_id: self.user_id }, ], ) .find_each { |allowed_user| remove_allowed_user(Discourse.system_user, allowed_user) } true end |
#is_category_topic? ⇒ Boolean
1911 1912 1913 |
# File 'app/models/topic.rb', line 1911 def is_category_topic? @is_category_topic ||= Category.exists?(topic_id: self.id.to_i) end |
#is_official_warning? ⇒ Boolean
501 502 503 |
# File 'app/models/topic.rb', line 501 def is_official_warning? subtype == TopicSubtype.moderator_warning end |
#last_post_url ⇒ Object
NOTE: These are probably better off somewhere else.
Having a model know about URLs seems a bit strange.
1439 1440 1441 |
# File 'app/models/topic.rb', line 1439 def last_post_url "#{Discourse.base_path}/t/#{slug}/#{id}/#{posts_count}" end |
#limit_private_messages_per_day ⇒ Object
524 525 526 527 |
# File 'app/models/topic.rb', line 524 def return unless apply_per_day_rate_limit_for("pms", :max_personal_messages_per_day) end |
#limit_topics_per_day ⇒ Object
Additional rate limits on topics: per day and private messages per day
515 516 517 518 519 520 521 522 |
# File 'app/models/topic.rb', line 515 def limit_topics_per_day return unless regular? if user && user.new_user_posting_on_first_day? limit_first_day_topics_per_day else apply_per_day_rate_limit_for("topics", :max_topics_per_day) end end |
#make_banner!(user, bannered_until = nil) ⇒ Object
1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 |
# File 'app/models/topic.rb', line 1349 def (user, = nil) if = begin Time.parse() rescue ArgumentError raise Discourse::InvalidParameters.new(:bannered_until) end end # only one banner at the same time = Topic.where(archetype: Archetype.).first .(user) if .present? UserProfile.where("dismissed_banner_key IS NOT NULL").update_all(dismissed_banner_key: nil) self.archetype = Archetype. self. = self.add_small_action(user, "banner.enabled") self.save MessageBus.publish("/site/banner", ) Jobs.cancel_scheduled_job(:remove_banner, topic_id: self.id) Jobs.enqueue_at(, :remove_banner, topic_id: self.id) if end |
#max_post_number ⇒ Object
1290 1291 1292 |
# File 'app/models/topic.rb', line 1290 def max_post_number posts.with_deleted.maximum(:post_number).to_i end |
#message_archived?(user) ⇒ Boolean
1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 |
# File 'app/models/topic.rb', line 1714 def (user) return false unless user && user.id # tricky query but this checks to see if message is archived for ALL groups you belong to # OR if you have it archived as a user explicitly sql = <<~SQL SELECT 1 WHERE ( SELECT count(*) FROM topic_allowed_groups tg JOIN group_archived_messages gm ON gm.topic_id = tg.topic_id AND gm.group_id = tg.group_id WHERE tg.group_id IN (SELECT g.group_id FROM group_users g WHERE g.user_id = :user_id) AND tg.topic_id = :topic_id ) = ( SELECT case when count(*) = 0 then -1 else count(*) end FROM topic_allowed_groups tg WHERE tg.group_id IN (SELECT g.group_id FROM group_users g WHERE g.user_id = :user_id) AND tg.topic_id = :topic_id ) UNION ALL SELECT 1 FROM topic_allowed_users tu JOIN user_archived_messages um ON um.user_id = tu.user_id AND um.topic_id = tu.topic_id WHERE tu.user_id = :user_id AND tu.topic_id = :topic_id SQL DB.exec(sql, user_id: user.id, topic_id: id) > 0 end |
#move_posts(moved_by, post_ids, opts) ⇒ Object
1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 |
# File 'app/models/topic.rb', line 1294 def move_posts(moved_by, post_ids, opts) post_mover = PostMover.new( self, moved_by, post_ids, move_to_pm: opts[:archetype].present? && opts[:archetype] == "private_message", options: { freeze_original: opts[:freeze_original], }, ) if opts[:destination_topic_id] topic = post_mover.to_topic( opts[:destination_topic_id], **opts.slice(:participants, :chronological_order), ) DiscourseEvent.trigger(:topic_merged, post_mover.original_topic, post_mover.destination_topic) topic elsif opts[:title] post_mover.to_new_topic(opts[:title], opts[:category_id], opts[:tags]) end end |
#muted?(user) ⇒ Boolean
1507 1508 1509 |
# File 'app/models/topic.rb', line 1507 def muted?(user) notifier.muted?(user.id) if user && user.id end |
#notifier ⇒ Object
1503 1504 1505 |
# File 'app/models/topic.rb', line 1503 def notifier @topic_notifier ||= TopicNotifier.new(self) end |
#open? ⇒ Boolean
705 706 707 |
# File 'app/models/topic.rb', line 705 def open? !self.closed? end |
#participant_groups_summary(options = {}) ⇒ Object
1345 1346 1347 |
# File 'app/models/topic.rb', line 1345 def participant_groups_summary( = {}) @participant_groups_summary ||= TopicParticipantGroupsSummary.new(self, ).summary end |
#participants_summary(options = {}) ⇒ Object
1341 1342 1343 |
# File 'app/models/topic.rb', line 1341 def participants_summary( = {}) @participants_summary ||= TopicParticipantsSummary.new(self, ).summary end |
#pm_with_non_human_user? ⇒ Boolean
1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 |
# File 'app/models/topic.rb', line 1880 def pm_with_non_human_user? sql = <<~SQL SELECT 1 FROM topics LEFT JOIN topic_allowed_groups ON topics.id = topic_allowed_groups.topic_id WHERE topic_allowed_groups.topic_id IS NULL AND topics.archetype = :private_message AND topics.id = :topic_id AND ( SELECT COUNT(*) FROM topic_allowed_users WHERE topic_allowed_users.topic_id = :topic_id AND topic_allowed_users.user_id > 0 ) = 1 SQL result = DB.exec(sql, private_message: Archetype., topic_id: self.id) result != 0 end |
#post_numbers ⇒ Object
659 660 661 |
# File 'app/models/topic.rb', line 659 def post_numbers @post_numbers ||= posts.order(:post_number).pluck(:post_number) end |
#posters_summary(options = {}) ⇒ Object
avatar lookup in options
1337 1338 1339 |
# File 'app/models/topic.rb', line 1337 def posters_summary( = {}) # avatar lookup in options @posters_summary ||= TopicPostersSummary.new(self, ).summary end |
#private_message? ⇒ Boolean
697 698 699 |
# File 'app/models/topic.rb', line 697 def self.archetype == Archetype. end |
#public_topic_timer ⇒ Object
1565 1566 1567 |
# File 'app/models/topic.rb', line 1565 def public_topic_timer @public_topic_timer ||= topic_timers.find_by(public_type: true) end |
#rate_limit_topic_invitation(invited_by) ⇒ Object
2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 |
# File 'app/models/topic.rb', line 2030 def rate_limit_topic_invitation(invited_by) RateLimiter.new( invited_by, "topic-invitations-per-day", SiteSetting.max_topic_invitations_per_day, 1.day.to_i, ).performed! RateLimiter.new( invited_by, "topic-invitations-per-minute", SiteSetting.max_topic_invitations_per_minute, 1.day.to_i, ).performed! end |
#re_pin_for(user) ⇒ Object
1474 1475 1476 1477 |
# File 'app/models/topic.rb', line 1474 def re_pin_for(user) return if user.blank? TopicUser.change(user.id, id, cleared_pinned_at: nil) end |
#reached_recipients_limit? ⇒ Boolean
1183 1184 1185 1186 1187 |
# File 'app/models/topic.rb', line 1183 def reached_recipients_limit? return false unless topic_allowed_users.count + topic_allowed_groups.count >= SiteSetting. end |
#read_restricted_category? ⇒ Boolean
1685 1686 1687 |
# File 'app/models/topic.rb', line 1685 def read_restricted_category? category && category.read_restricted end |
#recover!(recovered_by = nil) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'app/models/topic.rb', line 168 def recover!(recovered_by = nil) trigger_event = false unless deleted_at.nil? update_category_topic_count_by(1) if visible? CategoryTagStat.topic_recovered(self) if self..present? trigger_event = true end # Note parens are required because superclass doesn't take `recovered_by` super() DiscourseEvent.trigger(:topic_recovered, self) if trigger_event unless ( = TopicEmbed.with_deleted.find_by_topic_id(id)).nil? .recover! end end |
#regular? ⇒ Boolean
701 702 703 |
# File 'app/models/topic.rb', line 701 def regular? self.archetype == Archetype.default end |
#relative_url(post_number = nil) ⇒ Object
1465 1466 1467 |
# File 'app/models/topic.rb', line 1465 def relative_url(post_number = nil) Topic.relative_url(id, slug, post_number) end |
#reload(options = nil) ⇒ Object
651 652 653 654 655 656 657 |
# File 'app/models/topic.rb', line 651 def reload( = nil) @post_numbers = nil @public_topic_timer = nil @slow_mode_topic_timer = nil @is_category_topic = nil super() end |
#remove_allowed_group(removed_by, name) ⇒ Object
1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 |
# File 'app/models/topic.rb', line 1148 def remove_allowed_group(removed_by, name) if group = Group.find_by(name: name) group_user = topic_allowed_groups.find_by(group_id: group.id) if group_user group_user.destroy allowed_groups.reload add_small_action(removed_by, "removed_group", group.name) return true end end false end |
#remove_allowed_user(removed_by, username) ⇒ Object
1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 |
# File 'app/models/topic.rb', line 1162 def remove_allowed_user(removed_by, username) user = username.is_a?(User) ? username : User.find_by(username: username) if user topic_user = topic_allowed_users.find_by(user_id: user.id) if topic_user if user.id == removed_by&.id add_small_action(removed_by, "user_left", user.username) else add_small_action(removed_by, "removed_user", user.username) end topic_user.destroy return true end end false end |
#remove_banner!(user) ⇒ Object
1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 |
# File 'app/models/topic.rb', line 1376 def (user) self.archetype = Archetype.default self. = nil self.add_small_action(user, "banner.disabled") self.save MessageBus.publish("/site/banner", nil) Jobs.cancel_scheduled_job(:remove_banner, topic_id: self.id) end |
#reset_bumped_at(post_id = nil) ⇒ Object
1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 |
# File 'app/models/topic.rb', line 1915 def reset_bumped_at(post_id = nil) post = ( if post_id Post.find_by(id: post_id) else ordered_posts.where( user_deleted: false, hidden: false, post_type: Post.types[:regular], ).last || first_post end ) return if !post self.bumped_at = post.created_at self.save(validate: false) end |
#secure_audience_publish_messages ⇒ Object
2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 |
# File 'app/models/topic.rb', line 2071 def target_audience = {} if target_audience[:user_ids] = User.human_users.where("admin OR moderator").pluck(:id) target_audience[:user_ids] |= allowed_users.pluck(:id) target_audience[:user_ids] |= allowed_group_users.pluck(:id) else target_audience[:group_ids] = secure_group_ids end target_audience end |
#secure_group_ids ⇒ Object
1701 1702 1703 1704 |
# File 'app/models/topic.rb', line 1701 def secure_group_ids @secure_group_ids ||= (self.category.secure_group_ids if self.category && self.category.read_restricted?) end |
#set_or_create_timer(status_type, time, by_user: nil, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id, duration_minutes: nil, silent: nil) ⇒ Object
Valid arguments for the time:
* An integer, which is the number of hours from now to update the topic's status.
* A timestamp, like "2013-11-25 13:00", when the topic's status should update.
* A timestamp with timezone in JSON format. (e.g., "2013-11-26T21:00:00.000Z")
* `nil` to delete the topic's status update.
Options:
* by_user: User who is setting the topic's status update.
* based_on_last_post: True if time should be based on timestamp of the last post.
* category_id: Category that the update will apply to.
* duration_minutes: The duration of the timer in minutes, which is used if the timer is based
on the last post or if the timer type is delete_replies.
* silent: Affects whether the close topic timer status change will be silent or not.
1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 |
# File 'app/models/topic.rb', line 1593 def set_or_create_timer( status_type, time, by_user: nil, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id, duration_minutes: nil, silent: nil ) if time.blank? && duration_minutes.blank? return delete_topic_timer(status_type, by_user: by_user) end duration_minutes = duration_minutes ? duration_minutes.to_i : 0 public_topic_timer = !!TopicTimer.public_types[status_type] = { topic: self, public_type: public_topic_timer } .merge!(user: by_user) unless public_topic_timer .merge!(silent: silent) if silent topic_timer = TopicTimer.find_or_initialize_by() topic_timer.status_type = status_type time_now = Time.zone.now topic_timer.based_on_last_post = !based_on_last_post.blank? if status_type == TopicTimer.types[:publish_to_category] topic_timer.category = Category.find_by(id: category_id) end if topic_timer.based_on_last_post if duration_minutes > 0 last_post_created_at = self.ordered_posts.last.present? ? self.ordered_posts.last.created_at : time_now topic_timer.duration_minutes = duration_minutes topic_timer.execute_at = last_post_created_at + duration_minutes.minutes topic_timer.created_at = last_post_created_at end elsif topic_timer.status_type == TopicTimer.types[:delete_replies] if duration_minutes > 0 first_reply_created_at = (self.ordered_posts.where("post_number > 1").minimum(:created_at) || time_now) topic_timer.duration_minutes = duration_minutes topic_timer.execute_at = first_reply_created_at + duration_minutes.minutes topic_timer.created_at = first_reply_created_at end else utc = Time.find_zone("UTC") is_float = ( begin Float(time) rescue StandardError nil end ) if is_float num_hours = time.to_f topic_timer.execute_at = num_hours.hours.from_now if num_hours > 0 else = utc.parse(time) raise Discourse::InvalidParameters unless && > utc.now # a timestamp in client's time zone, like "2015-5-27 12:00" topic_timer.execute_at = end end if topic_timer.execute_at if by_user&.staff? || by_user&.trust_level == TrustLevel[4] topic_timer.user = by_user else topic_timer.user ||= ( if self.user.staff? || self.user.trust_level == TrustLevel[4] self.user else Discourse.system_user end ) end if self.persisted? # See TopicTimer.after_save for additional context; the topic # status may be changed by saving. topic_timer.save! else self.topic_timers << topic_timer end topic_timer end end |
#shared_draft? ⇒ Boolean
59 60 61 |
# File 'app/models/topic.rb', line 59 def shared_draft? SharedDraft.exists?(topic_id: id) end |
#slow_mode_topic_timer ⇒ Object
1569 1570 1571 |
# File 'app/models/topic.rb', line 1569 def slow_mode_topic_timer @slow_mode_topic_timer ||= topic_timers.find_by(status_type: TopicTimer.types[:clear_slow_mode]) end |
#slug ⇒ Object
Even if the slug column in the database is null, topic.slug will return something:
1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 |
# File 'app/models/topic.rb', line 1407 def slug unless slug = read_attribute(:slug) return "" if title.blank? slug = slug_for_topic(title) if new_record? write_attribute(:slug, slug) else update_column(:slug, slug) end end slug end |
#slug_for_topic(title) ⇒ Object
1396 1397 1398 1399 1400 1401 1402 1403 1404 |
# File 'app/models/topic.rb', line 1396 def slug_for_topic(title) return "" if title.blank? slug = Slug.for(title) # this is a hook for plugins that need to modify the generated slug self.class.slug_computed_callbacks.each { |callback| slug = callback.call(self, slug, title) } slug end |
#slugless_url(post_number = nil) ⇒ Object
1461 1462 1463 |
# File 'app/models/topic.rb', line 1461 def slugless_url(post_number = nil) Topic.relative_url(id, nil, post_number) end |
#thumbnail_info(enqueue_if_missing: false, extra_sizes: []) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'app/models/topic.rb', line 77 def thumbnail_info(enqueue_if_missing: false, extra_sizes: []) return nil unless original = image_upload return nil if original.filesize >= SiteSetting.max_image_size_kb.to_i.kilobytes return nil unless original.read_attribute(:width) && original.read_attribute(:height) infos = [] infos << { # Always add original max_width: nil, max_height: nil, width: original.width, height: original.height, url: original.url, } records = filtered_topic_thumbnails(extra_sizes: extra_sizes) records.each do |record| next unless record.optimized_image # Only serialize successful thumbnails infos << { max_width: record.max_width, max_height: record.max_height, width: record.optimized_image&.width, height: record.optimized_image&.height, url: record.optimized_image&.url, } end thumbnail_sizes = Topic.thumbnail_sizes + extra_sizes if SiteSetting.create_thumbnails && enqueue_if_missing && records.length < thumbnail_sizes.length && Discourse.redis.set(thumbnail_job_redis_key(extra_sizes), 1, nx: true, ex: 1.minute) Jobs.enqueue(:generate_topic_thumbnails, { topic_id: id, extra_sizes: extra_sizes }) end infos.each { |i| i[:url] = UrlHelper.cook_url(i[:url], secure: original.secure?, local: true) } infos.sort_by! { |i| -i[:width] * i[:height] } end |
#thumbnail_job_redis_key(sizes) ⇒ Object
63 64 65 |
# File 'app/models/topic.rb', line 63 def thumbnail_job_redis_key(sizes) "generate_topic_thumbnail_enqueue_#{id}_#{sizes.inspect}" end |
#title=(t) ⇒ Object
1430 1431 1432 1433 1434 1435 |
# File 'app/models/topic.rb', line 1430 def title=(t) slug = slug_for_topic(t.to_s) write_attribute(:slug, slug) write_attribute(:fancy_title, nil) write_attribute(:title, t) end |
#trash!(trashed_by = nil) ⇒ Object
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'app/models/topic.rb', line 152 def trash!(trashed_by = nil) trigger_event = false if deleted_at.nil? update_category_topic_count_by(-1) if visible? CategoryTagStat.topic_deleted(self) if self..present? trigger_event = true end super(trashed_by) DiscourseEvent.trigger(:topic_trashed, self) if trigger_event self..trash! if end |
#update_action_counts ⇒ Object
1330 1331 1332 1333 1334 1335 |
# File 'app/models/topic.rb', line 1330 def update_action_counts update_column( :like_count, Post.where.not(post_type: Post.types[:whisper]).where(topic_id: id).sum(:like_count), ) end |
#update_category_topic_count_by(num) ⇒ Object
1953 1954 1955 1956 1957 1958 1959 1960 |
# File 'app/models/topic.rb', line 1953 def update_category_topic_count_by(num) if category_id.present? Category .where("id = ?", category_id) .where("topic_id != ? OR topic_id IS NULL", self.id) .update_all("topic_count = topic_count + #{num.to_i}") end end |
#update_excerpt(excerpt) ⇒ Object
1875 1876 1877 1878 |
# File 'app/models/topic.rb', line 1875 def update_excerpt(excerpt) update_column(:excerpt, excerpt) ApplicationController..clear if archetype == "banner" end |
#update_pinned(status, global = false, pinned_until = nil) ⇒ Object
1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 |
# File 'app/models/topic.rb', line 1479 def update_pinned(status, global = false, pinned_until = nil) if pinned_until pinned_until = begin Time.parse(pinned_until) rescue ArgumentError raise Discourse::InvalidParameters.new(:pinned_until) end end update_columns( pinned_at: status ? Time.zone.now : nil, pinned_globally: global, pinned_until: pinned_until, ) Jobs.cancel_scheduled_job(:unpin_topic, topic_id: self.id) Jobs.enqueue_at(pinned_until, :unpin_topic, topic_id: self.id) if pinned_until end |
#update_statistics ⇒ Object
Updates the denormalized statistics of a topic including featured posters. They shouldn’t go out of sync unless you do something drastic live move posts from one topic to another. this recalculates everything.
1324 1325 1326 1327 1328 |
# File 'app/models/topic.rb', line 1324 def update_statistics feature_topic_users update_action_counts Topic.reset_highest(id) end |
#update_status(status, enabled, user, opts = {}) ⇒ Object
798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 |
# File 'app/models/topic.rb', line 798 def update_status(status, enabled, user, opts = {}) TopicStatusUpdater.new(self, user).update!(status, enabled, opts) DiscourseEvent.trigger(:topic_status_updated, self, status, enabled) if status == "closed" StaffActionLogger.new(user).log_topic_closed(self, closed: enabled) elsif status == "archived" StaffActionLogger.new(user).log_topic_archived(self, archived: enabled) end if enabled && && status.to_s["closed"] group_ids = user.groups.pluck(:id) if group_ids.present? allowed_group_ids = self.allowed_groups.where("topic_allowed_groups.group_id IN (?)", group_ids).pluck(:id) allowed_group_ids.each { |id| GroupArchivedMessage.archive!(id, self) } end end end |
#url(post_number = nil) ⇒ Object
1449 1450 1451 |
# File 'app/models/topic.rb', line 1449 def url(post_number = nil) self.class.url id, slug, post_number end |
#visible_tags(guardian) ⇒ Object
2116 2117 2118 |
# File 'app/models/topic.rb', line 2116 def (guardian) .reject { |tag| guardian.hidden_tag_names.include?(tag[:name]) } end |