DText is processed in three phases: a preprocessing phase, the regular parsing phases, and a postprocessing phase. In the preprocessing phase we extract all the wiki links from all the dtext messages on the page (more precisely, we do this in forum threads and on comment pages, because these are the main places with lots of dtext). This is so we can lookup all the tags and wiki pages in one query, which is necessary because in the worst case (in certain forum threads and in certain list_of_* wiki pages) there can be hundreds of tags per page. In the postprocessing phase we fixup the html generated by the ragel parser to add CSS classes to wiki links. We do this in a postprocessing step because it's easier than doing it in the ragel parser itself.
217 lines
5.7 KiB
Ruby
217 lines
5.7 KiB
Ruby
class WikiPage < ApplicationRecord
|
|
class RevertError < Exception ; end
|
|
|
|
META_WIKIS = ["list_of_", "tag_group:", "pool_group:", "howto:", "about:", "help:", "template:"]
|
|
|
|
before_save :normalize_title
|
|
before_save :normalize_other_names
|
|
after_save :create_version
|
|
validates_uniqueness_of :title, :case_sensitive => false
|
|
validates_presence_of :title
|
|
validates_presence_of :body, :unless => -> { is_deleted? || other_names.present? }
|
|
validate :validate_rename
|
|
validate :validate_not_locked
|
|
|
|
attr_accessor :skip_secondary_validations
|
|
array_attribute :other_names
|
|
belongs_to_creator
|
|
belongs_to_updater
|
|
has_one :tag, :foreign_key => "name", :primary_key => "title"
|
|
has_one :artist, -> {where(:is_active => true)}, :foreign_key => "name", :primary_key => "title"
|
|
has_many :versions, -> {order("wiki_page_versions.id ASC")}, :class_name => "WikiPageVersion", :dependent => :destroy
|
|
|
|
api_attributes including: [:creator_name, :category_name]
|
|
|
|
module SearchMethods
|
|
def titled(title)
|
|
where(title: normalize_title(title))
|
|
end
|
|
|
|
def active
|
|
where("is_deleted = false")
|
|
end
|
|
|
|
def recent
|
|
order("updated_at DESC").limit(25)
|
|
end
|
|
|
|
def other_names_include(name)
|
|
name = normalize_other_name(name).downcase
|
|
subquery = WikiPage.from("unnest(other_names) AS other_name").where("lower(other_name) = ?", name)
|
|
where(id: subquery)
|
|
end
|
|
|
|
def other_names_match(name)
|
|
if name =~ /\*/
|
|
subquery = WikiPage.from("unnest(other_names) AS other_name").where("other_name ILIKE ?", name.to_escaped_for_sql_like)
|
|
where(id: subquery)
|
|
else
|
|
other_names_include(name)
|
|
end
|
|
end
|
|
|
|
def default_order
|
|
order(updated_at: :desc)
|
|
end
|
|
|
|
def search(params = {})
|
|
q = super
|
|
|
|
q = q.search_attributes(params, :creator, :updater, :is_locked, :is_deleted, :body)
|
|
q = q.text_attribute_matches(:body, params[:body_matches], index_column: :body_index, ts_config: "danbooru")
|
|
|
|
if params[:title].present?
|
|
q = q.where_like(:title, normalize_title(params[:title]))
|
|
end
|
|
|
|
if params[:other_names_match].present?
|
|
q = q.other_names_match(params[:other_names_match])
|
|
end
|
|
|
|
if params[:hide_deleted].to_s.truthy?
|
|
q = q.where("is_deleted = false")
|
|
end
|
|
|
|
if params[:other_names_present].to_s.truthy?
|
|
q = q.where("other_names is not null and other_names != '{}'")
|
|
elsif params[:other_names_present].to_s.falsy?
|
|
q = q.where("other_names is null or other_names = '{}'")
|
|
end
|
|
|
|
params[:order] ||= params.delete(:sort)
|
|
case params[:order]
|
|
when "title"
|
|
q = q.order("title")
|
|
when "post_count"
|
|
q = q.includes(:tag).order("tags.post_count desc nulls last").references(:tags)
|
|
else
|
|
q = q.apply_default_order(params)
|
|
end
|
|
|
|
q
|
|
end
|
|
end
|
|
|
|
def creator_name
|
|
creator.name
|
|
end
|
|
|
|
extend SearchMethods
|
|
|
|
def validate_not_locked
|
|
if is_locked? && !CurrentUser.is_builder?
|
|
errors.add(:is_locked, "and cannot be updated")
|
|
end
|
|
end
|
|
|
|
def validate_rename
|
|
return if !will_save_change_to_title? || skip_secondary_validations
|
|
|
|
tag_was = Tag.find_by_name(Tag.normalize_name(title_was))
|
|
if tag_was.present? && tag_was.post_count > 0
|
|
errors.add(:title, "cannot be changed: '#{tag_was.name}' still has #{tag_was.post_count} posts. Move the posts and update any wikis linking to this page first.")
|
|
end
|
|
end
|
|
|
|
def revert_to(version)
|
|
if id != version.wiki_page_id
|
|
raise RevertError.new("You cannot revert to a previous version of another wiki page.")
|
|
end
|
|
|
|
self.title = version.title
|
|
self.body = version.body
|
|
self.is_locked = version.is_locked
|
|
self.other_names = version.other_names
|
|
end
|
|
|
|
def revert_to!(version)
|
|
revert_to(version)
|
|
save!
|
|
end
|
|
|
|
def self.normalize_title(title)
|
|
title.downcase.gsub(/[[:space:]]+/, "_").gsub(/__/, "_").gsub(/\A_|_\z/, "")
|
|
end
|
|
|
|
def normalize_title
|
|
self.title = WikiPage.normalize_title(title)
|
|
end
|
|
|
|
def normalize_other_names
|
|
self.other_names = other_names.map { |name| WikiPage.normalize_other_name(name) }.uniq
|
|
end
|
|
|
|
def self.normalize_other_name(name)
|
|
name.unicode_normalize(:nfkc).gsub(/[[:space:]]+/, " ").strip.tr(" ", "_")
|
|
end
|
|
|
|
def skip_secondary_validations=(value)
|
|
@skip_secondary_validations = value.to_s.truthy?
|
|
end
|
|
|
|
def category_name
|
|
Tag.category_for(title)
|
|
end
|
|
|
|
def pretty_title
|
|
title.tr("_", " ")
|
|
end
|
|
|
|
def self.is_meta_wiki?(title)
|
|
title.starts_with?(*META_WIKIS)
|
|
end
|
|
|
|
def wiki_page_changed?
|
|
saved_change_to_title? || saved_change_to_body? || saved_change_to_is_locked? || saved_change_to_is_deleted? || saved_change_to_other_names?
|
|
end
|
|
|
|
def merge_version
|
|
prev = versions.last
|
|
prev.update(title: title, body: body, is_locked: is_locked, is_deleted: is_deleted, other_names: other_names)
|
|
end
|
|
|
|
def merge_version?
|
|
prev = versions.last
|
|
prev && prev.updater_id == CurrentUser.user.id && prev.updated_at > 1.hour.ago
|
|
end
|
|
|
|
def create_new_version
|
|
versions.create(
|
|
:updater_id => CurrentUser.user.id,
|
|
:updater_ip_addr => CurrentUser.ip_addr,
|
|
:title => title,
|
|
:body => body,
|
|
:is_locked => is_locked,
|
|
:is_deleted => is_deleted,
|
|
:other_names => other_names
|
|
)
|
|
end
|
|
|
|
def create_version
|
|
if wiki_page_changed?
|
|
if merge_version?
|
|
merge_version
|
|
else
|
|
create_new_version
|
|
end
|
|
end
|
|
end
|
|
|
|
def post_set
|
|
@post_set ||= PostSets::WikiPage.new(title, 1, 4)
|
|
end
|
|
|
|
def presenter
|
|
@presenter ||= WikiPagePresenter.new(self)
|
|
end
|
|
|
|
def tags
|
|
titles = DText.parse_wiki_titles(body)
|
|
Tag.nonempty.where(name: titles.uniq).pluck(:name)
|
|
end
|
|
|
|
def visible?
|
|
artist.blank? || !artist.is_banned? || CurrentUser.is_builder?
|
|
end
|
|
end
|