503 行
18 KiB
Ruby
503 行
18 KiB
Ruby
class WikiPagesController < ApplicationController
|
|
rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict
|
|
|
|
def index
|
|
title = params[:title].to_s.strip
|
|
descriptions =
|
|
TagDescription.where(language_code: 'ja')
|
|
.includes(tag: :tag_names)
|
|
pages = unmigrated_wiki_pages.includes(tag_name: :tag)
|
|
|
|
tag_groups = TagGroup.all
|
|
if title.present?
|
|
pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%"
|
|
descriptions = descriptions.joins(tag: :tag_names)
|
|
.where(tag_names: { language_code: 'ja', primary_flg: true })
|
|
.where('tag_names.name LIKE ?', pattern).limit(20)
|
|
tag_groups = tag_groups.where(
|
|
'CONCAT(?, name) LIKE ?', TagGroupRepr::PREFIX, pattern).limit(20)
|
|
pages = pages.where('tag_names.name LIKE ?', pattern).limit(20)
|
|
end
|
|
|
|
json = descriptions.map { |description| WikiPageRepr.tag_description(description) }
|
|
json.concat(tag_groups.map { WikiPageRepr.tag_group(_1) })
|
|
json.concat(WikiPageRepr.base(pages))
|
|
json = json.first(20) if title.present?
|
|
render json:
|
|
end
|
|
|
|
def show
|
|
tag_description = TagDescription.find_by(id: params[:id])
|
|
|
|
return render_wiki_page_by_tag_description tag_description if tag_description
|
|
|
|
tag_group = TagGroup.find_by(id: params[:id])
|
|
return render_tag_group tag_group if tag_group
|
|
|
|
page = WikiPage.joins(:tag_name)
|
|
.includes(tag_name: :tag)
|
|
.find_by(id: params[:id])
|
|
|
|
render_wiki_page_or_404 page
|
|
end
|
|
|
|
def show_by_title
|
|
title = params[:title].to_s.strip
|
|
|
|
if title.start_with?(TagGroupRepr::PREFIX)
|
|
name = title.delete_prefix(TagGroupRepr::PREFIX)
|
|
tag_group = TagGroup.find_by(name:)
|
|
return render_tag_group tag_group if tag_group
|
|
return head :not_found
|
|
end
|
|
|
|
tag_description = tag_description_by_title(title)
|
|
|
|
return render_wiki_page_by_tag_description tag_description if tag_description
|
|
|
|
page = WikiPage.joins(:tag_name)
|
|
.includes(tag_name: :tag)
|
|
.find_by(tag_name: { name: title })
|
|
|
|
render_wiki_page_or_404 page
|
|
end
|
|
|
|
def exists
|
|
return head :no_content if TagDescription.exists?(id: params[:id])
|
|
return head :no_content if TagGroup.exists?(id: params[:id])
|
|
|
|
head WikiPage.exists?(id: params[:id]) ? :no_content : :not_found
|
|
end
|
|
|
|
def exists_by_title
|
|
title = params[:title].to_s.strip
|
|
if title.start_with?(TagGroupRepr::PREFIX)
|
|
exists = TagGroup.exists?(name: title.delete_prefix(TagGroupRepr::PREFIX))
|
|
return head exists ? :no_content : :not_found
|
|
end
|
|
return head :no_content if tag_description_by_title(title)
|
|
|
|
exists = WikiPage.joins(:tag_name).exists?(tag_names: { name: title })
|
|
head exists ? :no_content : :not_found
|
|
end
|
|
|
|
def diff
|
|
id = params[:id]
|
|
return render_bad_request('id は必須です.') if id.blank?
|
|
|
|
from = params[:from].presence
|
|
to = params[:to].presence
|
|
|
|
description = TagDescription.find_by(id:)
|
|
if description
|
|
versions = description_versions(description)
|
|
from_rev = from && versions.find(from)
|
|
to_rev = to ? versions.find(to) : versions.find_by!(version_no: description.version_no)
|
|
resource = WikiPageRepr.tag_description(description)
|
|
else
|
|
tag_group = TagGroup.find_by(id:)
|
|
if tag_group
|
|
from_rev = from && group_version(tag_group, from)
|
|
to_rev = to ? group_version(tag_group, to) : current_group_version(tag_group)
|
|
return head :not_found if (from && !from_rev) || !to_rev
|
|
|
|
from_body = from_rev && render_group_version(from_rev)
|
|
to_body = render_group_version(to_rev)
|
|
resource = { 'id' => tag_group.id,
|
|
'title' => "#{ TagGroupRepr::PREFIX }#{ tag_group.name }",
|
|
'deprecated_at' => nil }
|
|
else
|
|
page = WikiPage.joins(:tag_name).includes(tag_name: :tag).find(id)
|
|
from_rev = from && page.wiki_revisions.find(from)
|
|
to_rev = to ? page.wiki_revisions.find(to) : page.current_revision
|
|
if ((from_rev && !from_rev.content?) || !to_rev&.content?)
|
|
return render_unprocessable_entity('差分を表示できない版です.')
|
|
end
|
|
from_body = from_rev&.body
|
|
to_body = to_rev.body
|
|
resource = WikiPageRepr.base(page)
|
|
end
|
|
end
|
|
|
|
from_body ||= from_rev&.body
|
|
to_body ||= to_rev.body
|
|
diff_json = wiki_diff(from_body, to_body)
|
|
|
|
render json: { wiki_page_id: resource.fetch('id'),
|
|
title: resource.fetch('title'),
|
|
deprecated_at: resource.fetch('deprecated_at'),
|
|
older_revision_id: from_rev&.id,
|
|
newer_revision_id: to_rev.id,
|
|
diff: diff_json }
|
|
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
|
|
render_unprocessable_entity(error.message)
|
|
end
|
|
|
|
def create
|
|
return head :unauthorized unless current_user
|
|
return head :forbidden unless current_user.gte_member?
|
|
|
|
title = params[:title].to_s.strip
|
|
body = params[:body].to_s
|
|
message = params[:message].presence
|
|
|
|
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
|
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
|
|
|
if title.start_with?(TagGroupRepr::PREFIX)
|
|
name = title.delete_prefix(TagGroupRepr::PREFIX)
|
|
parsed = TagGroupMarkdownParser.parse(body:, group: nil)
|
|
group = TagGroupOperation.create!(name:, **parsed, actor: current_user,
|
|
require_compatibility: true)
|
|
version = current_group_version(group)
|
|
return render_group_representation(TagGroupRepr.one(group), version,
|
|
body: parsed.fetch(:compatibility_body),
|
|
status: :created)
|
|
end
|
|
|
|
page = if title.include?(':')
|
|
ApplicationRecord.transaction do
|
|
tag_name = TagName.find_or_create_by!(language_code: 'ja', name: title) do |name|
|
|
name.script_code = 'Jpan'
|
|
name.primary_flg = true
|
|
name.auto_generated = false
|
|
end
|
|
|
|
Wiki::Commit.create_legacy_content!(
|
|
tag_name:, body:, created_by_user: current_user, message:)
|
|
end
|
|
else
|
|
tag_name = TagName.find_by(language_code: 'ja', name: title)
|
|
return render_unprocessable_entity field: :title unless tag_name&.tag
|
|
|
|
TagDescriptionOperation.create!(
|
|
tag: tag_name.tag, body:, created_by_user: current_user, message:)
|
|
end
|
|
|
|
render json: WikiPageRepr.base(page), status: :created
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
render_validation_error e.record
|
|
rescue ActiveRecord::RecordNotUnique
|
|
render_record_not_unique
|
|
rescue TagGroupMarkdownParser::InvalidDocument => error
|
|
render_unprocessable_entity(error.message)
|
|
end
|
|
|
|
def update
|
|
return head :unauthorized unless current_user
|
|
return head :forbidden unless current_user.gte_member?
|
|
|
|
title = params[:title]&.strip
|
|
body = params[:body].to_s
|
|
|
|
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
|
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
|
|
|
group = TagGroup.find_by(id: params[:id])
|
|
if title.start_with?(TagGroupRepr::PREFIX)
|
|
return head :not_found unless group
|
|
|
|
name = title.delete_prefix(TagGroupRepr::PREFIX)
|
|
parsed = TagGroupMarkdownParser.parse(body:, group:)
|
|
TagGroupOperation.update!(group:, name:, **parsed, actor: current_user,
|
|
require_compatibility: true)
|
|
return head :ok
|
|
end
|
|
|
|
page = WikiPage.find_by(id: params[:id])
|
|
if legacy_group_page?(page)
|
|
return render_unprocessable_entity field: :title
|
|
end
|
|
if group && !legacy_update_identified?(page, title, group)
|
|
return render_unprocessable_entity field: :title
|
|
end
|
|
return head :not_found unless page
|
|
|
|
base_revision_id = params[:base_revision_id].presence
|
|
|
|
ApplicationRecord.transaction do
|
|
page.lock!
|
|
|
|
old_title = page.title
|
|
|
|
tag = page.tag_name.tag
|
|
|
|
if tag && title != old_title
|
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
|
end
|
|
|
|
page.tag_name.update!(name: title) if title != old_title
|
|
|
|
message = params[:message].presence
|
|
description = TagDescription.find_by(id: page.id)
|
|
description_page = description && (
|
|
tag&.id == description.tag_id ||
|
|
(tag.nil? && !old_title.include?(':')))
|
|
if description && !description_page
|
|
Wiki::Commit.legacy_content!(page:, body:, created_user: current_user,
|
|
message:, base_revision_id:)
|
|
else
|
|
Wiki::Commit.content!(page:, body:, created_user: current_user,
|
|
message:, base_revision_id:)
|
|
end
|
|
|
|
if tag && title != old_title
|
|
tag.reload
|
|
TagVersionRecorder.record!(tag:, event_type: :update, created_by_user: current_user)
|
|
end
|
|
end
|
|
|
|
head :ok
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
render_validation_error e.record
|
|
rescue ActiveRecord::RecordNotUnique
|
|
render_record_not_unique
|
|
rescue TagGroupMarkdownParser::InvalidDocument => error
|
|
render_unprocessable_entity(error.message)
|
|
end
|
|
|
|
def changes
|
|
id = params[:id].presence
|
|
description = id && TagDescription.find_by(id:)
|
|
if description
|
|
return render json: description_changes(TagDescription.where(id: description.id))
|
|
end
|
|
|
|
tag_group = id && TagGroup.find_by(id:)
|
|
if tag_group
|
|
return render json: tag_group_changes(TagGroupVersion.where(tag_group_id: tag_group.id))
|
|
end
|
|
|
|
pages = id ? WikiPage.where(id:) : unmigrated_wiki_pages
|
|
revisions = WikiRevision.joins(wiki_page: :tag_name)
|
|
.where(wiki_page_id: pages.select(:id))
|
|
.includes(:created_user, wiki_page: { tag_name: :tag })
|
|
.order(id: :desc).limit(200)
|
|
json = revisions.map { |rev|
|
|
{ revision_id: rev.id,
|
|
pred: rev.base_revision_id,
|
|
succ: nil,
|
|
wiki_page: { id: rev.wiki_page_id,
|
|
title: rev.wiki_page.title,
|
|
deprecated_at: rev.wiki_page.deprecated_at },
|
|
user: rev.created_user && { id: rev.created_user.id, name: rev.created_user.name },
|
|
kind: rev.kind,
|
|
message: rev.message,
|
|
timestamp: rev.created_at }
|
|
}
|
|
unless id
|
|
json.concat(tag_group_changes(TagGroupVersion.all))
|
|
json.concat(description_changes(TagDescription.where(language_code: 'ja')))
|
|
json = json.each_with_index.sort_by do |row, index|
|
|
[-row.fetch(:timestamp).to_f, index]
|
|
end.map(&:first)
|
|
end
|
|
render json: json.first(200)
|
|
end
|
|
|
|
private
|
|
|
|
def legacy_group_page? page
|
|
return false unless page
|
|
return true if page.title.start_with?(TagGroupRepr::PREFIX)
|
|
|
|
prefix = ApplicationRecord.sanitize_sql_like(TagGroupRepr::PREFIX)
|
|
page.wiki_versions.where('title LIKE ?', "#{ prefix }%").exists?
|
|
end
|
|
|
|
def legacy_update_identified? page, title, group
|
|
return false unless page
|
|
return true if page.title == title
|
|
return false if title == group.name
|
|
|
|
namespace = page.title.split(':', 2)
|
|
return true if namespace.size == 2 && title.start_with?("#{ namespace.first }:")
|
|
|
|
base = params[:base_revision_id].presence
|
|
base && page.wiki_revisions.exists?(id: base)
|
|
end
|
|
|
|
def render_tag_group group
|
|
version = if params[:version].present?
|
|
group_version(group, params[:version])
|
|
else
|
|
current_group_version(group)
|
|
end
|
|
return head :not_found unless version
|
|
|
|
representation = if params[:version].present?
|
|
TagGroupRepr.historical(version)
|
|
else
|
|
TagGroupRepr.one(group)
|
|
end
|
|
render_group_representation(representation, version)
|
|
end
|
|
|
|
def render_group_version version
|
|
representation = TagGroupRepr.historical(version)
|
|
TagGroupMarkdownRenderer.render(representation)
|
|
end
|
|
|
|
def render_group_representation representation, version, body: nil, status: :ok
|
|
body ||= TagGroupMarkdownRenderer.render(representation)
|
|
versions = TagGroupVersion.where(tag_group_id: version.tag_group_id)
|
|
pred = versions.find_by(version_no: version.version_no - 1)&.id
|
|
succ = versions.find_by(version_no: version.version_no + 1)&.id
|
|
updated_at = version.created_at
|
|
render json: {
|
|
'id' => version.tag_group_id,
|
|
'title' => "#{ TagGroupRepr::PREFIX }#{ representation.fetch('name') }",
|
|
'deprecated_at' => nil,
|
|
'body' => body,
|
|
'revision_id' => version.id,
|
|
'pred' => pred,
|
|
'succ' => succ,
|
|
'updated_at' => updated_at }, status:
|
|
rescue TagGroupMarkdownRenderer::UnsupportedStructure => error
|
|
render_unprocessable_entity(error.message)
|
|
end
|
|
|
|
def current_group_version group
|
|
TagGroupVersion.find_by(tag_group_id: group.id, version_no: group.version_no)
|
|
end
|
|
|
|
def group_version group, version_id
|
|
TagGroupVersion.find_by(tag_group_id: group.id, id: version_id)
|
|
end
|
|
|
|
def wiki_diff from_body, to_body
|
|
diffs = Diff::LCS.sdiff(from_body.to_s.lines, to_body.to_s.lines)
|
|
diffs.map { |change|
|
|
case change.action
|
|
when ?=
|
|
{ type: 'context', content: change.old_element }
|
|
when ?!
|
|
[{ type: 'removed', content: change.old_element },
|
|
{ type: 'added', content: change.new_element }]
|
|
when ?+
|
|
{ type: 'added', content: change.new_element }
|
|
when ?-
|
|
{ type: 'removed', content: change.old_element }
|
|
end
|
|
}.flatten.compact
|
|
end
|
|
|
|
def tag_group_changes versions
|
|
rows = versions.includes(:created_by_user).order(id: :desc).limit(200).to_a
|
|
groups = TagGroup.where(id: rows.map(&:tag_group_id).uniq).index_by(&:id)
|
|
predecessors = TagGroupVersion.where(tag_group_id: groups.keys,
|
|
version_no: rows.map { _1.version_no - 1 })
|
|
.pluck(:tag_group_id, :version_no, :id)
|
|
.to_h { |group_id, number, version_id| [[group_id, number], version_id] }
|
|
rows.map do |version|
|
|
group = groups.fetch(version.tag_group_id)
|
|
user = version.created_by_user
|
|
{ revision_id: version.id,
|
|
pred: predecessors[[group.id, version.version_no - 1]],
|
|
succ: nil,
|
|
wiki_page: { id: group.id, title: "#{ TagGroupRepr::PREFIX }#{ version.name }",
|
|
deprecated_at: nil },
|
|
user: user && { id: user.id, name: user.name },
|
|
kind: 'content', message: nil, timestamp: version.created_at }
|
|
end
|
|
end
|
|
|
|
def render_wiki_page_or_404 page
|
|
return head :not_found unless page
|
|
|
|
rev = find_revision(page)
|
|
return head :not_found unless rev
|
|
|
|
if rev.redirect?
|
|
return (
|
|
redirect_to wiki_page_by_title_path(title: rev.redirect_page.title),
|
|
status: :moved_permanently)
|
|
end
|
|
|
|
body = rev.body
|
|
revision_id = rev.id
|
|
pred = page.pred_revision_id(revision_id)
|
|
succ = page.succ_revision_id(revision_id)
|
|
updated_at = rev.created_at
|
|
|
|
render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:)
|
|
end
|
|
|
|
def render_wiki_page_by_tag_description description
|
|
versions = description_versions(description)
|
|
historical = params[:version].present?
|
|
rev = historical ? versions.find_by(id: params[:version]) :
|
|
versions.find_by(version_no: description.version_no)
|
|
return head :not_found unless rev
|
|
|
|
pred = versions.find_by(version_no: rev.version_no - 1)&.id
|
|
succ = versions.find_by(version_no: rev.version_no + 1)&.id
|
|
body = historical ? rev.body : description.body
|
|
updated_at = historical ? rev.created_at : description.updated_at
|
|
render json: WikiPageRepr.tag_description(description).merge(
|
|
body:, revision_id: rev.id, pred:, succ:, updated_at:)
|
|
end
|
|
|
|
def description_versions description
|
|
TagDescriptionVersion.where(tag_id: description.tag_id,
|
|
language_code: description.language_code)
|
|
end
|
|
|
|
def tag_description_by_title title
|
|
TagDescription.joins(tag: :tag_names)
|
|
.find_by(language_code: 'ja', tag_names: { language_code: 'ja', name: title })
|
|
end
|
|
|
|
def unmigrated_wiki_pages
|
|
descriptions = TagDescription.where(language_code: 'ja')
|
|
|
|
WikiPage
|
|
.joins(:tag_name)
|
|
.where.not(id: descriptions.select(:id))
|
|
.where.not('tag_names.name LIKE ?', "#{ TagGroupRepr::PREFIX }%")
|
|
.where('tag_names.tag_id IS NULL OR tag_names.tag_id NOT IN (?)',
|
|
descriptions.select(:tag_id))
|
|
end
|
|
|
|
def description_changes descriptions
|
|
versions = TagDescriptionVersion.where(language_code: 'ja',
|
|
tag_id: descriptions.select(:tag_id))
|
|
.order(id: :desc).limit(200).to_a
|
|
return [] if versions.empty?
|
|
|
|
by_tag = descriptions.where(tag_id: versions.map(&:tag_id))
|
|
.includes(tag: :tag_names).index_by(&:tag_id)
|
|
users = User.where(id: versions.map(&:created_by_user_id)).index_by(&:id)
|
|
predecessors = TagDescriptionVersion.where(
|
|
language_code: 'ja', tag_id: by_tag.keys,
|
|
version_no: versions.map { |version| version.version_no - 1 })
|
|
.pluck(:tag_id, :version_no, :id)
|
|
.to_h { |tag_id, version_no, id| [[tag_id, version_no], id] }
|
|
versions.map do |version|
|
|
description = by_tag.fetch(version.tag_id)
|
|
user = users[version.created_by_user_id]
|
|
{ revision_id: version.id,
|
|
pred: predecessors[[version.tag_id, version.version_no - 1]],
|
|
succ: nil,
|
|
wiki_page: { id: description.id, title: description.tag.name('ja'),
|
|
deprecated_at: description.tag.deprecated_at },
|
|
user: user && { id: user.id, name: user.name },
|
|
kind: 'content',
|
|
message: nil,
|
|
timestamp: version.created_at }
|
|
end
|
|
end
|
|
|
|
def find_revision page
|
|
if params[:version].present?
|
|
page.wiki_revisions.find_by(id: params[:version])
|
|
else
|
|
page.current_revision
|
|
end
|
|
end
|
|
|
|
def render_wiki_conflict err
|
|
render json: { error: 'conflict', message: err.message }, status: :conflict
|
|
end
|
|
end
|