Files
danbooru/app/models/forum_topic.rb
evazion 56722df753 forum: delete posts when topic is deleted.
Fix it so that when a forum topic is deleted, all posts in the topic are
deleted too. Also make it so that when a forum topic is undeleted, all
posts in it are undeleted too.

Before when a topic was deleted, only the topic itself was marked as
deleted, not the posts inside the topic. This meant that when a spam
topic was deleted, the OP wouldn't be marked as deleted, so any
modreports against it wouldn't be marked as handled.

Also change it so that it's not possible to undelete a post in a deleted
topic, or to delete the OP of a topic without deleting the topic itself.

Finally, add a fix script to delete all active posts in deleted topics,
and to undelete all deleted OPs in active topics.
2022-01-21 22:35:20 -06:00

202 lines
6.1 KiB
Ruby

# frozen_string_literal: true
class ForumTopic < ApplicationRecord
CATEGORIES = {
0 => "General",
1 => "Tags",
2 => "Bugs & Features",
}
MIN_LEVELS = {
None: 0,
Moderator: User::Levels::MODERATOR,
Admin: User::Levels::ADMIN,
}
belongs_to :creator, class_name: "User"
belongs_to_updater
has_many :forum_posts, foreign_key: "topic_id", dependent: :destroy, inverse_of: :topic
has_many :forum_topic_visits
has_one :forum_topic_visit_by_current_user, -> { where(user_id: CurrentUser.id) }, class_name: "ForumTopicVisit"
has_one :original_post, -> { order(id: :asc) }, class_name: "ForumPost", foreign_key: "topic_id", inverse_of: :topic
has_many :bulk_update_requests
has_many :tag_aliases
has_many :tag_implications
validates :title, presence: true, length: { maximum: 200 }, if: :title_changed?
validates :category_id, inclusion: { in: CATEGORIES.keys }
validates :min_level, inclusion: { in: MIN_LEVELS.values }
accepts_nested_attributes_for :original_post
after_update :update_posts_on_deletion_or_undeletion
after_update :update_original_post
after_save(:if => ->(rec) {rec.is_locked? && rec.saved_change_to_is_locked?}) do |rec|
ModAction.log("locked forum topic ##{id} (title: #{title})", :forum_topic_lock)
end
deletable
scope :public_only, -> { where(min_level: MIN_LEVELS[:None]) }
scope :private_only, -> { where.not(min_level: MIN_LEVELS[:None]) }
scope :pending, -> { where(id: BulkUpdateRequest.has_topic.pending.select(:forum_topic_id)) }
scope :approved, -> { where(category_id: 1).where(id: BulkUpdateRequest.approved.has_topic.select(:forum_topic_id)).where.not(id: BulkUpdateRequest.has_topic.pending.or(BulkUpdateRequest.has_topic.rejected).select(:forum_topic_id)) }
scope :rejected, -> { where(category_id: 1).where(id: BulkUpdateRequest.rejected.has_topic.select(:forum_topic_id)).where.not(id: BulkUpdateRequest.has_topic.pending.or(BulkUpdateRequest.has_topic.approved).select(:forum_topic_id)) }
module CategoryMethods
extend ActiveSupport::Concern
module ClassMethods
def categories
CATEGORIES.values
end
def reverse_category_mapping
@reverse_category_mapping ||= CATEGORIES.invert
end
end
def category_name
CATEGORIES[category_id]
end
end
module SearchMethods
def visible(user)
where("min_level <= ?", user.level)
end
def read_by_user(user)
last_forum_read_at = user.last_forum_read_at || "2000-01-01".to_time
read_topics = user.visited_forum_topics.where("forum_topic_visits.last_read_at >= forum_topics.updated_at")
old_topics = where("? >= forum_topics.updated_at", last_forum_read_at)
where(id: read_topics).or(where(id: old_topics))
end
def unread_by_user(user)
where.not(id: ForumTopic.read_by_user(user))
end
def sticky_first
order(is_sticky: :desc, updated_at: :desc)
end
def default_order
order(updated_at: :desc)
end
def search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :is_sticky, :is_locked, :is_deleted, :category_id, :title, :response_count, :creator, :updater, :forum_posts, :bulk_update_requests, :tag_aliases, :tag_implications)
q = q.text_attribute_matches(:title, params[:title_matches])
if params[:is_private].to_s.truthy?
q = q.private_only
elsif params[:is_private].to_s.falsy?
q = q.public_only
end
case params[:status]
when "pending"
q = q.pending
when "approved"
q = q.approved
when "rejected"
q = q.rejected
end
if params[:is_read].to_s.truthy?
q = q.read_by_user(CurrentUser.user)
elsif params[:is_read].to_s.falsy?
q = q.unread_by_user(CurrentUser.user)
end
case params[:order]
when "sticky"
q = q.sticky_first
when "id"
q = q.order(id: :desc)
else
q = q.apply_default_order(params)
end
q
end
end
module VisitMethods
def mark_as_read!(user = CurrentUser.user)
return if user.is_anonymous?
visit = ForumTopicVisit.find_by(user_id: user.id, forum_topic_id: id)
if visit
visit.update!(last_read_at: updated_at)
else
ForumTopicVisit.create(:user_id => user.id, :forum_topic_id => id, :last_read_at => updated_at)
end
unread_topics = ForumTopic.visible(user).active.unread_by_user(user)
if !unread_topics.exists?
user.update!(last_forum_read_at: Time.zone.now)
ForumTopicVisit.prune!(user)
end
end
end
extend SearchMethods
include CategoryMethods
include VisitMethods
# XXX forum_topic_visit_by_current_user is a hack to reduce queries on the forum index.
def is_read?
return true if CurrentUser.is_anonymous?
return true if new_record?
topic_last_read_at = forum_topic_visit_by_current_user&.last_read_at || "2000-01-01".to_time
forum_last_read_at = CurrentUser.last_forum_read_at || "2000-01-01".to_time
(topic_last_read_at >= updated_at) || (forum_last_read_at >= updated_at)
end
def is_private?
min_level > MIN_LEVELS[:None]
end
def create_mod_action_for_delete
ModAction.log("deleted forum topic ##{id} (title: #{title})", :forum_topic_delete)
end
def create_mod_action_for_undelete
ModAction.log("undeleted forum topic ##{id} (title: #{title})", :forum_topic_undelete)
end
def page_for(post_id)
(forum_posts.where("id < ?", post_id).count / Danbooru.config.posts_per_page.to_f).ceil
end
def last_page
(response_count / Danbooru.config.posts_per_page.to_f).ceil
end
# Delete all posts when the topic is deleted. Undelete all posts when the topic is undeleted.
def update_posts_on_deletion_or_undeletion
if saved_change_to_is_deleted?
forum_posts.update!(is_deleted: is_deleted) # XXX depends on current user
end
end
def update_original_post
original_post&.update_columns(:updater_id => updater.id, :updated_at => Time.now)
end
def pretty_title
title.gsub(/\A\[APPROVED\]|\[REJECTED\]/, "")
end
def self.available_includes
[:creator, :updater, :original_post]
end
end