コミットを比較

..
3 コミット
作成者 SHA1 メッセージ 日付
みてるぞ 971b2632b0 #414 2026-11-03 15:17:10 +09:00
みてるぞ ad04c03e14 #414 2026-11-03 13:37:09 +09:00
みてるぞ 2197a9192b #414 2026-11-03 12:22:15 +09:00
44個のファイルの変更、849行の追加、135行の削除
+9
ファイルの表示
@@ -0,0 +1,9 @@
class Event < ApplicationRecord
has_many :post_versions
has_many :tag_versions
has_many :material_versions
has_many :tag_description_versions
has_many :tag_group_versions
belongs_to :created_by_user, class_name: 'User', optional: true
end
+1 -3
ファイルの表示
@@ -11,7 +11,5 @@ class MaterialVersion < ApplicationRecord
belongs_to :parent, class_name: 'Material', optional: true belongs_to :parent, class_name: 'Material', optional: true
belongs_to :updated_by_user, class_name: 'User', optional: true belongs_to :updated_by_user, class_name: 'User', optional: true
def export_paths_hash def export_paths_hash = export_paths_json || { }
export_paths_json || {}
end
end end
+2
ファイルの表示
@@ -19,6 +19,8 @@ module VersionRecord
belongs_to :created_by_user, class_name: 'User', optional: true belongs_to :created_by_user, class_name: 'User', optional: true
belongs_to :event
enum :event_type, event_type_map, prefix: true, validate: true enum :event_type, event_type_map, prefix: true, validate: true
validates :version_no, presence: true, numericality: { only_integer: true, greater_than: 0 } validates :version_no, presence: true, numericality: { only_integer: true, greater_than: 0 }
+8 -5
ファイルの表示
@@ -1,20 +1,23 @@
class MaterialVersionRecorder < VersionRecorder class MaterialVersionRecorder < VersionRecorder
EVENT_TYPES = ['create', 'update', 'discard', 'restore'].freeze EVENT_TYPES = ['create', 'update', 'discard', 'restore'].freeze
def self.record! material:, event_type:, created_by_user:, file_snapshot: nil def self.record! material:, event_type:, event:, created_by_user:, file_snapshot: nil
new(material:, event_type:, created_by_user:, file_snapshot:).record! new(material:, event_type:, event:, created_by_user:, file_snapshot:).record!
end end
def initialize material:, event_type:, created_by_user:, file_snapshot: nil def initialize material:, event_type:, event:, created_by_user:, file_snapshot: nil
@file_snapshot = file_snapshot @file_snapshot = file_snapshot
super(record: material, event_type:, created_by_user:) super(record: material, event_type:, event:, created_by_user:)
end end
def self.ensure_snapshot! material, created_by_user: def self.ensure_snapshot! material, created_by_user:
return if material.material_versions.exists? return if material.material_versions.exists?
record!(material:, event_type: :create, event = Event.create!(event_type: 'materials.create',
created_by_user:,
created_at: material.created_at)
record!(material:, event_type: :create, event:,
created_by_user: material.created_by_user || created_by_user) created_by_user: material.created_by_user || created_by_user)
end end
+8 -5
ファイルの表示
@@ -1,16 +1,19 @@
class PostVersionRecorder < VersionRecorder class PostVersionRecorder < VersionRecorder
def self.record! post:, event_type:, created_by_user: def self.record! post:, event_type:, event:, created_by_user:
new(post:, event_type:, created_by_user:).record! new(post:, event_type:, event:, created_by_user:).record!
end end
def initialize post:, event_type:, created_by_user: def initialize post:, event_type:, event:, created_by_user:
super(record: post, event_type:, created_by_user:) super(record: post, event_type:, event:, created_by_user:)
end end
def self.ensure_snapshot! post, created_by_user: def self.ensure_snapshot! post, created_by_user:
return if post.post_versions.exists? return if post.post_versions.exists?
record!(post:, event_type: :create, created_by_user:) event = Event.create!(event_type: 'posts.create',
created_by_user:,
created_at: post.created_at)
record!(post:, event_type: :create, event:, created_by_user:)
end end
private private
+4 -4
ファイルの表示
@@ -1,13 +1,13 @@
class TagVersionRecorder < VersionRecorder class TagVersionRecorder < VersionRecorder
def self.record! tag:, event_type:, created_by_user:, **_options def self.record! tag:, event_type:, event:, created_by_user:, **_options
Tag.transaction do Tag.transaction do
SharedResourceIdAllocator.lock_space!(Tag, 'tags') SharedResourceIdAllocator.lock_space!(Tag, 'tags')
new(tag:, event_type:, created_by_user:).record! new(tag:, event_type:, event:, created_by_user:).record!
end end
end end
def initialize tag:, event_type:, created_by_user: def initialize tag:, event_type:, event:, created_by_user:
super(record: tag, event_type:, created_by_user:) super(record: tag, event_type:, event:, created_by_user:)
end end
private private
+5 -3
ファイルの表示
@@ -1,9 +1,10 @@
class VersionRecorder class VersionRecorder
EVENT_TYPES = ['create', 'update', 'discard', 'restore'].freeze EVENT_TYPES = ['create', 'update', 'discard', 'restore'].freeze
def initialize record:, event_type:, created_by_user: def initialize record:, event_type:, event:, created_by_user:
@record = record @record = record
@event_type = event_type.to_s @event_type = event_type.to_s
@event = event
@created_by_user = created_by_user @created_by_user = created_by_user
validate_event_type! validate_event_type!
@@ -42,8 +43,9 @@ class VersionRecorder
def base_attributes latest def base_attributes latest
{ version_no: (latest&.version_no || 0) + 1, { version_no: (latest&.version_no || 0) + 1,
event_type: @event_type, event_type: @event_type,
created_at: Time.current, event: @event,
created_by_user: @created_by_user } created_by_user: @created_by_user,
created_at: Time.current }
end end
def update_record_version_no! version_no def update_record_version_no! version_no
+19
ファイルの表示
@@ -0,0 +1,19 @@
class CreateEvents < ActiveRecord::Migration[8.0]
def change
create_table :events do |t|
t.string :event_type, null: false, index: true
t.text :reason
t.references :reverted_from_event, foreign_key: { to_table: :events }
t.json :metadata
t.binary :ip_address, limit: 16, index: true
t.references :created_by_user, index: true, foreign_key: false
t.datetime :created_at, null: false, index: true
end
add_reference :post_versions, :event, foreign_key: true, after: :original_created_before
add_reference :tag_versions, :event, foreign_key: true, after: :deprecated_at
add_reference :material_versions, :event, foreign_key: true
add_reference :tag_description_versions, :event, foreign_key: true, after: :source_version_no
add_reference :tag_group_versions, :event, foreign_key: true, after: :placements
end
end
+207
ファイルの表示
@@ -0,0 +1,207 @@
class BackfillEventsFromExistingVersions < ActiveRecord::Migration[8.0]
class MigrationEvent < ActiveRecord::Base
self.table_name = 'events'
end
class MigrationPostVersion < ActiveRecord::Base
self.table_name = 'post_versions'
end
class MigrationTagVersion < ActiveRecord::Base
self.table_name = 'tag_versions'
end
class MigrationMaterialVersion < ActiveRecord::Base
self.table_name = 'material_versions'
end
class MigrationTagDescriptionVersion < ActiveRecord::Base
self.table_name = 'tag_description_versions'
end
class MigrationTagGroupVersion < ActiveRecord::Base
self.table_name = 'tag_group_versions'
end
class MigrationLocale < ActiveRecord::Base
self.table_name = 'locales'
end
def up
# create → posts.create
# update → posts.update
raise if MigrationPostVersion.where.not(event_type: ['create', 'update']).exists?
# create → materials.create
# update → materials.update
if MigrationMaterialVersion.where.not(event_type: ['create', 'update']).exists?
raise
end
# create → posts.create / posts.update / materials.create / materials.update / unknown
# update → tags.update / locales.create
# discard → unknown
if MigrationTagVersion.where.not(event_type: ['create', 'update', 'discard']).exists?
raise
end
# create → tag_descriptions.create
# update → tag_descriptions.update
if MigrationTagDescriptionVersion.where.not(event_type: ['create', 'update']).exists?
raise
end
# create → tag_groups.create
# update → tag_groups.update
if MigrationTagGroupVersion.where.not(event_type: ['create', 'update']).exists?
raise
end
# post_versions の Backfill
MigrationPostVersion.find_each do |pv|
ev = MigrationEvent.create!(event_type: "posts.#{ pv.event_type }",
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: pv.created_by_user_id,
created_at: pv.created_at)
pv.update_columns(event_id: ev.id)
end
# material_versions の Backfill
MigrationMaterialVersion.find_each do |m|
ev = MigrationEvent.create!(event_type: "materials.#{ m.event_type }",
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: m.created_by_user_id,
created_at: m.created_at)
m.update_columns(event_id: ev.id)
end
# ロカール追加に伴ふ該当語言のタグ名称追加異動のみ分離
ids_cause_of_locale_creation = []
MigrationLocale.where('created_at > ?', MigrationLocale.minimum(:created_at)).find_each do |l|
ev = MigrationEvent.create!(event_type: 'locales.create',
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: nil,
created_at: l.created_at)
MigrationTagVersion
.where('created_at > ?', l.created_at)
.where(event_type: 'update', event_id: nil)
.find_each do |t|
next unless t.names.key?(l.language_code)
prev = MigrationTagVersion.find_by(tag_id: t.tag_id, version_no: t.version_no - 1)
next if prev.nil? || prev.names.key?(l.language_code)
t.update_columns(event_id: ev.id)
ids_cause_of_locale_creation << t.id
end
ev.destroy! unless MigrationTagVersion.where(event_id: ev.id).exists?
end
# tag_versions の Backfill
MigrationTagVersion.where.not(id: ids_cause_of_locale_creation).find_each do |t|
ev =
case t.event_type
when 'create'
post =
MigrationPostVersion
.where('JSON_CONTAINS(tags_json, ?) <> 0', { tag_id: t.tag_id }.to_json)
.find_by(created_at: MigrationPostVersion
.where('JSON_CONTAINS(tags_json, ?) <> 0',
{ tag_id: t.tag_id }.to_json)
.minimum(:created_at))
material =
MigrationMaterialVersion
.where(tag_id: t.tag_id)
.find_by(created_at: MigrationMaterialVersion
.where(tag_id: t.tag_id)
.minimum(:created_at))
if post
if material
if post.created_at < material.created_at
MigrationEvent.find(post.event_id)
else
MigrationEvent.find(material.event_id)
end
else
MigrationEvent.find(post.event_id)
end
else
if material
MigrationEvent.find(material.event_id)
else
MigrationEvent.create!(event_type: 'unknown',
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: t.created_by_user_id,
created_at: t.created_at)
end
end
when 'update'
MigrationEvent.create!(event_type: 'tags.update',
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: t.created_by_user_id,
created_at: t.created_at)
when 'discard'
MigrationEvent.create!(event_type: 'unknown',
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: t.created_by_user_id,
created_at: t.created_at)
end
t.update_columns(event_id: ev.id)
end
# tag_description_versions の Backfill
MigrationTagDescriptionVersion.find_each do |td|
ev = MigrationEvent.create!(event_type: "tag_descriptions.#{ td.event_type }",
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: td.created_by_user_id,
created_at: td.created_at)
td.update_columns(event_id: ev.id)
end
# tag_group_versions の Backfill
MigrationTagGroupVersion.find_each do |tg|
ev = MigrationEvent.create!(event_type: "tag_groups.#{ tg.event_type }",
reason: nil,
reverted_from_event_id: nil,
metadata: nil,
ip_address: nil,
created_by_user_id: tg.created_by_user_id,
created_at: tg.created_at)
tg.update_columns(event_id: ev.id)
end
change_column_null :post_versions, :event_id, false
change_column_null :tag_versions, :event_id, false
change_column_null :material_versions, :event_id, false
change_column_null :tag_description_versions, :event_id, false
change_column_null :tag_group_versions, :event_id, false
end
def down
raise ActiveRecord::IrreversibleMigration, '戻せません.'
end
end
生成ファイル
+32 -1
ファイルの表示
@@ -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_10_15_010000) do ActiveRecord::Schema[8.0].define(version: 2026_11_02_010000) 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
@@ -48,6 +48,21 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
t.index ["tag_id"], name: "index_deerjikists_on_tag_id" t.index ["tag_id"], name: "index_deerjikists_on_tag_id"
end end
create_table "events", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "event_type", null: false
t.text "reason"
t.bigint "reverted_from_event_id"
t.json "metadata"
t.binary "ip_address", limit: 16
t.bigint "created_by_user_id"
t.datetime "created_at", null: false
t.index ["created_at"], name: "index_events_on_created_at"
t.index ["created_by_user_id"], name: "index_events_on_created_by_user_id"
t.index ["event_type"], name: "index_events_on_event_type"
t.index ["ip_address"], name: "index_events_on_ip_address"
t.index ["reverted_from_event_id"], name: "index_events_on_reverted_from_event_id"
end
create_table "external_tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "external_tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "platform", limit: 16, null: false t.string "platform", limit: 16, null: false
t.string "name", null: false t.string "name", null: false
@@ -250,8 +265,10 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
t.string "source_path" t.string "source_path"
t.string "source_file_id" t.string "source_file_id"
t.string "normalized_source_key" t.string "normalized_source_key"
t.bigint "event_id", null: false
t.index ["created_by_user_id"], name: "index_material_versions_on_created_by_user_id" t.index ["created_by_user_id"], name: "index_material_versions_on_created_by_user_id"
t.index ["discarded_at"], name: "index_material_versions_on_discarded_at" t.index ["discarded_at"], name: "index_material_versions_on_discarded_at"
t.index ["event_id"], name: "index_material_versions_on_event_id"
t.index ["file_blob_id"], name: "index_material_versions_on_file_blob_id" t.index ["file_blob_id"], name: "index_material_versions_on_file_blob_id"
t.index ["material_id", "version_no"], name: "index_material_versions_on_material_id_and_version_no", unique: true t.index ["material_id", "version_no"], name: "index_material_versions_on_material_id_and_version_no", unique: true
t.index ["material_id"], name: "index_material_versions_on_material_id" t.index ["material_id"], name: "index_material_versions_on_material_id"
@@ -380,10 +397,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
t.text "parent_post_ids", null: false t.text "parent_post_ids", null: false
t.datetime "original_created_from" t.datetime "original_created_from"
t.datetime "original_created_before" t.datetime "original_created_before"
t.bigint "event_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.bigint "created_by_user_id" t.bigint "created_by_user_id"
t.integer "video_ms" t.integer "video_ms"
t.index ["created_by_user_id"], name: "index_post_versions_on_created_by_user_id" t.index ["created_by_user_id"], name: "index_post_versions_on_created_by_user_id"
t.index ["event_id"], name: "index_post_versions_on_event_id"
t.index ["post_id", "version_no"], name: "index_post_versions_on_post_id_and_version_no", unique: true t.index ["post_id", "version_no"], name: "index_post_versions_on_post_id_and_version_no", unique: true
t.index ["post_id"], name: "index_post_versions_on_post_id" t.index ["post_id"], name: "index_post_versions_on_post_id"
t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id" t.index ["video_ms", "post_id"], name: "idx_post_versions_video_ms_post_id"
@@ -448,9 +467,11 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
t.text "body", null: false t.text "body", null: false
t.string "source_language_code", limit: 16 t.string "source_language_code", limit: 16
t.integer "source_version_no" t.integer "source_version_no"
t.bigint "event_id", null: false
t.bigint "created_by_user_id" t.bigint "created_by_user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.index ["created_by_user_id"], name: "index_tag_description_versions_on_created_by_user_id" t.index ["created_by_user_id"], name: "index_tag_description_versions_on_created_by_user_id"
t.index ["event_id"], name: "index_tag_description_versions_on_event_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", "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", "source_language_code", "source_version_no"], name: "fk_rails_4f13094dbc"
t.index ["tag_id"], name: "index_tag_description_versions_on_tag_id" t.index ["tag_id"], name: "index_tag_description_versions_on_tag_id"
@@ -533,9 +554,11 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
t.text "description" t.text "description"
t.json "members", null: false t.json "members", null: false
t.json "placements", null: false t.json "placements", null: false
t.bigint "event_id", null: false
t.bigint "created_by_user_id" t.bigint "created_by_user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.index ["created_by_user_id"], name: "index_tag_group_versions_on_created_by_user_id" t.index ["created_by_user_id"], name: "index_tag_group_versions_on_created_by_user_id"
t.index ["event_id"], name: "index_tag_group_versions_on_event_id"
t.index ["tag_group_id", "version_no"], name: "index_tag_group_versions_on_tag_group_id_and_version_no", unique: true t.index ["tag_group_id", "version_no"], name: "index_tag_group_versions_on_tag_group_id_and_version_no", unique: true
t.index ["tag_group_id"], name: "index_tag_group_versions_on_tag_group_id" t.index ["tag_group_id"], name: "index_tag_group_versions_on_tag_group_id"
t.check_constraint "`version_no` > 0", name: "chk_tag_group_versions_version_no_positive" t.check_constraint "`version_no` > 0", name: "chk_tag_group_versions_version_no_positive"
@@ -611,11 +634,13 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
t.text "aliases", null: false t.text "aliases", null: false
t.text "parent_tag_ids", null: false t.text "parent_tag_ids", null: false
t.datetime "deprecated_at" t.datetime "deprecated_at"
t.bigint "event_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.bigint "created_by_user_id" t.bigint "created_by_user_id"
t.json "names", null: false t.json "names", null: false
t.index ["created_at"], name: "index_tag_versions_on_created_at" t.index ["created_at"], name: "index_tag_versions_on_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc } t.index ["created_by_user_id", "created_at"], name: "index_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
t.index ["event_id"], name: "index_tag_versions_on_event_id"
t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc } t.index ["tag_id", "created_at"], name: "index_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true t.index ["tag_id", "version_no"], name: "index_tag_versions_on_tag_id_and_version_no", unique: true
t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive" t.check_constraint "`version_no` > 0", name: "tag_versions_version_no_positive"
@@ -771,6 +796,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "events", "events", column: "reverted_from_event_id"
add_foreign_key "gekanator_ai_runs", "gekanator_question_suggestions" add_foreign_key "gekanator_ai_runs", "gekanator_question_suggestions"
add_foreign_key "gekanator_games", "posts", column: "correct_post_id" add_foreign_key "gekanator_games", "posts", column: "correct_post_id"
add_foreign_key "gekanator_games", "posts", column: "guessed_post_id" add_foreign_key "gekanator_games", "posts", column: "guessed_post_id"
@@ -792,6 +818,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
add_foreign_key "material_sync_sources", "users", column: "created_by_user_id" add_foreign_key "material_sync_sources", "users", column: "created_by_user_id"
add_foreign_key "material_sync_sources", "users", column: "updated_by_user_id" add_foreign_key "material_sync_sources", "users", column: "updated_by_user_id"
add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id" add_foreign_key "material_sync_suppressions", "users", column: "created_by_user_id"
add_foreign_key "material_versions", "events"
add_foreign_key "material_versions", "materials" add_foreign_key "material_versions", "materials"
add_foreign_key "material_versions", "materials", column: "parent_id" add_foreign_key "material_versions", "materials", column: "parent_id"
add_foreign_key "material_versions", "users", column: "created_by_user_id" add_foreign_key "material_versions", "users", column: "created_by_user_id"
@@ -813,10 +840,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
add_foreign_key "post_tags", "posts" add_foreign_key "post_tags", "posts"
add_foreign_key "post_tags", "tags" add_foreign_key "post_tags", "tags"
add_foreign_key "post_tags", "users", column: "created_user_id" add_foreign_key "post_tags", "users", column: "created_user_id"
add_foreign_key "post_versions", "events"
add_foreign_key "post_versions", "posts" add_foreign_key "post_versions", "posts"
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", "events"
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_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", "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", "tag_description_versions", column: ["tag_id", "source_language_code", "source_version_no"], primary_key: ["tag_id", "language_code", "version_no"]
@@ -835,6 +864,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
add_foreign_key "tag_group_tags", "tag_groups" add_foreign_key "tag_group_tags", "tag_groups"
add_foreign_key "tag_group_tags", "tags" add_foreign_key "tag_group_tags", "tags"
add_foreign_key "tag_group_tags", "users", column: "created_by_user_id" add_foreign_key "tag_group_tags", "users", column: "created_by_user_id"
add_foreign_key "tag_group_versions", "events"
add_foreign_key "tag_groups", "users", column: "created_by_user_id" add_foreign_key "tag_groups", "users", column: "created_by_user_id"
add_foreign_key "tag_groups", "users", column: "updated_by_user_id" add_foreign_key "tag_groups", "users", column: "updated_by_user_id"
add_foreign_key "tag_implications", "tags" add_foreign_key "tag_implications", "tags"
@@ -844,6 +874,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
add_foreign_key "tag_names", "tags" add_foreign_key "tag_names", "tags"
add_foreign_key "tag_similarities", "tags" add_foreign_key "tag_similarities", "tags"
add_foreign_key "tag_similarities", "tags", column: "target_tag_id" add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
add_foreign_key "tag_versions", "events"
add_foreign_key "tag_versions", "users", column: "created_by_user_id" add_foreign_key "tag_versions", "users", column: "created_by_user_id"
add_foreign_key "theatre_comments", "theatres" add_foreign_key "theatre_comments", "theatres"
add_foreign_key "theatre_comments", "users" add_foreign_key "theatre_comments", "users"
+1
ファイルの表示
@@ -102,6 +102,7 @@ RSpec.describe 'Tag group persistence constraints' do
6.times { nodes = [{ 'tag_id' => tag.id, 'children' => nodes }] } 6.times { nodes = [{ 'tag_id' => tag.id, 'children' => nodes }] }
reject_statement do reject_statement do
insert_group_row('tag_group_versions', { insert_group_row('tag_group_versions', {
event_id: create_event!(event_type: 'unknown').id,
tag_group_id: group_id, version_no: 2, event_type: 'update', name: '制約確認', tag_group_id: group_id, version_no: 2, event_type: 'update', name: '制約確認',
members: [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }].to_json, members: [{ 'tag_id' => tag.id, 'version_no' => tag.version_no }].to_json,
placements: nodes.to_json, created_at: Time.current }) placements: nodes.to_json, created_at: Time.current })
+1
ファイルの表示
@@ -10,6 +10,7 @@ RSpec.describe 'TagVersion names database constraint' do
def insert_snapshot!(snapshot) def insert_snapshot!(snapshot)
TagVersion.insert_all!([{ tag_id: tag.id, version_no: 1, event_type: 'create', TagVersion.insert_all!([{ tag_id: tag.id, version_no: 1, event_type: 'create',
event_id: create_event!(event_type: 'unknown').id,
name: 'snapshot_name', category: 'general', name: 'snapshot_name', category: 'general',
aliases: '', parent_tag_ids: '', names: snapshot, aliases: '', parent_tag_ids: '', names: snapshot,
created_at: Time.current }]) created_at: Time.current }])
+21 -7
ファイルの表示
@@ -9,7 +9,9 @@ RSpec.describe HistoryAccess do
locale locale
rows = ['ja', 'ain'].flat_map do |language| rows = ['ja', 'ain'].flat_map do |language|
['create', 'discard', 'restore'].each_with_index.map do |event, index| ['create', 'discard', 'restore'].each_with_index.map do |event, index|
TagDescriptionVersion.create!(tag_id: tag.id, language_code: language, TagDescriptionVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag_id: tag.id, language_code: language,
version_no: index + 1, event_type: language == 'ain' ? 'update' : event, version_no: index + 1, event_type: language == 'ain' ? 'update' : event,
body: "#{ language } #{ index }", created_at: Time.utc(2024)) body: "#{ language } #{ index }", created_at: Time.utc(2024))
end end
@@ -28,7 +30,9 @@ RSpec.describe HistoryAccess do
public_metadata = described_class.set(user: nil) { described_class.authority } public_metadata = described_class.set(user: nil) { described_class.authority }
admin = create(:user, :admin) admin = create(:user, :admin)
admin_metadata = described_class.set(user: admin) { described_class.authority } admin_metadata = described_class.set(user: admin) { described_class.authority }
TagVersion.create!(tag:, version_no: 1, event_type: 'discard', name: 'private', TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: 1, event_type: 'discard', name: 'private',
category: 'general', category: 'general',
names: { ja: { name: 'private', script_code: 'Jpan', names: { ja: { name: 'private', script_code: 'Jpan',
auto_generated: false, aliases: [] } }, auto_generated: false, aliases: [] } },
@@ -52,7 +56,9 @@ RSpec.describe HistoryAccess do
blob = ActiveStorage::Blob.create_before_direct_upload!(filename: 'private.png', blob = ActiveStorage::Blob.create_before_direct_upload!(filename: 'private.png',
byte_size: 1, checksum: Base64.strict_encode64('checksum'), content_type: 'image/png') byte_size: 1, checksum: Base64.strict_encode64('checksum'), content_type: 'image/png')
ActiveStorage::Attachment.create!(name: 'thumbnail', record: post, blob:) ActiveStorage::Attachment.create!(name: 'thumbnail', record: post, blob:)
PostVersion.create!(post:, version_no: 1, event_type: 'discard', url: post.url, PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 1, event_type: 'discard', url: post.url,
title: 'private', tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024)) title: 'private', tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024))
variant = ActiveStorage::VariantRecord.create!(blob:, variation_digest: 'spec-variation') variant = ActiveStorage::VariantRecord.create!(blob:, variation_digest: 'spec-variation')
derived = ActiveStorage::Blob.create_before_direct_upload!(filename: 'derived.png', derived = ActiveStorage::Blob.create_before_direct_upload!(filename: 'derived.png',
@@ -78,7 +84,9 @@ RSpec.describe 'Counts artifact policy and external references' do
it 'does not expose a private linked tag name through an external history snapshot' do it 'does not expose a private linked tag name through an external history snapshot' do
tag = create(:tag, primary_name: 'private-linked-name') tag = create(:tag, primary_name: 'private-linked-name')
TagVersion.create!(tag:, version_no: 1, event_type: 'discard', name: 'private-linked-name', TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: 1, event_type: 'discard', name: 'private-linked-name',
category: 'general', aliases: '', parent_tag_ids: '', created_at: Time.utc(2024), category: 'general', aliases: '', parent_tag_ids: '', created_at: Time.utc(2024),
names: { ja: { name: 'private-linked-name', script_code: 'Jpan', names: { ja: { name: 'private-linked-name', script_code: 'Jpan',
auto_generated: false, aliases: [] } }) auto_generated: false, aliases: [] } })
@@ -87,7 +95,9 @@ RSpec.describe 'Counts artifact policy and external references' do
name: 'nico:public-external', linked_tags: 'private-linked-name', name: 'nico:public-external', linked_tags: 'private-linked-name',
created_at: Time.utc(2024)) created_at: Time.utc(2024))
post = create(:post) post = create(:post)
snapshot = PostVersion.create!(post:, version_no: 1, event_type: 'create', snapshot = PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 1, event_type: 'create',
title: 'external reference', url: post.url, tags: '', title: 'external reference', url: post.url, tags: '',
tags_json: [{ external_tag_id: external.id }], parent_post_ids: '', tags_json: [{ external_tag_id: external.id }], parent_post_ids: '',
created_at: Time.utc(2024, 2)) created_at: Time.utc(2024, 2))
@@ -112,13 +122,17 @@ RSpec.describe 'Counts artifact policy and external references' do
child = create(:tag, primary_name: 'referencing child') child = create(:tag, primary_name: 'referencing child')
[[parent, 'discard', '', 'private parent'], [[parent, 'discard', '', 'private parent'],
[child, 'create', parent.id.to_s, 'referencing child']].each do |tag, event, parents, name| [child, 'create', parent.id.to_s, 'referencing child']].each do |tag, event, parents, name|
TagVersion.create!(tag:, version_no: 1, event_type: event, name:, TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: 1, event_type: event, name:,
category: 'general', aliases: '', parent_tag_ids: parents, created_at: Time.utc(2023), category: 'general', aliases: '', parent_tag_ids: parents, created_at: Time.utc(2023),
names: { ja: { name:, script_code: 'Jpan', names: { ja: { name:, script_code: 'Jpan',
auto_generated: false, aliases: [] } }) auto_generated: false, aliases: [] } })
end end
post = create(:post) post = create(:post)
row = PostVersion.create!(post:, version_no: 1, event_type: 'create', title: 'snapshot', row = PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 1, event_type: 'create', title: 'snapshot',
url: post.url, tags: '', parent_post_ids: '', url: post.url, tags: '', parent_post_ids: '',
tags_json: [{ tag_id: child.id, version_no: 1, name: 'referencing child', sections: [] }], tags_json: [{ tag_id: child.id, version_no: 1, name: 'referencing child', sections: [] }],
created_at: Time.utc(2024)) created_at: Time.utc(2024))
+24 -1
ファイルの表示
@@ -23,9 +23,22 @@ RSpec.describe 'Locale activation', type: :model do
.and_raise('backfill failed') .and_raise('backfill failed')
end end
it 'groups all tag histories from locale creation into one event' do
previous_ids = TagVersion.pluck(:id)
expect { build_locale.save! }.to change { event_rows.length }.by(1)
event = event_rows.last
expect(event).to include('event_type' => 'locales.create', 'created_by_user_id' => nil)
versions = TagVersion.where.not(id: previous_ids)
expect(versions.pluck(:tag_id)).to include(first.id, second.id)
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end
it 'rolls back a newly created locale and the preceding generated names' do it 'rolls back a newly created locale and the preceding generated names' do
locale = build_locale locale = build_locale
fail_second_generation! fail_second_generation!
events = event_rows
expect { locale.save! }.to raise_error('backfill failed') expect { locale.save! }.to raise_error('backfill failed')
@@ -33,6 +46,7 @@ RSpec.describe 'Locale activation', type: :model do
expect(generated_names).to be_empty expect(generated_names).to be_empty
expect(first.reload.name('ja')).to eq('activation_first') expect(first.reload.name('ja')).to eq('activation_first')
expect(second.reload.name('ja')).to eq('activation_second') expect(second.reload.name('ja')).to eq('activation_second')
expect(event_rows).to eq(events)
end end
it 'does not backfill while the locale is inactive' do it 'does not backfill while the locale is inactive' do
@@ -45,7 +59,14 @@ RSpec.describe 'Locale activation', type: :model do
locale = build_locale(deprecated_at: Time.current) locale = build_locale(deprecated_at: Time.current)
locale.save! locale.save!
locale.update!(deprecated_at: nil) previous_ids = TagVersion.pluck(:id)
expect { locale.update!(deprecated_at: nil) }.to change { event_rows.length }.by(1)
event = event_rows.last
expect(event).to include('event_type' => 'locales.restore', 'created_by_user_id' => nil)
versions = TagVersion.where.not(id: previous_ids)
expect(versions.pluck(:tag_id)).to include(first.id, second.id)
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
expect(generated_names.pluck(:tag_id)).to contain_exactly(first.id, second.id) expect(generated_names.pluck(:tag_id)).to contain_exactly(first.id, second.id)
expect(generated_names).to all(have_attributes( expect(generated_names).to all(have_attributes(
@@ -57,11 +78,13 @@ RSpec.describe 'Locale activation', type: :model do
locale.save! locale.save!
previous_deprecated_at = locale.reload.deprecated_at previous_deprecated_at = locale.reload.deprecated_at
fail_second_generation! fail_second_generation!
events = event_rows
expect { locale.update!(deprecated_at: nil) }.to raise_error('backfill failed') expect { locale.update!(deprecated_at: nil) }.to raise_error('backfill failed')
expect(locale.reload.deprecated_at).to eq(previous_deprecated_at) expect(locale.reload.deprecated_at).to eq(previous_deprecated_at)
expect(generated_names).to be_empty expect(generated_names).to be_empty
expect(event_rows).to eq(events)
end end
it 'preserves a human primary through deactivation and repeated activation' do it 'preserves a human primary through deactivation and repeated activation' do
+14 -14
ファイルの表示
@@ -12,20 +12,20 @@ RSpec.describe PostVersion, type: :model do
let!(:post_version) do let!(:post_version) do
PostVersion.create!( PostVersion.create!(
post: post_record, event_id: create_event!(event_type: 'unknown').id,
version_no: 1, post: post_record,
event_type: 'create', version_no: 1,
title: post_record.title, event_type: 'create',
url: post_record.url, title: post_record.title,
thumbnail_base: post_record.thumbnail_base, url: post_record.url,
tags: post_record.snapshot_tag_names.join(' '), thumbnail_base: post_record.thumbnail_base,
tags_json: post_record.snapshot_tags_json, tags: post_record.snapshot_tag_names.join(' '),
parent_post_ids: post_record.snapshot_parent_post_ids.join(' '), tags_json: post_record.snapshot_tags_json,
original_created_from: post_record.original_created_from, parent_post_ids: post_record.snapshot_parent_post_ids.join(' '),
original_created_before: post_record.original_created_before, original_created_from: post_record.original_created_from,
created_at: Time.current, original_created_before: post_record.original_created_before,
created_by_user: nil created_at: Time.current,
) created_by_user: nil)
end end
it 'is read only after create' do it 'is read only after create' do
+27
ファイルの表示
@@ -34,6 +34,31 @@ RSpec.describe TagNameSanitisationRule, type: :model do
described_class.create!(priority: 10, source_pattern: 'foo_', replacement: 'foo') described_class.create!(priority: 10, source_pattern: 'foo_', replacement: 'foo')
end end
it 'groups renames, merges and affected post histories into one event' do
target = create(:tag, primary_name: 'foobar')
source = create(:tag, primary_name: 'temporary_source')
renamed = create(:tag, primary_name: 'temporary_rename')
source.tag_name('ja').update_columns(name: 'foo_bar')
renamed.tag_name('ja').update_columns(name: 'foo_baz')
post = create(:post)
PostTag.create!(post:, tag: source)
previous_tag_ids = TagVersion.pluck(:id)
previous_post_ids = PostVersion.pluck(:id)
expect { described_class.apply! }.to change { event_rows.length }.by(1)
event = event_rows.last
expect(event).to include(
'event_type' => 'tag_name_sanitisation_rules.apply', 'created_by_user_id' => nil)
tag_versions = TagVersion.where.not(id: previous_tag_ids)
expect(tag_versions.pluck(:tag_id).uniq).to contain_exactly(target.id, source.id, renamed.id)
expect(tag_versions.pluck(:event_type)).to include('discard', 'update')
expect(tag_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
post_versions = PostVersion.where.not(id: previous_post_ids)
expect(post_versions.pluck(:post_id).uniq).to eq([post.id])
expect(post_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end
[:primary, :alias].each do |source_kind| [:primary, :alias].each do |source_kind|
it "keeps the primary identity when a #{ source_kind } collides within its Tag" do it "keeps the primary identity when a #{ source_kind } collides within its Tag" do
tag = create(:tag, primary_name: source_kind == :primary ? 'temporary' : 'foobar') tag = create(:tag, primary_name: source_kind == :primary ? 'temporary' : 'foobar')
@@ -83,6 +108,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
PostTag.create!(post:, tag: source) PostTag.create!(post:, tag: source)
models = [Tag, TagName, TagVersion, TagDescription, TagDescriptionVersion, PostTag] models = [Tag, TagName, TagVersion, TagDescription, TagDescriptionVersion, PostTag]
snapshots = models.map { |model| model.order(*Array(model.primary_key)).map(&:attributes) } snapshots = models.map { |model| model.order(*Array(model.primary_key)).map(&:attributes) }
events = event_rows
expect { described_class.apply! }.to raise_error(ActiveRecord::RecordInvalid) expect { described_class.apply! }.to raise_error(ActiveRecord::RecordInvalid)
@@ -90,6 +116,7 @@ RSpec.describe TagNameSanitisationRule, type: :model do
model.order(*Array(model.primary_key)).map(&:attributes) model.order(*Array(model.primary_key)).map(&:attributes)
end end
expect(after_state).to eq(snapshots) expect(after_state).to eq(snapshots)
expect(event_rows).to eq(events)
expect(post.reload.tags).to contain_exactly(source) expect(post.reload.tags).to contain_exactly(source)
expect(target.reload.name('ja')).to eq('foobar') expect(target.reload.name('ja')).to eq('foobar')
end end
+14 -14
ファイルの表示
@@ -665,20 +665,20 @@ RSpec.describe Tag, type: :model do
def create_post_version_for!(post, version_no: 1, event_type: 'create', created_by_user: nil) def create_post_version_for!(post, version_no: 1, event_type: 'create', created_by_user: nil)
PostVersion.create!( PostVersion.create!(
post: post, event_id: create_event!(event_type: 'unknown').id,
version_no: version_no, post: post,
event_type: event_type, version_no: version_no,
title: post.title, event_type: event_type,
url: post.url, title: post.title,
thumbnail_base: post.thumbnail_base, url: post.url,
tags: snapshot_tags(post), thumbnail_base: post.thumbnail_base,
tags_json: post.snapshot_tags_json, tags: snapshot_tags(post),
parent_post_ids: post.snapshot_parent_post_ids.join(' '), tags_json: post.snapshot_tags_json,
original_created_from: post.original_created_from, parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_before: post.original_created_before, original_created_from: post.original_created_from,
created_at: Time.current, original_created_before: post.original_created_before,
created_by_user: created_by_user created_at: Time.current,
) created_by_user: created_by_user)
end end
context 'when post versions are enabled' do context 'when post versions are enabled' do
+24 -22
ファイルの表示
@@ -6,17 +6,18 @@ RSpec.describe VersionRecord, type: :model do
it 'makes TagVersion read only after create' do it 'makes TagVersion read only after create' do
version = TagVersion.create!( version = TagVersion.create!(
tag: tag, event_id: create_event!(event_type: 'unknown').id,
version_no: 1, tag: tag,
event_type: 'create', version_no: 1,
name: primary_tag_name_for(tag, 'ja').name, event_type: 'create',
category: tag.category, name: primary_tag_name_for(tag, 'ja').name,
aliases: '', category: tag.category,
names: { ja: { name: primary_tag_name_for(tag, 'ja').name, aliases: '',
script_code: 'Jpan', auto_generated: false, aliases: [] } }, names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
parent_tag_ids: '', script_code: 'Jpan', auto_generated: false, aliases: [] } },
created_at: Time.current, parent_tag_ids: '',
created_by_user: nil) created_at: Time.current,
created_by_user: nil)
expect { expect {
version.update!(name: 'changed') version.update!(name: 'changed')
@@ -25,17 +26,18 @@ RSpec.describe VersionRecord, type: :model do
it 'prevents TagVersion destroy' do it 'prevents TagVersion destroy' do
version = TagVersion.create!( version = TagVersion.create!(
tag: tag, event_id: create_event!(event_type: 'unknown').id,
version_no: 1, tag: tag,
event_type: 'create', version_no: 1,
name: primary_tag_name_for(tag, 'ja').name, event_type: 'create',
category: tag.category, name: primary_tag_name_for(tag, 'ja').name,
aliases: '', category: tag.category,
names: { ja: { name: primary_tag_name_for(tag, 'ja').name, aliases: '',
script_code: 'Jpan', auto_generated: false, aliases: [] } }, names: { ja: { name: primary_tag_name_for(tag, 'ja').name,
parent_tag_ids: '', script_code: 'Jpan', auto_generated: false, aliases: [] } },
created_at: Time.current, parent_tag_ids: '',
created_by_user: nil) created_at: Time.current,
created_by_user: nil)
expect { expect {
version.destroy! version.destroy!
+1
ファイルの表示
@@ -44,6 +44,7 @@ RSpec.configure do |config|
end end
config.include TestRecords config.include TestRecords
config.include EventRecords
config.include RakeTaskHelper config.include RakeTaskHelper
# FactoryBot の create / create_list を使へるやぅにする # FactoryBot の create / create_list を使へるやぅにする
+3 -1
ファイルの表示
@@ -84,7 +84,9 @@ RSpec.describe 'Counts native GET acknowledgement', type: :request do
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
job, file, attempt = ready_job('json') job, file, attempt = ready_job('json')
record = create(:post) record = create(:post)
PostVersion.create!(post: record, version_no: 1, event_type: 'discard', title: 'private', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: record, version_no: 1, event_type: 'discard', title: 'private',
url: record.url, tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024)) url: record.url, tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024))
get "/counts/export/files/#{ file }", params: { job:, attempt: } get "/counts/export/files/#{ file }", params: { job:, attempt: }
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
+3 -1
ファイルの表示
@@ -8,7 +8,9 @@ RSpec.describe 'History boundaries shared with Counts', type: :request do
before do before do
['create', 'discard'].each_with_index do |event, index| ['create', 'discard'].each_with_index do |event, index|
TagDescriptionVersion.create!(tag_id: tag.id, language_code: 'ja', TagDescriptionVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag_id: tag.id, language_code: 'ja',
version_no: index + 1, event_type: event, body: 'private body', version_no: index + 1, event_type: event, body: 'private body',
created_at: Time.utc(2024)) created_at: Time.utc(2024))
end end
+6 -2
ファイルの表示
@@ -10,12 +10,16 @@ RSpec.describe 'Legacy Counts API', type: :request do
category: 'general', sections: [] } category: 'general', sections: [] }
[1, 2, 3].each do |number| [1, 2, 3].each do |number|
time = Time.utc(2024, number == 3 ? 2 : 1, number, 0, 0) time = Time.utc(2024, number == 3 ? 2 : 1, number, 0, 0)
PostVersion.create!(post:, version_no: number, PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: number,
event_type: number == 1 ? 'create' : 'update', event_type: number == 1 ? 'create' : 'update',
tags: '', url: post.url, title: "Snapshot #{ number }", parent_post_ids: '', tags: '', url: post.url, title: "Snapshot #{ number }", parent_post_ids: '',
tags_json: number == 1 ? [snapshot] : [], tags_json: number == 1 ? [snapshot] : [],
created_by_user: number == 3 ? nil : actor, created_at: time) created_by_user: number == 3 ? nil : actor, created_at: time)
TagVersion.create!(tag:, version_no: number, TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: number,
event_type: number == 1 ? 'create' : 'update', event_type: number == 1 ? 'create' : 'update',
name: 'counts tag', category: 'general', aliases: '', parent_tag_ids: '', name: 'counts tag', category: 'general', aliases: '', parent_tag_ids: '',
names: { ja: { name: 'counts tag', script_code: 'Jpan', names: { ja: { name: 'counts tag', script_code: 'Jpan',
+27 -9
ファイルの表示
@@ -14,7 +14,9 @@ RSpec.describe 'Counts table API', type: :request do
end end
before do before do
TagVersion.create!(tag:, version_no: 1, event_type: 'create', name: 'alpha', TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: 1, event_type: 'create', name: 'alpha',
category: 'general', aliases: '', parent_tag_ids: '', category: 'general', aliases: '', parent_tag_ids: '',
names: { ja: { name: 'alpha', script_code: 'Jpan', auto_generated: false, aliases: [] } }, names: { ja: { name: 'alpha', script_code: 'Jpan', auto_generated: false, aliases: [] } },
created_at: Time.utc(2023)) created_at: Time.utc(2023))
@@ -24,7 +26,9 @@ RSpec.describe 'Counts table API', type: :request do
{ tag_id: tag.id, version_no: 1, name: 'alpha', category: 'general', sections: [] }, { tag_id: tag.id, version_no: 1, name: 'alpha', category: 'general', sections: [] },
{ external_tag_id: external.id }] { external_tag_id: external.id }]
[1, 2, 3].each do |number| [1, 2, 3].each do |number|
PostVersion.create!(post: record, version_no: number, PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: record, version_no: number,
event_type: number == 1 ? 'create' : 'update', url: record.url, event_type: number == 1 ? 'create' : 'update', url: record.url,
title: "snapshot #{ number }", tags: '', parent_post_ids: '', title: "snapshot #{ number }", tags: '', parent_post_ids: '',
tags_json: snapshots.first(number == 1 ? 2 : (number == 2 ? 1 : 0)), tags_json: snapshots.first(number == 1 ? 2 : (number == 2 ? 1 : 0)),
@@ -134,7 +138,9 @@ RSpec.describe 'Counts table API', type: :request do
end end
it 'checks the current latest flag beyond the execution timestamp' do it 'checks the current latest flag beyond the execution timestamp' do
PostVersion.create!(post: record, version_no: 4, event_type: 'update', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: record, version_no: 4, event_type: 'update',
url: record.url, title: 'after execution', tags: '', tags_json: [], url: record.url, title: 'after execution', tags: '', tags_json: [],
parent_post_ids: '', created_at: Time.utc(2025, 2, 1)) parent_post_ids: '', created_at: Time.utc(2025, 2, 1))
result = query result = query
@@ -188,12 +194,16 @@ RSpec.describe 'Counts table API', type: :request do
tag.tag_names.find_by!(language_code: 'ja', primary_flg: true) tag.tag_names.find_by!(language_code: 'ja', primary_flg: true)
.update!(name: 'renamed-alpha') .update!(name: 'renamed-alpha')
create(:tag_name, :alias, tag:, name: 'alpha-alias') create(:tag_name, :alias, tag:, name: 'alpha-alias')
TagVersion.create!(tag:, version_no: 2, event_type: 'update', name: 'renamed-alpha', TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: 2, event_type: 'update', name: 'renamed-alpha',
category: 'general', aliases: 'alpha-alias', parent_tag_ids: '', category: 'general', aliases: 'alpha-alias', parent_tag_ids: '',
names: { ja: { name: 'renamed-alpha', script_code: 'Jpan', auto_generated: false, names: { ja: { name: 'renamed-alpha', script_code: 'Jpan', auto_generated: false,
aliases: [{ name: 'alpha-alias', script_code: 'Jpan' }] } }, aliases: [{ name: 'alpha-alias', script_code: 'Jpan' }] } },
created_at: Time.utc(2024, 2, 1)) created_at: Time.utc(2024, 2, 1))
PostVersion.create!(post: record, version_no: 4, event_type: 'update', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: record, version_no: 4, event_type: 'update',
url: record.url, title: 'after rename', tags: '', parent_post_ids: '', url: record.url, title: 'after rename', tags: '', parent_post_ids: '',
tags_json: [{ tag_id: tag.id, version_no: 2, name: 'renamed-alpha', tags_json: [{ tag_id: tag.id, version_no: 2, name: 'renamed-alpha',
category: 'general', sections: [] }], category: 'general', sections: [] }],
@@ -257,7 +267,9 @@ RSpec.describe 'Counts table API', type: :request do
it 'reports column overflow separately from malformed input' do it 'reports column overflow separately from malformed input' do
101.times do |index| 101.times do |index|
PostVersion.create!(post: record, version_no: index + 4, PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: record, version_no: index + 4,
event_type: 'update', url: record.url, title: "column #{ index }", event_type: 'update', url: record.url, title: "column #{ index }",
tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 3)) tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 3))
end end
@@ -301,7 +313,9 @@ RSpec.describe 'Counts table API', type: :request do
sign_in_as(create(:user, role)) if role sign_in_as(create(:user, role)) if role
# Equal timestamps deliberately require version ordering, not a date comparison. # Equal timestamps deliberately require version ordering, not a date comparison.
['discard', 'restore', 'discard', 'restore'].each_with_index do |event, index| ['discard', 'restore', 'discard', 'restore'].each_with_index do |event, index|
PostVersion.create!(post: record, version_no: index + 4, event_type: event, PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: record, version_no: index + 4, event_type: event,
url: record.url, title: event == 'restore' ? 'restored' : 'private snapshot', url: record.url, title: event == 'restore' ? 'restored' : 'private snapshot',
tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 3)) tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 3))
end end
@@ -329,7 +343,9 @@ RSpec.describe 'Counts table API', type: :request do
end end
it 'does not resolve private condition names even when the result set is empty' do it 'does not resolve private condition names even when the result set is empty' do
TagVersion.create!(tag:, version_no: 2, event_type: 'discard', name: 'secret tag', TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: 2, event_type: 'discard', name: 'secret tag',
category: 'general', aliases: '', parent_tag_ids: '', category: 'general', aliases: '', parent_tag_ids: '',
names: { ja: { name: 'secret tag', script_code: 'Jpan', auto_generated: false, aliases: [] } }, names: { ja: { name: 'secret tag', script_code: 'Jpan', auto_generated: false, aliases: [] } },
created_at: Time.utc(2024, 2)) created_at: Time.utc(2024, 2))
@@ -345,7 +361,9 @@ RSpec.describe 'Counts table API', type: :request do
end end
end end
it 'includes private history in the public timeline without an exclusion count' do it 'includes private history in the public timeline without an exclusion count' do
PostVersion.create!(post: record, version_no: 4, event_type: 'discard', url: record.url, PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: record, version_no: 4, event_type: 'discard', url: record.url,
title: 'hidden title', tags: '', tags_json: [], parent_post_ids: '', title: 'hidden title', tags: '', tags_json: [], parent_post_ids: '',
created_at: Time.utc(2024, 3)) created_at: Time.utc(2024, 3))
result = query(mode: 'summary', rows: 'created_at:month', cols: 'event_type') result = query(mode: 'summary', rows: 'created_at:month', cols: 'event_type')
+40 -1
ファイルの表示
@@ -314,6 +314,7 @@ RSpec.describe 'Materials API', type: :request do
.and change(Tag, :count).by(1) .and change(Tag, :count).by(1)
.and change(TagName, :count).by(1) .and change(TagName, :count).by(1)
.and change(MaterialVersion, :count).by(1) .and change(MaterialVersion, :count).by(1)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
@@ -325,6 +326,12 @@ RSpec.describe 'Materials API', type: :request do
expect(material.file.attached?).to be(true) expect(material.file.attached?).to be(true)
expect(material.version_no).to eq(1) expect(material.version_no).to eq(1)
expect(material.material_versions.first.event_type).to eq('create') expect(material.material_versions.first.event_type).to eq('create')
event = event_rows.last
expect(event).to include(
'event_type' => 'materials.create',
'created_by_user_id' => member_user.id)
expect(material.material_versions.first.event_id).to eq(event.fetch('id'))
expect(material.tag.tag_versions.pluck(:event_id)).to eq([event.fetch('id')])
expect(material.material_export_items.first.export_path).to eq('伊地知ニジカ/created.png') expect(material.material_export_items.first.export_path).to eq('伊地知ニジカ/created.png')
expect(material.material_versions.first.export_paths_json).to eq( expect(material.material_versions.first.export_paths_json).to eq(
'legacy_drive' => '伊地知ニジカ/created.png' 'legacy_drive' => '伊地知ニジカ/created.png'
@@ -509,6 +516,7 @@ RSpec.describe 'Materials API', type: :request do
export_paths: { legacy_drive: '伊地知ニジカ/updated.jpg' } export_paths: { legacy_drive: '伊地知ニジカ/updated.jpg' }
} }
end.to change(MaterialVersion, :count).by(2) end.to change(MaterialVersion, :count).by(2)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -517,6 +525,12 @@ RSpec.describe 'Materials API', type: :request do
expect(material.tag.category).to eq('material') expect(material.tag.category).to eq('material')
expect(material.url).to eq('https://example.com/updated-source') expect(material.url).to eq('https://example.com/updated-source')
expect(material.updated_by_user).to eq(member_user) expect(material.updated_by_user).to eq(member_user)
event = event_rows.last
expect(event).to include(
'event_type' => 'materials.update',
'created_by_user_id' => member_user.id)
expect(material.material_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
expect(material.tag.tag_versions.pluck(:event_id)).to eq([event.fetch('id')])
expect(material.file.attached?).to be(true) expect(material.file.attached?).to be(true)
expect(material.file.blob.id).not_to eq(old_blob_id) expect(material.file.blob.id).not_to eq(old_blob_id)
expect(ActiveStorage::Blob.where(id: old_blob_id).exists?).to be(true) expect(ActiveStorage::Blob.where(id: old_blob_id).exists?).to be(true)
@@ -632,6 +646,7 @@ RSpec.describe 'Materials API', type: :request do
MaterialVersionRecorder.record!(material:, event_type: :create, MaterialVersionRecorder.record!(material:, event_type: :create,
created_by_user: member_user) created_by_user: member_user)
TagVersioning.ensure_snapshot!(tag, created_by_user: member_user) TagVersioning.ensure_snapshot!(tag, created_by_user: member_user)
events = event_rows
expect do expect do
put "/materials/#{ material.id }", params: { put "/materials/#{ material.id }", params: {
@@ -643,6 +658,7 @@ RSpec.describe 'Materials API', type: :request do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(material.reload.version_no).to eq(1) expect(material.reload.version_no).to eq(1)
expect(event_rows).to eq(events)
end end
it 'records update version when only export_path changes' do it 'records update version when only export_path changes' do
@@ -807,6 +823,12 @@ RSpec.describe 'Materials API', type: :request do
it 'creates source suppression as member' do it 'creates source suppression as member' do
sign_in_as(member_user) sign_in_as(member_user)
materials = ['a', 'b'].map do |name|
Material.create!(
source_kind: 'google_drive_file', source_file_id: "blocked-#{ name }",
source_path: "伊地知ニジカ/危険素材/#{ name }.png",
url: "https://example.com/blocked-#{ name }", created_by_user: member_user)
end
expect do expect do
post '/materials/suppressions', params: { post '/materials/suppressions', params: {
@@ -815,6 +837,7 @@ RSpec.describe 'Materials API', type: :request do
reason: 'copyright_high_risk' reason: 'copyright_high_risk'
} }
end.to change(MaterialSyncSuppression, :count).by(1) end.to change(MaterialSyncSuppression, :count).by(1)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
expect(json).to include( expect(json).to include(
@@ -822,6 +845,14 @@ RSpec.describe 'Materials API', type: :request do
'drive_path' => '伊地知ニジカ/危険素材', 'drive_path' => '伊地知ニジカ/危険素材',
'normalized_source_key' => 'google_drive_path_prefix:伊地知ニジカ/危険素材' 'normalized_source_key' => 'google_drive_path_prefix:伊地知ニジカ/危険素材'
) )
event = event_rows.last
expect(event).to include(
'event_type' => 'materials.block', 'created_by_user_id' => member_user.id)
expect(materials.map { _1.reload.discarded? }).to eq([true, true])
versions = MaterialVersion.where(material_id: materials.map(&:id))
expect(versions.where(event_type: 'discard').pluck(:material_id))
.to match_array(materials.map(&:id))
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
end end
@@ -860,11 +891,19 @@ RSpec.describe 'Materials API', type: :request do
end end
it 'discards the material and returns 204' do it 'discards the material and returns 204' do
delete "/materials/#{ material.id }" expect { delete "/materials/#{ material.id }" }
.to change { event_rows.length }.by(1)
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
expect(Material.find_by(id: material.id)).to be_nil expect(Material.find_by(id: material.id)).to be_nil
expect(Material.with_discarded.find(material.id)).to be_discarded expect(Material.with_discarded.find(material.id)).to be_discarded
event = event_rows.last
expect(event).to include(
'event_type' => 'materials.destroy',
'created_by_user_id' => member_user.id)
versions = MaterialVersion.where(material_id: material.id)
expect(versions.order(:version_no).last.event_type).to eq('discard')
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
end end
end end
+5
ファイルの表示
@@ -177,6 +177,7 @@ RSpec.describe 'NicoTags', type: :request do
} }
}.to change(TagVersion, :count).by(2) }.to change(TagVersion, :count).by(2)
.and change(NicoTagVersion, :count).by(1) .and change(NicoTagVersion, :count).by(1)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -186,6 +187,10 @@ RSpec.describe 'NicoTags', type: :request do
linked_versions = TagVersion.where(tag: [linked_a, linked_b]).order(:tag_id) linked_versions = TagVersion.where(tag: [linked_a, linked_b]).order(:tag_id)
expect(linked_versions.map(&:event_type)).to eq(['create', 'create']) expect(linked_versions.map(&:event_type)).to eq(['create', 'create'])
expect(linked_versions.map(&:created_by_user_id)).to all(eq(admin.id)) expect(linked_versions.map(&:created_by_user_id)).to all(eq(admin.id))
event = event_rows.last
expect(event).to include(
'event_type' => 'tags.update', 'created_by_user_id' => admin.id)
expect(linked_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
versions = nico_tag.reload.nico_tag_versions.order(:version_no) versions = nico_tag.reload.nico_tag_versions.order(:version_no)
expect(versions.map(&:event_type)).to eq(['create', 'update']) expect(versions.map(&:event_type)).to eq(['create', 'update'])
+39 -16
ファイルの表示
@@ -50,6 +50,8 @@ RSpec.describe 'Posts API', type: :request do
post:, post:,
version_no: 1, version_no: 1,
event_type: 'create', event_type: 'create',
event_id: create_event!(
event_type: 'posts.create', created_by_user: post.uploaded_user).id,
title: post.title, title: post.title,
url: post.url, url: post.url,
thumbnail_base: post.thumbnail_base, thumbnail_base: post.thumbnail_base,
@@ -1027,7 +1029,7 @@ RSpec.describe 'Posts API', type: :request do
it 'dry-runs without persisting posts or new tags' do it 'dry-runs without persisting posts or new tags' do
sign_in_as(member) sign_in_as(member)
counts = [Post.count, Tag.count, TagName.count] counts = [Post.count, Tag.count, TagName.count, event_rows.length]
post '/posts?dry=1', params: post_write_params( post '/posts?dry=1', params: post_write_params(
title: 'dry-run post', title: 'dry-run post',
@@ -1043,7 +1045,7 @@ RSpec.describe 'Posts API', type: :request do
[{ 'name' => 'new_dry_run_tag', [{ 'name' => 'new_dry_run_tag',
'category' => 'character', 'category' => 'character',
'section_literals' => [] }]) 'section_literals' => [] }])
expect([Post.count, Tag.count, TagName.count]).to eq(counts) expect([Post.count, Tag.count, TagName.count, event_rows.length]).to eq(counts)
end end
it 'dry-runs an existing URL without validating its upload' do it 'dry-runs an existing URL without validating its upload' do
@@ -2218,20 +2220,20 @@ RSpec.describe 'Posts API', type: :request do
def create_post_version! post, version_no:, event_type:, created_by_user:, created_at: def create_post_version! post, version_no:, event_type:, created_by_user:, created_at:
PostVersion.create!( PostVersion.create!(
post:, event_id: create_event!(event_type: 'unknown').id,
version_no:, post:,
event_type:, version_no:,
title: post.title, event_type:,
url: post.url, title: post.title,
thumbnail_base: post.thumbnail_base, url: post.url,
tags: snapshot_tags(post), thumbnail_base: post.thumbnail_base,
tags_json: post.snapshot_tags_json, tags: snapshot_tags(post),
parent_post_ids: post.snapshot_parent_post_ids.join(' '), tags_json: post.snapshot_tags_json,
original_created_from: post.original_created_from, parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_before: post.original_created_before, original_created_from: post.original_created_from,
created_at:, original_created_before: post.original_created_before,
created_by_user: created_at:,
) created_by_user:)
end end
let!(:v1) do let!(:v1) do
@@ -2559,6 +2561,7 @@ RSpec.describe 'Posts API', type: :request do
tags: 'spec_tag', tags: 'spec_tag',
thumbnail: dummy_upload) thumbnail: dummy_upload)
end.to change(PostVersion, :count).by(1) end.to change(PostVersion, :count).by(1)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
@@ -2570,6 +2573,13 @@ RSpec.describe 'Posts API', type: :request do
expect(version.url).to eq('https://example.com/versioned-post') expect(version.url).to eq('https://example.com/versioned-post')
expect(version.created_by_user_id).to eq(member.id) expect(version.created_by_user_id).to eq(member.id)
expect(version.tags).to eq(snapshot_tags(created_post)) expect(version.tags).to eq(snapshot_tags(created_post))
event = event_rows.last
expect(event).to include(
'event_type' => 'posts.create', 'created_by_user_id' => member.id)
expect(version.event_id).to eq(event.fetch('id'))
tag_versions = TagVersion.where(tag_id: created_post.tags.select(:id))
expect(tag_versions).not_to be_empty
expect(tag_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
it 'creates next version on PUT /posts/:id when snapshot changes' do it 'creates next version on PUT /posts/:id when snapshot changes' do
@@ -2585,6 +2595,7 @@ RSpec.describe 'Posts API', type: :request do
title: 'updated title', title: 'updated title',
tags: 'spec_tag_2') tags: 'spec_tag_2')
end.to change(PostVersion, :count).by(1) end.to change(PostVersion, :count).by(1)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -2594,6 +2605,12 @@ RSpec.describe 'Posts API', type: :request do
expect(version.title).to eq('updated title') expect(version.title).to eq('updated title')
expect(version.created_by_user_id).to eq(member.id) expect(version.created_by_user_id).to eq(member.id)
expect(version.tags).to eq(snapshot_tags(post_record.reload)) expect(version.tags).to eq(snapshot_tags(post_record.reload))
event = event_rows.last
expect(event).to include(
'event_type' => 'posts.update', 'created_by_user_id' => member.id)
expect(version.event_id).to eq(event.fetch('id'))
tag_version = TagVersion.find_by!(tag_id: tag_name2.reload.tag_id, version_no: 1)
expect(tag_version.event_id).to eq(event.fetch('id'))
end end
it 'does not create a new version on PUT /posts/:id when snapshot is unchanged' do it 'does not create a new version on PUT /posts/:id when snapshot is unchanged' do
@@ -2602,6 +2619,7 @@ RSpec.describe 'Posts API', type: :request do
PostTag.create!(post: post_record, tag: Tag.no_deerjikist) PostTag.create!(post: post_record, tag: Tag.no_deerjikist)
base_version = create_post_version_for!(post_record.reload) base_version = create_post_version_for!(post_record.reload)
events = event_rows
expect { expect {
put "/posts/#{post_record.id}", params: post_write_params( put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no, base_version_no: base_version.version_no,
@@ -2615,10 +2633,12 @@ RSpec.describe 'Posts API', type: :request do
expect(version.version_no).to eq(1) expect(version.version_no).to eq(1)
expect(version.event_type).to eq('create') expect(version.event_type).to eq('create')
expect(version.tags).to eq(snapshot_tags(post_record)) expect(version.tags).to eq(snapshot_tags(post_record))
expect(event_rows).to eq(events)
end end
it 'does not create a version when POST /posts is invalid' do it 'does not create a version when POST /posts is invalid' do
sign_in_as(member) sign_in_as(member)
events = event_rows
expect do expect do
post '/posts', params: post_write_params( post '/posts', params: post_write_params(
@@ -2629,11 +2649,13 @@ RSpec.describe 'Posts API', type: :request do
end.not_to change(PostVersion, :count) end.not_to change(PostVersion, :count)
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_entity)
expect(event_rows).to eq(events)
end end
it 'does not create a version when PUT /posts/:id is invalid' do it 'does not create a version when PUT /posts/:id is invalid' do
sign_in_as(member) sign_in_as(member)
base_version = create_post_version_for!(post_record) base_version = create_post_version_for!(post_record)
events = event_rows
expect do expect do
put "/posts/#{post_record.id}", params: post_write_params( put "/posts/#{post_record.id}", params: post_write_params(
@@ -2645,6 +2667,7 @@ RSpec.describe 'Posts API', type: :request do
end.not_to change(PostVersion, :count) end.not_to change(PostVersion, :count)
expect(response).to have_http_status(:unprocessable_entity) expect(response).to have_http_status(:unprocessable_entity)
expect(event_rows).to eq(events)
end end
it 'rejects second-bearing original created timestamps on POST /posts' do it 'rejects second-bearing original created timestamps on POST /posts' do
+3 -1
ファイルの表示
@@ -119,7 +119,9 @@ RSpec.describe 'Sitemap entries API', type: :request do
# Legacy empty bodies can exist even though the write service rejects them. # Legacy empty bodies can exist even though the write service rejects them.
empty = TagDescription.create!(tag: empty_tag, language_code: 'ain', empty = TagDescription.create!(tag: empty_tag, language_code: 'ain',
body: '', version_no: 1) body: '', version_no: 1)
TagDescriptionVersion.create!(tag_id: empty_tag.id, language_code: 'ain', TagDescriptionVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag_id: empty_tag.id, language_code: 'ain',
body: '', version_no: 1, event_type: 'create', body: '', version_no: 1, event_type: 'create',
created_by_user_id: user.id, created_at: Time.current) created_by_user_id: user.id, created_at: Time.current)
+10
ファイルの表示
@@ -53,8 +53,13 @@ RSpec.describe "TagChildren", type: :request do
expect { do_request } expect { do_request }
.to change { parent.reload.children.ids.include?(child.id) } .to change { parent.reload.children.ids.include?(child.id) }
.from(false).to(true) .from(false).to(true)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
event = event_rows.last
expect(event).to include(
'event_type' => 'tags.update', 'created_by_user_id' => admin.id)
expect(child.tag_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
it 'returns 422 and does not create relation when the new link makes a cycle' do it 'returns 422 and does not create relation when the new link makes a cycle' do
@@ -132,6 +137,7 @@ RSpec.describe "TagChildren", type: :request do
expect { expect {
do_request do_request
}.to change(TagVersion, :count).by(2) }.to change(TagVersion, :count).by(2)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
@@ -140,6 +146,10 @@ RSpec.describe "TagChildren", type: :request do
expect(versions.first.parent_tag_ids.split).to include(parent.id.to_s) expect(versions.first.parent_tag_ids.split).to include(parent.id.to_s)
expect(versions.second.parent_tag_ids).to eq('') expect(versions.second.parent_tag_ids).to eq('')
expect(versions.second.created_by_user_id).to eq(admin.id) expect(versions.second.created_by_user_id).to eq(admin.id)
event = event_rows.last
expect(event).to include(
'event_type' => 'tags.update', 'created_by_user_id' => admin.id)
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
end end
+2
ファイルの表示
@@ -8,6 +8,7 @@ RSpec.describe 'Tag description Wiki compatibility', type: :request do
let!(:versions) do let!(:versions) do
['A', 'A', 'new-version'].each_with_index.map do |body, index| ['A', 'A', 'new-version'].each_with_index.map do |body, index|
TagDescriptionVersion.create!( TagDescriptionVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag_id: tag.id, language_code: 'ja', version_no: index + 1, tag_id: tag.id, language_code: 'ja', version_no: index + 1,
event_type: index.zero? ? 'create' : 'update', body:, event_type: index.zero? ? 'create' : 'update', body:,
created_by_user_id: user.id, created_at: Time.current) created_by_user_id: user.id, created_at: Time.current)
@@ -73,6 +74,7 @@ RSpec.describe 'Tag description Wiki compatibility', type: :request do
let!(:other_tag) { create(:tag, primary_name: 'other_description_contract') } let!(:other_tag) { create(:tag, primary_name: 'other_description_contract') }
let!(:other_version) do let!(:other_version) do
TagDescriptionVersion.create!( TagDescriptionVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag_id: other_tag.id, language_code: 'ja', version_no: 1, tag_id: other_tag.id, language_code: 'ja', version_no: 1,
event_type: 'create', body: 'other tag history', event_type: 'create', body: 'other tag history',
created_by_user_id: user.id, created_at: Time.current) created_by_user_id: user.id, created_at: Time.current)
+13 -1
ファイルの表示
@@ -13,7 +13,7 @@ RSpec.describe 'Tag description native API', type: :request do
def stored_state def stored_state
[TagDescription, TagDescriptionVersion, TagName, TagVersion].map do |model| [TagDescription, TagDescriptionVersion, TagName, TagVersion].map do |model|
model.order(:id).map(&:attributes) model.order(:id).map(&:attributes)
end end + [event_rows]
end end
def fail_save model def fail_save model
@@ -91,6 +91,8 @@ RSpec.describe 'Tag description native API', type: :request do
let!(:version) do let!(:version) do
TagDescriptionVersion.create!( TagDescriptionVersion.create!(
tag_id: tag.id, language_code: 'ja', body: 'native version', version_no: 1, tag_id: tag.id, language_code: 'ja', body: 'native version', version_no: 1,
event_id: create_event!(
event_type: 'tag_descriptions.create', created_by_user: author).id,
event_type: 'create', created_by_user_id: author.id, created_at: Time.current) event_type: 'create', created_by_user_id: author.id, created_at: Time.current)
end end
let(:name_endpoint) { "/tags/name/#{ tag_name.name }/description" } let(:name_endpoint) { "/tags/name/#{ tag_name.name }/description" }
@@ -185,6 +187,7 @@ RSpec.describe 'Tag description native API', type: :request do
expect { native_write(body: 'created natively') } expect { native_write(body: 'created natively') }
.to change(TagDescription, :count).by(1) .to change(TagDescription, :count).by(1)
.and change(TagDescriptionVersion, :count).by(1) .and change(TagDescriptionVersion, :count).by(1)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:created) expect(response).to have_http_status(:created)
description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja') description = TagDescription.find_by!(tag_id: tag.id, language_code: 'ja')
expect_current(description) expect_current(description)
@@ -192,6 +195,10 @@ RSpec.describe 'Tag description native API', type: :request do
expect(version).to have_attributes( expect(version).to have_attributes(
body: 'created natively', version_no: 1, event_type: 'create', body: 'created natively', version_no: 1, event_type: 'create',
created_by_user_id: editor.id) created_by_user_id: editor.id)
event = event_rows.last
expect(event).to include(
'event_type' => 'tag_descriptions.create', 'created_by_user_id' => editor.id)
expect(version.event_id).to eq(event.fetch('id'))
get "/wiki/#{ description.id }" get "/wiki/#{ description.id }"
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(json).to include('body' => description.body, 'revision_id' => version.id) expect(json).to include('body' => description.body, 'revision_id' => version.id)
@@ -223,6 +230,7 @@ RSpec.describe 'Tag description native API', type: :request do
expect(description.version_no).to eq(1) expect(description.version_no).to eq(1)
expect { native_write(body: 'after') } expect { native_write(body: 'after') }
.to change(TagDescriptionVersion, :count).by(1) .to change(TagDescriptionVersion, :count).by(1)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(description.reload).to have_attributes( expect(description.reload).to have_attributes(
body: 'after', version_no: 2, body: 'after', version_no: 2,
@@ -234,6 +242,10 @@ RSpec.describe 'Tag description native API', type: :request do
expect(version).to have_attributes( expect(version).to have_attributes(
tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update', tag_id: tag.id, language_code: 'ja', version_no: 2, event_type: 'update',
body: 'after', created_by_user_id: editor.id) body: 'after', created_by_user_id: editor.id)
event = event_rows.last
expect(event).to include(
'event_type' => 'tag_descriptions.update', 'created_by_user_id' => editor.id)
expect(version.event_id).to eq(event.fetch('id'))
expect(revision.body).to eq(version.body) expect(revision.body).to eq(version.body)
expect(description_versions(page).order(:version_no).last.body).to eq(version.body) expect(description_versions(page).order(:version_no).last.body).to eq(version.body)
get "/wiki/#{ description.id }" get "/wiki/#{ description.id }"
+5 -1
ファイルの表示
@@ -121,10 +121,14 @@ RSpec.describe 'Tag group native API', type: :request do
[:create, :update].each do |operation| [:create, :update].each do |operation|
it "#{ operation }s structured data and keeps unplaced membership" do it "#{ operation }s structured data and keeps unplaced membership" do
previous = group_version(group_id, 1) previous = group_version(group_id, 1)
native_write(operation) expect { native_write(operation) }.to change { event_rows.length }.by(1)
expect(response).to have_http_status(operation == :create ? :created : :ok) expect(response).to have_http_status(operation == :create ? :created : :ok)
id = operation == :create ? json.fetch('id') : group_id id = operation == :create ? json.fetch('id') : group_id
number = operation == :create ? 1 : 2 number = operation == :create ? 1 : 2
event = event_rows.last
event_type = operation == :create ? 'tag_groups.create' : 'tag_groups.update'
expect(event).to include('event_type' => event_type, 'created_by_user_id' => user.id)
expect(group_version(id, number).fetch('event_id')).to eq(event.fetch('id'))
expect_group_snapshot( expect_group_snapshot(
id, number:, members: snapshot_members, placements:, description: '全体の説明') id, number:, members: snapshot_members, placements:, description: '全体の説明')
expect(group_version(group_id, 1)).to eq(previous) expect(group_version(group_id, 1)).to eq(previous)
+1
ファイルの表示
@@ -30,6 +30,7 @@ RSpec.describe 'TagVersions API', type: :request do
) )
version = version =
TagVersion.create!( TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag: tag, tag: tag,
version_no: version_no, version_no: version_no,
event_type: event_type, event_type: event_type,
+10
ファイルの表示
@@ -705,6 +705,7 @@ RSpec.describe 'Tags API', type: :request do
expect { expect {
patch "/tags/#{tag.id}", params: { name: 'new_tag_name', category: 'meme' } patch "/tags/#{tag.id}", params: { name: 'new_tag_name', category: 'meme' }
}.to change(TagVersion, :count).by(2) }.to change(TagVersion, :count).by(2)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -719,6 +720,10 @@ RSpec.describe 'Tags API', type: :request do
expect(version_primary_name(versions.second, 'ja')).to eq('new_tag_name') expect(version_primary_name(versions.second, 'ja')).to eq('new_tag_name')
expect(versions.second.category).to eq('meme') expect(versions.second.category).to eq('meme')
expect(versions.second.created_by_user_id).to eq(member_user.id) expect(versions.second.created_by_user_id).to eq(member_user.id)
event = event_rows.last
expect(event).to include(
'event_type' => 'tags.update', 'created_by_user_id' => member_user.id)
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
it 'updates deprecated state and records it in tag versions' do it 'updates deprecated state and records it in tag versions' do
@@ -1275,6 +1280,7 @@ RSpec.describe 'Tags API', type: :request do
deprecated: '0', deprecated: '0',
} }
}.to change(TagVersion, :count).by(2) }.to change(TagVersion, :count).by(2)
.and change { event_rows.length }.by(1)
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
@@ -1290,6 +1296,10 @@ RSpec.describe 'Tags API', type: :request do
expect(versions.second.category).to eq('meta') expect(versions.second.category).to eq('meta')
expect(version_alias_names(versions.second, 'ja')).to include('spec_tag') expect(version_alias_names(versions.second, 'ja')).to include('spec_tag')
expect(versions.second.created_by_user_id).to eq(member_user.id) expect(versions.second.created_by_user_id).to eq(member_user.id)
event = event_rows.last
expect(event).to include(
'event_type' => 'tags.update', 'created_by_user_id' => member_user.id)
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
it '同じ tag の旧 alias へ戻しても rename できる' do it '同じ tag の旧 alias へ戻しても rename できる' do
+3 -1
ファイルの表示
@@ -143,7 +143,9 @@ RSpec.describe 'Counts export contracts' do
expect(CountsExportDownload.find(token, 'owner')).not_to be_nil expect(CountsExportDownload.find(token, 'owner')).not_to be_nil
expect(CountsExportDownload.find(token, 'other')).to be_nil expect(CountsExportDownload.find(token, 'other')).to be_nil
post = create(:post) post = create(:post)
PostVersion.create!(post:, version_no: 1, event_type: 'discard', url: post.url, PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 1, event_type: 'discard', url: post.url,
title: 'private', tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024)) title: 'private', tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024))
expect(CountsExportDownload.find(token, 'owner')).to be_nil expect(CountsExportDownload.find(token, 'owner')).to be_nil
end end
+21 -7
ファイルの表示
@@ -13,7 +13,9 @@ RSpec.describe 'Counts masked pivot contracts' do
let!(:tags) do let!(:tags) do
['A', 'B', 'C'].map do |name| ['A', 'B', 'C'].map do |name|
tag = create(:tag, primary_name: name) tag = create(:tag, primary_name: name)
TagVersion.create!(tag:, version_no: 1, event_type: 'create', name:, TagVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag:, version_no: 1, event_type: 'create', name:,
category: 'general', aliases: '', parent_tag_ids: '', category: 'general', aliases: '', parent_tag_ids: '',
names: { ja: { name:, script_code: 'Jpan', auto_generated: false, aliases: [] } }, names: { ja: { name:, script_code: 'Jpan', auto_generated: false, aliases: [] } },
created_at: Time.utc(2023)) created_at: Time.utc(2023))
@@ -25,12 +27,16 @@ RSpec.describe 'Counts masked pivot contracts' do
[[0, 1], [0], [1, 2]].each_with_index do |indexes, index| [[0, 1], [0], [1, 2]].each_with_index do |indexes, index|
post = create(:post) post = create(:post)
snapshot = indexes.map { { tag_id: tags[_1].id, version_no: 1, sections: [] } } snapshot = indexes.map { { tag_id: tags[_1].id, version_no: 1, sections: [] } }
PostVersion.create!(post:, version_no: 1, event_type: 'create', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 1, event_type: 'create',
title: "hidden-title-#{ index }", url: post.url, tags: '', tags_json: snapshot, title: "hidden-title-#{ index }", url: post.url, tags: '', tags_json: snapshot,
parent_post_ids: '', created_by_user: member, created_at: Time.utc(2024, 1)) parent_post_ids: '', created_by_user: member, created_at: Time.utc(2024, 1))
next if index == 2 next if index == 2
PostVersion.create!(post:, version_no: 2, event_type: 'discard', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 2, event_type: 'discard',
title: "private-discard-#{ index }", url: post.url, tags: '', tags_json: [], title: "private-discard-#{ index }", url: post.url, tags: '', tags_json: [],
parent_post_ids: '', created_by_user: member, created_at: Time.utc(2024, 2)) parent_post_ids: '', created_by_user: member, created_at: Time.utc(2024, 2))
end end
@@ -98,12 +104,16 @@ RSpec.describe 'Counts masked pivot contracts' do
it 'uses changed-record membership for totals and hides changes to private predecessors' do it 'uses changed-record membership for totals and hides changes to private predecessors' do
post = PostVersion.order(:id).last.post post = PostVersion.order(:id).last.post
PostVersion.create!(post:, version_no: 2, event_type: 'update', title: 'changed-public-title', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 2, event_type: 'update', title: 'changed-public-title',
url: post.url, tags: '', url: post.url, tags: '',
tags_json: [{ tag_id: tags.last.id, version_no: 1, sections: [] }], tags_json: [{ tag_id: tags.last.id, version_no: 1, sections: [] }],
parent_post_ids: '', created_at: Time.utc(2024, 3)) parent_post_ids: '', created_at: Time.utc(2024, 3))
restored = PostVersion.order(:id).first.post restored = PostVersion.order(:id).first.post
PostVersion.create!(post: restored, version_no: 3, event_type: 'restore', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post: restored, version_no: 3, event_type: 'restore',
title: 'restored-public-title', url: restored.url, tags: '', title: 'restored-public-title', url: restored.url, tags: '',
tags_json: [{ tag_id: tags.first.id, version_no: 1, sections: [] }], parent_post_ids: '', tags_json: [{ tag_id: tags.first.id, version_no: 1, sections: [] }], parent_post_ids: '',
created_at: Time.utc(2024, 4)) created_at: Time.utc(2024, 4))
@@ -134,7 +144,9 @@ RSpec.describe 'Counts masked pivot contracts' do
it 'keeps NULL as a public data value rather than treating it as a rollup' do it 'keeps NULL as a public data value rather than treating it as a rollup' do
post = create(:post) post = create(:post)
PostVersion.create!(post:, version_no: 1, event_type: 'create', title: 'public bot', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: 1, event_type: 'create', title: 'public bot',
url: post.url, tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 5)) url: post.url, tags: '', tags_json: [], parent_post_ids: '', created_at: Time.utc(2024, 5))
page = result(nil, 'rows' => 'user', 'cols' => 'event_type') page = result(nil, 'rows' => 'user', 'cols' => 'event_type')
expect(page[:cells].select { _1[:values].first.nil? }.sum { _1[:count] }).to eq(1) expect(page[:cells].select { _1[:values].first.nil? }.sum { _1[:count] }).to eq(1)
@@ -145,7 +157,9 @@ RSpec.describe 'Counts masked pivot contracts' do
it 'sorts public numeric values numerically alongside a masked group' do it 'sorts public numeric values numerically alongside a masked group' do
post = PostVersion.order(:id).last.post post = PostVersion.order(:id).last.post
[2, 10].each do |number| [2, 10].each do |number|
PostVersion.create!(post:, version_no: number, event_type: 'update', PostVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
post:, version_no: number, event_type: 'update',
title: 'public numeric sort', url: post.url, tags: '', tags_json: [], title: 'public numeric sort', url: post.url, tags: '', tags_json: [],
parent_post_ids: '', created_at: Time.utc(2024, 5)) parent_post_ids: '', created_at: Time.utc(2024, 5))
end end
+2
ファイルの表示
@@ -56,6 +56,7 @@ RSpec.describe MaterialSyncImporter do
profile: 'legacy_drive', profile: 'legacy_drive',
export_path: '素材/a.png', export_path: '素材/a.png',
created_by_user: user) created_by_user: user)
events = event_rows
result = result =
described_class.import!( described_class.import!(
@@ -73,6 +74,7 @@ RSpec.describe MaterialSyncImporter do
expect(result.action).to eq(:unchanged) expect(result.action).to eq(:unchanged)
expect(material.reload.tag).to eq(tag) expect(material.reload.tag).to eq(tag)
expect(material.url).to eq('https://example.com/human') expect(material.url).to eq('https://example.com/human')
expect(event_rows).to eq(events)
end end
it 'suppresses a Google Drive candidate by relative path prefix' do it 'suppresses a Google Drive candidate by relative path prefix' do
+41
ファイルの表示
@@ -14,6 +14,47 @@ RSpec.describe MaterialSyncRunner do
let(:drive_client) { instance_double(GoogleDrive::ApiClient) } let(:drive_client) { instance_double(GoogleDrive::ApiClient) }
describe '#sync!' do describe '#sync!' do
it 'groups created and updated materials across sync sources into one event' do
existing = Material.create!(
source_kind: 'google_drive_file', source_file_id: 'existing-file',
source_path: 'old.png', url: 'https://example.com/existing',
created_by_user: user, updated_by_user: user)
source
MaterialSyncSource.create!(
name: 'Second Drive source', source_kind: 'google_drive_path',
source_file_id: 'folder-456', profile: 'legacy_drive',
created_by_user: user, updated_by_user: user)
allow(GoogleDrive::ApiClient).to receive(:new).and_return(drive_client)
allow(drive_client).to receive(:each_material_file_under_folder) do |folder_id, &block|
id = folder_id == 'folder-123' ? 'existing-file' : 'new-file'
block.call(id:, name: "#{ id }.png", mime_type: 'image/png',
relative_path: "素材/#{ id }.png", sha256_checksum: nil,
web_view_link: "https://drive.google.com/file/d/#{ id }/view")
end
allow(drive_client).to receive(:download_to_tempfile) do |id, **|
Tempfile.new(['sync-event', '.png']).tap do |file|
file.binmode
file.write(id)
file.rewind
end
end
allow(MaterialThumbnailGenerator).to receive(:generate!)
previous_ids = MaterialVersion.pluck(:id)
result = nil
expect { result = described_class.sync_enabled! }
.to change { event_rows.length }.by(1)
expect(result).to have_attributes(imported: 1, updated: 1, failed: 0)
event = event_rows.last
expect(event.fetch('event_type')).to eq('materials.sync')
created = Material.find_by!(source_file_id: 'new-file')
versions = MaterialVersion.where.not(id: previous_ids)
expect(versions.pluck(:material_id).uniq).to contain_exactly(existing.id, created.id)
expect(versions.pluck(:event_type)).to include('create', 'update')
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end
it 'imports google drive path candidates as they are yielded' do it 'imports google drive path candidates as they are yielded' do
first_entry = { id: 'file-1', first_entry = { id: 'file-1',
name: 'a.png', name: 'a.png',
+83
ファイルの表示
@@ -88,3 +88,86 @@ RSpec.describe PostBulkCreator do
expect(creator).to have_received(:create!).with(locale).once expect(creator).to have_received(:create!).with(locale).once
end end
end end
RSpec.describe 'PostBulkCreator event grouping', type: :model do
self.use_transactional_tests = false
before do
expect(Rails.env.test?).to be(true)
expect(ApplicationRecord.connection.open_transactions).to eq(0)
@prefix = "bulk_event_#{ SecureRandom.hex(8) }"
@actor = create(:user, :member)
allow(PostCreatePreflight).to receive(:new) do |attributes:, **|
specs = [{ name: attributes.fetch(:title), category: :general }]
instance_double(PostCreatePreflight, run: attributes.merge(
existing_post_id: nil, snapshot_tag_specs: specs, post_tag_specs: specs,
normalised_parent_post_ids: [], tag_sections: { }, video_ms: nil))
end
end
after do
if @actor
posts = Post.where(uploaded_user: @actor)
PostTagSection.where(post_id: posts.select(:id)).delete_all
PostTag.where(post_id: posts.select(:id)).delete_all
PostVersion.where(post_id: posts.select(:id)).delete_all
posts.delete_all
tag_ids = TagName.where(name: ["#{ @prefix }_1", "#{ @prefix }_2"]).pluck(:tag_id)
TagVersion.where(tag_id: tag_ids).delete_all
TagName.where(tag_id: tag_ids).delete_all
Tag.where(id: tag_ids).delete_all
EventRecords::EventRow.where(created_by_user_id: @actor.id).delete_all
@actor.delete
end
end
it 'shares one creation event between real workers and their tag histories' do
posts = [1, 2].map do |number|
{ title: "#{ @prefix }_#{ number }",
url: "https://example.com/#{ @prefix }/#{ number }" }
end
result = nil
expect {
result = PostBulkCreator.new(actor: @actor, posts:, thumbnails: { })
.run(Locale.nipponese)
}.to change { event_rows.length }.by(1)
expect(result.fetch(:results).map { _1.fetch(:status) }).to eq(['created', 'created'])
event = event_rows.last
expect(event).to include(
'event_type' => 'posts.create', 'created_by_user_id' => @actor.id)
versions = PostVersion.where(created_by_user_id: @actor.id)
expect(versions.count).to eq(2)
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
tag_versions = TagVersion.where(created_by_user_id: @actor.id)
expect(tag_versions.count).to eq(2)
expect(tag_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end
it 'does not leave an event when every row fails validation' do
preflight = instance_double(PostCreatePreflight)
allow(PostCreatePreflight).to receive(:new).and_return(preflight)
allow(preflight).to receive(:run).and_raise(
PostCreatePreflight::ValidationFailed.new(fields: { url: ['invalid'] }))
events = event_rows
result = PostBulkCreator.new(
actor: @actor, posts: [{ title: 'invalid' }], thumbnails: { }).run(Locale.nipponese)
expect(result.fetch(:results).map { _1.fetch(:status) }).to eq(['failed'])
expect(event_rows).to eq(events)
end
it 'does not leave an event when every row is skipped' do
preflight = instance_double(PostCreatePreflight, run: { existing_post_id: 123 })
allow(PostCreatePreflight).to receive(:new).and_return(preflight)
events = event_rows
result = PostBulkCreator.new(
actor: @actor, posts: [{ title: 'existing' }], thumbnails: { }).run(Locale.nipponese)
expect(result.fetch(:results).map { _1.fetch(:status) }).to eq(['skipped'])
expect(event_rows).to eq(events)
end
end
+10
ファイルの表示
@@ -16,6 +16,7 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
@tag = create(:tag, primary_name: "#{ @prefix }_owner") @tag = create(:tag, primary_name: "#{ @prefix }_owner")
@group = TagGroup.create!(name: "#{ @prefix }_owner", version_no: 1) @group = TagGroup.create!(name: "#{ @prefix }_owner", version_no: 1)
@tag_ids = Queue.new @tag_ids = Queue.new
@event_ids = Queue.new
@threads = [] @threads = []
@release = Queue.new @release = Queue.new
end end
@@ -42,6 +43,9 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
ids << @tag.id if @tag ids << @tag.id if @tag
TagName.where(tag_id: ids).delete_all TagName.where(tag_id: ids).delete_all
Tag.where(id: ids).delete_all Tag.where(id: ids).delete_all
event_ids = []
event_ids << @event_ids.pop until @event_ids.empty? if @event_ids
EventRecords::EventRow.where(id: event_ids).delete_all
end end
end end
@@ -72,11 +76,17 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
when :description when :description
TagDescription.create!(tag: @tag, language_code: 'ja', body: '', version_no: 1) TagDescription.create!(tag: @tag, language_code: 'ja', body: '', version_no: 1)
when :description_version when :description_version
event = create_event!(event_type: 'unknown')
@event_ids << event.id
TagDescriptionVersion.create!( TagDescriptionVersion.create!(
event_id: event.id,
tag_id: @tag.id, language_code: 'ja', body: 'body', version_no: 1, tag_id: @tag.id, language_code: 'ja', body: 'body', version_no: 1,
event_type: 'create', created_at: Time.current) event_type: 'create', created_at: Time.current)
when :group_version when :group_version
event = create_event!(event_type: 'unknown')
@event_ids << event.id
TagGroupVersion.create!( TagGroupVersion.create!(
event_id: event.id,
tag_group: @group, name: @group.name, version_no: 1, event_type: 'create', tag_group: @group, name: @group.name, version_no: 1, event_type: 'create',
members: [], placements: [], created_at: Time.current) members: [], placements: [], created_at: Time.current)
when :group when :group
+3
ファイルの表示
@@ -15,10 +15,12 @@ RSpec.describe SharedResourceIdAllocator do
TagGroup.new(id:, name: "group #{ number }", version_no: number) TagGroup.new(id:, name: "group #{ number }", version_no: number)
when 'tag_description_versions' when 'tag_description_versions'
TagDescriptionVersion.new( TagDescriptionVersion.new(
event_id: create_event!(event_type: 'unknown').id,
id:, tag_id: tag.id, language_code: 'ja', body: 'body', version_no: number, id:, tag_id: tag.id, language_code: 'ja', body: 'body', version_no: number,
event_type: 'create', created_at: Time.current) event_type: 'create', created_at: Time.current)
when 'tag_group_versions' when 'tag_group_versions'
TagGroupVersion.new( TagGroupVersion.new(
event_id: create_event!(event_type: 'unknown').id,
id:, tag_group_id: group.id, name: group.name, version_no: number, id:, tag_group_id: group.id, name: group.name, version_no: number,
event_type: 'create', members: [], placements: [], created_at: Time.current) event_type: 'create', members: [], placements: [], created_at: Time.current)
end end
@@ -82,6 +84,7 @@ RSpec.describe SharedResourceIdAllocator do
it 'reserves a Tag ID referenced only by remaining description history' do it 'reserves a Tag ID referenced only by remaining description history' do
high = [Tag.maximum(:id).to_i, ExternalTag.maximum(:id).to_i].max + 1000 high = [Tag.maximum(:id).to_i, ExternalTag.maximum(:id).to_i].max + 1000
TagDescriptionVersion.create!( TagDescriptionVersion.create!(
event_id: create_event!(event_type: 'unknown').id,
tag_id: high, language_code: 'ja', version_no: 1, body: 'history', tag_id: high, language_code: 'ja', version_no: 1, body: 'history',
event_type: 'create', created_at: Time.current) event_type: 'create', created_at: Time.current)
expect(create(:tag).id).to be > high expect(create(:tag).id).to be > high
+35
ファイルの表示
@@ -42,6 +42,41 @@ RSpec.describe Youtube::Sync do
end end
describe '#sync!' do describe '#sync!' do
it 'groups created and updated post histories into one sync event' do
allow(PostVersionRecorder).to receive(:record!).and_call_original
allow(PostVersionRecorder).to receive(:ensure_snapshot!).and_call_original
existing = create(:post, url: 'https://youtu.be/video-1', title: 'old title')
allow(sync).to receive(:query_terms).and_return([])
allow(sync).to receive(:playlist_ids).and_return(['PL123'])
allow(client).to receive(:playlist_items).with(playlist_id: 'PL123', page_token: nil)
.and_return('items' => [
{ 'contentDetails' => { 'videoId' => 'video-1' } },
{ 'contentDetails' => { 'videoId' => 'video-2' } }])
allow(client).to receive(:videos).with(['video-1', 'video-2'])
.and_return('items' => [
youtube_video_item(id: 'video-1', title: 'updated', channel_id: 'UC_NONE'),
youtube_video_item(id: 'video-2', title: 'created', channel_id: 'UC_NONE')])
previous_post_ids = PostVersion.pluck(:id)
previous_tag_ids = TagVersion.pluck(:id)
expect { sync.sync! }.to change { event_rows.length }.by(1)
event = event_rows.last
expect(event).to include('event_type' => 'posts.sync', 'created_by_user_id' => nil)
versions = PostVersion.where.not(id: previous_post_ids)
created = Post.find_by!(url: 'https://www.youtube.com/watch?v=video-2')
expect(versions.pluck(:post_id).uniq).to contain_exactly(existing.id, created.id)
expect(versions.pluck(:event_type)).to include('create', 'update')
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
tag_versions = TagVersion.where.not(id: previous_tag_ids)
expect(tag_versions).not_to be_empty
expect(tag_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
events = event_rows
expect { sync.sync! }.not_to change(PostVersion, :count)
expect(event_rows).to eq(events)
end
it 'returns without fetching video details when no video ids are discovered' do it 'returns without fetching video details when no video ids are discovered' do
allow(sync).to receive(:query_terms).and_return([]) allow(sync).to receive(:query_terms).and_return([])
allow(sync).to receive(:playlist_ids).and_return([]) allow(sync).to receive(:playlist_ids).and_return([])
+15
ファイルの表示
@@ -0,0 +1,15 @@
module EventRecords
# Read the database contract without requiring an application Event model.
class EventRow < ActiveRecord::Base
self.table_name = 'events'
end
def event_rows
EventRow.order(:id).map(&:attributes)
end
def create_event! event_type:, created_by_user: nil
EventRow.create!(event_type:, created_by_user_id: created_by_user&.id,
created_at: Time.current)
end
end
+4 -1
ファイルの表示
@@ -3,11 +3,13 @@ require 'cgi'
# SQL fixtures use the migration contract without inventing application models. # SQL fixtures use the migration contract without inventing application models.
# Include explicitly; these helpers must not affect unrelated examples. # Include explicitly; these helpers must not affect unrelated examples.
module TagGroupRecords module TagGroupRecords
include EventRecords
GROUP_TABLES = [ GROUP_TABLES = [
'tag_groups', 'tag_group_tags', 'tag_group_groups', 'tag_groups', 'tag_group_tags', 'tag_group_groups',
'tag_group_placements', 'tag_group_versions'].freeze 'tag_group_placements', 'tag_group_versions'].freeze
WRITE_TABLES = GROUP_TABLES + [ WRITE_TABLES = GROUP_TABLES + [
'tag_names', 'tag_description_versions', 'events', 'tag_names', 'tag_description_versions',
'tag_descriptions'] 'tag_descriptions']
def group_connection = ApplicationRecord.connection def group_connection = ApplicationRecord.connection
@@ -114,6 +116,7 @@ module TagGroupRecords
group_id = insert_group_row('tag_groups', attributes) group_id = insert_group_row('tag_groups', attributes)
insert_group_row('tag_group_versions', { insert_group_row('tag_group_versions', {
tag_group_id: group_id, version_no: 1, event_type: 'create', tag_group_id: group_id, version_no: 1, event_type: 'create',
event_id: create_event!(event_type: 'tag_groups.create', created_by_user: user).id,
name:, description:, members: members.to_json, placements: placements.to_json, name:, description:, members: members.to_json, placements: placements.to_json,
created_by_user_id: user.id, created_at: now }) created_by_user_id: user.id, created_at: now })
members.each do |member| members.each do |member|
+43 -14
ファイルの表示
@@ -174,20 +174,20 @@ RSpec.describe 'nico:sync' do
def create_post_version_for!(post, version_no: 1, event_type: 'create', created_by_user: nil) def create_post_version_for!(post, version_no: 1, event_type: 'create', created_by_user: nil)
PostVersion.create!( PostVersion.create!(
post: post, event_id: create_event!(event_type: 'unknown').id,
version_no: version_no, post: post,
event_type: event_type, version_no: version_no,
title: post.title, event_type: event_type,
url: post.url, title: post.title,
thumbnail_base: post.thumbnail_base, url: post.url,
tags: snapshot_tags(post), thumbnail_base: post.thumbnail_base,
tags_json: post.snapshot_tags_json, tags: snapshot_tags(post),
parent_post_ids: post.snapshot_parent_post_ids.join(' '), tags_json: post.snapshot_tags_json,
original_created_from: post.original_created_from, parent_post_ids: post.snapshot_parent_post_ids.join(' '),
original_created_before: post.original_created_before, original_created_from: post.original_created_from,
created_at: Time.current, original_created_before: post.original_created_before,
created_by_user: created_by_user created_at: Time.current,
) created_by_user: created_by_user)
end end
it '新規 post 作成時に version 1 を作る' do it '新規 post 作成時に version 1 を作る' do
@@ -209,6 +209,7 @@ RSpec.describe 'nico:sync' do
expect { expect {
run_rake_task('nico:sync') run_rake_task('nico:sync')
}.to change(PostVersion, :count).by(1) }.to change(PostVersion, :count).by(1)
.and change { event_rows.length }.by(1)
post = Post.find_by!(url: 'https://www.nicovideo.jp/watch/sm9') post = Post.find_by!(url: 'https://www.nicovideo.jp/watch/sm9')
version = post.post_versions.order(:version_no).last version = post.post_versions.order(:version_no).last
@@ -217,6 +218,32 @@ RSpec.describe 'nico:sync' do
expect(version.event_type).to eq('create') expect(version.event_type).to eq('create')
expect(version.created_by_user).to be_nil expect(version.created_by_user).to be_nil
expect(version.tags).to eq(snapshot_tags(post.reload)) expect(version.tags).to eq(snapshot_tags(post.reload))
event = event_rows.last
expect(event).to include('event_type' => 'posts.sync', 'created_by_user_id' => nil)
expect(version.event_id).to eq(event.fetch('id'))
end
it 'groups created and updated post histories into one sync event' do
existing = create(:post, title: 'old', url: 'https://www.nicovideo.jp/watch/sm9')
stub_python([
{ 'code' => 'sm9', 'title' => 'updated', 'tags' => [] },
{ 'code' => 'sm10', 'title' => 'created', 'tags' => [] }])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
previous_post_ids = PostVersion.pluck(:id)
previous_tag_ids = TagVersion.pluck(:id)
expect { run_rake_task('nico:sync') }.to change { event_rows.length }.by(1)
event = event_rows.last
expect(event).to include('event_type' => 'posts.sync', 'created_by_user_id' => nil)
created = Post.find_by!(url: 'https://www.nicovideo.jp/watch/sm10')
versions = PostVersion.where.not(id: previous_post_ids)
expect(versions.pluck(:post_id).uniq).to contain_exactly(existing.id, created.id)
expect(versions.pluck(:event_type)).to include('create', 'update')
expect(versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
tag_versions = TagVersion.where.not(id: previous_tag_ids)
expect(tag_versions).not_to be_empty
expect(tag_versions.pluck(:event_id).uniq).to eq([event.fetch('id')])
end end
it '既存 post の内容または tags が変わったとき update version を作る' do it '既存 post の内容または tags が変わったとき update version を作る' do
@@ -282,6 +309,7 @@ RSpec.describe 'nico:sync' do
}]) }])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>')) allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
events = event_rows
expect { expect {
run_rake_task('nico:sync') run_rake_task('nico:sync')
@@ -291,6 +319,7 @@ RSpec.describe 'nico:sync' do
expect(version.version_no).to eq(1) expect(version.version_no).to eq(1)
expect(version.event_type).to eq('create') expect(version.event_type).to eq('create')
expect(version.tags).to eq(snapshot_tags(post.reload)) expect(version.tags).to eq(snapshot_tags(post.reload))
expect(event_rows).to eq(events)
end end
it '新規 nico tag に nico tag version を作る' do it '新規 nico tag に nico tag version を作る' do