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
Constants included from Searchable
Constants included from HasCustomFields
HasCustomFields::CUSTOM_FIELDS_MAX_ITEMS, HasCustomFields::CUSTOM_FIELDS_MAX_VALUE_LENGTH
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.
-
#meta_data ⇒ Object
Returns the value of attribute meta_data.
-
#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).
Class Method Summary collapse
- .count_exceeds_minimum? ⇒ Boolean
- .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
- .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) ⇒ 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 ⇒ 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.
- #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_meta_data(data) ⇒ 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_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.
368 369 370 |
# File 'app/models/topic.rb', line 368 def advance_draft @advance_draft end |
#allowed_group_ids ⇒ Object
Returns the value of attribute allowed_group_ids.
31 32 33 |
# File 'app/models/topic.rb', line 31 def allowed_group_ids @allowed_group_ids end |
#allowed_user_ids ⇒ Object
Returns the value of attribute allowed_user_ids.
31 32 33 |
# File 'app/models/topic.rb', line 31 def allowed_user_ids @allowed_user_ids end |
#category_user_data ⇒ Object
Returns the value of attribute category_user_data.
291 292 293 |
# File 'app/models/topic.rb', line 291 def category_user_data @category_user_data end |
#dismissed ⇒ Object
Returns the value of attribute dismissed.
292 293 294 |
# File 'app/models/topic.rb', line 292 def dismissed @dismissed end |
#ignore_category_auto_close ⇒ Object
Returns the value of attribute ignore_category_auto_close.
366 367 368 |
# File 'app/models/topic.rb', line 366 def ignore_category_auto_close @ignore_category_auto_close end |
#import_mode ⇒ Object
set to true to optimize creation and save for imports
300 301 302 |
# File 'app/models/topic.rb', line 300 def import_mode @import_mode end |
#include_last_poster ⇒ Object
Returns the value of attribute include_last_poster.
299 300 301 |
# File 'app/models/topic.rb', line 299 def include_last_poster @include_last_poster end |
#includes_destination_category ⇒ Object
Returns the value of attribute includes_destination_category.
31 32 33 |
# File 'app/models/topic.rb', line 31 def includes_destination_category @includes_destination_category end |
#meta_data ⇒ Object
Returns the value of attribute meta_data.
298 299 300 |
# File 'app/models/topic.rb', line 298 def @meta_data end |
#participant_groups ⇒ Object
Returns the value of attribute participant_groups.
296 297 298 |
# File 'app/models/topic.rb', line 296 def participant_groups @participant_groups end |
#participants ⇒ Object
Returns the value of attribute participants.
295 296 297 |
# File 'app/models/topic.rb', line 295 def participants @participants end |
#posters ⇒ Object
TODO: can replace with posters_summary once we remove old list code
294 295 296 |
# File 'app/models/topic.rb', line 294 def posters @posters end |
#skip_callbacks ⇒ Object
Returns the value of attribute skip_callbacks.
367 368 369 |
# File 'app/models/topic.rb', line 367 def skip_callbacks @skip_callbacks end |
#tags_changed ⇒ Object
Returns the value of attribute tags_changed.
31 32 33 |
# File 'app/models/topic.rb', line 31 def @tags_changed end |
#topic_list ⇒ Object
Returns the value of attribute topic_list.
297 298 299 |
# File 'app/models/topic.rb', line 297 def topic_list @topic_list end |
#user_data ⇒ Object
When we want to temporarily attach some data to a forum topic (usually before serialization)
290 291 292 |
# File 'app/models/topic.rb', line 290 def user_data @user_data end |
Class Method Details
.count_exceeds_minimum? ⇒ Boolean
454 455 456 |
# File 'app/models/topic.rb', line 454 def self.count_exceeds_minimum? count > SiteSetting.minimum_topics_similar end |
.ensure_consistency! ⇒ Object
1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 |
# File 'app/models/topic.rb', line 1463 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
502 503 504 505 506 |
# File 'app/models/topic.rb', line 502 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
1373 1374 1375 1376 1377 1378 1379 1380 |
# File 'app/models/topic.rb', line 1373 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.
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 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 |
# File 'app/models/topic.rb', line 530 def self.for_digest(user, since, opts = nil) opts = 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 muted and shared draft categories remove_category_ids = CategoryUser.where( user_id: user.id, notification_level: CategoryUser.notification_levels[:muted], ).pluck(:category_id) if SiteSetting.digest_suppress_categories.present? topics = topics.where( "topics.category_id NOT IN (?)", SiteSetting.digest_suppress_categories.split("|").map(&:to_i), ) end if SiteSetting..present? tag_ids = Tag.where_name(SiteSetting..split("|")).pluck(:id) if tag_ids.present? topics = topics.joins("LEFT JOIN topic_tags tg ON topics.id = tg.topic_id").where( "tg.tag_id NOT IN (?) OR tg.tag_id IS NULL", tag_ids, ) end end 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
466 467 468 |
# File 'app/models/topic.rb', line 466 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
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 |
# File 'app/models/topic.rb', line 653 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
33 34 35 |
# File 'app/models/topic.rb', line 33 def self.max_fancy_title_length 400 end |
.next_post_number(topic_id, opts = {}) ⇒ Object
Atomically creates the next post number
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 |
# File 'app/models/topic.rb', line 803 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
1861 1862 1863 1864 1865 1866 1867 1868 |
# File 'app/models/topic.rb', line 1861 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
2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 |
# File 'app/models/topic.rb', line 2036 def self.publish_stats_to_clients!(topic_id, type, opts = {}) topic = Topic.find_by(id: topic_id) return unless topic.present? 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
450 451 452 |
# File 'app/models/topic.rb', line 450 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
1405 1406 1407 1408 1409 1410 1411 |
# File 'app/models/topic.rb', line 1405 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
840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 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 |
# File 'app/models/topic.rb', line 840 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 ) 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 FROM X, Y WHERE topics.archetype <> 'private_message' AND X.topic_id = topics.id AND Y.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 ) 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 ) 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 FROM X, Y WHERE topics.archetype = 'private_message' AND X.topic_id = topics.id AND Y.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 ) SQL end |
.reset_highest(topic_id) ⇒ Object
If a post is deleted we have to update our highest post counters and last post information
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 949 950 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 |
# File 'app/models/topic.rb', line 915 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} ), 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
37 38 39 |
# File 'app/models/topic.rb', line 37 def self.share_thumbnail_size [1024, 1024] end |
.similar_to(title, raw, user = nil) ⇒ Object
697 698 699 700 701 702 703 704 705 706 707 708 709 710 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 |
# File 'app/models/topic.rb', line 697 def self.similar_to(title, raw, user = nil) 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
41 42 43 |
# File 'app/models/topic.rb', line 41 def self.thumbnail_sizes [self.share_thumbnail_size] + DiscoursePluginRegistry.topic_thumbnail_sizes end |
.time_to_first_response(sql, opts = nil) ⇒ Object
1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 |
# File 'app/models/topic.rb', line 1723 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
1746 1747 1748 1749 1750 1751 |
# File 'app/models/topic.rb', line 1746 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
1753 1754 1755 1756 |
# File 'app/models/topic.rb', line 1753 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
446 447 448 |
# File 'app/models/topic.rb', line 446 def self.top_viewed(max = 10) Topic.listable_topics.visible.secured.order("views desc").limit(max) end |
.url(id, slug, post_number = nil) ⇒ Object
1395 1396 1397 1398 1399 |
# File 'app/models/topic.rb', line 1395 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 |
.visible_post_types(viewed_by = nil, include_moderator_actions: true) ⇒ Object
438 439 440 441 442 443 444 |
# File 'app/models/topic.rb', line 438 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
1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 |
# File 'app/models/topic.rb', line 1772 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
1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 |
# File 'app/models/topic.rb', line 1805 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
1913 1914 1915 1916 1917 1918 1919 1920 |
# File 'app/models/topic.rb', line 1913 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
1645 1646 1647 |
# File 'app/models/topic.rb', line 1645 def acting_user @acting_user || user end |
#acting_user=(u) ⇒ Object
1649 1650 1651 |
# File 'app/models/topic.rb', line 1649 def acting_user=(u) @acting_user = u end |
#add_moderator_post(user, text, opts = nil) ⇒ Object
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 1084 1085 1086 |
# File 'app/models/topic.rb', line 1055 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
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 |
# File 'app/models/topic.rb', line 1042 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
424 425 426 427 428 429 430 |
# File 'app/models/topic.rb', line 424 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
649 650 651 |
# File 'app/models/topic.rb', line 649 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
479 480 481 482 483 484 485 |
# File 'app/models/topic.rb', line 479 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
1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 |
# File 'app/models/topic.rb', line 1886 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
1339 1340 1341 1342 1343 |
# File 'app/models/topic.rb', line 1339 def post = self.ordered_posts.first { html: post.cooked, key: self.id, url: self.url } end |
#best_post ⇒ Object
458 459 460 461 462 463 464 |
# File 'app/models/topic.rb', line 458 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
1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 |
# File 'app/models/topic.rb', line 1997 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
1641 1642 1643 |
# File 'app/models/topic.rb', line 1641 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
1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 |
# File 'app/models/topic.rb', line 1088 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
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 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 |
# File 'app/models/topic.rb', line 979 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
1421 1422 1423 1424 |
# File 'app/models/topic.rb', line 1421 def clear_pin_for(user) return unless user.present? TopicUser.change(user.id, id, cleared_pinned_at: Time.now) end |
#convert_to_private_message(user) ⇒ Object
1826 1827 1828 1829 1830 1831 1832 |
# File 'app/models/topic.rb', line 1826 def (user) read_restricted = category.read_restricted private_topic = TopicConverter.new(self, user). Tag.update_counters(private_topic., { public_topic_count: -1 }) if !read_restricted add_small_action(user, "private_topic") if private_topic private_topic end |
#convert_to_public_topic(user, category_id: nil) ⇒ Object
1819 1820 1821 1822 1823 1824 |
# File 'app/models/topic.rb', line 1819 def convert_to_public_topic(user, category_id: nil) public_topic = TopicConverter.new(self, user).convert_to_public_topic(category_id) Tag.update_counters(public_topic., { public_topic_count: 1 }) if !category.read_restricted add_small_action(user, "public_topic") if public_topic public_topic end |
#create_invite_notification!(target_user, notification_type, invited_by, post_number: 1) ⇒ Object
1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 |
# File 'app/models/topic.rb', line 1960 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
1525 1526 1527 1528 1529 1530 1531 |
# File 'app/models/topic.rb', line 1525 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
1451 1452 1453 |
# File 'app/models/topic.rb', line 1451 def draft_key "#{Draft::EXISTING_TOPIC}#{id}" end |
#email_already_exists_for?(invite) ⇒ Boolean
1234 1235 1236 |
# File 'app/models/topic.rb', line 1234 def email_already_exists_for?(invite) invite.email_already_exists && end |
#ensure_topic_has_a_category ⇒ Object
432 433 434 435 436 |
# File 'app/models/topic.rb', line 432 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
1662 1663 1664 |
# File 'app/models/topic.rb', line 1662 def SiteSetting. && end |
#fancy_title ⇒ Object
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 |
# File 'app/models/topic.rb', line 508 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
1857 1858 1859 |
# File 'app/models/topic.rb', line 1857 def featured_link_root_domain MiniSuffix.domain(UrlHelper.encode_and_parse(self.featured_link).hostname) end |
#featured_users ⇒ Object
130 131 132 |
# File 'app/models/topic.rb', line 130 def featured_users @featured_users ||= TopicFeaturedUsers.new(self) end |
#filtered_topic_thumbnails(extra_sizes: []) ⇒ Object
49 50 51 52 53 54 55 56 57 |
# File 'app/models/topic.rb', line 49 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
2018 2019 2020 |
# File 'app/models/topic.rb', line 2018 def first_smtp_enabled_group self.allowed_groups.where(smtp_enabled: true).first end |
#generate_thumbnails!(extra_sizes: []) ⇒ Object
99 100 101 102 103 104 105 106 107 108 109 |
# File 'app/models/topic.rb', line 99 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
1238 1239 1240 1241 1242 1243 |
# File 'app/models/topic.rb', line 1238 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
2063 2064 2065 |
# File 'app/models/topic.rb', line 2063 def group_pm? && all_allowed_users.count > 2 end |
#has_flags? ⇒ Boolean
470 471 472 |
# File 'app/models/topic.rb', line 470 def has_flags? self.class.has_flag_scope.exists?(topic_id: self.id) end |
#has_topic_embed? ⇒ Boolean
1658 1659 1660 |
# File 'app/models/topic.rb', line 1658 def TopicEmbed.where(topic_id: id).exists? end |
#image_url(enqueue_if_missing: false) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'app/models/topic.rb', line 111 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
1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 |
# File 'app/models/topic.rb', line 1922 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
1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 |
# File 'app/models/topic.rb', line 1481 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
1475 1476 1477 1478 1479 |
# File 'app/models/topic.rb', line 1475 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
419 420 421 422 |
# File 'app/models/topic.rb', line 419 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
1185 1186 1187 1188 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 1229 1230 1231 1232 |
# File 'app/models/topic.rb', line 1185 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) ⇒ Object
1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 |
# File 'app/models/topic.rb', line 1146 def invite_group(user, group) 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 Jobs.enqueue(:post_alert, post_id: last_post.id) add_small_action(user, "invited_group", group.name) Jobs.enqueue(:group_pm_alert, user_id: user.id, group_id: group.id, post_id: last_post.id) 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
1870 1871 1872 |
# File 'app/models/topic.rb', line 1870 def is_category_topic? @is_category_topic ||= Category.exists?(topic_id: self.id.to_i) end |
#is_official_warning? ⇒ Boolean
474 475 476 |
# File 'app/models/topic.rb', line 474 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.
1391 1392 1393 |
# File 'app/models/topic.rb', line 1391 def last_post_url "#{Discourse.base_path}/t/#{slug}/#{id}/#{posts_count}" end |
#limit_private_messages_per_day ⇒ Object
497 498 499 500 |
# File 'app/models/topic.rb', line 497 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
488 489 490 491 492 493 494 495 |
# File 'app/models/topic.rb', line 488 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
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 |
# File 'app/models/topic.rb', line 1301 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
1245 1246 1247 |
# File 'app/models/topic.rb', line 1245 def max_post_number posts.with_deleted.maximum(:post_number).to_i end |
#message_archived?(user) ⇒ Boolean
1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 |
# File 'app/models/topic.rb', line 1666 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
1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 |
# File 'app/models/topic.rb', line 1249 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", ) 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
1459 1460 1461 |
# File 'app/models/topic.rb', line 1459 def muted?(user) notifier.muted?(user.id) if user && user.id end |
#notifier ⇒ Object
1455 1456 1457 |
# File 'app/models/topic.rb', line 1455 def notifier @topic_notifier ||= TopicNotifier.new(self) end |
#open? ⇒ Boolean
691 692 693 |
# File 'app/models/topic.rb', line 691 def open? !self.closed? end |
#participant_groups_summary(options = {}) ⇒ Object
1297 1298 1299 |
# File 'app/models/topic.rb', line 1297 def participant_groups_summary( = {}) @participant_groups_summary ||= TopicParticipantGroupsSummary.new(self, ).summary end |
#participants_summary(options = {}) ⇒ Object
1293 1294 1295 |
# File 'app/models/topic.rb', line 1293 def participants_summary( = {}) @participants_summary ||= TopicParticipantsSummary.new(self, ).summary end |
#pm_with_non_human_user? ⇒ Boolean
1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 |
# File 'app/models/topic.rb', line 1839 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
645 646 647 |
# File 'app/models/topic.rb', line 645 def post_numbers @post_numbers ||= posts.order(:post_number).pluck(:post_number) end |
#posters_summary(options = {}) ⇒ Object
avatar lookup in options
1289 1290 1291 |
# File 'app/models/topic.rb', line 1289 def posters_summary( = {}) # avatar lookup in options @posters_summary ||= TopicPostersSummary.new(self, ).summary end |
#private_message? ⇒ Boolean
683 684 685 |
# File 'app/models/topic.rb', line 683 def self.archetype == Archetype. end |
#public_topic_timer ⇒ Object
1517 1518 1519 |
# File 'app/models/topic.rb', line 1517 def public_topic_timer @public_topic_timer ||= topic_timers.find_by(public_type: true) end |
#rate_limit_topic_invitation(invited_by) ⇒ Object
1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 |
# File 'app/models/topic.rb', line 1981 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
1426 1427 1428 1429 |
# File 'app/models/topic.rb', line 1426 def re_pin_for(user) return unless user.present? TopicUser.change(user.id, id, cleared_pinned_at: nil) end |
#reached_recipients_limit? ⇒ Boolean
1140 1141 1142 1143 1144 |
# File 'app/models/topic.rb', line 1140 def reached_recipients_limit? return false unless topic_allowed_users.count + topic_allowed_groups.count >= SiteSetting. end |
#read_restricted_category? ⇒ Boolean
1637 1638 1639 |
# File 'app/models/topic.rb', line 1637 def read_restricted_category? category && category.read_restricted end |
#recover!(recovered_by = nil) ⇒ Object
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'app/models/topic.rb', line 150 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
687 688 689 |
# File 'app/models/topic.rb', line 687 def regular? self.archetype == Archetype.default end |
#relative_url(post_number = nil) ⇒ Object
1417 1418 1419 |
# File 'app/models/topic.rb', line 1417 def relative_url(post_number = nil) Topic.relative_url(id, slug, post_number) end |
#reload(options = nil) ⇒ Object
637 638 639 640 641 642 643 |
# File 'app/models/topic.rb', line 637 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
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 |
# File 'app/models/topic.rb', line 1105 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
1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 |
# File 'app/models/topic.rb', line 1119 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
1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 |
# File 'app/models/topic.rb', line 1328 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 ⇒ Object
1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 |
# File 'app/models/topic.rb', line 1874 def reset_bumped_at post = ordered_posts.where( user_deleted: false, hidden: false, post_type: Post.types[:regular], ).last || first_post self.bumped_at = post.created_at self.save(validate: false) end |
#secure_audience_publish_messages ⇒ Object
2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 |
# File 'app/models/topic.rb', line 2022 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
1653 1654 1655 1656 |
# File 'app/models/topic.rb', line 1653 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.
1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 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 |
# File 'app/models/topic.rb', line 1545 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 |
#slow_mode_topic_timer ⇒ Object
1521 1522 1523 |
# File 'app/models/topic.rb', line 1521 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:
1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 |
# File 'app/models/topic.rb', line 1359 def slug unless slug = read_attribute(:slug) return "" unless title.present? 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
1348 1349 1350 1351 1352 1353 1354 1355 1356 |
# File 'app/models/topic.rb', line 1348 def slug_for_topic(title) return "" unless title.present? 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
1413 1414 1415 |
# File 'app/models/topic.rb', line 1413 def slugless_url(post_number = nil) Topic.relative_url(id, nil, post_number) end |
#thumbnail_info(enqueue_if_missing: false, extra_sizes: []) ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'app/models/topic.rb', line 59 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.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
45 46 47 |
# File 'app/models/topic.rb', line 45 def thumbnail_job_redis_key(sizes) "generate_topic_thumbnail_enqueue_#{id}_#{sizes.inspect}" end |
#title=(t) ⇒ Object
1382 1383 1384 1385 1386 1387 |
# File 'app/models/topic.rb', line 1382 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
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'app/models/topic.rb', line 134 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
1282 1283 1284 1285 1286 1287 |
# File 'app/models/topic.rb', line 1282 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
1904 1905 1906 1907 1908 1909 1910 1911 |
# File 'app/models/topic.rb', line 1904 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
1834 1835 1836 1837 |
# File 'app/models/topic.rb', line 1834 def update_excerpt(excerpt) update_column(:excerpt, excerpt) ApplicationController..clear if archetype == "banner" end |
#update_meta_data(data) ⇒ Object
632 633 634 635 |
# File 'app/models/topic.rb', line 632 def (data) custom_fields.update(data) save end |
#update_pinned(status, global = false, pinned_until = nil) ⇒ Object
1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 |
# File 'app/models/topic.rb', line 1431 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.
1276 1277 1278 1279 1280 |
# File 'app/models/topic.rb', line 1276 def update_statistics feature_topic_users update_action_counts Topic.reset_highest(id) end |
#update_status(status, enabled, user, opts = {}) ⇒ Object
782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 |
# File 'app/models/topic.rb', line 782 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
1401 1402 1403 |
# File 'app/models/topic.rb', line 1401 def url(post_number = nil) self.class.url id, slug, post_number end |
#visible_tags(guardian) ⇒ Object
2067 2068 2069 |
# File 'app/models/topic.rb', line 2067 def (guardian) .reject { |tag| guardian.hidden_tag_names.include?(tag[:name]) } end |