2024-02-25 12:15:55 -05:00
# frozen_string_literal: true
2017-06-14 22:27:53 -04:00
class WikiPage < ApplicationRecord
2025-02-26 08:02:53 -05:00
class RevertError < Exception ; end
2016-10-10 06:24:49 -04:00
2023-06-12 10:08:52 -04:00
before_validation :normalize_title
before_validation :normalize_other_names
2024-07-26 15:43:45 -04:00
before_validation :normalize_parent
2025-02-26 08:02:53 -05:00
before_save :log_changes
before_save :update_tag , if : :tag_changed?
before_destroy :validate_not_used_as_help_page
before_destroy :log_destroy
2010-02-15 17:32:32 -05:00
after_save :create_version
2025-02-26 08:02:53 -05:00
after_save :update_help_page , if : :saved_change_to_title?
2024-11-10 23:22:40 -05:00
normalizes :body , with : - > ( body ) { body . gsub ( " \r \n " , " \n " ) }
2025-02-26 08:02:53 -05:00
validates :title , uniqueness : { case_sensitive : false }
2023-11-13 10:55:40 -05:00
validates :title , presence : true
validates :title , tag_name : true , if : :title_changed?
2024-07-26 15:43:45 -04:00
validates :body , presence : { unless : - > { is_deleted? || other_names . present? || parent . present? } }
2019-09-05 08:59:51 -04:00
validates :title , length : { minimum : 1 , maximum : 100 }
2021-10-31 23:30:37 -04:00
validates :body , length : { maximum : Danbooru . config . wiki_page_max_size }
2023-06-11 05:40:11 -04:00
validate :user_not_limited
2017-04-08 03:23:42 -04:00
validate :validate_rename
2024-07-26 15:43:45 -04:00
validate :validate_redirect
2013-09-12 18:20:20 -04:00
validate :validate_not_locked
2018-11-13 19:08:20 -05:00
2019-11-15 23:00:54 -05:00
attr_accessor :skip_secondary_validations , :edit_reason
2025-02-26 08:02:53 -05:00
2018-11-13 19:08:20 -05:00
array_attribute :other_names
belongs_to_creator
belongs_to_updater
2024-07-20 14:45:36 -04:00
has_one :tag , foreign_key : " name " , primary_key : " title "
has_one :artist , foreign_key : " name " , primary_key : " title "
has_many :versions , - > { order ( " wiki_page_versions.id ASC " ) } , class_name : " WikiPageVersion " , dependent : :destroy
2024-07-25 13:42:42 -04:00
has_one :help_page , foreign_key : " wiki_page " , primary_key : " title "
2023-04-10 12:00:13 -04:00
2019-04-17 22:13:26 -04:00
def log_changes
2021-03-20 14:51:16 -04:00
if title_changed? && ! new_record?
2025-02-26 08:02:53 -05:00
ModAction . log ( :wiki_page_rename , { new_title : title , old_title : title_was } )
2019-04-17 22:13:26 -04:00
end
if is_locked_changed?
2025-02-26 08:02:53 -05:00
ModAction . log ( is_locked ? :wiki_page_lock : :wiki_page_unlock , { wiki_page : title } )
2019-04-17 22:13:26 -04:00
end
end
2025-02-26 08:02:53 -05:00
def log_destroy
ModAction . log ( :wiki_page_delete , { wiki_page : title , wiki_page_id : id } )
end
2013-01-10 17:45:52 -05:00
module SearchMethods
def titled ( title )
2024-09-02 10:40:41 -04:00
find_by ( title : WikiPage . normalize_name ( title ) )
2010-02-15 17:32:32 -05:00
end
2013-03-19 08:10:10 -04:00
2016-10-18 18:17:50 -04:00
def active
where ( " is_deleted = false " )
end
2013-01-10 17:45:52 -05:00
def recent
order ( " updated_at DESC " ) . limit ( 25 )
end
2013-03-19 08:10:10 -04:00
2018-11-13 19:08:20 -05:00
def other_names_include ( name )
2018-12-16 12:27:04 -05:00
name = normalize_other_name ( name ) . downcase
subquery = WikiPage . from ( " unnest(other_names) AS other_name " ) . where ( " lower(other_name) = ? " , name )
where ( id : subquery )
2014-05-22 20:07:15 -04:00
end
2017-05-23 21:59:48 -04:00
def other_names_match ( name )
if name =~ / \ * /
2018-11-13 19:08:20 -05:00
subquery = WikiPage . from ( " unnest(other_names) AS other_name " ) . where ( " other_name ILIKE ? " , name . to_escaped_for_sql_like )
2017-05-23 21:59:48 -04:00
where ( id : subquery )
else
2018-11-13 19:08:20 -05:00
other_names_include ( name )
2017-05-23 21:59:48 -04:00
end
end
2018-01-28 23:46:18 -05:00
def default_order
order ( updated_at : :desc )
end
2023-06-02 10:15:17 -04:00
def search ( params )
2017-12-17 17:58:34 -05:00
q = super
2013-01-10 17:45:52 -05:00
2013-02-19 12:27:17 -05:00
if params [ :title ] . present?
2022-04-09 08:23:12 -04:00
q = q . where ( " title LIKE ? ESCAPE E' \\ \\ ' " , params [ :title ] . downcase . strip . tr ( " " , " _ " ) . to_escaped_for_sql_like )
2013-01-10 17:45:52 -05:00
end
2023-02-19 14:01:54 -05:00
q = q . attribute_matches ( :body , params [ :body_matches ] )
2013-03-19 08:10:10 -04:00
2014-05-22 20:07:15 -04:00
if params [ :other_names_match ] . present?
2017-05-23 21:59:48 -04:00
q = q . other_names_match ( params [ :other_names_match ] )
2014-05-22 20:07:15 -04:00
end
2023-08-03 16:01:53 -04:00
q = q . where_user ( :creator_id , :creator , params )
2013-03-19 08:10:10 -04:00
2018-05-03 19:53:35 -04:00
if params [ :hide_deleted ] . to_s . truthy?
2016-10-18 18:17:50 -04:00
q = q . where ( " is_deleted = false " )
end
2024-07-26 15:43:45 -04:00
q = q . attribute_matches ( :parent , params [ :parent ] . try ( :tr , " " , " _ " ) )
2018-05-03 19:53:35 -04:00
if params [ :other_names_present ] . to_s . truthy?
2018-11-13 19:08:20 -05:00
q = q . where ( " other_names is not null and other_names != '{}' " )
2018-05-03 19:53:35 -04:00
elsif params [ :other_names_present ] . to_s . falsy?
2018-11-13 19:08:20 -05:00
q = q . where ( " other_names is null or other_names = '{}' " )
2014-06-14 14:17:06 -04:00
end
2018-05-03 16:29:43 -04:00
q = q . attribute_matches ( :is_locked , params [ :is_locked ] )
q = q . attribute_matches ( :is_deleted , params [ :is_deleted ] )
2017-03-30 14:14:57 -04:00
case params [ :order ]
when " title "
2013-03-21 00:30:24 -04:00
q = q . order ( " title " )
2017-03-30 14:14:57 -04:00
when " post_count "
2017-04-23 17:53:34 -04:00
q = q . includes ( :tag ) . order ( " tags.post_count desc nulls last " ) . references ( :tags )
2017-03-30 14:14:57 -04:00
else
2023-07-07 08:32:57 -04:00
q = q . apply_basic_order ( params )
2013-02-04 21:34:25 -05:00
end
2013-01-10 17:45:52 -05:00
q
2010-02-15 17:32:32 -05:00
end
end
2013-03-19 08:10:10 -04:00
2013-03-11 13:14:25 -04:00
module ApiMethods
2016-10-25 15:54:25 -04:00
def method_attributes
2025-02-26 08:02:53 -05:00
super + % i [ creator_name category_id ]
2013-03-11 13:14:25 -04:00
end
end
2013-03-19 08:10:10 -04:00
2024-07-25 13:42:42 -04:00
module HelpPageMethods
def validate_not_used_as_help_page
if help_page . present?
errors . add ( :wiki_page , " is used by a help page " )
throw :abort
end
end
def update_help_page
HelpPage . find_by ( wiki_page : title_before_last_save ) & . update ( wiki_page : title )
end
end
2025-02-26 08:02:53 -05:00
module TagMethods
def tag
@tag || = super
end
def category_id
return @category_id if instance_variable_defined? ( :@category_id )
@category_id = tag & . category
end
def category_id = ( value )
return if value . blank? || value . to_i == category_id
category_id_will_change!
@category_id = value . to_i
end
def category_is_locked
return @category_is_locked if instance_variable_defined? ( :@category_is_locked )
@category_is_locked = tag & . is_locked || false
end
def category_is_locked = ( value )
return if value == category_is_locked
category_is_locked_will_change!
@category_is_locked = value
end
def category_id_changed?
attribute_changed? ( " category_id " )
end
def category_id_will_change!
attribute_will_change! ( " category_id " )
end
def category_is_locked_changed?
attribute_changed? ( " category_is_locked " )
end
def category_is_locked_will_change!
attribute_will_change! ( " category_is_locked " )
end
def tag_update_map
{ } . tap do | updates |
updates [ :category ] = @category_id if category_id_changed?
updates [ :is_locked ] = @category_is_locked if category_is_locked_changed?
end
end
def tag_changed?
tag_update_map . present?
end
def update_tag
updates = tag_update_map
@tag = Tag . find_or_create_by_name ( title )
return if updates . empty?
unless @tag . category_editable_by? ( CurrentUser . user )
reload_tag_attributes
errors . add ( :category_id , " Cannot be changed " )
throw ( :abort )
end
@tag . update ( updates )
@tag . save
if @tag . invalid?
errors . add ( :category_id , @tag . errors . full_messages . join ( " , " ) )
throw ( :abort )
end
reload_tag_attributes
end
def reload_tag_attributes
remove_instance_variable ( :@category_id ) if instance_variable_defined? ( :@category_id )
remove_instance_variable ( :@category_is_locked ) if instance_variable_defined? ( :@category_is_locked )
end
end
2013-01-10 17:45:52 -05:00
extend SearchMethods
2013-03-11 13:14:25 -04:00
include ApiMethods
2024-07-25 13:42:42 -04:00
include HelpPageMethods
2025-02-26 08:02:53 -05:00
include TagMethods
2013-03-19 08:10:10 -04:00
2019-06-27 11:33:02 -04:00
def user_not_limited
allowed = CurrentUser . can_wiki_edit_with_reason
if allowed != true
errors . add ( :base , " User #{ User . throttle_reason ( allowed ) } . " )
false
end
true
end
2013-09-12 18:20:20 -04:00
def validate_not_locked
2019-06-29 15:58:28 -04:00
if is_locked? && ! CurrentUser . is_janitor?
2013-09-12 18:20:20 -04:00
errors . add ( :is_locked , " and cannot be updated " )
2025-02-26 08:02:53 -05:00
false
2013-09-12 18:20:20 -04:00
end
end
2017-04-08 03:23:42 -04:00
def validate_rename
2024-07-25 13:42:42 -04:00
return unless will_save_change_to_title?
if ! CurrentUser . user . is_admin? && HelpPage . find_by ( wiki_page : title_was ) . present?
errors . add ( :title , " is used as a help page and cannot be changed " )
return
end
return if skip_secondary_validations
2017-04-08 03:23:42 -04:00
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
2024-07-26 15:43:45 -04:00
def validate_redirect
return unless will_save_change_to_parent? && parent . present?
if WikiPage . find_by ( title : parent ) . blank?
errors . add ( :parent , " does not exist " )
return
end
if HelpPage . find_by ( wiki_page : title ) . present?
errors . add ( :title , " is used as a help page and cannot be redirected " )
end
end
2010-10-27 16:56:12 -04:00
def revert_to ( version )
2016-10-10 06:24:49 -04:00
if id != version . wiki_page_id
raise RevertError . new ( " You cannot revert to a previous version of another wiki page. " )
end
2010-02-15 17:32:32 -05:00
self . title = version . title
self . body = version . body
2024-07-26 15:43:45 -04:00
self . parent = version . parent
2014-05-22 20:07:15 -04:00
self . other_names = version . other_names
2010-02-15 17:32:32 -05:00
end
2013-03-19 08:10:10 -04:00
2010-10-27 16:56:12 -04:00
def revert_to! ( version )
revert_to ( version )
2010-02-15 17:32:32 -05:00
save!
end
def normalize_title
2025-02-26 08:02:53 -05:00
title = self . title . downcase . tr ( " " , " _ " )
if title =~ / \ A( #{ Tag . categories . regexp } ):(.+) \ Z /
self . category_id = Tag . categories . value_for ( $1 )
title = $2
end
self . title = title
2010-02-15 17:32:32 -05:00
end
2014-05-24 12:58:14 -04:00
def normalize_other_names
2018-12-16 12:27:04 -05:00
self . other_names = other_names . map { | name | WikiPage . normalize_other_name ( name ) } . uniq
end
2024-07-26 15:43:45 -04:00
def normalize_parent
self . parent = nil if parent == " "
end
2018-12-16 12:27:04 -05:00
def self . normalize_other_name ( name )
name . unicode_normalize ( :nfkc ) . gsub ( / [[:space:]]+ / , " " ) . strip . tr ( " " , " _ " )
2014-05-24 12:58:14 -04:00
end
2024-09-02 10:40:41 -04:00
def self . normalize_name ( name )
name & . downcase & . tr ( " " , " _ " )
end
2017-04-08 03:23:42 -04:00
def skip_secondary_validations = ( value )
2018-05-03 19:53:35 -04:00
@skip_secondary_validations = value . to_s . truthy?
2017-04-08 03:23:42 -04:00
end
2010-02-15 17:32:32 -05:00
def pretty_title
2025-02-26 08:02:53 -05:00
title & . tr ( " _ " , " " ) || " "
2010-02-15 17:32:32 -05:00
end
2013-03-19 08:10:10 -04:00
2019-08-11 00:13:26 -04:00
def pretty_title_with_category
2025-02-26 08:02:53 -05:00
return pretty_title if category_id . blank? || category_id == 0
2023-02-03 12:28:17 -05:00
" #{ Tag . category_for_value ( category_id ) } : #{ pretty_title } "
2019-08-11 00:13:26 -04:00
end
2018-01-14 23:11:41 -05:00
def wiki_page_changed?
2024-07-26 15:43:45 -04:00
saved_change_to_title? || saved_change_to_body? || saved_change_to_is_locked? || saved_change_to_is_deleted? || saved_change_to_other_names? || saved_change_to_parent?
2018-01-14 23:11:41 -05:00
end
2014-03-18 16:06:00 -04:00
def create_new_version
versions . create (
2024-07-26 15:43:45 -04:00
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 ,
parent : parent ,
reason : edit_reason ,
2014-03-18 16:06:00 -04:00
)
end
2010-02-15 17:32:32 -05:00
def create_version
2018-01-14 23:11:41 -05:00
if wiki_page_changed?
2019-11-15 23:00:54 -05:00
create_new_version
2010-02-24 15:40:55 -05:00
end
2010-02-15 17:32:32 -05:00
end
2013-03-19 08:10:10 -04:00
2011-03-03 18:26:50 -05:00
def post_set
2024-04-04 16:52:49 -04:00
@post_set || = PostSets :: Post . new ( title , 1 , limit : 4 )
2011-03-03 18:26:50 -05:00
end
2013-03-19 08:10:10 -04:00
2011-09-30 16:40:24 -04:00
def tags
body . scan ( / \ [ \ [(.+?) \ ] \ ] / ) . flatten . map do | match |
if match =~ / ^(.+?) \ |(.+) /
$1
else
match
end
2025-02-26 08:02:53 -05:00
end . map { | x | x . downcase . tr ( " " , " _ " ) . to_s } . uniq
2014-04-17 17:17:11 -04:00
end
2010-02-15 13:59:58 -05:00
end