This commit is contained in:
2026-04-24 02:20:12 +09:00
parent 2ad25b3950
commit 478bc15ad0
3 changed files with 238 additions and 14 deletions
@@ -39,18 +39,23 @@ class TagVersionsController < ApplicationController
cur_aliases = split_values(row.aliases) cur_aliases = split_values(row.aliases)
prev_aliases = split_values(row.attributes['prev_aliases']) prev_aliases = split_values(row.attributes['prev_aliases'])
cur_parent_tags = cur_parent_tag_ids = split_parent_tag_ids(row.parent_tag_ids)
TagRepr.many( prev_parent_tag_ids = split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])
all_parent_tag_ids = (cur_parent_tag_ids | prev_parent_tag_ids)
tags_by_id =
Tag Tag
.includes(:tag_name, :materials, { tag_name: :wiki_page }) .includes(:tag_name, :materials, { tag_name: :wiki_page })
.where(id: split_parent_tag_ids(row.parent_tag_ids)) .where(id: all_parent_tag_ids)
.to_a) .index_by(&:id)
prev_parent_tags =
TagRepr.many( parent_tags =
Tag build_version_values(cur_parent_tag_ids, prev_parent_tag_ids, key: :tag_id)
.includes(:tag_name, :materials, { tag_name: :wiki_page }) .map do |h|
.where(id: split_parent_tag_ids(row.attributes['prev_parent_tag_ids'])) { tag: TagRepr.base(tags_by_id[h[:tag_id]]),
.to_a) type: h[:type] }
end
{ tag_id: row.tag_id, { tag_id: row.tag_id,
version_no: row.version_no, version_no: row.version_no,
@@ -58,7 +63,7 @@ class TagVersionsController < ApplicationController
name: { current: row.name, prev: row.attributes['prev_name'] }, name: { current: row.name, prev: row.attributes['prev_name'] },
category: { current: row.category, prev: row.attributes['prev_category'] }, category: { current: row.category, prev: row.attributes['prev_category'] },
aliases: build_version_values(cur_aliases, prev_aliases, key: :name), aliases: build_version_values(cur_aliases, prev_aliases, key: :name),
parent_tags: build_version_values(cur_parent_tags, prev_parent_tags, key: :tag), parent_tags:,
created_at: row.created_at.iso8601, created_at: row.created_at.iso8601,
created_by_user: row.created_by_user_id && created_by_user: row.created_by_user_id &&
{ id: row.created_by_user_id, { id: row.created_by_user_id,
+218
View File
@@ -0,0 +1,218 @@
require 'rails_helper'
RSpec.describe 'TagVersions API', type: :request do
let(:member) { create(:user, :member, name: 'version member') }
let!(:tag) { create(:tag, name: 'tag_versions_target', category: :general) }
let!(:other_tag) { create(:tag, name: 'tag_versions_other', category: :general) }
let!(:parent_shared) { create(:tag, name: 'parent_shared', category: :general) }
let!(:parent_old) { create(:tag, name: 'parent_old', category: :general) }
let!(:parent_new) { create(:tag, name: 'parent_new', category: :general) }
let!(:other_parent) { create(:tag, name: 'other_parent', category: :general) }
let(:t_v1) { Time.zone.local(2020, 1, 1, 12, 0, 0) }
let(:t_v2) { Time.zone.local(2020, 1, 2, 12, 0, 0) }
let(:t_other) { Time.zone.local(2020, 1, 3, 12, 0, 0) }
def create_tag_version!(
tag:,
version_no:,
event_type:,
name:,
category:,
aliases: [],
parent_tags: [],
created_by_user:,
created_at:
)
TagVersion.create!(
tag: tag,
version_no: version_no,
event_type: event_type,
name: name,
category: category,
aliases: Array(aliases).join(' '),
parent_tag_ids: Array(parent_tags).map(&:id).join(' '),
created_by_user: created_by_user,
created_at: created_at
)
end
let!(:v1) do
create_tag_version!(
tag: tag,
version_no: 1,
event_type: 'create',
name: 'old_tag_name',
category: 'general',
aliases: ['alias_shared', 'alias_old'],
parent_tags: [parent_shared, parent_old],
created_by_user: member,
created_at: t_v1
)
end
let!(:v2) do
create_tag_version!(
tag: tag,
version_no: 2,
event_type: 'update',
name: 'new_tag_name',
category: 'meme',
aliases: ['alias_shared', 'alias_new'],
parent_tags: [parent_shared, parent_new],
created_by_user: member,
created_at: t_v2
)
end
let!(:other_v1) do
create_tag_version!(
tag: other_tag,
version_no: 1,
event_type: 'create',
name: 'other_tag_name',
category: 'general',
aliases: ['other_alias'],
parent_tags: [other_parent],
created_by_user: member,
created_at: t_other
)
end
describe 'GET /tags/versions' do
it 'returns all versions in reverse chronological order when id is omitted' do
get '/tags/versions'
expect(response).to have_http_status(:ok)
expect(json).to include('versions', 'count')
expect(json.fetch('count')).to eq(3)
versions = json.fetch('versions')
expect(versions.map { |v| [v['tag_id'], v['version_no']] }).to eq([
[other_tag.id, 1],
[tag.id, 2],
[tag.id, 1]
])
end
it 'returns versions for the specified tag with diffs' do
get '/tags/versions', params: { id: tag.id }
expect(response).to have_http_status(:ok)
expect(json).to include('versions', 'count')
expect(json.fetch('count')).to eq(2)
versions = json.fetch('versions')
expect(versions.map { |v| v['tag_id'] }.uniq).to eq([tag.id])
expect(versions.map { |v| v['version_no'] }).to eq([2, 1])
latest = versions.first
expect(latest).to include(
'tag_id' => tag.id,
'version_no' => 2,
'event_type' => 'update',
'created_by_user' => {
'id' => member.id,
'name' => member.name
}
)
expect(latest.fetch('name')).to eq(
'current' => 'new_tag_name',
'prev' => 'old_tag_name'
)
expect(latest.fetch('category')).to eq(
'current' => 'meme',
'prev' => 'general'
)
expect(latest.fetch('aliases')).to include(
{ 'name' => 'alias_shared', 'type' => 'context' },
{ 'name' => 'alias_new', 'type' => 'added' },
{ 'name' => 'alias_old', 'type' => 'removed' }
)
expect(latest.fetch('parent_tags')).to include(
a_hash_including(
'type' => 'context',
'tag' => a_hash_including(
'id' => parent_shared.id
)
),
a_hash_including(
'type' => 'added',
'tag' => a_hash_including(
'id' => parent_new.id
)
),
a_hash_including(
'type' => 'removed',
'tag' => a_hash_including(
'id' => parent_old.id
)
)
)
expect(latest.fetch('created_at')).to eq(t_v2.iso8601)
first = versions.second
expect(first).to include(
'tag_id' => tag.id,
'version_no' => 1,
'event_type' => 'create',
'created_by_user' => {
'id' => member.id,
'name' => member.name
}
)
expect(first.fetch('name')).to eq(
'current' => 'old_tag_name',
'prev' => nil
)
expect(first.fetch('category')).to eq(
'current' => 'general',
'prev' => nil
)
expect(first.fetch('aliases')).to include(
{ 'name' => 'alias_shared', 'type' => 'added' },
{ 'name' => 'alias_old', 'type' => 'added' }
)
expect(first.fetch('parent_tags')).to include(
a_hash_including(
'type' => 'added',
'tag' => a_hash_including(
'id' => parent_shared.id
)
),
a_hash_including(
'type' => 'added',
'tag' => a_hash_including(
'id' => parent_old.id
)
)
)
expect(first.fetch('created_at')).to eq(t_v1.iso8601)
end
it 'returns empty when the specified tag has no versions' do
fresh_tag = create(:tag, name: 'no_versions_tag', category: :general)
get '/tags/versions', params: { id: fresh_tag.id }
expect(response).to have_http_status(:ok)
expect(json.fetch('versions')).to eq([])
expect(json.fetch('count')).to eq(0)
end
it 'clamps page and limit to at least 1' do
get '/tags/versions', params: { id: tag.id, page: 0, limit: 0 }
expect(response).to have_http_status(:ok)
expect(json.fetch('count')).to eq(2)
versions = json.fetch('versions')
expect(versions.size).to eq(1)
expect(versions.first['version_no']).to eq(2)
end
end
end
+2 -1
View File
@@ -47,7 +47,8 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
{ name: `広場 (${ postCount || 0 })`, { name: `広場 (${ postCount || 0 })`,
to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`, to: `/posts?tags=${ encodeURIComponent (tag?.name ?? '') }`,
visible: tagFlg }, visible: tagFlg },
{ name: '履歴', to: `/tags/changes?id=${ tag?.id }`, visible: false }] }, { name: '履歴', to: `/tags/changes?id=${ tag?.id }`,
visible: tagFlg && tag?.category !== 'nico' }] },
{ name: '素材', to: '/materials', visible: false, subMenu: [ { name: '素材', to: '/materials', visible: false, subMenu: [
{ name: '一覧', to: '/materials' }, { name: '一覧', to: '/materials' },
{ name: '検索', to: '/materials/search', visible: false }, { name: '検索', to: '/materials/search', visible: false },