コミットを比較
11
コミット
main
...
feature/422
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
|
|
f78a08bea1 | ||
|
|
99ae691ba1 | ||
|
|
d63e0b95fa | ||
|
|
9c4d3285f3 | ||
|
|
6cd1fd43ee | ||
|
|
d5b5fc1646 | ||
|
|
6f2f56b7b4 | ||
|
|
593e867fdd | ||
|
|
93e2f48355 | ||
|
|
bf0a2977fc | ||
|
|
11eedc242d |
@@ -0,0 +1,57 @@
|
|||||||
|
class TagDescriptionsController < ApplicationController
|
||||||
|
def show
|
||||||
|
description = TagDescription.find_by(tag_id: params[:id], language_code: 'ja')
|
||||||
|
return head :not_found unless description
|
||||||
|
|
||||||
|
render json: representation(description)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show_by_title
|
||||||
|
tag_name = TagName.find_by(language_code: 'ja', name: params[:name].to_s.strip)
|
||||||
|
description = tag_name&.tag && TagDescription.find_by(tag: tag_name.tag, language_code: 'ja')
|
||||||
|
return head :not_found unless description
|
||||||
|
|
||||||
|
render json: representation(description)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
return head :unauthorized unless current_user
|
||||||
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
|
body = params[:body].to_s
|
||||||
|
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
||||||
|
|
||||||
|
tag = Tag.find_by(id: params[:id])
|
||||||
|
return head :not_found unless tag
|
||||||
|
|
||||||
|
description = TagDescription.find_by(tag:, language_code: 'ja')
|
||||||
|
|
||||||
|
if description
|
||||||
|
TagDescriptionOperation.update!(tag:, body:, updated_by_user: current_user)
|
||||||
|
description.reload
|
||||||
|
render json: representation(description)
|
||||||
|
else
|
||||||
|
TagDescriptionOperation.create!(tag:, body:, created_by_user: current_user)
|
||||||
|
description = TagDescription.find_by!(tag:, language_code: 'ja')
|
||||||
|
render json: representation(description), status: :created
|
||||||
|
end
|
||||||
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
|
render_validation_error e.record
|
||||||
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
render_record_not_unique
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def representation description
|
||||||
|
description.as_json(only: [
|
||||||
|
:tag_id,
|
||||||
|
:language_code,
|
||||||
|
:body,
|
||||||
|
:version_no,
|
||||||
|
:created_by_user_id,
|
||||||
|
:updated_by_user_id,
|
||||||
|
:created_at,
|
||||||
|
:updated_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -464,7 +464,6 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
old_name = tag.name(locale.language_code)
|
old_name = tag.name(locale.language_code)
|
||||||
name_changed = name != old_name
|
name_changed = name != old_name
|
||||||
wiki_page = tag.tag_name(locale.language_code)&.wiki_page if name_changed
|
|
||||||
|
|
||||||
if tag.deprecated? == deprecated
|
if tag.deprecated? == deprecated
|
||||||
tag.update!(category:)
|
tag.update!(category:)
|
||||||
@@ -485,9 +484,7 @@ class TagsController < ApplicationController
|
|||||||
tag,
|
tag,
|
||||||
event_type: :update,
|
event_type: :update,
|
||||||
created_by_user: current_user,
|
created_by_user: current_user,
|
||||||
language_code: locale.language_code,
|
language_code: locale.language_code)
|
||||||
name_changed:,
|
|
||||||
wiki_page:)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
||||||
@@ -518,7 +515,6 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
old_name = tag.name(locale.language_code)
|
old_name = tag.name(locale.language_code)
|
||||||
name_changed = name.present? && name != old_name
|
name_changed = name.present? && name != old_name
|
||||||
wiki_page = tag.tag_name(locale.language_code)&.wiki_page if name_changed
|
|
||||||
|
|
||||||
rename_tag_name!(locale, tag, name) if name_changed
|
rename_tag_name!(locale, tag, name) if name_changed
|
||||||
tag.update!(category:) if category.present?
|
tag.update!(category:) if category.present?
|
||||||
@@ -532,9 +528,7 @@ class TagsController < ApplicationController
|
|||||||
tag,
|
tag,
|
||||||
event_type: :update,
|
event_type: :update,
|
||||||
created_by_user: current_user,
|
created_by_user: current_user,
|
||||||
language_code: locale.language_code,
|
language_code: locale.language_code)
|
||||||
name_changed:,
|
|
||||||
wiki_page:)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
render json: TagRepr.base(tag.reload, language_code: locale.language_code)
|
||||||
@@ -722,19 +716,8 @@ class TagsController < ApplicationController
|
|||||||
language_code:))
|
language_code:))
|
||||||
end
|
end
|
||||||
|
|
||||||
def record_tag_version! tag, event_type:, created_by_user:, language_code: 'ja',
|
def record_tag_version! tag, event_type:, created_by_user:, language_code: 'ja'
|
||||||
name_changed: false, wiki_page: nil
|
|
||||||
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
|
TagVersionRecorder.record!(tag:, event_type:, created_by_user:, language_code:)
|
||||||
|
|
||||||
return unless name_changed
|
|
||||||
|
|
||||||
wiki_page ||= tag.tag_name('ja')&.wiki_page
|
|
||||||
return unless wiki_page&.wiki_versions&.exists?
|
|
||||||
|
|
||||||
WikiVersionRecorder.record!(
|
|
||||||
page: wiki_page,
|
|
||||||
event_type: :update,
|
|
||||||
created_by_user:)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_tag_rename locale, tag, name
|
def validate_tag_rename locale, tag, name
|
||||||
|
|||||||
@@ -2,47 +2,67 @@ class WikiPagesController < ApplicationController
|
|||||||
rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict
|
rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
# 互換性のための化石リソース
|
||||||
|
# TODO: ヘルプおよびタグ・グループ追加後に,
|
||||||
|
# TODO: タグ説明とヘルプ、タグ・グループから取得する作りに変更する.
|
||||||
|
#
|
||||||
title = params[:title].to_s.strip
|
title = params[:title].to_s.strip
|
||||||
if title.blank?
|
descriptions = TagDescription.where(language_code: 'ja')
|
||||||
return render json: WikiPageRepr.base(
|
.includes(tag: :tag_names)
|
||||||
WikiPage.joins(:tag_name).includes(tag_name: :tag))
|
pages = unmigrated_wiki_pages.includes(tag_name: :tag)
|
||||||
|
|
||||||
|
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)
|
||||||
|
pages = pages.where('tag_names.name LIKE ?', pattern).limit(20)
|
||||||
end
|
end
|
||||||
|
|
||||||
q = WikiPage.joins(:tag_name).includes(tag_name: :tag)
|
json = descriptions.map { |description| WikiPageRepr.tag_description(description) }
|
||||||
.where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
|
json.concat(WikiPageRepr.base(pages))
|
||||||
render json: WikiPageRepr.base(q.limit(20))
|
json = json.first(20) if title.present?
|
||||||
|
render json:
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
tag_description = TagDescription.find_by(id: params[:id])
|
||||||
|
|
||||||
|
return render_wiki_page_by_tag_description tag_description if tag_description
|
||||||
|
|
||||||
page = WikiPage.joins(:tag_name)
|
page = WikiPage.joins(:tag_name)
|
||||||
.includes(tag_name: :tag)
|
.includes(tag_name: :tag)
|
||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
|
|
||||||
render_wiki_page_or_404 page
|
render_wiki_page_or_404 page
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_by_title
|
def show_by_title
|
||||||
title = params[:title].to_s.strip
|
title = params[:title].to_s.strip
|
||||||
|
|
||||||
|
tag_description = tag_description_by_title(title)
|
||||||
|
|
||||||
|
return render_wiki_page_by_tag_description tag_description if tag_description
|
||||||
|
|
||||||
page = WikiPage.joins(:tag_name)
|
page = WikiPage.joins(:tag_name)
|
||||||
.includes(tag_name: :tag)
|
.includes(tag_name: :tag)
|
||||||
.find_by(tag_name: { name: title })
|
.find_by(tag_name: { name: title })
|
||||||
|
|
||||||
render_wiki_page_or_404 page
|
render_wiki_page_or_404 page
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists
|
def exists
|
||||||
if WikiPage.exists?(id: params[:id])
|
return head :no_content if TagDescription.exists?(id: params[:id])
|
||||||
head :no_content
|
|
||||||
else
|
head WikiPage.exists?(id: params[:id]) ? :no_content : :not_found
|
||||||
head :not_found
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists_by_title
|
def exists_by_title
|
||||||
title = params[:title].to_s.strip
|
title = params[:title].to_s.strip
|
||||||
if WikiPage.joins(:tag_name).exists?(tag_names: { name: title })
|
return head :no_content if tag_description_by_title(title)
|
||||||
head :no_content
|
|
||||||
else
|
exists = WikiPage.joins(:tag_name).exists?(tag_names: { name: title })
|
||||||
head :not_found
|
head exists ? :no_content : :not_found
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def diff
|
def diff
|
||||||
@@ -52,12 +72,20 @@ class WikiPagesController < ApplicationController
|
|||||||
from = params[:from].presence
|
from = params[:from].presence
|
||||||
to = params[:to].presence
|
to = params[:to].presence
|
||||||
|
|
||||||
page = WikiPage.joins(:tag_name).includes(tag_name: :tag).find(id)
|
description = TagDescription.find_by(id:)
|
||||||
|
if description
|
||||||
from_rev = from && page.wiki_revisions.find(from)
|
versions = description_versions(description)
|
||||||
to_rev = to ? page.wiki_revisions.find(to) : page.current_revision
|
from_rev = from && versions.find(from)
|
||||||
if ((from_rev && !(from_rev.content?)) || !(to_rev&.content?))
|
to_rev = to ? versions.find(to) : versions.find_by!(version_no: description.version_no)
|
||||||
return render_unprocessable_entity('差分を表示できない版です.')
|
resource = WikiPageRepr.tag_description(description)
|
||||||
|
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
|
||||||
|
resource = WikiPageRepr.base(page)
|
||||||
end
|
end
|
||||||
|
|
||||||
diffs = Diff::LCS.sdiff(from_rev&.body&.lines || [], to_rev.body.lines)
|
diffs = Diff::LCS.sdiff(from_rev&.body&.lines || [], to_rev.body.lines)
|
||||||
@@ -75,9 +103,9 @@ class WikiPagesController < ApplicationController
|
|||||||
end
|
end
|
||||||
}.flatten.compact
|
}.flatten.compact
|
||||||
|
|
||||||
render json: { wiki_page_id: page.id,
|
render json: { wiki_page_id: resource.fetch('id'),
|
||||||
title: page.title,
|
title: resource.fetch('title'),
|
||||||
deprecated_at: page.deprecated_at,
|
deprecated_at: resource.fetch('deprecated_at'),
|
||||||
older_revision_id: from_rev&.id,
|
older_revision_id: from_rev&.id,
|
||||||
newer_revision_id: to_rev.id,
|
newer_revision_id: to_rev.id,
|
||||||
diff: diff_json }
|
diff: diff_json }
|
||||||
@@ -94,18 +122,24 @@ class WikiPagesController < ApplicationController
|
|||||||
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank?
|
||||||
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank?
|
||||||
|
|
||||||
tag_name = TagName.find_or_create_by!(language_code: 'ja', name: title) do |name|
|
page = if title.include?(':')
|
||||||
name.script_code = 'Jpan'
|
ApplicationRecord.transaction do
|
||||||
name.primary_flg = true
|
tag_name = TagName.find_or_create_by!(language_code: 'ja', name: title) do |name|
|
||||||
name.auto_generated = false
|
name.script_code = 'Jpan'
|
||||||
end
|
name.primary_flg = true
|
||||||
|
name.auto_generated = false
|
||||||
|
end
|
||||||
|
|
||||||
page =
|
Wiki::Commit.create_legacy_content!(
|
||||||
Wiki::Commit.create_content!(
|
tag_name:, body:, created_by_user: current_user, message:)
|
||||||
tag_name:,
|
end
|
||||||
body:,
|
else
|
||||||
created_by_user: current_user,
|
tag_name = TagName.find_by(language_code: 'ja', name: title)
|
||||||
message:)
|
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
|
render json: WikiPageRepr.base(page), status: :created
|
||||||
rescue ActiveRecord::RecordInvalid => e
|
rescue ActiveRecord::RecordInvalid => e
|
||||||
@@ -132,7 +166,7 @@ class WikiPagesController < ApplicationController
|
|||||||
|
|
||||||
old_title = page.title
|
old_title = page.title
|
||||||
|
|
||||||
tag = Tag.find_by(tag_name_id: page.tag_name_id)
|
tag = page.tag_name.tag
|
||||||
|
|
||||||
if tag && title != old_title
|
if tag && title != old_title
|
||||||
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)
|
||||||
@@ -156,29 +190,35 @@ class WikiPagesController < ApplicationController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
|
||||||
index
|
|
||||||
end
|
|
||||||
|
|
||||||
def changes
|
def changes
|
||||||
id = params[:id].presence
|
id = params[:id].presence
|
||||||
q = WikiRevision.joins(wiki_page: :tag_name)
|
description = id && TagDescription.find_by(id:)
|
||||||
.includes(:created_user, wiki_page: { tag_name: :tag })
|
if description
|
||||||
.order(id: :desc)
|
return render json: description_changes(TagDescription.where(id: description.id))
|
||||||
q = q.where(wiki_page_id: id) if id
|
end
|
||||||
|
|
||||||
render json: q.limit(200).map { |rev|
|
pages = id ? WikiPage.where(id:) : unmigrated_wiki_pages
|
||||||
{ revision_id: rev.id,
|
revisions = WikiRevision.joins(wiki_page: :tag_name)
|
||||||
pred: rev.base_revision_id,
|
.where(wiki_page_id: pages.select(:id))
|
||||||
succ: nil,
|
.includes(:created_user, wiki_page: { tag_name: :tag })
|
||||||
wiki_page: { id: rev.wiki_page_id,
|
.order(id: :desc).limit(200)
|
||||||
title: rev.wiki_page.title,
|
json = revisions.map { |rev|
|
||||||
deprecated_at: rev.wiki_page.deprecated_at },
|
{ revision_id: rev.id,
|
||||||
user: rev.created_user && { id: rev.created_user.id, name: rev.created_user.name },
|
pred: rev.base_revision_id,
|
||||||
kind: rev.kind,
|
succ: nil,
|
||||||
message: rev.message,
|
wiki_page: { id: rev.wiki_page_id,
|
||||||
timestamp: rev.created_at }
|
title: rev.wiki_page.title,
|
||||||
}.compact
|
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(description_changes(TagDescription.where(language_code: 'ja')))
|
||||||
|
json.sort_by! { |row| -row[:revision_id] }
|
||||||
|
end
|
||||||
|
render json: json.first(200)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -204,6 +244,67 @@ class WikiPagesController < ApplicationController
|
|||||||
render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:)
|
render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:)
|
||||||
end
|
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('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
|
def find_revision page
|
||||||
if params[:version].present?
|
if params[:version].present?
|
||||||
page.wiki_revisions.find_by(id: params[:version])
|
page.wiki_revisions.find_by(id: params[:version])
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class TagDescription < ApplicationRecord
|
||||||
|
belongs_to :tag
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
class TagDescriptionVersion < ApplicationRecord
|
||||||
|
;
|
||||||
|
end
|
||||||
@@ -48,8 +48,11 @@ class WikiPage < ApplicationRecord
|
|||||||
page
|
page
|
||||||
end
|
end
|
||||||
|
|
||||||
def pred_revision_id(revision_id) =
|
def pred_revision_id revision_id
|
||||||
wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id)
|
wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id)
|
||||||
def succ_revision_id(revision_id) =
|
end
|
||||||
wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id)
|
|
||||||
|
def succ_revision_id revision_id
|
||||||
|
wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,21 @@ module WikiPageRepr
|
|||||||
wiki_page.as_json(BASE)
|
wiki_page.as_json(BASE)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def tag_description description
|
||||||
|
tag = description.tag
|
||||||
|
{ 'id' => description.id,
|
||||||
|
'tag_name_id' => tag.tag_name('ja')&.id,
|
||||||
|
'body' => description.body,
|
||||||
|
'created_user_id' => description.created_by_user_id,
|
||||||
|
'updated_user_id' => description.updated_by_user_id,
|
||||||
|
'created_at' => description.created_at,
|
||||||
|
'updated_at' => description.updated_at,
|
||||||
|
'discarded_at' => nil,
|
||||||
|
'version_no' => description.version_no,
|
||||||
|
'title' => tag.name('ja'),
|
||||||
|
'deprecated_at' => tag.deprecated_at }
|
||||||
|
end
|
||||||
|
|
||||||
def many wiki_pages
|
def many wiki_pages
|
||||||
wiki_pages.map { |p| base(p) }
|
wiki_pages.map { |p| base(p) }
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
class TagDescriptionOperation
|
||||||
|
LANGUAGE_CODE = 'ja'
|
||||||
|
|
||||||
|
def self.create!(tag:, body:, created_by_user:, message: nil)
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
tag.lock!
|
||||||
|
raise ActiveRecord::RecordNotUnique if TagDescription.exists?(
|
||||||
|
tag_id: tag.id, language_code: LANGUAGE_CODE)
|
||||||
|
|
||||||
|
page = Wiki::CompatibilityWriter.create!(
|
||||||
|
tag_name: tag.tag_name(LANGUAGE_CODE), body:, created_by_user:, message:)
|
||||||
|
revision = page.wiki_revisions.order(id: :desc).first
|
||||||
|
record_version!(tag:, revision:, body:, user: created_by_user,
|
||||||
|
description: nil)
|
||||||
|
page
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.update!(tag:, body:, updated_by_user:, message: nil,
|
||||||
|
base_revision_id: nil)
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
tag.lock!
|
||||||
|
description = TagDescription.lock.find_by!(
|
||||||
|
tag_id: tag.id, language_code: LANGUAGE_CODE)
|
||||||
|
page = WikiPage.lock.find(description.id)
|
||||||
|
revision = Wiki::CompatibilityWriter.update!(
|
||||||
|
page:, body:, updated_by_user:, message:,
|
||||||
|
base_revision_id:)
|
||||||
|
next unless revision
|
||||||
|
|
||||||
|
record_version!(tag:, revision:, body: revision.body,
|
||||||
|
user: updated_by_user, description:)
|
||||||
|
revision
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.record_version! tag:, revision:, body:, user:, description:
|
||||||
|
version_no = description ? description.version_no + 1 : 1
|
||||||
|
TagDescriptionVersion.create!(
|
||||||
|
id: revision.id, tag_id: tag.id, language_code: LANGUAGE_CODE,
|
||||||
|
version_no:, event_type: description ? 'update' : 'create', body:,
|
||||||
|
created_by_user_id: user.id, created_at: revision.created_at)
|
||||||
|
if description
|
||||||
|
description.update!(body:, version_no:, updated_by_user_id: user.id)
|
||||||
|
else
|
||||||
|
TagDescription.create!(
|
||||||
|
id: revision.wiki_page_id, tag_id: tag.id,
|
||||||
|
language_code: LANGUAGE_CODE, version_no:, body:,
|
||||||
|
created_by_user_id: user.id, updated_by_user_id: user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private_class_method :record_version!
|
||||||
|
end
|
||||||
@@ -8,6 +8,15 @@ module Wiki
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.create_content! tag_name:, body:, created_by_user:, message: nil
|
def self.create_content! tag_name:, body:, created_by_user:, message: nil
|
||||||
|
if tag_name.tag && tag_name.language_code == 'ja'
|
||||||
|
return TagDescriptionOperation.create!(
|
||||||
|
tag: tag_name.tag, body:, created_by_user:, message:)
|
||||||
|
end
|
||||||
|
|
||||||
|
create_legacy_content!(tag_name:, body:, created_by_user:, message:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create_legacy_content! tag_name:, body:, created_by_user:, message: nil
|
||||||
normalised = normalise_body(body)
|
normalised = normalise_body(body)
|
||||||
|
|
||||||
page = WikiPage.new(tag_name:,
|
page = WikiPage.new(tag_name:,
|
||||||
@@ -33,6 +42,18 @@ module Wiki
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
|
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
|
||||||
|
description = TagDescription.find_by(id: page.id)
|
||||||
|
if description
|
||||||
|
return TagDescriptionOperation.update!(
|
||||||
|
tag: description.tag, body:, updated_by_user: created_user,
|
||||||
|
message:, base_revision_id:)
|
||||||
|
end
|
||||||
|
|
||||||
|
legacy_content!(page:, body:, created_user:, message:, base_revision_id:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.legacy_content! page:, body:, created_user:, message: nil,
|
||||||
|
base_revision_id: nil
|
||||||
new(page:, created_user:).content!(body:, message:, base_revision_id:)
|
new(page:, created_user:).content!(body:, message:, base_revision_id:)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,9 +78,6 @@ module Wiki
|
|||||||
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
|
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
|
||||||
tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))
|
tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))
|
||||||
|
|
||||||
line_id_by_sha = upsert_lines!(lines, line_shas)
|
|
||||||
line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@page.lock!
|
@page.lock!
|
||||||
|
|
||||||
@@ -72,6 +90,8 @@ module Wiki
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
line_id_by_sha = upsert_lines!(lines, line_shas)
|
||||||
|
line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }
|
||||||
@page.update!(body: normalised)
|
@page.update!(body: normalised)
|
||||||
|
|
||||||
WikiVersionRecorder.record!(
|
WikiVersionRecorder.record!(
|
||||||
@@ -94,7 +114,6 @@ module Wiki
|
|||||||
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
|
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
|
||||||
end
|
end
|
||||||
WikiRevisionLine.insert_all!(rows) if rows.any?
|
WikiRevisionLine.insert_all!(rows) if rows.any?
|
||||||
|
|
||||||
rev
|
rev
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
module Wiki
|
||||||
|
class CompatibilityWriter
|
||||||
|
def self.create! tag_name:, body:, created_by_user:, message: nil
|
||||||
|
Commit.create_legacy_content!(
|
||||||
|
tag_name:, body:, created_by_user:, message:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.update! page:, body:, updated_by_user:, message: nil,
|
||||||
|
base_revision_id: nil
|
||||||
|
normalised = Commit.normalise_body(body)
|
||||||
|
if normalised.blank?
|
||||||
|
return Commit.legacy_content!(
|
||||||
|
page:, body: normalised, created_user: updated_by_user, message:,
|
||||||
|
base_revision_id:)
|
||||||
|
end
|
||||||
|
|
||||||
|
if base_revision_id.present?
|
||||||
|
current_id = page.wiki_revisions.maximum(:id)
|
||||||
|
if current_id && current_id != base_revision_id.to_i
|
||||||
|
raise Commit::Conflict,
|
||||||
|
"競合が発生してゐます" +
|
||||||
|
"(現在の Id.:#{ current_id },ベース Id.:#{ base_revision_id })."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return if Commit.normalise_body(page.body) == normalised
|
||||||
|
|
||||||
|
Commit.legacy_content!(
|
||||||
|
page:, body: normalised, created_user: updated_by_user, message:,
|
||||||
|
base_revision_id:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -14,6 +14,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
scope :name do
|
scope :name do
|
||||||
get ':name/deerjikists', action: :deerjikists_by_name
|
get ':name/deerjikists', action: :deerjikists_by_name
|
||||||
|
get ':name/description', to: 'tag_descriptions#show_by_title'
|
||||||
get ':name/materials', action: :materials_by_name
|
get ':name/materials', action: :materials_by_name
|
||||||
get ':name', action: :show_by_name
|
get ':name', action: :show_by_name
|
||||||
end
|
end
|
||||||
@@ -25,6 +26,9 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
get :deerjikists
|
get :deerjikists
|
||||||
put :deerjikists, action: :update_deerjikists
|
put :deerjikists, action: :update_deerjikists
|
||||||
|
|
||||||
|
get :description, to: 'tag_descriptions#show'
|
||||||
|
put :description, to: 'tag_descriptions#update'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -35,7 +39,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :wiki_pages, path: 'wiki', only: [:index, :show, :create, :update] do
|
resources :wiki_pages, path: 'wiki', only: [:index, :show, :create, :update] do
|
||||||
collection do
|
collection do
|
||||||
get :search
|
get :search, action: :index
|
||||||
get :changes
|
get :changes
|
||||||
|
|
||||||
scope :title do
|
scope :title do
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
class CreateTagDescriptions < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :tag_description_versions do |t|
|
||||||
|
t.references :tag, null: false, foreign_key: false
|
||||||
|
t.string :language_code, limit: 16, null: false
|
||||||
|
t.integer :version_no, null: false
|
||||||
|
t.string :event_type, null: false
|
||||||
|
t.text :body, null: false
|
||||||
|
t.string :source_language_code, limit: 16
|
||||||
|
t.integer :source_version_no
|
||||||
|
t.references :created_by_user
|
||||||
|
t.datetime :created_at, null: false
|
||||||
|
|
||||||
|
t.index [:tag_id, :language_code, :version_no], unique: true
|
||||||
|
t.foreign_key :tag_description_versions,
|
||||||
|
column: [:tag_id, :source_language_code, :source_version_no],
|
||||||
|
primary_key: [:tag_id, :language_code, :version_no]
|
||||||
|
|
||||||
|
t.check_constraint 'version_no > 0',
|
||||||
|
name: 'chk_tag_description_versions_version_no_positive'
|
||||||
|
|
||||||
|
t.check_constraint "event_type IN ('create', 'update', 'discard', 'restore')",
|
||||||
|
name: 'tag_description_versions_event_type_valid'
|
||||||
|
|
||||||
|
t.check_constraint ('(source_language_code IS NULL AND source_version_no IS NULL) OR ' +
|
||||||
|
'(source_language_code IS NOT NULL AND source_version_no IS NOT NULL)'),
|
||||||
|
name: 'chk_tag_description_versions_source_pair'
|
||||||
|
|
||||||
|
t.check_constraint 'source_language_code IS NULL OR source_language_code <> language_code',
|
||||||
|
name: 'chk_tag_description_versions_source_language_code_different'
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :tag_descriptions do |t|
|
||||||
|
t.references :tag, null: false, foreign_key: true
|
||||||
|
t.string :language_code, limit: 16, null: false, index: true
|
||||||
|
t.integer :version_no, null: false
|
||||||
|
t.text :body, null: false
|
||||||
|
t.string :source_language_code, limit: 16
|
||||||
|
t.integer :source_version_no
|
||||||
|
t.references :created_by_user, foreign_key: { to_table: :users }
|
||||||
|
t.references :updated_by_user, foreign_key: { to_table: :users }
|
||||||
|
t.timestamps
|
||||||
|
|
||||||
|
t.foreign_key :languages, column: :language_code, primary_key: :code
|
||||||
|
t.foreign_key :tag_description_versions,
|
||||||
|
column: [:tag_id, :source_language_code, :source_version_no],
|
||||||
|
primary_key: [:tag_id, :language_code, :version_no]
|
||||||
|
|
||||||
|
t.index [:tag_id, :language_code], unique: true
|
||||||
|
t.index [:source_language_code, :source_version_no]
|
||||||
|
|
||||||
|
t.check_constraint 'version_no > 0',
|
||||||
|
name: 'chk_tag_descriptions_version_no_positive'
|
||||||
|
|
||||||
|
t.check_constraint ('(source_language_code IS NULL AND source_version_no IS NULL) OR ' +
|
||||||
|
'(source_language_code IS NOT NULL AND source_version_no IS NOT NULL)'),
|
||||||
|
name: 'chk_tag_descriptions_source_pair'
|
||||||
|
|
||||||
|
t.check_constraint 'source_language_code IS NULL OR source_language_code <> language_code',
|
||||||
|
name: 'chk_tag_descriptions_source_language_code_different'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
class MigrateTagDescriptionsFromWikiPages < ActiveRecord::Migration[8.0]
|
||||||
|
class WikiPage < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_pages'
|
||||||
|
end
|
||||||
|
|
||||||
|
class WikiRevision < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_revisions'
|
||||||
|
end
|
||||||
|
|
||||||
|
class WikiRevisionLine < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_revision_lines'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagName < ActiveRecord::Base
|
||||||
|
self.table_name = 'tag_names'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagDescription < ActiveRecord::Base
|
||||||
|
self.table_name = 'tag_descriptions'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagDescriptionVersion < ActiveRecord::Base
|
||||||
|
self.table_name = 'tag_description_versions'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
WikiPage.find_each do |page|
|
||||||
|
tag_name = TagName.find_by(id: page.tag_name_id, language_code: 'ja')
|
||||||
|
next unless tag_name&.tag_id
|
||||||
|
|
||||||
|
revisions = WikiRevision.where(wiki_page_id: page.id).order(:id).to_a
|
||||||
|
|
||||||
|
revisions.each_with_index do |revision, index|
|
||||||
|
version_no = index + 1
|
||||||
|
|
||||||
|
tag_id = tag_name[:tag_id]
|
||||||
|
|
||||||
|
unless tag_id
|
||||||
|
raise (
|
||||||
|
'tag_id is nil: ' +
|
||||||
|
"wiki_page_id=#{ page.id }, " +
|
||||||
|
"tag_name_id=#{ tag_name.id }, " +
|
||||||
|
"tag_name=#{ tag_name.attributes.inspect}")
|
||||||
|
end
|
||||||
|
|
||||||
|
revision_id = revision.id
|
||||||
|
|
||||||
|
say (
|
||||||
|
"wiki_page_id=#{ page.id }, " +
|
||||||
|
"tag_name_id=#{ tag_name.id }, " +
|
||||||
|
"tag_id=#{ tag_id }, " +
|
||||||
|
"revision_id=#{ revision_id }")
|
||||||
|
|
||||||
|
version = TagDescriptionVersion.new(
|
||||||
|
id: revision_id,
|
||||||
|
tag_id:,
|
||||||
|
language_code: 'ja',
|
||||||
|
version_no:,
|
||||||
|
event_type: version_no == 1 ? 'create' : 'update',
|
||||||
|
body: revision_body(revision_id),
|
||||||
|
source_language_code: nil,
|
||||||
|
source_version_no: nil,
|
||||||
|
created_by_user_id: revision.created_user_id,
|
||||||
|
created_at: revision.created_at)
|
||||||
|
|
||||||
|
say version.attributes.inspect
|
||||||
|
|
||||||
|
version.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
next if page.discarded_at.present?
|
||||||
|
next if revisions.empty?
|
||||||
|
|
||||||
|
TagDescription.create!(
|
||||||
|
id: page.id,
|
||||||
|
tag_id: tag_name.tag_id,
|
||||||
|
language_code: 'ja',
|
||||||
|
version_no: revisions.length,
|
||||||
|
body: page.body,
|
||||||
|
source_language_code: nil,
|
||||||
|
source_version_no: nil,
|
||||||
|
created_by_user_id: page.created_user_id,
|
||||||
|
updated_by_user_id: page.updated_user_id,
|
||||||
|
created_at: page.created_at,
|
||||||
|
updated_at: page.updated_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def revision_body revision_id
|
||||||
|
WikiRevisionLine
|
||||||
|
.joins('INNER JOIN wiki_lines ' +
|
||||||
|
'ON wiki_lines.id = wiki_revision_lines.wiki_line_id')
|
||||||
|
.where(wiki_revision_id: revision_id)
|
||||||
|
.order(:position)
|
||||||
|
.pluck('wiki_lines.body')
|
||||||
|
.join("\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
class RepairWikiUpdatedAt < ActiveRecord::Migration[8.0]
|
||||||
|
class WikiPage < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_pages'
|
||||||
|
end
|
||||||
|
|
||||||
|
class WikiVersion < ActiveRecord::Base
|
||||||
|
self.table_name = 'wiki_versions'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagDescription < ActiveRecord::Base
|
||||||
|
self.table_name = 'tag_descriptions'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagDescriptionVersion < ActiveRecord::Base
|
||||||
|
self.table_name = 'tag_description_versions'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
say_with_time 'Repairing wiki_pages.updated_at from wiki_versions' do
|
||||||
|
WikiPage.find_each do |page|
|
||||||
|
version =
|
||||||
|
WikiVersion
|
||||||
|
.where(wiki_page_id: page.id)
|
||||||
|
.order(version_no: :desc)
|
||||||
|
.first
|
||||||
|
|
||||||
|
next unless version
|
||||||
|
|
||||||
|
page.update_columns(updated_at: version.created_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
say_with_time 'Repairing tag_descriptions.updated_at from tag_description_versions' do
|
||||||
|
TagDescription.find_each do |description|
|
||||||
|
version =
|
||||||
|
TagDescriptionVersion
|
||||||
|
.where(
|
||||||
|
tag_id: description.tag_id,
|
||||||
|
language_code: description.language_code)
|
||||||
|
.order(version_no: :desc)
|
||||||
|
.first
|
||||||
|
|
||||||
|
next unless version
|
||||||
|
|
||||||
|
description.update_columns(updated_at: version.created_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
;
|
||||||
|
end
|
||||||
|
end
|
||||||
生成ファイル
+50
-17
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do
|
ActiveRecord::Schema[8.0].define(version: 2026_10_05_000000) do
|
||||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -440,6 +440,49 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do
|
|||||||
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
t.index ["user_id"], name: "index_settings_on_user_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "tag_description_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.bigint "tag_id", null: false
|
||||||
|
t.string "language_code", limit: 16, null: false
|
||||||
|
t.integer "version_no", null: false
|
||||||
|
t.string "event_type", null: false
|
||||||
|
t.text "body", null: false
|
||||||
|
t.string "source_language_code", limit: 16
|
||||||
|
t.integer "source_version_no"
|
||||||
|
t.bigint "created_by_user_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.index ["created_by_user_id"], name: "index_tag_description_versions_on_created_by_user_id"
|
||||||
|
t.index ["tag_id", "language_code", "version_no"], name: "idx_on_tag_id_language_code_version_no_ced77e57e6", unique: true
|
||||||
|
t.index ["tag_id", "source_language_code", "source_version_no"], name: "fk_rails_4f13094dbc"
|
||||||
|
t.index ["tag_id"], name: "index_tag_description_versions_on_tag_id"
|
||||||
|
t.check_constraint "((`source_language_code` is null) and (`source_version_no` is null)) or ((`source_language_code` is not null) and (`source_version_no` is not null))", name: "chk_tag_description_versions_source_pair"
|
||||||
|
t.check_constraint "(`source_language_code` is null) or (`source_language_code` <> `language_code`)", name: "chk_tag_description_versions_source_language_code_different"
|
||||||
|
t.check_constraint "`event_type` in (_utf8mb4'create',_utf8mb4'update',_utf8mb4'discard',_utf8mb4'restore')", name: "tag_description_versions_event_type_valid"
|
||||||
|
t.check_constraint "`version_no` > 0", name: "chk_tag_description_versions_version_no_positive"
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "tag_descriptions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.bigint "tag_id", null: false
|
||||||
|
t.string "language_code", limit: 16, null: false
|
||||||
|
t.integer "version_no", null: false
|
||||||
|
t.text "body", null: false
|
||||||
|
t.string "source_language_code", limit: 16
|
||||||
|
t.integer "source_version_no"
|
||||||
|
t.bigint "created_by_user_id"
|
||||||
|
t.bigint "updated_by_user_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["created_by_user_id"], name: "index_tag_descriptions_on_created_by_user_id"
|
||||||
|
t.index ["language_code"], name: "index_tag_descriptions_on_language_code"
|
||||||
|
t.index ["source_language_code", "source_version_no"], name: "idx_on_source_language_code_source_version_no_00117fe02a"
|
||||||
|
t.index ["tag_id", "language_code"], name: "index_tag_descriptions_on_tag_id_and_language_code", unique: true
|
||||||
|
t.index ["tag_id", "source_language_code", "source_version_no"], name: "fk_rails_d1be0a7696"
|
||||||
|
t.index ["tag_id"], name: "index_tag_descriptions_on_tag_id"
|
||||||
|
t.index ["updated_by_user_id"], name: "index_tag_descriptions_on_updated_by_user_id"
|
||||||
|
t.check_constraint "((`source_language_code` is null) and (`source_version_no` is null)) or ((`source_language_code` is not null) and (`source_version_no` is not null))", name: "chk_tag_descriptions_source_pair"
|
||||||
|
t.check_constraint "(`source_language_code` is null) or (`source_language_code` <> `language_code`)", name: "chk_tag_descriptions_source_language_code_different"
|
||||||
|
t.check_constraint "`version_no` > 0", name: "chk_tag_descriptions_version_no_positive"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "parent_tag_id", null: false
|
t.bigint "parent_tag_id", null: false
|
||||||
@@ -656,19 +699,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do
|
|||||||
t.index ["banned_at"], name: "index_users_on_banned_at"
|
t.index ["banned_at"], name: "index_users_on_banned_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "wiki_assets", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "wiki_page_id", null: false
|
|
||||||
t.integer "no", null: false
|
|
||||||
t.string "alt_text"
|
|
||||||
t.binary "sha256", limit: 32, null: false
|
|
||||||
t.bigint "created_by_user_id", null: false
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["created_by_user_id"], name: "index_wiki_assets_on_created_by_user_id"
|
|
||||||
t.index ["wiki_page_id", "no"], name: "index_wiki_assets_on_wiki_page_id_and_no", unique: true
|
|
||||||
t.index ["wiki_page_id", "sha256"], name: "index_wiki_assets_on_wiki_page_id_and_sha256", unique: true
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "sha256", limit: 64, null: false
|
t.string "sha256", limit: 64, null: false
|
||||||
t.text "body", null: false
|
t.text "body", null: false
|
||||||
@@ -685,7 +715,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.datetime "discarded_at"
|
t.datetime "discarded_at"
|
||||||
t.integer "next_asset_no", default: 1, null: false
|
|
||||||
t.integer "version_no", null: false
|
t.integer "version_no", null: false
|
||||||
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
||||||
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
|
||||||
@@ -788,6 +817,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do
|
|||||||
add_foreign_key "post_versions", "users", column: "created_by_user_id"
|
add_foreign_key "post_versions", "users", column: "created_by_user_id"
|
||||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||||
add_foreign_key "settings", "users"
|
add_foreign_key "settings", "users"
|
||||||
|
add_foreign_key "tag_description_versions", "tag_description_versions", column: ["tag_id", "source_language_code", "source_version_no"], primary_key: ["tag_id", "language_code", "version_no"]
|
||||||
|
add_foreign_key "tag_descriptions", "languages", column: "language_code", primary_key: "code"
|
||||||
|
add_foreign_key "tag_descriptions", "tag_description_versions", column: ["tag_id", "source_language_code", "source_version_no"], primary_key: ["tag_id", "language_code", "version_no"]
|
||||||
|
add_foreign_key "tag_descriptions", "tags"
|
||||||
|
add_foreign_key "tag_descriptions", "users", column: "created_by_user_id"
|
||||||
|
add_foreign_key "tag_descriptions", "users", column: "updated_by_user_id"
|
||||||
add_foreign_key "tag_implications", "tags"
|
add_foreign_key "tag_implications", "tags"
|
||||||
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
||||||
add_foreign_key "tag_names", "languages", column: "language_code", primary_key: "code"
|
add_foreign_key "tag_names", "languages", column: "language_code", primary_key: "code"
|
||||||
@@ -822,8 +857,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_09_22_040000) do
|
|||||||
add_foreign_key "user_post_views", "posts"
|
add_foreign_key "user_post_views", "posts"
|
||||||
add_foreign_key "user_post_views", "users"
|
add_foreign_key "user_post_views", "users"
|
||||||
add_foreign_key "user_theme_slots", "users"
|
add_foreign_key "user_theme_slots", "users"
|
||||||
add_foreign_key "wiki_assets", "users", column: "created_by_user_id"
|
|
||||||
add_foreign_key "wiki_assets", "wiki_pages"
|
|
||||||
add_foreign_key "wiki_pages", "tag_names"
|
add_foreign_key "wiki_pages", "tag_names"
|
||||||
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
||||||
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
require_relative '../../db/migrate/20261003010000_migrate_tag_descriptions_from_wiki_pages'
|
||||||
|
|
||||||
|
RSpec.describe 'Tag description migration identity' do
|
||||||
|
it 'preserves every revision ID and repeated body in an independent sequence' do
|
||||||
|
user = create_member_user!
|
||||||
|
tag_name = create(:tag_name, name: 'description_migration')
|
||||||
|
page = Wiki::Commit.create_content!(
|
||||||
|
tag_name:, body: 'A', created_by_user: user)
|
||||||
|
revisions = [page.current_revision]
|
||||||
|
['A', 'B', 'B', 'A'].each do |body|
|
||||||
|
revisions << Wiki::Commit.content!(
|
||||||
|
page:, body:, created_user: user, base_revision_id: revisions.last.id)
|
||||||
|
end
|
||||||
|
tag = create(:tag, primary_tag_name: tag_name)
|
||||||
|
legacy_state = [page.reload.attributes, revisions.map(&:attributes)]
|
||||||
|
expect(page.wiki_versions.count).to eq(3)
|
||||||
|
|
||||||
|
migration = MigrateTagDescriptionsFromWikiPages.new
|
||||||
|
migration.suppress_messages { migration.up }
|
||||||
|
|
||||||
|
versions = TagDescriptionVersion.where(tag_id: tag.id, language_code: 'ja')
|
||||||
|
.order(:version_no)
|
||||||
|
expect(versions.pluck(:id)).to eq(revisions.map(&:id))
|
||||||
|
expect(versions.pluck(:version_no)).to eq([1, 2, 3, 4, 5])
|
||||||
|
expect(versions.pluck(:body)).to eq(['A', 'A', 'B', 'B', 'A'])
|
||||||
|
expect(versions.pluck(:created_by_user_id)).to eq([user.id] * 5)
|
||||||
|
expect(versions.pluck(:created_at)).to eq(revisions.map(&:created_at))
|
||||||
|
expect(TagDescription.find(page.id)).to have_attributes(
|
||||||
|
tag_id: tag.id, language_code: 'ja', version_no: 5, body: 'A')
|
||||||
|
expect([page.reload.attributes, revisions.map { |revision| revision.reload.attributes }])
|
||||||
|
.to eq(legacy_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Tag description Wiki compatibility', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
let!(:tag_name) { create(:tag_name, name: 'description_contract') }
|
||||||
|
let!(:page) do
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name:, body: 'old-revision', created_by_user: user)
|
||||||
|
end
|
||||||
|
let!(:revisions) do
|
||||||
|
first = page.current_revision
|
||||||
|
second = Wiki::Commit.content!(page:, body: 'old-second', created_user: user)
|
||||||
|
third = Wiki::Commit.content!(page:, body: 'old-third', created_user: user)
|
||||||
|
[first, second, third]
|
||||||
|
end
|
||||||
|
let!(:tag) { create(:tag, primary_tag_name: tag_name) }
|
||||||
|
let!(:versions) do
|
||||||
|
revisions.each_with_index.map do |revision, index|
|
||||||
|
TagDescriptionVersion.create!(
|
||||||
|
id: revision.id, tag_id: tag.id, language_code: 'ja',
|
||||||
|
version_no: index + 1, event_type: index.zero? ? 'create' : 'update',
|
||||||
|
body: index == 2 ? 'new-version' : 'A',
|
||||||
|
created_by_user_id: user.id, created_at: revision.created_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
let!(:description) do
|
||||||
|
TagDescription.create!(
|
||||||
|
id: page.id, tag:, language_code: 'ja', version_no: 3, body: 'new',
|
||||||
|
created_by_user_id: user.id, updated_by_user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
before { page.update_columns(body: 'old') }
|
||||||
|
|
||||||
|
# Observe real SELECTs, including cached queries, only during the request.
|
||||||
|
def without_legacy_reads
|
||||||
|
queries = []
|
||||||
|
subscriber = lambda do |*args|
|
||||||
|
sql = args.last.fetch(:sql)
|
||||||
|
if sql.match?(/\bSELECT\b/i) && sql.match?(/\bwiki_(pages|revisions|versions)\b/i)
|
||||||
|
queries << sql
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') { yield }
|
||||||
|
expect(queries).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'show' do
|
||||||
|
it 'uses current content and revision identity without legacy reads' do
|
||||||
|
without_legacy_reads { get "/wiki/#{ description.id }" }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'new', 'revision_id' => versions.last.id,
|
||||||
|
'pred' => versions.second.id, 'succ' => nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
[0, 1, 2].each do |index|
|
||||||
|
it "navigates displayed version #{ index + 1 }, preserving repeated bodies" do
|
||||||
|
without_legacy_reads do
|
||||||
|
get "/wiki/#{ description.id }", params: { version: versions[index].id }
|
||||||
|
end
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include(
|
||||||
|
'body' => versions[index].body,
|
||||||
|
'revision_id' => versions[index].id,
|
||||||
|
'pred' => index.zero? ? nil : versions[index - 1].id,
|
||||||
|
'succ' => index == 2 ? nil : versions[index + 1].id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not fall back to a revision absent from new history' do
|
||||||
|
versions.first.destroy!
|
||||||
|
without_legacy_reads do
|
||||||
|
get "/wiki/#{ description.id }", params: { version: revisions.first.id }
|
||||||
|
end
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not fall back when the new current revision is missing' do
|
||||||
|
versions.last.destroy!
|
||||||
|
without_legacy_reads { get "/wiki/#{ description.id }" }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'revision resource scope' do
|
||||||
|
let!(:other_tag) { create(:tag, primary_name: 'other_description_contract') }
|
||||||
|
let!(:other_version) do
|
||||||
|
TagDescriptionVersion.create!(
|
||||||
|
tag_id: other_tag.id, language_code: 'ja', version_no: 1,
|
||||||
|
event_type: 'create', body: 'other tag history',
|
||||||
|
created_by_user_id: user.id, created_at: Time.current)
|
||||||
|
end
|
||||||
|
let!(:other_description) do
|
||||||
|
TagDescription.create!(
|
||||||
|
tag: other_tag, language_code: 'ja', version_no: 1, body: 'other tag history',
|
||||||
|
created_by_user_id: user.id, updated_by_user_id: user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when show specifies another tag description version' do
|
||||||
|
get "/wiki/#{ description.id }", params: { version: other_version.id }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
expect(response.body).not_to include(other_version.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when diff from belongs to another tag description' do
|
||||||
|
get "/wiki/#{ description.id }/diff",
|
||||||
|
params: { from: other_version.id, to: versions.last.id }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
expect(response.body).not_to include(other_version.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when diff to belongs to another tag description' do
|
||||||
|
get "/wiki/#{ description.id }/diff",
|
||||||
|
params: { from: versions.first.id, to: other_version.id }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
expect(response.body).not_to include(other_version.body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'show_by_title' do
|
||||||
|
it 'resolves TagName and reads current content from new tables' do
|
||||||
|
without_legacy_reads { get "/wiki/title/#{ tag_name.name }" }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('id' => description.id, 'body' => 'new',
|
||||||
|
'revision_id' => versions.last.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses historical content and navigation from the new history' do
|
||||||
|
get "/wiki/title/#{ tag_name.name }", params: { version: versions.second.id }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'A', 'revision_id' => versions.second.id,
|
||||||
|
'pred' => versions.first.id, 'succ' => versions.last.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores changes to same-title legacy content and revision metadata' do
|
||||||
|
get "/wiki/title/#{ tag_name.name }"
|
||||||
|
original = json
|
||||||
|
page.update_columns(body: 'changed legacy body')
|
||||||
|
revisions.last.update_columns(message: 'legacy message', created_at: 1.day.ago)
|
||||||
|
revisions.last.wiki_lines.update_all(body: 'changed legacy revision')
|
||||||
|
get "/wiki/title/#{ tag_name.name }"
|
||||||
|
expect(json).to eq(original)
|
||||||
|
expect(json.fetch('body')).to eq('new')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'exists' do
|
||||||
|
it 'recognises an ID without reading legacy tables' do
|
||||||
|
without_legacy_reads { get "/wiki/#{ description.id }/exists" }
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognises a title without reading legacy tables' do
|
||||||
|
without_legacy_reads { get "/wiki/title/#{ tag_name.name }/exists" }
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without a legacy page' do
|
||||||
|
before do
|
||||||
|
WikiVersion.where(wiki_page_id: page.id).delete_all
|
||||||
|
page.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognises the description ID alone' do
|
||||||
|
get "/wiki/#{ description.id }/exists"
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognises the TagName alone' do
|
||||||
|
get "/wiki/title/#{ tag_name.name }/exists"
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows the description by title' do
|
||||||
|
get "/wiki/title/#{ tag_name.name }"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'new', 'revision_id' => versions.last.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'diff' do
|
||||||
|
[false, true].each do |current|
|
||||||
|
it "uses new bodies with #{ current ? 'implicit current' : 'explicit to' }" do
|
||||||
|
params = { from: versions.first.id }
|
||||||
|
params[:to] = versions.last.id unless current
|
||||||
|
get "/wiki/#{ description.id }/diff", params: params
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include(
|
||||||
|
'wiki_page_id' => description.id,
|
||||||
|
'older_revision_id' => versions.first.id,
|
||||||
|
'newer_revision_id' => versions.last.id,
|
||||||
|
'diff' => [{ 'type' => 'removed', 'content' => 'A' },
|
||||||
|
{ 'type' => 'added', 'content' => 'new-version' }])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not query legacy tables for diff' do
|
||||||
|
without_legacy_reads do
|
||||||
|
get "/wiki/#{ description.id }/diff",
|
||||||
|
params: { from: versions.first.id, to: versions.last.id }
|
||||||
|
end
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps distinct revision IDs for identical consecutive bodies' do
|
||||||
|
get "/wiki/#{ description.id }/diff",
|
||||||
|
params: { from: versions.first.id, to: versions.second.id }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include(
|
||||||
|
'older_revision_id' => versions.first.id,
|
||||||
|
'newer_revision_id' => versions.second.id,
|
||||||
|
'diff' => [{ 'type' => 'context', 'content' => 'A' }])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'changes' do
|
||||||
|
it 'lists every new version including consecutive identical bodies' do
|
||||||
|
without_legacy_reads { get '/wiki/changes', params: { id: description.id } }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
|
||||||
|
expect(json.first.keys).to match_array(
|
||||||
|
['revision_id', 'pred', 'succ', 'wiki_page', 'user', 'kind', 'message', 'timestamp'])
|
||||||
|
expect(json.first).to include(
|
||||||
|
'pred' => versions.second.id, 'succ' => nil, 'kind' => 'content',
|
||||||
|
'timestamp' => versions.last.created_at.iso8601(3),
|
||||||
|
'user' => { 'id' => user.id, 'name' => user.name })
|
||||||
|
expect(json.first.fetch('wiki_page')).to include('id' => description.id,
|
||||||
|
'title' => tag_name.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'ignores legacy-only revisions and metadata changes' do
|
||||||
|
get '/wiki/changes', params: { id: description.id }
|
||||||
|
original = json
|
||||||
|
revisions.first.update_columns(message: 'legacy changed', created_at: 1.day.ago)
|
||||||
|
WikiRevision.create!(
|
||||||
|
wiki_page: page, created_user: user, kind: :content,
|
||||||
|
tree_sha256: Digest::SHA256.hexdigest('legacy only'), lines_count: 0)
|
||||||
|
get '/wiki/changes', params: { id: description.id }
|
||||||
|
expect(json).to eq(original)
|
||||||
|
expect(json.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'mixed changes' do
|
||||||
|
it 'includes unmigrated history and ignores migrated legacy-only revisions' do
|
||||||
|
legacy_page = Wiki::Commit.create_content!(
|
||||||
|
tag_name: create(:tag_name, name: 'Help_mixed'),
|
||||||
|
body: 'help', created_by_user: user)
|
||||||
|
legacy_revision = legacy_page.current_revision
|
||||||
|
extra = WikiRevision.create!(
|
||||||
|
wiki_page: page, created_user: user, kind: :content,
|
||||||
|
tree_sha256: Digest::SHA256.hexdigest('legacy only'), lines_count: 0)
|
||||||
|
|
||||||
|
get '/wiki/changes'
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
migrated = json.select { |row| row.fetch('wiki_page').fetch('id') == description.id }
|
||||||
|
expect(migrated.map { |row| row.fetch('revision_id') }).to eq(versions.reverse.map(&:id))
|
||||||
|
expect(json.map { |row| row.fetch('revision_id') }).not_to include(extra.id)
|
||||||
|
expect(json).to include(include(
|
||||||
|
'revision_id' => legacy_revision.id,
|
||||||
|
'wiki_page' => include('id' => legacy_page.id, 'title' => 'Help_mixed')))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'index' do
|
||||||
|
[nil, 'description_contract'].each do |title|
|
||||||
|
it "returns new content once in #{ title ? 'filtered' : 'unfiltered' } index" do
|
||||||
|
get '/wiki', params: { title: }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
rows = json.select { |row| row.fetch('id') == description.id }
|
||||||
|
expect(rows.size).to eq(1)
|
||||||
|
expect(rows.first).to include('body' => 'new', 'title' => tag_name.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Tag description Wiki dual writes', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
let!(:tag_name) { create(:tag_name, name: 'description_write') }
|
||||||
|
let(:tag) { create(:tag, primary_tag_name: tag_name) }
|
||||||
|
let(:headers) { { 'X-Transfer-Code' => user.inheritance_code } }
|
||||||
|
|
||||||
|
def stored_state
|
||||||
|
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion,
|
||||||
|
TagName, TagVersion].map do |model|
|
||||||
|
model.order(:id).map(&:attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fail after a real INSERT/UPDATE, exercising rollback rather than a mock write.
|
||||||
|
def fail_save model
|
||||||
|
failure = Class.new(StandardError)
|
||||||
|
callback = ->(_record) { raise failure, 'injected persistence failure' }
|
||||||
|
model.set_callback(:save, :after, callback)
|
||||||
|
expect { yield }.to raise_error(failure)
|
||||||
|
ensure
|
||||||
|
model.skip_callback(:save, :after, callback)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /wiki' do
|
||||||
|
before { tag }
|
||||||
|
|
||||||
|
def create_description
|
||||||
|
post '/wiki', params: { title: tag_name.name, body: 'initial', message: 'create' },
|
||||||
|
headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates both current records and histories with a shared revision ID' do
|
||||||
|
expect { create_description }
|
||||||
|
.to change(WikiPage, :count).by(1)
|
||||||
|
.and change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
.and change(TagDescription, :count).by(1)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
|
page = WikiPage.find(json.fetch('id'))
|
||||||
|
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
||||||
|
version = TagDescriptionVersion.find_by!(tag_id: tag.id, language_code: 'ja')
|
||||||
|
expect(description).to have_attributes(id: page.id, body: 'initial', version_no: 1)
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
id: page.current_revision.id, body: 'initial', version_no: 1,
|
||||||
|
event_type: 'create', created_by_user_id: user.id)
|
||||||
|
expect(page.body).to eq(description.body)
|
||||||
|
expect(page.current_revision.body).to eq(version.body)
|
||||||
|
expect(page.wiki_versions.last.body).to eq(version.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
['missing_description', 'orphan_description'].each do |title|
|
||||||
|
it "rejects the bare title #{ title } without writing any resource" do
|
||||||
|
create(:tag_name, name: title) if title == 'orphan_description'
|
||||||
|
before_state = stored_state
|
||||||
|
|
||||||
|
post '/wiki', params: { title:, body: 'initial' }, headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json).to include('type' => 'validation_error')
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a namespaced legacy Wiki resource without a Tag' do
|
||||||
|
expect do
|
||||||
|
post '/wiki', params: { title: 'Help:description_write', body: 'legacy' },
|
||||||
|
headers: headers
|
||||||
|
end.to change(WikiPage, :count).by(1)
|
||||||
|
.and change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
.and change(TagDescription, :count).by(0)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(0)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
page = WikiPage.find(json.fetch('id'))
|
||||||
|
expect(page.tag_name.tag).to be_nil
|
||||||
|
expect(page.body).to eq('legacy')
|
||||||
|
expect(page.current_revision.body).to eq('legacy')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates only legacy Wiki for a namespaced title with an existing Tag' do
|
||||||
|
namespaced_name = create(:tag_name, name: 'help:foo')
|
||||||
|
namespaced_tag = create(:tag, primary_tag_name: namespaced_name)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post '/wiki', params: { title: namespaced_name.name, body: 'legacy' },
|
||||||
|
headers: headers
|
||||||
|
end.to change(WikiPage, :count).by(1)
|
||||||
|
.and change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
.and change(TagName, :count).by(0)
|
||||||
|
.and change(TagDescription, :count).by(0)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(0)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
page = WikiPage.find(json.fetch('id'))
|
||||||
|
expect(page).to have_attributes(tag_name_id: namespaced_name.id, body: 'legacy')
|
||||||
|
expect(page.tag_name.tag).to eq(namespaced_tag)
|
||||||
|
expect(page.current_revision.body).to eq('legacy')
|
||||||
|
expect(page.wiki_versions.last.body).to eq('legacy')
|
||||||
|
end
|
||||||
|
|
||||||
|
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
||||||
|
it "rolls back both systems when #{ model.name } persistence fails" do
|
||||||
|
tag_name
|
||||||
|
before_state = stored_state
|
||||||
|
aggregate_failures do
|
||||||
|
fail_save(model) { create_description }
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /wiki/:id' do
|
||||||
|
let!(:page) do
|
||||||
|
tag
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name:, body: 'before', created_by_user: user)
|
||||||
|
end
|
||||||
|
let!(:first_revision) { page.current_revision }
|
||||||
|
|
||||||
|
let!(:description) do
|
||||||
|
TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_description body: 'after', title: tag_name.name, base: first_revision.id
|
||||||
|
put "/wiki/#{ page.id }",
|
||||||
|
params: { title:, body:, message: 'edit', base_revision_id: base },
|
||||||
|
headers: headers
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'increments the independent description sequence and shares revision identity' do
|
||||||
|
expect(page.reload.version_no).to eq(1)
|
||||||
|
expect(description.version_no).to eq(1)
|
||||||
|
expect { update_description }
|
||||||
|
.to change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
revision = page.reload.current_revision
|
||||||
|
version = TagDescriptionVersion.find(revision.id)
|
||||||
|
expect(description.reload).to have_attributes(body: 'after', version_no: 2)
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
body: 'after', version_no: 2, event_type: 'update', created_by_user_id: user.id)
|
||||||
|
expect(page).to have_attributes(body: 'after', version_no: 2)
|
||||||
|
expect(revision.body).to eq(version.body)
|
||||||
|
expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses description identity even when its Wiki TagName no longer owns the Tag' do
|
||||||
|
tag_name.update_columns(tag_id: nil)
|
||||||
|
|
||||||
|
expect { update_description }
|
||||||
|
.to change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(1)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(description.reload).to have_attributes(body: 'after', version_no: 2)
|
||||||
|
expect(TagDescriptionVersion.find(page.reload.current_revision.id))
|
||||||
|
.to have_attributes(tag_id: tag.id, body: 'after', version_no: 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not add body history for a title-only update' do
|
||||||
|
revision_count = WikiRevision.count
|
||||||
|
wiki_version_count = WikiVersion.count
|
||||||
|
description_version_count = TagDescriptionVersion.count
|
||||||
|
wiki_version_no = page.reload.version_no
|
||||||
|
description_version_no = description.version_no
|
||||||
|
current_revision_id = page.current_revision.id
|
||||||
|
page_state = page.attributes
|
||||||
|
description_state = description.attributes
|
||||||
|
|
||||||
|
update_description(body: 'before', title: 'Help:description_renamed')
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(tag_name.reload.name).to eq('Help:description_renamed')
|
||||||
|
expect(WikiRevision.count).to eq(revision_count)
|
||||||
|
expect(WikiVersion.count).to eq(wiki_version_count)
|
||||||
|
expect(TagDescriptionVersion.count).to eq(description_version_count)
|
||||||
|
expect(page.reload).to have_attributes(body: 'before', version_no: wiki_version_no)
|
||||||
|
expect(page.current_revision.id).to eq(current_revision_id)
|
||||||
|
expect(page.reload.attributes).to eq(page_state)
|
||||||
|
expect(description.reload).to have_attributes(
|
||||||
|
body: 'before', version_no: description_version_no)
|
||||||
|
expect(description.reload.attributes).to eq(description_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not add body history for a TagName rename through tags' do
|
||||||
|
revision_count = WikiRevision.count
|
||||||
|
wiki_version_count = WikiVersion.count
|
||||||
|
description_version_count = TagDescriptionVersion.count
|
||||||
|
page_version_no = page.reload.version_no
|
||||||
|
current_revision_id = page.current_revision.id
|
||||||
|
|
||||||
|
patch "/tags/#{ tag.id }",
|
||||||
|
params: { name: 'description_tag_renamed' },
|
||||||
|
headers: headers
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(tag_name.reload.name).to eq('description_tag_renamed')
|
||||||
|
expect(WikiRevision.count).to eq(revision_count)
|
||||||
|
expect(WikiVersion.count).to eq(wiki_version_count)
|
||||||
|
expect(TagDescriptionVersion.count).to eq(description_version_count)
|
||||||
|
expect(page.reload.version_no).to eq(page_version_no)
|
||||||
|
expect(page.current_revision.id).to eq(current_revision_id)
|
||||||
|
expect(description.reload).to have_attributes(
|
||||||
|
body: 'before', version_no: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps both systems and the title unchanged on a stale revision conflict' do
|
||||||
|
update_description(body: 'current body', base: first_revision.id)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
|
stale_revision_id = first_revision.id
|
||||||
|
before_state = stored_state
|
||||||
|
|
||||||
|
update_description(
|
||||||
|
body: 'conflicting body',
|
||||||
|
title: 'conflicting_rename',
|
||||||
|
base: stale_revision_id)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:conflict)
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
expect(tag_name.reload.name).not_to eq('conflicting_rename')
|
||||||
|
end
|
||||||
|
|
||||||
|
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
||||||
|
it "rolls back both systems when #{ model.name } persistence fails" do
|
||||||
|
before_state = stored_state
|
||||||
|
aggregate_failures do
|
||||||
|
fail_save(model) do
|
||||||
|
update_description(body: 'rollback body', title: 'rollback_rename')
|
||||||
|
end
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[['identical body', 'before'],
|
||||||
|
['normalised-equivalent body', "before\n\n"]].each do |label, body|
|
||||||
|
it "does not write histories for #{ label }" do
|
||||||
|
before_state = stored_state
|
||||||
|
page_state = page.reload.attributes
|
||||||
|
description_state = description.reload.attributes
|
||||||
|
current_revision_id = page.current_revision.id
|
||||||
|
wiki_lines = WikiLine.order(:id).map(&:attributes)
|
||||||
|
revision_lines = WikiRevisionLine.order(:id).map(&:attributes)
|
||||||
|
page_version_no = page.version_no
|
||||||
|
description_version_no = description.version_no
|
||||||
|
|
||||||
|
update_description(body:)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(WikiRevision.count).to eq(before_state[1].length)
|
||||||
|
expect(WikiVersion.count).to eq(before_state[2].length)
|
||||||
|
expect(TagDescriptionVersion.count).to eq(before_state[4].length)
|
||||||
|
expect(page.reload.version_no).to eq(page_version_no)
|
||||||
|
expect(description.reload.version_no).to eq(description_version_no)
|
||||||
|
expect(page.current_revision.id).to eq(current_revision_id)
|
||||||
|
expect(page.reload.attributes).to eq(page_state)
|
||||||
|
expect(description.reload.attributes).to eq(description_state)
|
||||||
|
expect(WikiLine.order(:id).map(&:attributes)).to eq(wiki_lines)
|
||||||
|
expect(WikiRevisionLine.order(:id).map(&:attributes)).to eq(revision_lines)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Tag description native API', type: :request do
|
||||||
|
let!(:author) { create_member_user! }
|
||||||
|
let(:editor) { create(:user, :member, name: 'description editor') }
|
||||||
|
let(:tag_name) { create(:tag_name, name: 'native_description') }
|
||||||
|
let(:tag) { create(:tag, primary_tag_name: tag_name) }
|
||||||
|
let(:headers) { { 'X-Transfer-Code' => editor.inheritance_code } }
|
||||||
|
let(:endpoint) { "/tags/#{ tag.id }/description" }
|
||||||
|
|
||||||
|
def stored_state
|
||||||
|
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion,
|
||||||
|
WikiLine, WikiRevisionLine, TagName, TagVersion].map do |model|
|
||||||
|
model.order(:id).map(&:attributes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fail_save model
|
||||||
|
failure = Class.new(StandardError)
|
||||||
|
callback = ->(_record) { raise failure, 'injected persistence failure' }
|
||||||
|
model.set_callback(:save, :after, callback)
|
||||||
|
expect { yield }.to raise_error(failure)
|
||||||
|
ensure
|
||||||
|
model.skip_callback(:save, :after, callback)
|
||||||
|
end
|
||||||
|
|
||||||
|
def without_legacy_reads
|
||||||
|
queries = []
|
||||||
|
subscriber = lambda do |*args|
|
||||||
|
sql = args.last.fetch(:sql)
|
||||||
|
if sql.match?(/\bSELECT\b/i) && sql.match?(/\bwiki_(pages|revisions|versions)\b/i)
|
||||||
|
queries << sql
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ActiveSupport::Notifications.subscribed(subscriber, 'sql.active_record') { yield }
|
||||||
|
expect(queries).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
def native_get path, action: 'show'
|
||||||
|
get path
|
||||||
|
# A missing route must not make the resource-not-found examples pass.
|
||||||
|
expect(request.path_parameters).to include(controller: 'tag_descriptions', action:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def native_write body:, request_headers: headers
|
||||||
|
put endpoint, params: { body: }, headers: request_headers
|
||||||
|
expect(request.path_parameters).to include(
|
||||||
|
controller: 'tag_descriptions', action: 'update')
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_current description
|
||||||
|
expect(json).to include(
|
||||||
|
'tag_id' => description.tag_id,
|
||||||
|
'language_code' => 'ja', 'body' => description.body,
|
||||||
|
'version_no' => description.version_no,
|
||||||
|
'created_by_user_id' => description.created_by_user_id,
|
||||||
|
'updated_by_user_id' => description.updated_by_user_id,
|
||||||
|
'created_at' => description.created_at.iso8601(3),
|
||||||
|
'updated_at' => description.updated_at.iso8601(3))
|
||||||
|
expect(json.keys).not_to include('title', 'name', 'wiki_page', 'tag_name_id')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'routing', type: :routing do
|
||||||
|
it 'routes GET by Tag ID to show' do
|
||||||
|
expect(get: '/tags/123/description').to route_to(
|
||||||
|
controller: 'tag_descriptions', action: 'show', id: '123')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes GET by TagName to show_by_title' do
|
||||||
|
expect(get: '/tags/name/native_description/description').to route_to(
|
||||||
|
controller: 'tag_descriptions', action: 'show_by_title', name: 'native_description')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not route POST by Tag ID' do
|
||||||
|
expect(post: '/tags/123/description').not_to be_routable
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'routes PUT by Tag ID to update' do
|
||||||
|
expect(put: '/tags/123/description').to route_to(
|
||||||
|
controller: 'tag_descriptions', action: 'update', id: '123')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'native reads' do
|
||||||
|
let!(:description) do
|
||||||
|
TagDescription.create!(
|
||||||
|
id: tag.id + 1_000_000, tag:, language_code: 'ja', body: 'native current',
|
||||||
|
version_no: 1, created_by_user_id: author.id, updated_by_user_id: editor.id)
|
||||||
|
end
|
||||||
|
let!(:version) do
|
||||||
|
TagDescriptionVersion.create!(
|
||||||
|
tag_id: tag.id, language_code: 'ja', body: 'native version', version_no: 1,
|
||||||
|
event_type: 'create', created_by_user_id: author.id, created_at: Time.current)
|
||||||
|
end
|
||||||
|
let(:name_endpoint) { "/tags/name/#{ tag_name.name }/description" }
|
||||||
|
|
||||||
|
it 'reads the current resource by Tag ID without a WikiPage' do
|
||||||
|
expect(description.id).not_to eq(tag.id)
|
||||||
|
expect(WikiPage.exists?(id: description.id)).to be(false)
|
||||||
|
without_legacy_reads { native_get(endpoint) }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect_current(description)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not interpret a compatibility handle as a Tag ID' do
|
||||||
|
other_tag = create(:tag, id: description.id, primary_name: 'handle_collision')
|
||||||
|
other = TagDescription.create!(
|
||||||
|
tag: other_tag, language_code: 'ja', body: 'other current', version_no: 1,
|
||||||
|
created_by_user_id: author.id, updated_by_user_id: author.id)
|
||||||
|
without_legacy_reads { native_get("/tags/#{ other_tag.id }/description") }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect_current(other)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with another language for the same Tag' do
|
||||||
|
before do
|
||||||
|
# Seed only the reference row; V1 does not enable an English locale.
|
||||||
|
Language.connection.execute(<<~SQL)
|
||||||
|
INSERT IGNORE INTO languages (code, name, created_at)
|
||||||
|
VALUES ('en', 'English', CURRENT_TIMESTAMP)
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'selects the Japanese resource' do
|
||||||
|
TagDescription.create!(
|
||||||
|
tag:, language_code: 'en', body: 'English current', version_no: 4,
|
||||||
|
created_by_user_id: author.id, updated_by_user_id: author.id)
|
||||||
|
without_legacy_reads { native_get(endpoint) }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect_current(description)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 if only the English description exists' do
|
||||||
|
description.update!(language_code: 'en')
|
||||||
|
without_legacy_reads { native_get(endpoint) }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resolves the primary Japanese TagName without a WikiPage' do
|
||||||
|
without_legacy_reads { native_get(name_endpoint, action: 'show_by_title') }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect_current(description)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resolves an alias through the existing TagName canonicalisation semantics' do
|
||||||
|
alias_name = create(:tag_name, :alias, tag:, name: 'native_alias')
|
||||||
|
expect(TagName.canonicalise(Locale.nipponese, [alias_name.name])).to eq([tag_name.name])
|
||||||
|
without_legacy_reads do
|
||||||
|
native_get("/tags/name/#{ alias_name.name }/description", action: 'show_by_title')
|
||||||
|
end
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect_current(description)
|
||||||
|
end
|
||||||
|
|
||||||
|
[:id, :name].each do |lookup|
|
||||||
|
it "ignores divergent legacy body and history for #{ lookup } lookup" do
|
||||||
|
page = WikiPage.create!(
|
||||||
|
id: description.id, tag_name:, body: 'old current',
|
||||||
|
created_user: author, updated_user: author)
|
||||||
|
revision = WikiRevision.create!(
|
||||||
|
wiki_page: page, created_user: author, kind: :content,
|
||||||
|
lines_count: 1, tree_sha256: Digest::SHA256.hexdigest('old revision'))
|
||||||
|
line = WikiLine.create!(body: 'old revision', sha256: revision.tree_sha256)
|
||||||
|
WikiRevisionLine.create!(wiki_revision: revision, wiki_line: line, position: 0)
|
||||||
|
WikiVersionRecorder.record!(page:, event_type: :create, created_by_user: author)
|
||||||
|
path = lookup == :id ? endpoint : name_endpoint
|
||||||
|
action = lookup == :id ? 'show' : 'show_by_title'
|
||||||
|
|
||||||
|
without_legacy_reads { native_get(path, action:) }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect_current(description)
|
||||||
|
original = json
|
||||||
|
page.update_columns(body: 'changed old current')
|
||||||
|
line.update_columns(body: 'changed old revision')
|
||||||
|
revision.update_columns(message: 'changed old history', created_at: 1.day.ago)
|
||||||
|
without_legacy_reads { native_get(path, action:) }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to eq(original)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'missing native resources' do
|
||||||
|
it 'returns 404 for a Tag without a description' do
|
||||||
|
without_legacy_reads { native_get(endpoint) }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 for a primary name without a description' do
|
||||||
|
tag
|
||||||
|
without_legacy_reads do
|
||||||
|
native_get("/tags/name/#{ tag_name.name }/description", action: 'show_by_title')
|
||||||
|
end
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 for a missing Tag' do
|
||||||
|
missing_id = Tag.maximum(:id).to_i + 1
|
||||||
|
without_legacy_reads { native_get("/tags/#{ missing_id }/description") }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /tags/:id/description' do
|
||||||
|
context 'without an existing description' do
|
||||||
|
before { tag }
|
||||||
|
|
||||||
|
it 'creates both current resources and histories with matching compatibility IDs' do
|
||||||
|
expect { native_write(body: 'created natively') }
|
||||||
|
.to change(TagDescription, :count).by(1)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(1)
|
||||||
|
.and change(WikiPage, :count).by(1)
|
||||||
|
.and change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
||||||
|
page = WikiPage.find(description.id)
|
||||||
|
revision = page.current_revision
|
||||||
|
version = TagDescriptionVersion.find(revision.id)
|
||||||
|
expect(description).to have_attributes(
|
||||||
|
body: 'created natively', version_no: 1,
|
||||||
|
created_by_user_id: editor.id, updated_by_user_id: editor.id)
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
tag_id: tag.id, language_code: 'ja', body: description.body,
|
||||||
|
version_no: 1, event_type: 'create', created_by_user_id: editor.id)
|
||||||
|
expect(page).to have_attributes(tag_name_id: tag_name.id, body: description.body)
|
||||||
|
expect(revision.body).to eq(version.body)
|
||||||
|
expect(page.wiki_versions.last.body).to eq(version.body)
|
||||||
|
expect_current(description)
|
||||||
|
|
||||||
|
get "/wiki/#{ description.id }"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => description.body, 'revision_id' => version.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
||||||
|
it "rolls back native create when #{ model.name } persistence fails" do
|
||||||
|
headers
|
||||||
|
before_state = stored_state
|
||||||
|
aggregate_failures do
|
||||||
|
fail_save(model) { native_write(body: 'rollback create') }
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an existing description' do
|
||||||
|
let!(:page) do
|
||||||
|
tag
|
||||||
|
Wiki::Commit.create_content!(tag_name:, body: 'before', created_by_user: author)
|
||||||
|
end
|
||||||
|
let!(:description) do
|
||||||
|
TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'writes matching histories when the body changes' do
|
||||||
|
expect(page.reload.version_no).to eq(1)
|
||||||
|
expect(description.version_no).to eq(1)
|
||||||
|
expect { native_write(body: 'after') }
|
||||||
|
.to change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(description.reload).to have_attributes(
|
||||||
|
body: 'after', version_no: 2,
|
||||||
|
created_by_user_id: author.id, updated_by_user_id: editor.id)
|
||||||
|
expect(page.reload).to have_attributes(body: 'after', version_no: 2)
|
||||||
|
|
||||||
|
revision = page.current_revision
|
||||||
|
version = TagDescriptionVersion.find(revision.id)
|
||||||
|
expect(version).to have_attributes(
|
||||||
|
tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update',
|
||||||
|
body: 'after', created_by_user_id: editor.id)
|
||||||
|
expect(revision.body).to eq(version.body)
|
||||||
|
expect(page.wiki_versions.order(:version_no).last.body).to eq(version.body)
|
||||||
|
get "/wiki/#{ description.id }"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'after', 'revision_id' => version.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
['before', "before\n\n"].each do |body|
|
||||||
|
it 'does not write histories when the normalised body is unchanged' do
|
||||||
|
before_state = stored_state
|
||||||
|
page_state = page.reload.attributes
|
||||||
|
description_state = description.reload.attributes
|
||||||
|
current_revision_id = page.current_revision.id
|
||||||
|
|
||||||
|
native_write(body:)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
expect(page.reload.attributes).to eq(page_state)
|
||||||
|
expect(description.reload.attributes).to eq(description_state)
|
||||||
|
expect(page.current_revision.id).to eq(current_revision_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
[WikiPage, WikiRevision, WikiVersion, TagDescription, TagDescriptionVersion].each do |model|
|
||||||
|
it "rolls back native update when #{ model.name } persistence fails" do
|
||||||
|
headers
|
||||||
|
before_state = stored_state
|
||||||
|
aggregate_failures do
|
||||||
|
fail_save(model) { native_write(body: 'rollback update') }
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'write access and validation' do
|
||||||
|
before { tag }
|
||||||
|
|
||||||
|
it 'requires authentication without changing either system' do
|
||||||
|
before_state = stored_state
|
||||||
|
native_write(body: 'unauthorised', request_headers: { })
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects a guest without changing either system' do
|
||||||
|
guest = create(:user, :guest)
|
||||||
|
before_state = stored_state
|
||||||
|
native_write(body: 'forbidden',
|
||||||
|
request_headers: { 'X-Transfer-Code' => guest.inheritance_code })
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects a banned member without changing either system' do
|
||||||
|
banned = create(:user, :member, :banned)
|
||||||
|
before_state = stored_state
|
||||||
|
native_write(body: 'forbidden',
|
||||||
|
request_headers: { 'X-Transfer-Code' => banned.inheritance_code })
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'rejects blank body using the existing validation error format' do
|
||||||
|
headers
|
||||||
|
before_state = stored_state
|
||||||
|
native_write(body: '')
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(json).to include('type' => 'validation_error',
|
||||||
|
'errors' => include('body' => a_kind_of(Array)))
|
||||||
|
expect(stored_state).to eq(before_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when PUT targets a missing Tag' do
|
||||||
|
missing_id = Tag.maximum(:id).to_i + 1
|
||||||
|
put "/tags/#{ missing_id }/description",
|
||||||
|
params: { body: 'missing' },
|
||||||
|
headers: headers
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -25,9 +25,10 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'PATCH /tags/:id' do
|
describe 'PATCH /tags/:id' do
|
||||||
it 'records wiki_version when tag name changes and tag has wiki' do
|
it 'renames the current Wiki resource without adding body history' do
|
||||||
tag = create_tag!(name: 'patch_tag_wiki_before')
|
tag = create_tag!(name: 'patch_tag_wiki_before')
|
||||||
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
||||||
|
wiki_history = wiki_page.wiki_versions.map(&:attributes)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
patch "/tags/#{ tag.id }", params: {
|
patch "/tags/#{ tag.id }", params: {
|
||||||
@@ -35,23 +36,17 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.to change(TagVersion, :count).by(2)
|
.to change(TagVersion, :count).by(2)
|
||||||
.and change(WikiVersion, :count).by(1)
|
.and change(WikiVersion, :count).by(0)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
tag.reload
|
tag.reload
|
||||||
wiki_page.reload
|
wiki_page.reload
|
||||||
version = wiki_page.wiki_versions.order(:version_no).last
|
|
||||||
|
|
||||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after')
|
expect(primary_tag_name_for(tag, 'ja').name).to eq('patch_tag_wiki_after')
|
||||||
expect(wiki_page.title).to eq('patch_tag_wiki_after')
|
expect(wiki_page.title).to eq('patch_tag_wiki_after')
|
||||||
|
|
||||||
expect(version).to have_attributes(
|
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history)
|
||||||
event_type: 'update',
|
|
||||||
title: 'patch_tag_wiki_after',
|
|
||||||
body: 'wiki body before',
|
|
||||||
created_by_user_id: member_user.id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not record wiki_version when only category changes' do
|
it 'does not record wiki_version when only category changes' do
|
||||||
@@ -79,9 +74,10 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT /tags/:id' do
|
describe 'PUT /tags/:id' do
|
||||||
it 'records wiki_version when tag name changes and tag has wiki' do
|
it 'renames the current Wiki resource without adding body history' do
|
||||||
tag = create_tag!(name: 'put_tag_wiki_before')
|
tag = create_tag!(name: 'put_tag_wiki_before')
|
||||||
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
wiki_page = create_wiki_for_tag!(tag:, body: 'wiki body before')
|
||||||
|
wiki_history = wiki_page.wiki_versions.map(&:attributes)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
put "/tags/#{ tag.id }", params: {
|
put "/tags/#{ tag.id }", params: {
|
||||||
@@ -93,23 +89,17 @@ RSpec.describe 'Tag and wiki history integrity', type: :request do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.to change(TagVersion, :count).by(2)
|
.to change(TagVersion, :count).by(2)
|
||||||
.and change(WikiVersion, :count).by(1)
|
.and change(WikiVersion, :count).by(0)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
tag.reload
|
tag.reload
|
||||||
wiki_page.reload
|
wiki_page.reload
|
||||||
version = wiki_page.wiki_versions.order(:version_no).last
|
|
||||||
|
|
||||||
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after')
|
expect(primary_tag_name_for(tag, 'ja').name).to eq('put_tag_wiki_after')
|
||||||
expect(wiki_page.title).to eq('put_tag_wiki_after')
|
expect(wiki_page.title).to eq('put_tag_wiki_after')
|
||||||
|
|
||||||
expect(version).to have_attributes(
|
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history)
|
||||||
event_type: 'update',
|
|
||||||
title: 'put_tag_wiki_after',
|
|
||||||
body: 'wiki body before',
|
|
||||||
created_by_user_id: member_user.id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not record wiki_version when only category changes' do
|
it 'does not record wiki_version when only category changes' do
|
||||||
|
|||||||
@@ -727,7 +727,7 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
expect(tag.reload.category).to eq('general')
|
expect(tag.reload.category).to eq('general')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'PATCH で tag の name を変更すると対応する wiki version を作成する' do
|
it 'PATCH の名称変更で Wiki の本文履歴を追加しない' do
|
||||||
wiki_page =
|
wiki_page =
|
||||||
Wiki::Commit.create_content!(
|
Wiki::Commit.create_content!(
|
||||||
tag_name: primary_tag_name_for(tag, 'ja'),
|
tag_name: primary_tag_name_for(tag, 'ja'),
|
||||||
@@ -735,24 +735,20 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
created_by_user: member_user,
|
created_by_user: member_user,
|
||||||
message: 'init')
|
message: 'init')
|
||||||
|
|
||||||
|
wiki_history = wiki_page.wiki_versions.map(&:attributes)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
patch "/tags/#{ tag.id }", params: {
|
patch "/tags/#{ tag.id }", params: {
|
||||||
name: 'patch_wiki_renamed_tag',
|
name: 'patch_wiki_renamed_tag',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.to change(TagVersion, :count).by(2)
|
.to change(TagVersion, :count).by(2)
|
||||||
.and change(WikiVersion, :count).by(1)
|
.and change(WikiVersion, :count).by(0)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
version = wiki_page.reload.wiki_versions.order(:version_no).last
|
expect(wiki_page.reload.title).to eq('patch_wiki_renamed_tag')
|
||||||
|
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history)
|
||||||
expect(version).to have_attributes(
|
|
||||||
event_type: 'update',
|
|
||||||
title: 'patch_wiki_renamed_tag',
|
|
||||||
body: 'wiki body before',
|
|
||||||
created_by_user_id: member_user.id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tag の category だけを変更しても wiki version は作成しない' do
|
it 'tag の category だけを変更しても wiki version は作成しない' do
|
||||||
@@ -1380,7 +1376,7 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
expect(system_tag.category).to eq(old_category)
|
expect(system_tag.category).to eq(old_category)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'wiki を持つ tag を更新すると wiki version も作成する' do
|
it 'Wiki を持つ Tag の名称変更で本文履歴を追加しない' do
|
||||||
wiki_page =
|
wiki_page =
|
||||||
Wiki::Commit.create_content!(
|
Wiki::Commit.create_content!(
|
||||||
tag_name: primary_tag_name_for(tag, 'ja'),
|
tag_name: primary_tag_name_for(tag, 'ja'),
|
||||||
@@ -1395,6 +1391,8 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
message: 'init'
|
message: 'init'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
wiki_history = wiki_page.wiki_versions.map(&:attributes)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
put "/tags/#{ tag.id }", params: {
|
put "/tags/#{ tag.id }", params: {
|
||||||
name: 'put_wiki_version_tag',
|
name: 'put_wiki_version_tag',
|
||||||
@@ -1405,18 +1403,12 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.to change(TagVersion, :count).by(2)
|
.to change(TagVersion, :count).by(2)
|
||||||
.and change(WikiVersion, :count).by(1)
|
.and change(WikiVersion, :count).by(0)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
version = wiki_page.reload.wiki_versions.order(:version_no).last
|
expect(wiki_page.reload.title).to eq('put_wiki_version_tag')
|
||||||
|
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history)
|
||||||
expect(version).to have_attributes(
|
|
||||||
event_type: 'update',
|
|
||||||
title: 'put_wiki_version_tag',
|
|
||||||
body: 'wiki body before',
|
|
||||||
created_by_user_id: member_user.id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do
|
it 'wiki を持つ tag を旧 alias へ戻しても wiki を新しい正本名へ移す' do
|
||||||
@@ -1426,6 +1418,7 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
body: 'wiki body before',
|
body: 'wiki body before',
|
||||||
created_by_user: member_user,
|
created_by_user: member_user,
|
||||||
message: 'init')
|
message: 'init')
|
||||||
|
wiki_history = wiki_page.wiki_versions.map(&:attributes)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
put "/tags/#{ tag.id }", params: {
|
put "/tags/#{ tag.id }", params: {
|
||||||
@@ -1445,7 +1438,7 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.to change(TagVersion, :count).by(3)
|
.to change(TagVersion, :count).by(3)
|
||||||
.and change(WikiVersion, :count).by(2)
|
.and change(WikiVersion, :count).by(0)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
@@ -1457,10 +1450,8 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b'))
|
expect(TagName.find_by!(language_code: 'ja', name: 'put_wiki_roundtrip_b'))
|
||||||
.to have_attributes(tag_id: tag.id, primary_flg: false)
|
.to have_attributes(tag_id: tag.id, primary_flg: false)
|
||||||
|
|
||||||
versions = wiki_page.wiki_versions.order(:version_no).last(2)
|
expect(wiki_page.title).to eq('spec_tag')
|
||||||
|
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history)
|
||||||
expect(versions.map(&:event_type)).to eq(['update', 'update'])
|
|
||||||
expect(versions.map(&:title)).to eq(['put_wiki_roundtrip_b', 'spec_tag'])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do
|
it '別名を他 tag から奪った場合、奪はれた側の tag version も作成する' do
|
||||||
@@ -1570,7 +1561,7 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
expect(TagImplication.where(tag:, parent_tag: child)).not_to exist
|
expect(TagImplication.where(tag:, parent_tag: child)).not_to exist
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tag の name を変更すると対応する wiki version を作成する' do
|
it 'Tag の名称変更で Wiki の本文履歴を追加しない' do
|
||||||
wiki_page =
|
wiki_page =
|
||||||
Wiki::Commit.create_content!(
|
Wiki::Commit.create_content!(
|
||||||
tag_name: primary_tag_name_for(tag, 'ja'),
|
tag_name: primary_tag_name_for(tag, 'ja'),
|
||||||
@@ -1578,6 +1569,8 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
created_by_user: member_user,
|
created_by_user: member_user,
|
||||||
message: 'init')
|
message: 'init')
|
||||||
|
|
||||||
|
wiki_history = wiki_page.wiki_versions.map(&:attributes)
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
put "/tags/#{ tag.id }", params: {
|
put "/tags/#{ tag.id }", params: {
|
||||||
name: 'put_wiki_renamed_tag',
|
name: 'put_wiki_renamed_tag',
|
||||||
@@ -1588,18 +1581,12 @@ RSpec.describe 'Tags API', type: :request do
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.to change(TagVersion, :count).by(2)
|
.to change(TagVersion, :count).by(2)
|
||||||
.and change(WikiVersion, :count).by(1)
|
.and change(WikiVersion, :count).by(0)
|
||||||
|
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
version = wiki_page.reload.wiki_versions.order(:version_no).last
|
expect(wiki_page.reload.title).to eq('put_wiki_renamed_tag')
|
||||||
|
expect(wiki_page.wiki_versions.reload.map(&:attributes)).to eq(wiki_history)
|
||||||
expect(version).to have_attributes(
|
|
||||||
event_type: 'update',
|
|
||||||
title: 'put_wiki_renamed_tag',
|
|
||||||
body: 'wiki body before',
|
|
||||||
created_by_user_id: member_user.id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ RSpec.describe 'Wiki history integrity', type: :request do
|
|||||||
expect {
|
expect {
|
||||||
post '/wiki',
|
post '/wiki',
|
||||||
params: {
|
params: {
|
||||||
title: 'wiki_history_create_atomic',
|
title: 'Help:wiki_history_create_atomic',
|
||||||
body: "a\nb\nc",
|
body: "a\nb\nc",
|
||||||
message: 'initial commit',
|
message: 'initial commit',
|
||||||
},
|
},
|
||||||
@@ -37,7 +37,7 @@ RSpec.describe 'Wiki history integrity', type: :request do
|
|||||||
revision = page.current_revision
|
revision = page.current_revision
|
||||||
version = page.wiki_versions.order(:version_no).last
|
version = page.wiki_versions.order(:version_no).last
|
||||||
|
|
||||||
expect(page.title).to eq('wiki_history_create_atomic')
|
expect(page.title).to eq('Help:wiki_history_create_atomic')
|
||||||
expect(page.body).to eq("a\nb\nc")
|
expect(page.body).to eq("a\nb\nc")
|
||||||
|
|
||||||
expect(revision).to be_content
|
expect(revision).to be_content
|
||||||
@@ -47,7 +47,7 @@ RSpec.describe 'Wiki history integrity', type: :request do
|
|||||||
expect(version).to have_attributes(
|
expect(version).to have_attributes(
|
||||||
version_no: 1,
|
version_no: 1,
|
||||||
event_type: 'create',
|
event_type: 'create',
|
||||||
title: 'wiki_history_create_atomic',
|
title: 'Help:wiki_history_create_atomic',
|
||||||
body: "a\nb\nc",
|
body: "a\nb\nc",
|
||||||
reason: 'initial commit',
|
reason: 'initial commit',
|
||||||
created_by_user_id: user.id
|
created_by_user_id: user.id
|
||||||
@@ -58,7 +58,7 @@ RSpec.describe 'Wiki history integrity', type: :request do
|
|||||||
expect {
|
expect {
|
||||||
post '/wiki',
|
post '/wiki',
|
||||||
params: {
|
params: {
|
||||||
title: 'wiki_history_blank_body',
|
title: 'Help:wiki_history_blank_body',
|
||||||
body: "\r\n\r\n",
|
body: "\r\n\r\n",
|
||||||
message: 'blank',
|
message: 'blank',
|
||||||
},
|
},
|
||||||
@@ -67,16 +67,17 @@ RSpec.describe 'Wiki history integrity', type: :request do
|
|||||||
.not_to change(WikiPage, :count)
|
.not_to change(WikiPage, :count)
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(WikiPage.joins(:tag_name).where(tag_names: { name: 'wiki_history_blank_body' })).not_to exist
|
expect(WikiPage.joins(:tag_name)
|
||||||
|
.where(tag_names: { name: 'Help:wiki_history_blank_body' })).not_to exist
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 422 and creates no partial page when title already exists' do
|
it 'returns 422 and creates no partial page when title already exists' do
|
||||||
create_wiki_page(title: 'wiki_history_duplicate_title', body: 'first')
|
create_wiki_page(title: 'Help:wiki_history_duplicate_title', body: 'first')
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
post '/wiki',
|
post '/wiki',
|
||||||
params: {
|
params: {
|
||||||
title: 'wiki_history_duplicate_title',
|
title: 'Help:wiki_history_duplicate_title',
|
||||||
body: 'second',
|
body: 'second',
|
||||||
message: 'duplicate',
|
message: 'duplicate',
|
||||||
},
|
},
|
||||||
@@ -85,7 +86,8 @@ RSpec.describe 'Wiki history integrity', type: :request do
|
|||||||
.not_to change(WikiPage, :count)
|
.not_to change(WikiPage, :count)
|
||||||
|
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(WikiPage.joins(:tag_name).where(tag_names: { name: 'wiki_history_duplicate_title' }).count).to eq(1)
|
expect(WikiPage.joins(:tag_name)
|
||||||
|
.where(tag_names: { name: 'Help:wiki_history_duplicate_title' }).count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
let!(:user) { create_member_user! }
|
let!(:user) { create_member_user! }
|
||||||
|
|
||||||
let!(:tn) { create(:tag_name, name: 'spec_wiki_title') }
|
let!(:tn) { create(:tag_name, name: 'spec_wiki_title') }
|
||||||
|
let!(:tag) do
|
||||||
|
create(:tag,
|
||||||
|
primary_tag_name: tn,
|
||||||
|
category: :general,
|
||||||
|
deprecated_at: Time.zone.local(2026, 6, 1))
|
||||||
|
end
|
||||||
let!(:page) do
|
let!(:page) do
|
||||||
Wiki::Commit.create_content!(
|
Wiki::Commit.create_content!(
|
||||||
tag_name: tn,
|
tag_name: tn,
|
||||||
@@ -18,12 +24,6 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
created_by_user: user,
|
created_by_user: user,
|
||||||
message: 'init')
|
message: 'init')
|
||||||
end
|
end
|
||||||
let!(:tag) do
|
|
||||||
create(:tag,
|
|
||||||
primary_tag_name: tn,
|
|
||||||
category: :general,
|
|
||||||
deprecated_at: Time.zone.local(2026, 6, 1))
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'GET /wiki' do
|
describe 'GET /wiki' do
|
||||||
it 'returns wiki pages with title' do
|
it 'returns wiki pages with title' do
|
||||||
@@ -111,7 +111,7 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
context 'when success' do
|
context 'when success' do
|
||||||
it 'creates wiki_page and first content revision' do
|
it 'creates wiki_page and first content revision' do
|
||||||
expect do
|
expect do
|
||||||
post endpoint, params: { title: 'TestPage', body: "a\nb\nc", message: 'init' },
|
post endpoint, params: { title: 'Help:TestPage', body: "a\nb\nc", message: 'init' },
|
||||||
headers: auth_headers(member)
|
headers: auth_headers(member)
|
||||||
end
|
end
|
||||||
.to change(WikiPage, :count).by(1)
|
.to change(WikiPage, :count).by(1)
|
||||||
@@ -121,7 +121,7 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
|
||||||
page_id = json.fetch('id')
|
page_id = json.fetch('id')
|
||||||
expect(json.fetch('title')).to eq('TestPage')
|
expect(json.fetch('title')).to eq('Help:TestPage')
|
||||||
|
|
||||||
created_page = WikiPage.find(page_id)
|
created_page = WikiPage.find(page_id)
|
||||||
version = created_page.wiki_versions.order(:version_no).last
|
version = created_page.wiki_versions.order(:version_no).last
|
||||||
@@ -129,7 +129,7 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
expect(version).to have_attributes(
|
expect(version).to have_attributes(
|
||||||
version_no: 1,
|
version_no: 1,
|
||||||
event_type: 'create',
|
event_type: 'create',
|
||||||
title: 'TestPage',
|
title: 'Help:TestPage',
|
||||||
body: "a\nb\nc",
|
body: "a\nb\nc",
|
||||||
created_by_user_id: member.id
|
created_by_user_id: member.id
|
||||||
)
|
)
|
||||||
@@ -151,7 +151,7 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
WikiLine.create!(sha256: Digest::SHA256.hexdigest('a'), body: 'a', created_at: Time.current, updated_at: Time.current)
|
WikiLine.create!(sha256: Digest::SHA256.hexdigest('a'), body: 'a', created_at: Time.current, updated_at: Time.current)
|
||||||
|
|
||||||
post endpoint,
|
post endpoint,
|
||||||
params: { title: 'Reuse', body: "a\na" },
|
params: { title: 'Help:Reuse', body: "a\na" },
|
||||||
headers: auth_headers(member)
|
headers: auth_headers(member)
|
||||||
|
|
||||||
page = WikiPage.find(JSON.parse(response.body).fetch('id'))
|
page = WikiPage.find(JSON.parse(response.body).fetch('id'))
|
||||||
@@ -166,7 +166,7 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
duplicated = 'duplicated_line_for_wiki_line_upsert_spec'
|
duplicated = 'duplicated_line_for_wiki_line_upsert_spec'
|
||||||
|
|
||||||
post endpoint,
|
post endpoint,
|
||||||
params: { title: 'DuplicateNewLine', body: "#{ duplicated }\n#{ duplicated }" },
|
params: { title: 'Help:DuplicateNewLine', body: "#{ duplicated }\n#{ duplicated }" },
|
||||||
headers: auth_headers(member)
|
headers: auth_headers(member)
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
@@ -182,7 +182,8 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
|
|
||||||
it 'normalises CRLF and strips trailing newlines' do
|
it 'normalises CRLF and strips trailing newlines' do
|
||||||
post endpoint,
|
post endpoint,
|
||||||
params: { title: 'NormalisedBody', body: "a\r\nb\r\n\r\n", message: 'normalise' },
|
params: { title: 'Help:NormalisedBody',
|
||||||
|
body: "a\r\nb\r\n\r\n", message: 'normalise' },
|
||||||
headers: auth_headers(member)
|
headers: auth_headers(member)
|
||||||
|
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
@@ -413,10 +414,10 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
top = json.first
|
top = json.first
|
||||||
expect(top).to include(
|
expect(top).to include(
|
||||||
'revision_id' => rev2.id,
|
'revision_id' => rev2.id,
|
||||||
'pred' => rev2.base_revision_id,
|
'pred' => rev1.id,
|
||||||
'succ' => nil,
|
'succ' => nil,
|
||||||
'kind' => 'content',
|
'kind' => 'content',
|
||||||
'message' => 'r2'
|
'message' => nil
|
||||||
)
|
)
|
||||||
expect(top['wiki_page']).to include(
|
expect(top['wiki_page']).to include(
|
||||||
'id' => page.id,
|
'id' => page.id,
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ RSpec.describe 'Wiki title collision', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 422 when creating wiki with existing title' do
|
it 'returns 422 when creating wiki with existing title' do
|
||||||
create_wiki_page(title: 'wiki_collision_create', body: 'already exists')
|
create_wiki_page(title: 'Help:wiki_collision_create', body: 'already exists')
|
||||||
|
|
||||||
expect {
|
expect {
|
||||||
post '/wiki',
|
post '/wiki',
|
||||||
params: {
|
params: {
|
||||||
title: 'wiki_collision_create',
|
title: 'Help:wiki_collision_create',
|
||||||
body: 'new body',
|
body: 'new body',
|
||||||
message: 'duplicate create',
|
message: 'duplicate create',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Unmigrated Wiki compatibility', type: :request do
|
||||||
|
let!(:user) { create_member_user! }
|
||||||
|
let!(:tag_name) { create(:tag_name, name: 'Help_contract') }
|
||||||
|
let!(:page) do
|
||||||
|
Wiki::Commit.create_content!(tag_name:, body: 'before', created_by_user: user)
|
||||||
|
end
|
||||||
|
let!(:first_revision) { page.current_revision }
|
||||||
|
let!(:last_revision) do
|
||||||
|
Wiki::Commit.content!(page:, body: 'after', created_user: user,
|
||||||
|
message: 'legacy edit', base_revision_id: first_revision.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
before { expect(TagDescription.exists?(id: page.id)).to be(false) }
|
||||||
|
|
||||||
|
it 'keeps a Tag-linked legacy page in the legacy domain when its title changes' do
|
||||||
|
create(:tag, primary_tag_name: tag_name)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
put "/wiki/#{ page.id }",
|
||||||
|
params: { title: 'legacy_tag_renamed', body: 'changed legacy body',
|
||||||
|
message: 'legacy rename', base_revision_id: last_revision.id },
|
||||||
|
headers: { 'X-Transfer-Code' => user.inheritance_code }
|
||||||
|
end.to change(WikiRevision, :count).by(1)
|
||||||
|
.and change(WikiVersion, :count).by(1)
|
||||||
|
.and change(TagDescription, :count).by(0)
|
||||||
|
.and change(TagDescriptionVersion, :count).by(0)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(page.reload).to have_attributes(body: 'changed legacy body')
|
||||||
|
expect(page.title).to eq('legacy_tag_renamed')
|
||||||
|
expect(page.current_revision).to have_attributes(message: 'legacy rename')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows legacy current and historical content by ID' do
|
||||||
|
get "/wiki/#{ page.id }"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'after', 'revision_id' => last_revision.id,
|
||||||
|
'pred' => first_revision.id, 'succ' => nil)
|
||||||
|
get "/wiki/#{ page.id }", params: { version: first_revision.id }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'before', 'revision_id' => first_revision.id,
|
||||||
|
'pred' => nil, 'succ' => last_revision.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows legacy current and historical content by title' do
|
||||||
|
get "/wiki/title/#{ tag_name.name }"
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'after', 'revision_id' => last_revision.id)
|
||||||
|
get "/wiki/title/#{ tag_name.name }", params: { version: first_revision.id }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include('body' => 'before', 'revision_id' => first_revision.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognises legacy IDs' do
|
||||||
|
get "/wiki/#{ page.id }/exists"
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'recognises legacy titles' do
|
||||||
|
get "/wiki/title/#{ tag_name.name }/exists"
|
||||||
|
expect(response).to have_http_status(:no_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'diffs legacy revisions with explicit and current target' do
|
||||||
|
[last_revision.id, nil].each do |to|
|
||||||
|
get "/wiki/#{ page.id }/diff", params: { from: first_revision.id, to: }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json).to include(
|
||||||
|
'wiki_page_id' => page.id, 'title' => tag_name.name,
|
||||||
|
'older_revision_id' => first_revision.id, 'newer_revision_id' => last_revision.id,
|
||||||
|
'diff' => [{ 'type' => 'removed', 'content' => 'before' },
|
||||||
|
{ 'type' => 'added', 'content' => 'after' }])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'revision resource scope' do
|
||||||
|
let!(:other_page) do
|
||||||
|
Wiki::Commit.create_content!(
|
||||||
|
tag_name: create(:tag_name, name: 'Help_other_contract'),
|
||||||
|
body: 'other page history', created_by_user: user)
|
||||||
|
end
|
||||||
|
let(:other_revision) { other_page.current_revision }
|
||||||
|
|
||||||
|
it 'returns 404 when show specifies another page revision' do
|
||||||
|
get "/wiki/#{ page.id }", params: { version: other_revision.id }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when diff from belongs to another page' do
|
||||||
|
get "/wiki/#{ page.id }/diff",
|
||||||
|
params: { from: other_revision.id, to: last_revision.id }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns 404 when diff to belongs to another page' do
|
||||||
|
get "/wiki/#{ page.id }/diff",
|
||||||
|
params: { from: first_revision.id, to: other_revision.id }
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'retains legacy history metadata and ordering' do
|
||||||
|
get '/wiki/changes', params: { id: page.id }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(json.map { |row| row.fetch('revision_id') }).to eq(
|
||||||
|
[last_revision.id, first_revision.id])
|
||||||
|
expect(json.first).to include(
|
||||||
|
'pred' => first_revision.id, 'succ' => nil, 'kind' => 'content',
|
||||||
|
'message' => 'legacy edit', 'timestamp' => last_revision.created_at.iso8601(3),
|
||||||
|
'wiki_page' => { 'id' => page.id, 'title' => tag_name.name, 'deprecated_at' => nil },
|
||||||
|
'user' => { 'id' => user.id, 'name' => user.name })
|
||||||
|
end
|
||||||
|
end
|
||||||
新しいイシューから参照
ユーザーをブロックする