2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
class Topic < ActiveRecord :: Base
2017-05-02 17:43:33 +08:00
class UserExists < StandardError ; end
2020-07-31 23:52:19 +08:00
class NotAllowed < StandardError ; end
2013-07-04 03:43:29 +08:00
include ActionView :: Helpers :: SanitizeHelper
2013-02-06 03:16:51 +08:00
include RateLimiter :: OnCreateRecord
2014-04-28 16:31:51 +08:00
include HasCustomFields
2013-05-07 12:39:01 +08:00
include Trashable
2017-08-15 23:46:57 +08:00
include Searchable
2015-02-26 03:53:21 +08:00
include LimitedEdit
2013-08-30 17:12:44 +08:00
extend Forwardable
2020-06-02 14:21:38 +08:00
self . ignored_columns = [
" avg_time " , # TODO(2021-01-04): remove
" image_url " # TODO(2021-06-01): remove
]
2020-04-30 09:22:20 +08:00
2013-08-30 17:12:44 +08:00
def_delegator :featured_users , :user_ids , :featured_user_ids
def_delegator :featured_users , :choose , :feature_topic_users
def_delegator :notifier , :watch! , :notify_watch!
2015-12-15 06:17:09 +08:00
def_delegator :notifier , :track! , :notify_tracking!
2013-08-30 17:12:44 +08:00
def_delegator :notifier , :regular! , :notify_regular!
2015-12-15 06:17:09 +08:00
def_delegator :notifier , :mute! , :notify_muted!
2013-08-30 17:12:44 +08:00
def_delegator :notifier , :toggle_mute , :toggle_mute
2013-02-06 03:16:51 +08:00
2018-03-29 03:36:12 +08:00
attr_accessor :allowed_user_ids , :tags_changed , :includes_destination_category
2014-05-12 15:32:49 +08:00
2017-10-18 04:37:13 +08:00
def self . max_fancy_title_length
400
end
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
def self . share_thumbnail_size
[ 1024 , 1024 ]
end
def self . thumbnail_sizes
2020-05-23 12:56:13 +08:00
[ self . share_thumbnail_size ] + DiscoursePluginRegistry . topic_thumbnail_sizes
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
end
2020-05-23 12:56:13 +08:00
def thumbnail_job_redis_key ( sizes )
" generate_topic_thumbnail_enqueue_ #{ id } _ #{ sizes . inspect } "
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
end
def filtered_topic_thumbnails ( extra_sizes : [ ] )
return nil unless original = image_upload
2020-05-18 19:04:29 +08:00
return nil unless original . read_attribute ( :width ) && original . read_attribute ( :height )
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
thumbnail_sizes = Topic . thumbnail_sizes + extra_sizes
topic_thumbnails . filter { | record | thumbnail_sizes . include? ( [ record . max_width , record . max_height ] ) }
end
def thumbnail_info ( enqueue_if_missing : false , extra_sizes : [ ] )
return nil unless original = image_upload
2020-07-17 04:30:23 +08:00
return nil unless original . filesize < SiteSetting . max_image_size_kb . kilobytes
2020-05-18 19:04:29 +08:00
return nil unless original . read_attribute ( :width ) && original . read_attribute ( :height )
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
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 &&
2020-05-23 12:56:13 +08:00
Discourse . redis . set ( thumbnail_job_redis_key ( thumbnail_sizes ) , 1 , nx : true , ex : 1 . minute )
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
Jobs . enqueue ( :generate_topic_thumbnails , { topic_id : id , extra_sizes : extra_sizes } )
end
2020-12-31 02:13:13 +08:00
infos . each { | i | i [ :url ] = UrlHelper . cook_url ( i [ :url ] , secure : original . secure? , local : true ) }
2020-05-15 20:35:20 +08:00
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
infos . sort_by! { | i | - i [ :width ] * i [ :height ] }
end
def generate_thumbnails! ( extra_sizes : [ ] )
return nil unless SiteSetting . create_thumbnails
return nil unless original = image_upload
2020-07-17 04:30:23 +08:00
return nil unless original . filesize < SiteSetting . max_image_size_kb . kilobytes
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
return nil unless original . width && original . height
2020-07-07 05:30:57 +08:00
extra_sizes = [ ] unless extra_sizes . kind_of? ( Array )
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
( Topic . thumbnail_sizes + extra_sizes ) . each do | dim |
TopicThumbnail . find_or_create_for! ( original , max_width : dim [ 0 ] , max_height : dim [ 1 ] )
end
end
2020-07-06 17:59:21 +08:00
def image_url ( enqueue_if_missing : false )
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
thumbnail = topic_thumbnails . detect do | record |
record . max_width == Topic . share_thumbnail_size [ 0 ] &&
record . max_height == Topic . share_thumbnail_size [ 1 ]
end
2020-05-15 20:35:20 +08:00
2020-07-06 17:59:21 +08:00
if thumbnail . nil? &&
image_upload &&
SiteSetting . create_thumbnails &&
2020-07-17 04:30:23 +08:00
image_upload . filesize < SiteSetting . max_image_size_kb . kilobytes &&
2020-08-14 06:54:28 +08:00
image_upload . read_attribute ( :width ) &&
image_upload . read_attribute ( :height ) &&
2020-07-06 17:59:21 +08:00
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
2020-05-15 20:35:20 +08:00
raw_url = thumbnail & . optimized_image & . url || image_upload & . url
2020-12-31 02:13:13 +08:00
UrlHelper . cook_url ( raw_url , secure : image_upload & . secure? , local : true ) if raw_url
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
end
2013-08-26 18:41:56 +08:00
def featured_users
@featured_users || = TopicFeaturedUsers . new ( self )
2013-03-13 00:33:42 +08:00
end
2013-02-06 03:16:51 +08:00
2013-07-10 03:20:18 +08:00
def trash! ( trashed_by = nil )
2018-02-09 05:36:39 +08:00
if deleted_at . nil?
2021-02-16 23:45:12 +08:00
update_category_topic_count_by ( - 1 ) if visible?
2018-02-09 05:36:39 +08:00
CategoryTagStat . topic_deleted ( self ) if self . tags . present?
2019-10-03 19:53:48 +08:00
DiscourseEvent . trigger ( :topic_trashed , self )
2018-02-09 05:36:39 +08:00
end
2013-07-10 03:20:18 +08:00
super ( trashed_by )
2017-04-25 02:29:04 +08:00
self . topic_embed . trash! if has_topic_embed?
2013-05-07 12:39:01 +08:00
end
2019-01-04 01:03:01 +08:00
def recover! ( recovered_by = nil )
2018-02-09 05:36:39 +08:00
unless deleted_at . nil?
2021-02-16 23:45:12 +08:00
update_category_topic_count_by ( 1 ) if visible?
2018-02-09 05:36:39 +08:00
CategoryTagStat . topic_recovered ( self ) if self . tags . present?
2019-10-03 19:53:48 +08:00
DiscourseEvent . trigger ( :topic_recovered , self )
2018-02-09 05:36:39 +08:00
end
2019-01-04 01:03:01 +08:00
# Note parens are required because superclass doesn't take `recovered_by`
super ( )
2017-04-25 02:29:04 +08:00
unless ( topic_embed = TopicEmbed . with_deleted . find_by_topic_id ( id ) ) . nil?
topic_embed . recover!
end
2013-05-07 12:39:01 +08:00
end
2013-02-06 09:13:41 +08:00
2013-02-06 03:16:51 +08:00
rate_limit :default_rate_limiter
rate_limit :limit_topics_per_day
rate_limit :limit_private_messages_per_day
2014-08-12 04:55:26 +08:00
validates :title , if : Proc . new { | t | t . new_record? || t . title_changed? } ,
2014-08-02 05:28:00 +08:00
presence : true ,
2013-06-05 05:58:25 +08:00
topic_title_length : true ,
2017-01-09 16:48:10 +08:00
censored_words : true ,
2019-10-02 08:38:34 +08:00
watched_words : true ,
2013-05-23 12:52:12 +08:00
quality_title : { unless : :private_message? } ,
2018-02-17 13:10:30 +08:00
max_emojis : true ,
2013-05-23 12:52:12 +08:00
unique_among : { unless : Proc . new { | t | ( SiteSetting . allow_duplicate_topic_titles? || t . private_message? ) } ,
message : :has_already_been_used ,
allow_blank : true ,
case_sensitive : false ,
2020-06-18 23:19:47 +08:00
collection : Proc . new { | t |
SiteSetting . allow_duplicate_topic_titles_category? ?
Topic . listable_topics . where ( " category_id = ? " , t . category_id ) :
Topic . listable_topics
}
}
2013-02-06 03:16:51 +08:00
2014-04-24 07:19:59 +08:00
validates :category_id ,
presence : true ,
exclusion : {
in : Proc . new { [ SiteSetting . uncategorized_category_id ] }
} ,
if : Proc . new { | t |
( t . new_record? || t . category_id_changed? ) &&
! SiteSetting . allow_uncategorized_topics &&
2019-02-28 22:51:13 +08:00
( t . archetype . nil? || t . regular? )
2014-04-24 07:19:59 +08:00
}
2013-10-24 07:05:51 +08:00
2017-12-11 16:27:33 +08:00
validates :featured_link , allow_nil : true , url : true
2016-12-05 20:31:43 +08:00
validate if : :featured_link do
2017-08-31 12:06:56 +08:00
errors . add ( :featured_link , :invalid_category ) unless ! featured_link_changed? ||
Guardian . new . can_edit_featured_link? ( category_id )
2016-12-05 20:31:43 +08:00
end
2013-10-09 02:40:31 +08:00
2013-06-01 03:22:34 +08:00
before_validation do
2013-05-23 12:52:12 +08:00
self . title = TextCleaner . clean_title ( TextSentinel . title_sentinel ( title ) . text ) if errors [ :title ] . empty?
2017-11-23 03:53:35 +08:00
self . featured_link = self . featured_link . strip . presence if self . featured_link
2013-05-23 12:52:12 +08:00
end
2013-02-06 03:16:51 +08:00
belongs_to :category
2016-07-07 03:56:40 +08:00
has_many :category_users , through : :category
2013-02-06 03:16:51 +08:00
has_many :posts
2020-02-13 14:26:02 +08:00
has_many :bookmarks
2014-07-29 04:50:49 +08:00
has_many :ordered_posts , - > { order ( post_number : :asc ) } , class_name : " Post "
2013-02-06 03:16:51 +08:00
has_many :topic_allowed_users
2013-05-02 13:15:17 +08:00
has_many :topic_allowed_groups
2020-07-10 17:05:55 +08:00
has_many :incoming_email
2013-05-02 13:15:17 +08:00
2015-12-23 08:09:17 +08:00
has_many :group_archived_messages , dependent : :destroy
has_many :user_archived_messages , dependent : :destroy
2013-05-02 13:15:17 +08:00
has_many :allowed_groups , through : :topic_allowed_groups , source : :group
2017-08-31 12:06:56 +08:00
has_many :allowed_group_users , through : :allowed_groups , source : :users
2013-02-06 03:16:51 +08:00
has_many :allowed_users , through : :topic_allowed_users , source : :user
2013-03-29 01:02:59 +08:00
2018-02-09 05:36:39 +08:00
has_many :topic_tags
has_many :tags , through : :topic_tags , dependent : :destroy # dependent destroy applies to the topic_tags records
2016-07-07 03:56:40 +08:00
has_many :tag_users , through : :tags
2016-05-05 02:02:47 +08:00
2013-12-24 07:50:36 +08:00
has_one :top_topic
2018-03-14 03:59:12 +08:00
has_one :shared_draft , dependent : :destroy
2020-04-09 00:52:36 +08:00
has_one :published_page
2018-03-14 03:59:12 +08:00
2013-02-06 03:16:51 +08:00
belongs_to :user
belongs_to :last_poster , class_name : 'User' , foreign_key : :last_post_user_id
belongs_to :featured_user1 , class_name : 'User' , foreign_key : :featured_user1_id
belongs_to :featured_user2 , class_name : 'User' , foreign_key : :featured_user2_id
belongs_to :featured_user3 , class_name : 'User' , foreign_key : :featured_user3_id
belongs_to :featured_user4 , class_name : 'User' , foreign_key : :featured_user4_id
has_many :topic_users
2021-02-04 08:27:34 +08:00
has_many :dismissed_topic_users
2013-02-06 03:16:51 +08:00
has_many :topic_links
has_many :topic_invites
has_many :invites , through : :topic_invites , source : :invite
2017-05-12 06:23:18 +08:00
has_many :topic_timers , dependent : :destroy
2019-01-04 01:03:01 +08:00
has_many :reviewables
2019-12-10 03:15:47 +08:00
has_many :user_profiles
2013-02-06 03:16:51 +08:00
2017-04-15 12:11:02 +08:00
has_one :user_warning
2017-08-31 12:06:56 +08:00
has_one :first_post , - > { where post_number : 1 } , class_name : 'Post'
2020-07-09 10:08:04 +08:00
has_one :topic_search_data
2017-04-25 02:29:04 +08:00
has_one :topic_embed , dependent : :destroy
2020-11-02 14:48:48 +08:00
has_one :linked_topic , dependent : :destroy
2017-04-25 02:29:04 +08:00
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
belongs_to :image_upload , class_name : 'Upload'
has_many :topic_thumbnails , through : :image_upload
2013-02-06 03:16:51 +08:00
# When we want to temporarily attach some data to a forum topic (usually before serialization)
attr_accessor :user_data
2019-11-14 08:16:13 +08:00
attr_accessor :category_user_data
2021-02-04 08:27:34 +08:00
attr_accessor :dismissed
2015-06-22 16:09:08 +08:00
2013-02-06 03:16:51 +08:00
attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code
2014-05-12 15:32:49 +08:00
attr_accessor :participants
2013-04-03 04:52:51 +08:00
attr_accessor :topic_list
2014-04-26 00:24:22 +08:00
attr_accessor :meta_data
2013-10-17 14:44:56 +08:00
attr_accessor :include_last_poster
2014-07-04 02:43:24 +08:00
attr_accessor :import_mode # set to true to optimize creation and save for imports
2013-02-06 03:16:51 +08:00
# The regular order
2014-05-08 01:04:39 +08:00
scope :topic_list_order , - > { order ( 'topics.bumped_at desc' ) }
2013-02-06 03:16:51 +08:00
# Return private message topics
2014-05-08 01:04:39 +08:00
scope :private_messages , - > { where ( archetype : Archetype . private_message ) }
2013-02-06 03:16:51 +08:00
2020-03-23 19:02:24 +08:00
PRIVATE_MESSAGES_SQL = << ~ SQL
SELECT topic_id
FROM topic_allowed_users
WHERE user_id = :user_id
UNION ALL
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
scope :private_messages_for_user , - > ( user ) {
private_messages . where ( " topics.id IN ( #{ PRIVATE_MESSAGES_SQL } ) " , user_id : user . id )
}
2016-05-31 01:48:46 +08:00
scope :listable_topics , - > { where ( 'topics.archetype <> ?' , Archetype . private_message ) }
2013-02-06 03:16:51 +08:00
2014-02-28 12:07:55 +08:00
scope :by_newest , - > { order ( 'topics.created_at desc, topics.id desc' ) }
2013-02-28 11:36:12 +08:00
2013-07-04 04:04:22 +08:00
scope :visible , - > { where ( visible : true ) }
2013-05-30 19:23:40 +08:00
2015-01-29 23:40:26 +08:00
scope :created_since , lambda { | time_ago | where ( 'topics.created_at > ?' , time_ago ) }
2013-05-30 19:23:40 +08:00
2019-11-19 22:24:18 +08:00
scope :exclude_scheduled_bump_topics , - > { where . not ( id : TopicTimer . scheduled_bump_topics ) }
2014-05-08 01:04:39 +08:00
scope :secured , lambda { | guardian = nil |
2013-06-08 21:52:06 +08:00
ids = guardian . secure_category_ids if guardian
2013-06-13 01:43:59 +08:00
# Query conditions
2014-05-08 01:04:39 +08:00
condition = if ids . present?
2016-05-02 21:26:23 +08:00
[ " NOT read_restricted OR id IN (:cats) " , cats : ids ]
2014-05-08 01:04:39 +08:00
else
2016-05-02 21:26:23 +08:00
[ " NOT read_restricted " ]
2014-05-08 01:04:39 +08:00
end
2013-06-13 01:43:59 +08:00
2016-05-02 21:26:23 +08:00
where ( " topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{ condition [ 0 ] } ) " , condition [ 1 ] )
2013-06-13 01:43:59 +08:00
}
2013-06-08 21:52:06 +08:00
2018-08-10 08:50:05 +08:00
scope :in_category_and_subcategories , lambda { | category_id |
2020-04-09 20:42:24 +08:00
where ( " topics.category_id IN (?) " , Category . subcategory_ids ( category_id . to_i ) ) if category_id
2018-08-10 08:50:05 +08:00
}
2018-03-27 16:30:08 +08:00
scope :with_subtype , - > ( subtype ) { where ( 'topics.subtype = ?' , subtype ) }
2014-10-11 00:21:44 +08:00
attr_accessor :ignore_category_auto_close
2014-10-28 05:06:43 +08:00
attr_accessor :skip_callbacks
2014-10-11 00:21:44 +08:00
2013-02-06 03:16:51 +08:00
before_create do
2014-10-28 05:06:43 +08:00
initialize_default_values
2013-02-06 03:16:51 +08:00
end
after_create do
2013-12-13 14:04:45 +08:00
unless skip_callbacks
changed_to_category ( category )
2014-10-28 05:06:43 +08:00
advance_draft_sequence
2013-02-06 03:16:51 +08:00
end
end
2013-05-08 02:25:41 +08:00
before_save do
2013-12-13 14:04:45 +08:00
unless skip_callbacks
2014-10-28 05:06:43 +08:00
ensure_topic_has_a_category
2013-10-24 07:05:51 +08:00
end
2018-09-18 06:54:44 +08:00
2015-09-24 11:37:53 +08:00
if title_changed?
2018-09-18 06:54:44 +08:00
write_attribute ( :fancy_title , Topic . fancy_title ( title ) )
2015-09-24 11:37:53 +08:00
end
2017-05-31 16:40:21 +08:00
if category_id_changed? || new_record?
inherit_auto_close_from_category
end
2013-05-08 02:25:41 +08:00
end
after_save do
2020-04-30 14:48:34 +08:00
banner = " banner "
2014-11-14 12:39:17 +08:00
2017-08-31 12:06:56 +08:00
if archetype_before_last_save == banner || archetype == banner
2014-11-14 12:39:17 +08:00
ApplicationController . banner_json_cache . clear
end
2016-07-08 10:58:18 +08:00
2020-07-17 11:12:31 +08:00
if tags_changed || saved_change_to_attribute? ( :category_id ) || saved_change_to_attribute? ( :title )
2018-02-20 11:41:00 +08:00
SearchIndexer . queue_post_reindex ( self . id )
if tags_changed
TagUser . auto_watch ( topic_id : id )
TagUser . auto_track ( topic_id : id )
self . tags_changed = false
end
2016-07-08 10:58:18 +08:00
end
2016-12-22 10:13:14 +08:00
SearchIndexer . index ( self )
2013-05-08 02:25:41 +08:00
end
2018-02-09 05:36:39 +08:00
after_update do
if saved_changes [ :category_id ] && self . tags . present?
CategoryTagStat . topic_moved ( self , * saved_changes [ :category_id ] )
2019-12-10 03:15:47 +08:00
elsif saved_changes [ :category_id ] && self . category & . read_restricted?
UserProfile . remove_featured_topic_from_all_profiles ( self )
2018-02-09 05:36:39 +08:00
end
end
2014-10-28 05:06:43 +08:00
def initialize_default_values
self . bumped_at || = Time . now
self . last_post_user_id || = user_id
end
2014-03-07 15:59:47 +08:00
2014-10-28 05:06:43 +08:00
def advance_draft_sequence
2018-03-05 15:38:05 +08:00
if self . private_message?
2014-10-28 05:06:43 +08:00
DraftSequence . next! ( user , Draft :: NEW_PRIVATE_MESSAGE )
else
DraftSequence . next! ( user , Draft :: NEW_TOPIC )
end
end
def ensure_topic_has_a_category
2018-03-05 16:18:23 +08:00
if category_id . nil? && ( archetype . nil? || self . regular? )
2018-06-05 15:29:17 +08:00
self . category_id = category & . id || SiteSetting . uncategorized_category_id
2014-10-28 05:06:43 +08:00
end
2013-12-12 10:41:34 +08:00
end
2015-09-22 06:50:52 +08:00
def self . visible_post_types ( viewed_by = nil )
2015-09-11 04:01:23 +08:00
types = Post . types
result = [ types [ :regular ] , types [ :moderator_action ] , types [ :small_action ] ]
2017-09-08 13:07:22 +08:00
result << types [ :whisper ] if viewed_by & . staff?
2015-09-11 04:01:23 +08:00
result
end
2013-11-14 01:26:32 +08:00
def self . top_viewed ( max = 10 )
2014-02-28 12:07:55 +08:00
Topic . listable_topics . visible . secured . order ( 'views desc' ) . limit ( max )
2013-11-14 01:26:32 +08:00
end
def self . recent ( max = 10 )
2014-02-28 12:07:55 +08:00
Topic . listable_topics . visible . secured . order ( 'created_at desc' ) . limit ( max )
2013-11-14 01:26:32 +08:00
end
2013-10-28 14:12:07 +08:00
def self . count_exceeds_minimum?
count > SiteSetting . minimum_topics_similar
end
2013-06-04 04:12:24 +08:00
def best_post
2016-08-20 01:19:08 +08:00
posts . where ( post_type : Post . types [ :regular ] , user_deleted : false ) . order ( 'score desc nulls last' ) . limit ( 1 ) . first
2013-06-04 04:12:24 +08:00
end
2015-02-16 20:03:04 +08:00
def has_flags?
2019-01-04 01:03:01 +08:00
ReviewableFlaggedPost . pending . default_visible . where ( topic_id : id ) . exists?
2015-02-16 20:03:04 +08:00
end
2016-04-11 20:37:28 +08:00
def is_official_warning?
subtype == TopicSubtype . moderator_warning
end
2013-05-02 13:15:17 +08:00
# all users (in groups or directly targetted) that are going to get the pm
def all_allowed_users
2016-04-11 20:37:28 +08:00
moderators_sql = " UNION #{ User . moderators . to_sql } " if private_message? && ( has_flags? || is_official_warning? )
2015-10-30 01:39:30 +08:00
User . from ( " ( #{ allowed_users . to_sql } UNION #{ allowed_group_users . to_sql } #{ moderators_sql } ) as users " )
2013-05-02 13:15:17 +08:00
end
2013-02-06 03:16:51 +08:00
# Additional rate limits on topics: per day and private messages per day
def limit_topics_per_day
2020-11-05 08:23:49 +08:00
return unless regular?
2016-06-21 04:38:15 +08:00
if user && user . new_user_posting_on_first_day?
2015-09-25 00:04:41 +08:00
limit_first_day_topics_per_day
else
apply_per_day_rate_limit_for ( " topics " , :max_topics_per_day )
end
2013-02-06 03:16:51 +08:00
end
def limit_private_messages_per_day
return unless private_message?
2018-01-31 14:16:25 +08:00
apply_per_day_rate_limit_for ( " pms " , :max_personal_messages_per_day )
2013-02-06 03:16:51 +08:00
end
2015-09-24 11:37:53 +08:00
def self . fancy_title ( title )
2018-09-18 06:54:44 +08:00
return unless escaped = ERB :: Util . html_escape ( title )
2019-07-18 17:55:49 +08:00
fancy_title = Emoji . unicode_unescape ( HtmlPrettify . render ( escaped ) )
2018-09-18 06:54:44 +08:00
fancy_title . length > Topic . max_fancy_title_length ? escaped : fancy_title
2015-09-24 11:37:53 +08:00
end
2013-02-26 00:42:20 +08:00
def fancy_title
2015-09-24 11:37:53 +08:00
return ERB :: Util . html_escape ( title ) unless SiteSetting . title_fancy_entities?
2014-04-18 12:48:38 +08:00
2015-09-24 11:37:53 +08:00
unless fancy_title = read_attribute ( :fancy_title )
fancy_title = Topic . fancy_title ( title )
write_attribute ( :fancy_title , fancy_title )
2017-10-19 15:41:03 +08:00
if ! new_record? && ! Discourse . readonly_mode?
2015-09-24 11:37:53 +08:00
# make sure data is set in table, this also allows us to change algorithm
# by simply nulling this column
2018-06-19 14:13:14 +08:00
DB . exec ( " UPDATE topics SET fancy_title = :fancy_title where id = :id " , id : self . id , fancy_title : fancy_title )
2015-09-24 11:37:53 +08:00
end
end
2013-02-20 05:08:23 +08:00
2015-09-24 11:37:53 +08:00
fancy_title
2013-04-22 11:48:05 +08:00
end
2013-06-04 04:12:24 +08:00
# Returns hot topics since a date for display in email digest.
2014-04-18 04:42:40 +08:00
def self . for_digest ( user , since , opts = nil )
opts = opts || { }
2014-04-18 03:14:54 +08:00
score = " #{ ListController . best_period_for ( since ) } _score "
2013-11-07 04:05:06 +08:00
topics = Topic
. visible
. secured ( Guardian . new ( user ) )
2014-04-18 03:21:55 +08:00
. joins ( " LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{ user . id . to_i } " )
2016-08-16 04:16:04 +08:00
. joins ( " LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{ user . id . to_i } " )
2015-01-29 23:40:26 +08:00
. joins ( " LEFT OUTER JOIN users ON users.id = topics.user_id " )
2013-11-07 04:05:06 +08:00
. where ( closed : false , archived : false )
2014-04-18 03:21:55 +08:00
. where ( " COALESCE(topic_users.notification_level, 1) <> ? " , TopicUser . notification_levels [ :muted ] )
2013-11-07 04:05:06 +08:00
. created_since ( since )
2017-08-15 00:47:33 +08:00
. where ( 'topics.created_at < ?' , ( SiteSetting . editing_grace_period || 0 ) . seconds . ago )
2013-11-07 04:05:06 +08:00
. listable_topics
2014-04-18 03:43:24 +08:00
. includes ( :category )
2014-04-18 04:42:40 +08:00
2016-12-20 03:53:53 +08:00
unless opts [ :include_tl0 ] || user . user_option . try ( :include_tl0_in_digests )
2016-03-18 05:35:23 +08:00
topics = topics . where ( " COALESCE(users.trust_level, 0) > 0 " )
end
2014-04-18 04:42:40 +08:00
if ! ! opts [ :top_order ]
topics = topics . joins ( " LEFT OUTER JOIN top_topics ON top_topics.topic_id = topics.id " )
2016-08-16 04:16:04 +08:00
. order ( TopicQuerySQL . order_top_with_notification_levels ( score ) )
2014-04-18 04:42:40 +08:00
end
if opts [ :limit ]
topics = topics . limit ( opts [ :limit ] )
end
2013-11-07 04:05:06 +08:00
2014-04-18 03:14:54 +08:00
# Remove category topics
2013-11-07 04:05:06 +08:00
category_topic_ids = Category . pluck ( :topic_id ) . compact!
if category_topic_ids . present?
2014-04-18 03:14:54 +08:00
topics = topics . where ( " topics.id NOT IN (?) " , category_topic_ids )
end
2020-08-05 01:35:48 +08:00
# Remove muted and shared draft categories
remove_category_ids = CategoryUser . where ( user_id : user . id , notification_level : CategoryUser . notification_levels [ :muted ] ) . pluck ( :category_id )
2016-03-26 03:12:00 +08:00
if SiteSetting . digest_suppress_categories . present?
2020-08-05 01:35:48 +08:00
remove_category_ids += SiteSetting . digest_suppress_categories . split ( " | " ) . map ( & :to_i )
2016-03-26 03:12:00 +08:00
end
2020-08-05 01:46:26 +08:00
if SiteSetting . shared_drafts_enabled?
remove_category_ids << SiteSetting . shared_drafts_category
2020-08-05 01:35:48 +08:00
end
if remove_category_ids . present?
remove_category_ids . uniq!
topics = topics . where ( " topics.category_id NOT IN (?) " , remove_category_ids )
2013-11-07 04:05:06 +08:00
end
2016-08-19 05:16:52 +08:00
# Remove muted tags
2016-08-09 03:14:18 +08:00
muted_tag_ids = TagUser . lookup ( user , :muted ) . pluck ( :tag_id )
unless muted_tag_ids . empty?
2017-07-05 04:12:10 +08:00
# 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)
2016-08-09 03:14:18 +08:00
end
2013-11-07 04:05:06 +08:00
topics
2013-02-06 03:16:51 +08:00
end
2013-02-07 23:45:24 +08:00
2014-04-26 00:24:22 +08:00
def meta_data = ( data )
custom_fields . replace ( data )
end
def meta_data
custom_fields
end
2013-02-06 03:16:51 +08:00
def update_meta_data ( data )
2014-04-26 00:24:22 +08:00
custom_fields . update ( data )
2013-02-06 03:16:51 +08:00
save
end
2013-04-22 11:48:05 +08:00
def reload ( options = nil )
@post_numbers = nil
2017-05-17 09:37:11 +08:00
@public_topic_timer = nil
2018-05-24 16:41:51 +08:00
@is_category_topic = nil
2013-04-22 11:48:05 +08:00
super ( options )
end
2013-02-06 03:16:51 +08:00
def post_numbers
@post_numbers || = posts . order ( :post_number ) . pluck ( :post_number )
end
2013-12-07 05:39:35 +08:00
def age_in_minutes
( ( Time . zone . now - created_at ) / 1 . minute ) . round
2013-05-28 08:58:57 +08:00
end
2020-04-22 16:52:50 +08:00
def self . listable_count_per_day ( start_date , end_date , category_id = nil , include_subcategories = false )
2018-05-11 11:30:21 +08:00
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)' )
2020-04-22 16:52:50 +08:00
result = result . where ( category_id : include_subcategories ? Category . subcategory_ids ( category_id ) : category_id ) if category_id
2018-04-26 20:49:41 +08:00
result . count
2013-03-08 00:07:59 +08:00
end
2013-02-07 23:45:24 +08:00
def private_message?
2021-01-13 06:49:29 +08:00
self . archetype == Archetype . private_message
2013-02-06 03:16:51 +08:00
end
2018-03-05 16:18:23 +08:00
def regular?
self . archetype == Archetype . default
end
2021-01-13 06:49:29 +08:00
def open?
! self . closed?
end
2017-09-16 07:03:29 +08:00
MAX_SIMILAR_BODY_LENGTH || = 200
2013-06-13 01:43:59 +08:00
def self . similar_to ( title , raw , user = nil )
2017-09-16 07:03:29 +08:00
return [ ] if title . blank?
raw = raw . presence || " "
2013-03-15 02:45:29 +08:00
2020-07-28 13:23:53 +08:00
tsquery = Search . set_tsquery_weight_filter (
2020-07-28 11:53:25 +08:00
Search . prepare_data ( title . strip ) ,
'A'
)
2020-07-28 13:23:53 +08:00
if raw . present?
2020-07-28 15:20:18 +08:00
cooked = SearchIndexer :: HtmlScrubber . scrub (
PrettyText . cook ( raw [ 0 ... MAX_SIMILAR_BODY_LENGTH ] . strip )
)
2020-09-22 05:53:12 +08:00
prepared_data = cooked . present? && Search . prepare_data ( cooked )
if prepared_data . present?
2020-08-21 10:51:37 +08:00
raw_tsquery = Search . set_tsquery_weight_filter (
2020-09-22 05:53:12 +08:00
prepared_data ,
2020-08-21 10:51:37 +08:00
'B'
)
2020-07-28 13:23:53 +08:00
2020-08-21 10:51:37 +08:00
tsquery = " #{ tsquery } & #{ raw_tsquery } "
end
2020-07-28 13:23:53 +08:00
end
2020-07-28 11:53:25 +08:00
2020-07-28 13:23:53 +08:00
tsquery = Search . to_tsquery ( term : tsquery , joiner : " | " )
2014-04-15 03:20:41 +08:00
2017-09-16 07:03:29 +08:00
candidates = Topic
. visible
2014-08-08 10:12:53 +08:00
. listable_topics
2017-09-16 07:03:29 +08:00
. secured ( Guardian . new ( user ) )
. joins ( " JOIN topic_search_data s ON topics.id = s.topic_id " )
. joins ( " LEFT JOIN categories c ON topics.id = c.topic_id " )
2020-07-28 11:53:25 +08:00
. where ( " search_data @@ #{ tsquery } " )
2017-09-16 07:03:29 +08:00
. where ( " c.topic_id IS NULL " )
2020-07-28 11:53:25 +08:00
. order ( " ts_rank(search_data, #{ tsquery } ) DESC " )
2014-08-08 10:12:53 +08:00
. limit ( SiteSetting . max_similar_results * 3 )
candidate_ids = candidates . pluck ( :id )
2017-09-16 07:03:29 +08:00
return [ ] if candidate_ids . blank?
2014-08-08 10:12:53 +08:00
2017-09-16 07:03:29 +08:00
similars = Topic
2014-08-08 10:12:53 +08:00
. joins ( " JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1 " )
. where ( " topics.id IN (?) " , candidate_ids )
2017-09-16 07:03:29 +08:00
. order ( " similarity DESC " )
. limit ( SiteSetting . max_similar_results )
2014-08-08 10:12:53 +08:00
2017-09-16 07:03:29 +08:00
if raw . present?
similars
2020-12-11 07:56:26 +08:00
. select ( DB . sql_fragment ( " topics.*, similarity(topics.title, :title) + similarity(p.raw, :raw) AS similarity, p.cooked AS blurb " , title : title , raw : raw ) )
2017-09-16 07:03:29 +08:00
. where ( " similarity(topics.title, :title) + similarity(p.raw, :raw) > 0.2 " , title : title , raw : raw )
else
similars
2020-12-11 07:56:26 +08:00
. select ( DB . sql_fragment ( " topics.*, similarity(topics.title, :title) AS similarity, p.cooked AS blurb " , title : title ) )
2017-09-16 07:03:29 +08:00
. where ( " similarity(topics.title, :title) > 0.2 " , title : title )
end
2013-03-15 02:45:29 +08:00
end
2013-03-07 04:17:07 +08:00
2015-07-29 22:34:21 +08:00
def update_status ( status , enabled , user , opts = { } )
2017-03-22 11:12:02 +08:00
TopicStatusUpdater . new ( self , user ) . update! ( status , enabled , opts )
2018-02-27 11:07:37 +08:00
DiscourseEvent . trigger ( :topic_status_updated , self , status , enabled )
2020-02-27 13:39:37 +08:00
2020-07-22 02:29:02 +08:00
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
2020-02-27 13:39:37 +08:00
if enabled && private_message? && 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 do | id |
GroupArchivedMessage . archive! ( id , self )
end
end
end
2013-02-06 03:16:51 +08:00
end
# Atomically creates the next post number
2019-03-08 16:49:34 +08:00
def self . next_post_number ( topic_id , opts = { } )
2018-06-19 14:13:14 +08:00
highest = DB . query_single ( " SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ? " , topic_id ) . first . to_i
2013-02-06 03:16:51 +08:00
2019-03-08 16:49:34 +08:00
if opts [ :whisper ]
2016-12-02 14:03:31 +08:00
2018-06-19 14:13:14 +08:00
result = DB . query_single ( << ~ SQL , highest , topic_id )
UPDATE topics
SET highest_staff_post_number = ? + 1
WHERE id = ?
RETURNING highest_staff_post_number
SQL
2016-12-02 14:03:31 +08:00
2018-06-19 14:13:14 +08:00
result . first . to_i
2016-12-02 14:03:31 +08:00
else
2019-03-08 16:49:34 +08:00
reply_sql = opts [ :reply ] ? " , reply_count = reply_count + 1 " : " "
posts_sql = opts [ :post ] ? " , posts_count = posts_count + 1 " : " "
2016-12-02 14:03:31 +08:00
2018-06-19 14:13:14 +08:00
result = DB . query_single ( << ~ SQL , highest : highest , topic_id : topic_id )
UPDATE topics
SET highest_staff_post_number = :highest + 1 ,
2019-03-08 16:49:34 +08:00
highest_post_number = :highest + 1
#{reply_sql}
#{posts_sql}
2018-06-19 14:13:14 +08:00
WHERE id = :topic_id
RETURNING highest_post_number
SQL
2016-12-02 14:03:31 +08:00
2018-06-19 14:13:14 +08:00
result . first . to_i
2016-12-02 14:03:31 +08:00
end
end
def self . reset_all_highest!
2018-06-19 14:13:14 +08:00
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
2019-03-08 16:49:34 +08:00
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
2018-06-19 14:13:14 +08:00
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
2013-02-06 03:16:51 +08:00
end
# If a post is deleted we have to update our highest post counters
def self . reset_highest ( topic_id )
2019-10-21 18:32:27 +08:00
archetype = Topic . where ( id : topic_id ) . pluck_first ( :archetype )
2019-03-08 16:49:34 +08:00
# ignore small_action replies for private messages
post_type = archetype == Archetype . private_message ? " AND post_type <> #{ Post . types [ :small_action ] } " : ''
2018-06-19 14:13:14 +08:00
result = DB . query_single ( << ~ SQL , topic_id : topic_id )
UPDATE topics
SET
2019-03-08 16:49:34 +08:00
highest_staff_post_number = (
2018-06-19 14:13:14 +08:00
SELECT COALESCE ( MAX ( post_number ) , 0 ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL
) ,
2019-03-08 16:49:34 +08:00
highest_post_number = (
2018-06-19 14:13:14 +08:00
SELECT COALESCE ( MAX ( post_number ) , 0 ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type < > 4
2019-03-08 16:49:34 +08:00
#{post_type}
2018-06-19 14:13:14 +08:00
) ,
posts_count = (
SELECT count ( * ) FROM posts
WHERE deleted_at IS NULL AND
topic_id = :topic_id AND
post_type < > 4
2019-03-08 16:49:34 +08:00
#{post_type}
2018-06-19 14:13:14 +08:00
) ,
last_posted_at = (
SELECT MAX ( created_at ) FROM posts
WHERE topic_id = :topic_id AND
deleted_at IS NULL AND
post_type < > 4
2019-03-08 16:49:34 +08:00
#{post_type}
2018-06-19 14:13:14 +08:00
)
WHERE id = :topic_id
RETURNING highest_post_number
SQL
highest_post_number = result . first . to_i
2013-02-06 03:16:51 +08:00
# Update the forum topic user records
2018-06-19 14:13:14 +08:00
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 ,
highest_seen_post_number = CASE
WHEN highest_seen_post_number > :highest THEN :highest
ELSE highest_seen_post_number
END
WHERE topic_id = :topic_id
SQL
2013-02-06 03:16:51 +08:00
end
2019-05-10 09:37:37 +08:00
cattr_accessor :update_featured_topics
2014-10-28 05:06:43 +08:00
def changed_to_category ( new_category )
2018-06-01 09:44:14 +08:00
return true if new_category . blank? || Category . exists? ( topic_id : id )
2014-10-28 05:06:43 +08:00
return false if new_category . id == SiteSetting . uncategorized_category_id && ! SiteSetting . allow_uncategorized_topics
2013-02-06 03:16:51 +08:00
Topic . transaction do
old_category = category
2014-10-28 05:06:43 +08:00
if self . category_id != new_category . id
2018-08-14 22:06:52 +08:00
self . update_attribute ( :category_id , new_category . id )
2018-05-07 21:29:06 +08:00
if old_category
Category
. where ( id : old_category . id )
. update_all ( " topic_count = topic_count - 1 " )
end
2016-07-08 10:58:18 +08:00
# 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 )
2018-05-07 21:29:06 +08:00
2018-05-24 23:27:43 +08:00
if post = self . ordered_posts . first
notified_user_ids = [ post . user_id , post . last_editor_id ] . uniq
2018-07-24 16:41:55 +08:00
DB . after_commit do
2018-07-19 05:04:43 +08:00
Jobs . enqueue ( :notify_category_change , post_id : post . id , notified_user_ids : notified_user_ids )
end
2018-05-07 21:29:06 +08:00
end
2021-01-29 07:03:44 +08:00
# 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 do
Jobs . enqueue ( :update_topic_upload_security , topic_id : self . id )
end
2013-02-06 03:16:51 +08:00
end
2014-10-28 05:06:43 +08:00
Category . where ( id : new_category . id ) . update_all ( " topic_count = topic_count + 1 " )
2019-05-10 09:37:37 +08:00
if Topic . update_featured_topics != false
CategoryFeaturedTopic . feature_topics_for ( old_category ) unless @import_mode
CategoryFeaturedTopic . feature_topics_for ( new_category ) unless @import_mode || old_category . try ( :id ) == new_category . id
end
2013-02-07 23:45:24 +08:00
end
2014-10-28 05:06:43 +08:00
2013-10-09 02:40:31 +08:00
true
2013-02-06 03:16:51 +08:00
end
2018-07-18 08:17:33 +08:00
def add_small_action ( user , action_code , who = nil , opts = { } )
2016-01-19 07:57:55 +08:00
custom_fields = { }
custom_fields [ " action_code_who " ] = who if who . present?
2018-07-18 08:17:33 +08:00
opts = opts . merge (
post_type : Post . types [ :small_action ] ,
action_code : action_code ,
custom_fields : custom_fields
)
add_moderator_post ( user , nil , opts )
2016-01-19 07:57:55 +08:00
end
2015-07-25 04:39:03 +08:00
def add_moderator_post ( user , text , opts = nil )
opts || = { }
2013-02-06 03:16:51 +08:00
new_post = nil
2015-10-15 08:56:10 +08:00
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 ,
2020-12-03 07:43:19 +08:00
silent : opts [ :silent ] ,
2016-01-11 19:42:06 +08:00
skip_validations : true ,
2020-07-10 17:05:55 +08:00
custom_fields : opts [ :custom_fields ] ,
import_mode : opts [ :import_mode ] )
2013-02-06 03:16:51 +08:00
2016-06-20 15:47:29 +08:00
if ( new_post = creator . create ) && new_post . present?
increment! ( :moderator_posts_count ) if new_post . persisted?
2013-02-06 03:16:51 +08:00
# If we are moving posts, we want to insert the moderator post where the previous posts were
# in the stream, not at the end.
2019-04-29 15:32:25 +08:00
new_post . update! ( post_number : opts [ :post_number ] , sort_order : opts [ :post_number ] ) if opts [ :post_number ] . present?
2013-02-06 03:16:51 +08:00
# Grab any links that are present
TopicLink . extract_from ( new_post )
2014-07-15 15:47:24 +08:00
QuotedPost . extract_from ( new_post )
2013-02-06 03:16:51 +08:00
end
2013-02-07 23:45:24 +08:00
2013-02-06 03:16:51 +08:00
new_post
end
2014-07-17 03:39:39 +08:00
def change_category_to_id ( category_id )
2014-09-11 15:39:20 +08:00
return false if private_message?
2014-10-28 05:06:43 +08:00
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
2013-02-06 03:16:51 +08:00
2014-10-28 05:06:43 +08:00
return true if self . category_id == new_category_id
cat = Category . find_by ( id : new_category_id )
2013-10-24 07:05:51 +08:00
return false unless cat
2014-10-28 05:06:43 +08:00
2019-01-04 01:03:01 +08:00
reviewables . update_all ( category_id : new_category_id )
2013-02-06 03:16:51 +08:00
changed_to_category ( cat )
end
2016-06-20 14:29:11 +08:00
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
2021-03-02 22:46:50 +08:00
allowed_groups . reload
2016-06-20 14:29:11 +08:00
add_small_action ( removed_by , " removed_group " , group . name )
return true
end
end
false
end
2016-01-11 19:42:06 +08:00
def remove_allowed_user ( removed_by , username )
2017-10-10 16:26:56 +08:00
user = username . is_a? ( User ) ? username : User . find_by ( username : username )
if user
2014-05-06 21:41:59 +08:00
topic_user = topic_allowed_users . find_by ( user_id : user . id )
2017-10-10 16:26:56 +08:00
2013-10-03 01:11:48 +08:00
if topic_user
topic_user . destroy
2017-10-10 16:26:56 +08:00
if user . id == removed_by & . id
removed_by = Discourse . system_user
add_small_action ( removed_by , " user_left " , user . username )
else
add_small_action ( removed_by , " removed_user " , user . username )
end
2014-09-25 23:44:48 +08:00
return true
2013-10-03 01:11:48 +08:00
end
2013-06-18 15:17:01 +08:00
end
2014-09-25 23:44:48 +08:00
false
2013-06-18 15:17:01 +08:00
end
2018-08-23 12:36:49 +08:00
def reached_recipients_limit?
return false unless private_message?
topic_allowed_users . count + topic_allowed_groups . count > = SiteSetting . max_allowed_message_recipients
end
2016-06-20 14:29:11 +08:00
def invite_group ( user , group )
TopicAllowedGroup . create! ( topic_id : id , group_id : group . id )
2021-03-02 22:46:50 +08:00
allowed_groups . reload
2016-06-20 14:29:11 +08:00
last_post = posts . order ( 'post_number desc' ) . where ( 'not hidden AND posts.deleted_at IS NULL' ) . first
if last_post
2018-05-24 23:27:43 +08:00
Jobs . enqueue ( :post_alert , post_id : last_post . id )
2016-06-20 14:29:11 +08:00
add_small_action ( user , " invited_group " , group . name )
2017-06-14 13:53:49 +08:00
2017-07-26 14:51:44 +08:00
group_id = group . id
2019-08-16 16:23:51 +08:00
group . set_message_default_notification_levels! ( self , ignore_existing : true )
2017-06-14 13:53:49 +08:00
group . users . where (
2019-08-16 15:22:18 +08:00
" group_users.notification_level = :level " ,
level : NotificationLevels . all [ :tracking ] ,
id : user . id
) . find_each do | u |
PostAlerter . new . notify_group_summary ( u , last_post )
end
group . users . where (
" group_users.notification_level in (:levels) AND user_id != :id " ,
levels : [ NotificationLevels . all [ :watching ] , NotificationLevels . all [ :watching_first_post ] ] ,
id : user . id
2017-06-14 13:53:49 +08:00
) . find_each do | u |
u . notifications . create! (
notification_type : Notification . types [ :invited_to_private_message ] ,
topic_id : self . id ,
post_number : 1 ,
data : {
topic_title : self . title ,
2017-07-26 14:51:44 +08:00
display_username : user . username ,
group_id : group_id
2017-06-14 13:53:49 +08:00
} . to_json
)
2019-08-16 16:23:51 +08:00
2017-06-14 13:53:49 +08:00
end
2016-06-20 14:29:11 +08:00
end
true
end
2016-06-08 01:24:45 +08:00
def invite ( invited_by , username_or_email , group_ids = nil , custom_message = nil )
2018-02-26 13:19:52 +08:00
target_user = User . find_by_username_or_email ( username_or_email )
2018-03-08 04:04:17 +08:00
guardian = Guardian . new ( invited_by )
2018-12-05 23:43:07 +08:00
is_email = username_or_email =~ / ^.+@.+$ /
2017-07-12 18:01:10 +08:00
2018-12-05 23:43:07 +08:00
if target_user
if topic_allowed_users . exists? ( user_id : target_user . id )
raise UserExists . new ( I18n . t ( " topic_invite.user_exists " ) )
end
2018-03-01 12:41:36 +08:00
2020-11-06 22:58:10 +08:00
if MutedUser
. where ( user : target_user , muted_user : invited_by )
. joins ( :muted_user )
. where ( 'NOT admin AND NOT moderator' )
. exists?
raise NotAllowed . new ( I18n . t ( " topic_invite.muted_invitee " ) )
2020-07-31 23:52:19 +08:00
end
2020-11-06 22:58:10 +08:00
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 ! target_user . staff? &&
target_user & . user_option & . enable_allowed_pm_users &&
! AllowedPmUser . where ( user : target_user , allowed_pm_user : invited_by ) . exists?
raise NotAllowed . new ( I18n . t ( " topic_invite.receiver_does_not_allow_pm " ) )
end
if ! target_user . staff? &&
invited_by & . user_option & . enable_allowed_pm_users &&
! AllowedPmUser . where ( user : invited_by , allowed_pm_user : target_user ) . exists?
raise NotAllowed . new ( I18n . t ( " topic_invite.sender_does_not_allow_pm " ) )
end
2018-12-05 23:43:07 +08:00
if private_message?
! ! invite_to_private_message ( invited_by , target_user , guardian )
2017-07-12 18:01:10 +08:00
else
2018-12-05 23:43:07 +08:00
! ! invite_to_topic ( invited_by , target_user , group_ids , guardian )
2017-07-12 18:01:10 +08:00
end
2018-12-05 23:43:07 +08:00
elsif is_email && guardian . can_invite_via_email? ( self )
! ! Invite . invite_by_email (
username_or_email , invited_by , self , group_ids , custom_message
2018-02-26 10:42:06 +08:00
)
2013-06-19 08:31:19 +08:00
end
2013-02-06 03:16:51 +08:00
end
2013-10-04 05:06:14 +08:00
def email_already_exists_for? ( invite )
invite . email_already_exists && private_message?
end
def grant_permission_to_user ( lower_email )
2017-04-27 02:47:36 +08:00
user = User . find_by_email ( lower_email )
2019-03-30 00:03:33 +08:00
topic_allowed_users . create! ( user_id : user . id ) unless topic_allowed_users . exists? ( user_id : user . id )
2013-10-04 05:06:14 +08:00
end
2013-05-26 08:37:23 +08:00
def max_post_number
2014-08-21 00:28:34 +08:00
posts . with_deleted . maximum ( :post_number ) . to_i
2013-05-26 08:37:23 +08:00
end
2013-05-09 01:33:58 +08:00
def move_posts ( moved_by , post_ids , opts )
2018-12-31 19:47:22 +08:00
post_mover = PostMover . new ( self , moved_by , post_ids , move_to_pm : opts [ :archetype ] . present? && opts [ :archetype ] == " private_message " )
2013-05-09 01:33:58 +08:00
2013-05-26 08:40:33 +08:00
if opts [ :destination_topic_id ]
2018-12-31 19:47:22 +08:00
topic = post_mover . to_topic ( opts [ :destination_topic_id ] , participants : opts [ :participants ] )
2018-07-10 09:48:57 +08:00
DiscourseEvent . trigger ( :topic_merged ,
post_mover . original_topic ,
post_mover . destination_topic
)
topic
2013-05-26 08:40:33 +08:00
elsif opts [ :title ]
2018-07-07 00:21:32 +08:00
post_mover . to_new_topic ( opts [ :title ] , opts [ :category_id ] , opts [ :tags ] )
2013-02-06 03:16:51 +08:00
end
end
2013-03-13 00:33:42 +08:00
# 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.
def update_statistics
feature_topic_users
update_action_counts
Topic . reset_highest ( id )
end
def update_action_counts
2020-07-13 14:30:00 +08:00
update_column (
:like_count ,
Post
. where . not ( post_type : Post . types [ :whisper ] )
. where ( topic_id : id )
. sum ( :like_count )
)
2013-03-13 00:33:42 +08:00
end
2017-02-18 06:54:43 +08:00
def posters_summary ( options = { } ) # avatar lookup in options
2013-05-23 14:21:19 +08:00
@posters_summary || = TopicPostersSummary . new ( self , options ) . summary
2013-02-06 03:16:51 +08:00
end
2014-05-12 15:32:49 +08:00
def participants_summary ( options = { } )
@participants_summary || = TopicParticipantsSummary . new ( self , options ) . summary
end
2014-06-17 01:21:21 +08:00
def make_banner! ( user )
# only one banner at the same time
previous_banner = Topic . where ( archetype : Archetype . banner ) . first
previous_banner . remove_banner! ( user ) if previous_banner . present?
2017-02-04 04:07:38 +08:00
UserProfile . where ( " dismissed_banner_key IS NOT NULL " )
. update_all ( dismissed_banner_key : nil )
2014-06-17 01:21:21 +08:00
self . archetype = Archetype . banner
2017-03-17 05:31:27 +08:00
self . add_small_action ( user , " banner.enabled " )
2014-06-17 01:21:21 +08:00
self . save
2014-06-19 02:04:10 +08:00
2015-05-04 10:21:00 +08:00
MessageBus . publish ( '/site/banner' , banner )
2014-06-17 01:21:21 +08:00
end
def remove_banner! ( user )
self . archetype = Archetype . default
2017-03-17 05:31:27 +08:00
self . add_small_action ( user , " banner.disabled " )
2014-06-17 01:21:21 +08:00
self . save
2014-06-19 02:04:10 +08:00
2015-05-04 10:21:00 +08:00
MessageBus . publish ( '/site/banner' , nil )
2014-06-19 02:04:10 +08:00
end
def banner
2017-02-24 19:56:13 +08:00
post = self . ordered_posts . first
2014-06-19 02:04:10 +08:00
{
html : post . cooked ,
2015-06-10 01:31:14 +08:00
key : self . id ,
url : self . url
2014-06-19 02:04:10 +08:00
}
2014-06-17 01:21:21 +08:00
end
2013-06-08 02:17:12 +08:00
# Even if the slug column in the database is null, topic.slug will return something:
2013-02-06 03:16:51 +08:00
def slug
2013-04-24 10:46:43 +08:00
unless slug = read_attribute ( :slug )
return '' unless title . present?
2015-05-04 19:48:37 +08:00
slug = Slug . for ( title )
2013-04-24 10:46:43 +08:00
if new_record?
write_attribute ( :slug , slug )
else
update_column ( :slug , slug )
end
end
slug
end
2019-10-17 04:08:43 +08:00
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
2013-04-24 10:46:43 +08:00
def title = ( t )
2015-05-04 19:48:37 +08:00
slug = Slug . for ( t . to_s )
2013-04-24 10:46:43 +08:00
write_attribute ( :slug , slug )
2015-09-24 11:37:53 +08:00
write_attribute ( :fancy_title , nil )
2013-04-24 10:46:43 +08:00
write_attribute ( :title , t )
2013-02-06 03:16:51 +08:00
end
2013-05-24 14:06:38 +08:00
# NOTE: These are probably better off somewhere else.
# Having a model know about URLs seems a bit strange.
2013-02-06 03:16:51 +08:00
def last_post_url
2020-10-09 19:51:24 +08:00
" #{ Discourse . base_path } /t/ #{ slug } / #{ id } / #{ posts_count } "
2013-02-06 03:16:51 +08:00
end
2013-05-09 15:37:34 +08:00
def self . url ( id , slug , post_number = nil )
2019-05-13 20:51:45 +08:00
url = + " #{ Discourse . base_url } /t/ #{ slug } / #{ id } "
2013-05-09 15:37:34 +08:00
url << " / #{ post_number } " if post_number . to_i > 1
url
end
2013-05-26 08:38:15 +08:00
def url ( post_number = nil )
self . class . url id , slug , post_number
end
2015-09-28 14:43:38 +08:00
def self . relative_url ( id , slug , post_number = nil )
2020-10-09 19:51:24 +08:00
url = + " #{ Discourse . base_path } /t/ "
2017-04-25 03:26:06 +08:00
url << " #{ slug } / " if slug . present?
url << id . to_s
2013-05-09 15:37:34 +08:00
url << " / #{ post_number } " if post_number . to_i > 1
2013-02-06 03:16:51 +08:00
url
end
2017-04-25 03:26:06 +08:00
def slugless_url ( post_number = nil )
Topic . relative_url ( id , nil , post_number )
end
2015-09-28 14:43:38 +08:00
def relative_url ( post_number = nil )
Topic . relative_url ( id , slug , post_number )
end
2013-03-07 04:17:07 +08:00
def clear_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : Time . now )
end
2014-04-10 08:56:56 +08:00
def re_pin_for ( user )
return unless user . present?
TopicUser . change ( user . id , id , cleared_pinned_at : nil )
end
2018-03-28 16:20:08 +08:00
def update_pinned ( status , global = false , pinned_until = " " )
pinned_until || = ''
pinned_until = begin
Time . parse ( pinned_until )
rescue ArgumentError
end
2015-07-29 22:34:21 +08:00
update_columns (
2018-03-28 16:20:08 +08:00
pinned_at : status ? Time . zone . now : nil ,
2015-07-29 22:34:21 +08:00
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
2013-02-06 03:16:51 +08:00
end
def draft_key
2013-03-01 02:54:12 +08:00
" #{ Draft :: EXISTING_TOPIC } #{ id } "
2013-02-06 03:16:51 +08:00
end
2013-05-24 14:06:38 +08:00
def notifier
@topic_notifier || = TopicNotifier . new ( self )
end
def muted? ( user )
if user && user . id
notifier . muted? ( user . id )
end
end
2015-07-29 22:34:21 +08:00
def self . ensure_consistency!
# unpin topics that might have been missed
Topic . where ( " pinned_until < now() " ) . update_all ( pinned_at : nil , pinned_globally : false , pinned_until : nil )
end
2021-02-17 05:51:39 +08:00
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
2017-05-17 02:49:42 +08:00
def public_topic_timer
2017-05-17 09:37:11 +08:00
@public_topic_timer || = topic_timers . find_by ( deleted_at : nil , public_type : true )
2013-11-11 07:52:44 +08:00
end
2017-08-22 14:22:48 +08:00
def delete_topic_timer ( status_type , by_user : Discourse . system_user )
options = { status_type : status_type }
options . merge! ( user : by_user ) unless TopicTimer . public_types [ status_type ]
self . topic_timers . find_by ( options ) & . trash! ( by_user )
2021-02-17 05:51:39 +08:00
@public_topic_timer = nil
2017-08-22 14:52:16 +08:00
nil
2017-08-22 14:22:48 +08:00
end
2017-03-22 11:12:02 +08:00
# 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.
2013-11-27 08:06:20 +08:00
# * A timestamp with timezone in JSON format. (e.g., "2013-11-26T21:00:00.000Z")
2017-03-22 11:12:02 +08:00
# * `nil` to delete the topic's status update.
2015-05-28 00:22:34 +08:00
# Options:
2017-03-22 11:12:02 +08:00
# * by_user: User who is setting the topic's status update.
2017-04-03 17:28:41 +08:00
# * 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.
2021-02-05 08:12:56 +08:00
# * duration: TODO(2021-06-01): DEPRECATED - do not use
# * 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.
def set_or_create_timer ( status_type , time , by_user : nil , based_on_last_post : false , category_id : SiteSetting . uncategorized_category_id , duration : nil , duration_minutes : nil , silent : nil )
return delete_topic_timer ( status_type , by_user : by_user ) if time . blank? && duration_minutes . blank? && duration . blank?
duration_minutes = duration_minutes ? duration_minutes . to_i : 0
# TODO(2021-06-01): deprecated - remove this when plugins calling set_or_create_timer
# have been fixed to use duration_minutes
duration = duration ? duration . to_i : 0
2017-08-22 14:22:48 +08:00
2017-10-04 16:31:40 +08:00
public_topic_timer = ! ! TopicTimer . public_types [ status_type ]
topic_timer_options = { topic : self , public_type : public_topic_timer }
topic_timer_options . merge! ( user : by_user ) unless public_topic_timer
2020-12-03 07:43:19 +08:00
topic_timer_options . merge! ( silent : silent ) if silent
2017-05-17 09:37:11 +08:00
topic_timer = TopicTimer . find_or_initialize_by ( topic_timer_options )
2017-06-21 14:31:15 +08:00
topic_timer . status_type = status_type
2017-03-22 11:12:02 +08:00
time_now = Time . zone . now
2017-05-12 06:23:18 +08:00
topic_timer . based_on_last_post = ! based_on_last_post . blank?
2017-03-22 11:12:02 +08:00
2017-05-12 06:23:18 +08:00
if status_type == TopicTimer . types [ :publish_to_category ]
topic_timer . category = Category . find_by ( id : category_id )
2017-04-03 17:28:41 +08:00
end
2017-05-12 06:23:18 +08:00
if topic_timer . based_on_last_post
2021-02-05 08:12:56 +08:00
if duration > 0 || duration_minutes > 0
2017-03-31 20:27:46 +08:00
last_post_created_at = self . ordered_posts . last . present? ? self . ordered_posts . last . created_at : time_now
2021-02-05 08:12:56 +08:00
# TODO(2021-06-01): deprecated - remove this when plugins calling set_or_create_timer
# have been fixed to use duration_minutes
if duration > 0
duration_minutes = duration * 60
end
topic_timer . duration_minutes = duration_minutes
topic_timer . execute_at = last_post_created_at + duration_minutes . minutes
2017-05-12 06:23:18 +08:00
topic_timer . created_at = last_post_created_at
2014-10-11 00:21:44 +08:00
end
2020-03-19 23:36:31 +08:00
elsif topic_timer . status_type == TopicTimer . types [ :delete_replies ]
2021-02-05 08:12:56 +08:00
if duration > 0 || duration_minutes > 0
2020-03-19 23:36:31 +08:00
first_reply_created_at = ( self . ordered_posts . where ( " post_number > 1 " ) . minimum ( :created_at ) || time_now )
2021-02-05 08:12:56 +08:00
# TODO(2021-06-01): deprecated - remove this when plugins calling set_or_create_timer
# have been fixed to use duration_minutes
if duration > 0
duration_minutes = duration * 60 * 24
end
topic_timer . duration_minutes = duration_minutes
topic_timer . execute_at = first_reply_created_at + duration_minutes . minutes
2020-03-19 23:36:31 +08:00
topic_timer . created_at = first_reply_created_at
end
2013-11-27 08:06:20 +08:00
else
2015-05-28 00:22:34 +08:00
utc = Time . find_zone ( " UTC " )
2018-03-26 11:32:52 +08:00
is_float = ( Float ( time ) rescue nil )
2017-03-22 11:12:02 +08:00
2018-03-26 11:32:52 +08:00
if is_float
num_hours = time . to_f
topic_timer . execute_at = num_hours . hours . from_now if num_hours > 0
else
timestamp = utc . parse ( time )
2020-12-09 01:13:45 +08:00
raise Discourse :: InvalidParameters unless timestamp && timestamp > utc . now
2015-05-28 00:22:34 +08:00
# a timestamp in client's time zone, like "2015-5-27 12:00"
2017-05-12 06:23:18 +08:00
topic_timer . execute_at = timestamp
2014-10-11 00:21:44 +08:00
end
2013-11-27 08:06:20 +08:00
end
2017-05-12 06:23:18 +08:00
if topic_timer . execute_at
2017-03-22 11:12:02 +08:00
if by_user & . staff? || by_user & . trust_level == TrustLevel [ 4 ]
2017-05-12 06:23:18 +08:00
topic_timer . user = by_user
2014-10-11 00:21:44 +08:00
else
2017-05-12 06:23:18 +08:00
topic_timer . user || = ( self . user . staff? || self . user . trust_level == TrustLevel [ 4 ] ? self . user : Discourse . system_user )
2014-10-11 00:21:44 +08:00
end
2017-03-22 11:12:02 +08:00
if self . persisted?
2021-01-19 11:30:58 +08:00
# See TopicTimer.after_save for additional context; the topic
# status may be changed by saving.
2017-05-12 06:23:18 +08:00
topic_timer . save!
2013-06-07 05:04:10 +08:00
else
2017-05-12 06:23:18 +08:00
self . topic_timers << topic_timer
2015-12-08 20:43:23 +08:00
end
2017-05-12 06:23:18 +08:00
topic_timer
2013-06-07 05:04:10 +08:00
end
2013-05-08 02:25:41 +08:00
end
2013-07-14 09:24:16 +08:00
def read_restricted_category?
category && category . read_restricted
2013-05-20 14:04:53 +08:00
end
2013-07-09 03:23:20 +08:00
2013-12-12 10:41:34 +08:00
def acting_user
@acting_user || user
end
def acting_user = ( u )
@acting_user = u
end
2014-03-24 09:19:08 +08:00
def secure_group_ids
@secure_group_ids || = if self . category && self . category . read_restricted?
self . category . secure_group_ids
end
end
2014-04-02 03:29:15 +08:00
def has_topic_embed?
TopicEmbed . where ( topic_id : id ) . exists?
end
def expandable_first_post?
2015-08-19 05:15:46 +08:00
SiteSetting . embed_truncate? && has_topic_embed?
2014-04-02 03:29:15 +08:00
end
2015-12-30 10:26:21 +08:00
def message_archived? ( user )
return false unless user && user . id
2018-04-05 15:17:31 +08:00
# 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
2018-06-19 14:13:14 +08:00
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
)
2018-04-05 15:17:31 +08:00
2018-06-19 14:13:14 +08:00
UNION ALL
2018-04-05 15:17:31 +08:00
2018-06-19 14:13:14 +08:00
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
2015-12-30 10:26:21 +08:00
2018-06-19 14:13:14 +08:00
DB . exec ( sql , user_id : user . id , topic_id : id ) > 0
2015-12-30 10:26:21 +08:00
end
2015-06-23 01:46:51 +08:00
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
2015-06-24 21:19:39 +08:00
def self . time_to_first_response ( sql , opts = nil )
opts || = { }
2018-06-20 15:48:02 +08:00
builder = DB . build ( sql )
2015-06-24 21:19:39 +08:00
builder . where ( " t.created_at >= :start_date " , start_date : opts [ :start_date ] ) if opts [ :start_date ]
2015-06-26 06:45:11 +08:00
builder . where ( " t.created_at < :end_date " , end_date : opts [ :end_date ] ) if opts [ :end_date ]
2020-04-22 16:52:50 +08:00
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
2015-06-23 01:46:51 +08:00
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
builder . where ( " p.deleted_at IS NULL " )
builder . where ( " p.post_number > 1 " )
2015-06-26 06:45:11 +08:00
builder . where ( " p.user_id != t.user_id " )
2015-09-15 01:36:41 +08:00
builder . where ( " p.user_id in (:user_ids) " , user_ids : opts [ :user_ids ] ) if opts [ :user_ids ]
2017-02-03 06:27:41 +08:00
builder . where ( " p.post_type = :post_type " , post_type : Post . types [ :regular ] )
2015-06-23 01:46:51 +08:00
builder . where ( " EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0 " )
2018-06-20 15:48:02 +08:00
builder . query_hash
2015-06-23 01:46:51 +08:00
end
2015-09-15 01:36:41 +08:00
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 ) )
2015-06-23 01:46:51 +08:00
end
2015-06-24 21:19:39 +08:00
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 )
2015-06-23 01:46:51 +08:00
end
2015-06-26 06:45:11 +08:00
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
2017-02-03 06:27:41 +08:00
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]}
2015-06-26 06:45:11 +08:00
/ *where* /
GROUP BY t . id
) tt
2017-02-03 06:27:41 +08:00
WHERE tt . first_reply IS NULL OR tt . first_reply < 2
2015-06-26 06:45:11 +08:00
GROUP BY tt . created_at
ORDER BY tt . created_at
SQL
2020-04-22 16:52:50 +08:00
def self . with_no_response_per_day ( start_date , end_date , category_id = nil , include_subcategories = nil )
2018-06-20 15:48:02 +08:00
builder = DB . build ( WITH_NO_RESPONSE_SQL )
2015-06-26 06:45:11 +08:00
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
2020-04-22 16:52:50 +08:00
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
2015-06-26 06:45:11 +08:00
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
2018-06-20 15:48:02 +08:00
builder . query_hash
2015-06-23 01:46:51 +08:00
end
2015-06-26 06:45:11 +08:00
WITH_NO_RESPONSE_TOTAL_SQL || = <<-SQL
SELECT COUNT ( * ) as count
FROM (
SELECT t . id , MIN ( p . post_number ) first_reply
FROM topics t
2017-02-03 06:27:41 +08:00
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]}
2015-06-26 06:45:11 +08:00
/ *where* /
GROUP BY t . id
) tt
2017-02-03 06:27:41 +08:00
WHERE tt . first_reply IS NULL OR tt . first_reply < 2
2015-06-26 06:45:11 +08:00
SQL
def self . with_no_response_total ( opts = { } )
2018-06-20 15:48:02 +08:00
builder = DB . build ( WITH_NO_RESPONSE_TOTAL_SQL )
2020-04-22 16:52:50 +08:00
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
2015-06-26 06:45:11 +08:00
builder . where ( " t.archetype <> ' #{ Archetype . private_message } ' " )
builder . where ( " t.deleted_at IS NULL " )
2018-06-20 15:48:02 +08:00
builder . query_single . first . to_i
2015-06-23 01:46:51 +08:00
end
2019-07-19 23:52:50 +08:00
def convert_to_public_topic ( user , category_id : nil )
public_topic = TopicConverter . new ( self , user ) . convert_to_public_topic ( category_id )
2016-05-01 19:48:43 +08:00
add_small_action ( user , " public_topic " ) if public_topic
public_topic
end
def convert_to_private_message ( user )
private_topic = TopicConverter . new ( self , user ) . convert_to_private_message
add_small_action ( user , " private_topic " ) if private_topic
private_topic
end
2020-05-23 12:56:13 +08:00
def update_excerpt ( excerpt )
update_column ( :excerpt , excerpt )
if archetype == " banner "
ApplicationController . banner_json_cache . clear
end
end
2017-04-27 11:53:53 +08:00
def pm_with_non_human_user?
2017-09-12 14:05:25 +08:00
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
2018-06-19 14:13:14 +08:00
result = DB . exec ( sql , private_message : Archetype . private_message , topic_id : self . id )
result != 0
2017-04-27 11:53:53 +08:00
end
2017-11-29 21:52:41 +08:00
def featured_link_root_domain
2019-12-12 10:49:21 +08:00
MiniSuffix . domain ( UrlHelper . encode_and_parse ( self . featured_link ) . hostname )
2017-11-29 21:52:41 +08:00
end
2018-03-27 16:30:08 +08:00
def self . private_message_topics_count_per_day ( start_date , end_date , topic_subtype )
2018-06-05 15:29:17 +08:00
private_messages
. 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
2018-03-27 16:30:08 +08:00
end
2018-05-24 16:41:51 +08:00
def is_category_topic?
@is_category_topic || = Category . exists? ( topic_id : self . id . to_i )
end
2018-08-10 08:51:03 +08:00
def reset_bumped_at
post = ordered_posts . where (
user_deleted : false ,
hidden : false ,
2018-11-15 01:56:22 +08:00
post_type : Post . types [ :regular ]
2018-12-22 00:37:32 +08:00
) . last || first_post
2018-08-10 08:51:03 +08:00
2019-11-27 01:42:47 +08:00
self . bumped_at = post . created_at
self . save ( validate : false )
2018-08-10 08:51:03 +08:00
end
2019-01-04 01:03:01 +08:00
def auto_close_threshold_reached?
return if user & . staff?
scores = ReviewableScore . pending
. joins ( :reviewable )
2020-04-08 21:44:31 +08:00
. 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)' )
2019-01-04 01:03:01 +08:00
. first
2019-05-25 02:13:03 +08:00
scores [ 0 ] > = SiteSetting . num_flaggers_to_close_topic && scores [ 1 ] > = Reviewable . score_to_auto_close_topic
2019-01-04 01:03:01 +08:00
end
2019-04-16 15:16:23 +08:00
def update_category_topic_count_by ( num )
if category_id . present?
Category
2021-02-16 23:45:12 +08:00
. where ( 'id = ?' , category_id )
. where ( 'topic_id != ? OR topic_id IS NULL' , self . id )
. update_all ( " topic_count = topic_count + #{ num . to_i } " )
2019-04-16 15:16:23 +08:00
end
end
2019-07-04 16:12:39 +08:00
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
2021-01-15 08:54:46 +08:00
def incoming_email_addresses ( group : nil , received_before : Time . zone . now )
email_addresses = Set . new
# TODO(martin) Look at improving this N1, it will just get slower the
# more replies/incoming emails there are for the topic.
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.
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
2013-07-09 03:23:20 +08:00
private
2018-12-05 23:43:07 +08:00
def invite_to_private_message ( invited_by , target_user , guardian )
if ! guardian . can_send_private_message? ( target_user )
raise UserExists . new ( I18n . t (
" activerecord.errors.models.topic.attributes.base.cant_send_pm "
) )
end
Topic . transaction do
rate_limit_topic_invitation ( invited_by )
2019-03-30 00:03:33 +08:00
topic_allowed_users . create! ( user_id : target_user . id ) unless topic_allowed_users . exists? ( user_id : target_user . id )
2020-02-27 20:45:20 +08:00
user_in_allowed_group = ( user . group_ids & topic_allowed_groups . map ( & :group_id ) ) . present?
add_small_action ( invited_by , " invited_user " , target_user . username ) if ! user_in_allowed_group
2018-12-05 23:43:07 +08:00
create_invite_notification! (
target_user ,
Notification . types [ :invited_to_private_message ] ,
invited_by . username
)
end
end
def invite_to_topic ( invited_by , target_user , group_ids , guardian )
Topic . transaction do
rate_limit_topic_invitation ( invited_by )
if group_ids
(
self . category . groups . where ( id : group_ids ) . where ( automatic : false ) -
target_user . groups . where ( automatic : false )
) . each do | group |
if guardian . can_edit_group? ( group )
group . add ( target_user )
GroupActionLogger
. new ( invited_by , group )
. log_add_user_to_group ( target_user )
end
end
end
if Guardian . new ( target_user ) . can_see_topic? ( self )
create_invite_notification! (
target_user ,
Notification . types [ :invited_to_topic ] ,
invited_by . username
)
end
end
end
2013-10-16 17:28:18 +08:00
def limit_first_day_topics_per_day
apply_per_day_rate_limit_for ( " first-day-topics " , :max_topics_in_first_day )
end
def apply_per_day_rate_limit_for ( key , method_name )
2019-05-07 09:00:09 +08:00
RateLimiter . new ( user , " #{ key } -per-day " , SiteSetting . get ( method_name ) , 1 . day . to_i )
2013-10-16 17:28:18 +08:00
end
2013-08-14 12:14:56 +08:00
2018-02-26 13:19:52 +08:00
def create_invite_notification! ( target_user , notification_type , username )
target_user . notifications . create! (
2018-02-26 10:42:06 +08:00
notification_type : notification_type ,
topic_id : self . id ,
post_number : 1 ,
data : {
topic_title : self . title ,
display_username : username
} . to_json
)
end
2018-03-01 12:41:36 +08:00
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!
true
end
2013-02-06 03:16:51 +08:00
end
2013-05-24 10:48:32 +08:00
# == Schema Information
#
# Table name: topics
#
2017-11-24 04:55:44 +08:00
# id :integer not null, primary key
2019-01-12 03:29:56 +08:00
# title :string not null
2017-11-24 04:55:44 +08:00
# last_posted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# views :integer default(0), not null
# posts_count :integer default(0), not null
# user_id :integer
# last_post_user_id :integer not null
# reply_count :integer default(0), not null
# featured_user1_id :integer
# featured_user2_id :integer
# featured_user3_id :integer
# deleted_at :datetime
# highest_post_number :integer default(0), not null
# like_count :integer default(0), not null
# incoming_link_count :integer default(0), not null
# category_id :integer
# visible :boolean default(TRUE), not null
# moderator_posts_count :integer default(0), not null
# closed :boolean default(FALSE), not null
# archived :boolean default(FALSE), not null
# bumped_at :datetime not null
# has_summary :boolean default(FALSE), not null
2019-01-12 03:29:56 +08:00
# archetype :string default("regular"), not null
2017-11-24 04:55:44 +08:00
# featured_user4_id :integer
# notify_moderators_count :integer default(0), not null
# spam_count :integer default(0), not null
# pinned_at :datetime
# score :float
2019-01-04 01:03:01 +08:00
# percent_rank :float default(1.0), not null
2019-01-12 03:29:56 +08:00
# subtype :string
# slug :string
2017-11-24 04:55:44 +08:00
# deleted_by_id :integer
# participant_count :integer default(1)
# word_count :integer
# excerpt :string(1000)
# pinned_globally :boolean default(FALSE), not null
# pinned_until :datetime
# fancy_title :string(400)
# highest_staff_post_number :integer default(0), not null
# featured_link :string
2019-01-04 01:03:01 +08:00
# reviewable_score :float default(0.0), not null
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
# image_upload_id :bigint
2020-10-28 02:12:33 +08:00
# slow_mode_seconds :integer default(0), not null
2013-05-24 10:48:32 +08:00
#
# Indexes
#
2015-09-18 08:41:10 +08:00
# idx_topics_front_page (deleted_at,visible,archetype,category_id,id)
2018-07-16 14:18:07 +08:00
# idx_topics_user_id_deleted_at (user_id) WHERE (deleted_at IS NULL)
# idxtopicslug (slug) WHERE ((deleted_at IS NULL) AND (slug IS NOT NULL))
2020-10-28 02:12:33 +08:00
# index_forum_threads_on_bumped_at (bumped_at)
2018-07-16 14:18:07 +08:00
# index_topics_on_created_at_and_visible (created_at,visible) WHERE ((deleted_at IS NULL) AND ((archetype)::text <> 'private_message'::text))
2015-09-18 08:41:10 +08:00
# index_topics_on_id_and_deleted_at (id,deleted_at)
2019-11-01 08:21:57 +08:00
# index_topics_on_id_filtered_banner (id) UNIQUE WHERE (((archetype)::text = 'banner'::text) AND (deleted_at IS NULL))
FEATURE: Include optimized thumbnails for topics (#9215)
This introduces new APIs for obtaining optimized thumbnails for topics. There are a few building blocks required for this:
- Introduces new `image_upload_id` columns on the `posts` and `topics` table. This replaces the old `image_url` column, which means that thumbnails are now restricted to uploads. Hotlinked thumbnails are no longer possible. In normal use (with pull_hotlinked_images enabled), this has no noticeable impact
- A migration attempts to match existing urls to upload records. If a match cannot be found then the posts will be queued for rebake
- Optimized thumbnails are generated during post_process_cooked. If thumbnails are missing when serializing a topic list, then a sidekiq job is queued
- Topic lists and topics now include a `thumbnails` key, which includes all the available images:
```
"thumbnails": [
{
"max_width": null,
"max_height": null,
"url": "//example.com/original-image.png",
"width": 1380,
"height": 1840
},
{
"max_width": 1024,
"max_height": 1024,
"url": "//example.com/optimized-image.png",
"width": 768,
"height": 1024
}
]
```
- Themes can request additional thumbnail sizes by using a modifier in their `about.json` file:
```
"modifiers": {
"topic_thumbnail_sizes": [
[200, 200],
[800, 800]
],
...
```
Remember that these are generated asynchronously, so your theme should include logic to fallback to other available thumbnails if your requested size has not yet been generated
- Two new raw plugin outlets are introduced, to improve the customisability of the topic list. `topic-list-before-columns` and `topic-list-before-link`
2020-05-05 16:07:50 +08:00
# index_topics_on_image_upload_id (image_upload_id)
2017-10-06 11:13:01 +08:00
# index_topics_on_lower_title (lower((title)::text))
2018-07-16 14:18:07 +08:00
# index_topics_on_pinned_at (pinned_at) WHERE (pinned_at IS NOT NULL)
# index_topics_on_pinned_globally (pinned_globally) WHERE pinned_globally
2020-10-28 02:12:33 +08:00
# index_topics_on_timestamps_private (bumped_at,created_at,updated_at) WHERE ((deleted_at IS NULL) AND ((archetype)::text = 'private_message'::text))
2019-04-05 17:13:12 +08:00
# index_topics_on_updated_at_public (updated_at,visible,highest_staff_post_number,highest_post_number,category_id,created_at,id) WHERE (((archetype)::text <> 'private_message'::text) AND (deleted_at IS NULL))
2013-05-24 10:48:32 +08:00
#