コミットを比較
2 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 83cd476325 | |||
| acd062f289 |
@@ -89,15 +89,13 @@ class MaterialsController < ApplicationController
|
||||
|
||||
begin
|
||||
Material.transaction do
|
||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||
tag = tag_name.tag
|
||||
tag = Tag.create!(tag_name:, category: :material) unless tag
|
||||
|
||||
tag = resolve_material_tag!(tag_name_raw)
|
||||
material = Material.new(tag:, url:,
|
||||
created_by_user: current_user,
|
||||
updated_by_user: current_user)
|
||||
material.file.attach(uploaded_blob) if uploaded_blob
|
||||
material.save!
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
|
||||
upsert_export_paths!(material)
|
||||
MaterialVersionRecorder.record!(material:, event_type: :create,
|
||||
created_by_user: current_user)
|
||||
@@ -139,10 +137,7 @@ class MaterialsController < ApplicationController
|
||||
begin
|
||||
Material.transaction do
|
||||
MaterialVersionRecorder.ensure_snapshot!(material, created_by_user: current_user)
|
||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||
tag = tag_name.tag
|
||||
tag = Tag.create!(tag_name:, category: :material) unless tag
|
||||
|
||||
tag = resolve_material_tag!(tag_name_raw)
|
||||
material.assign_attributes(tag:, url:, updated_by_user: current_user)
|
||||
if uploaded_blob
|
||||
material.file.attach(uploaded_blob)
|
||||
@@ -150,6 +145,7 @@ class MaterialsController < ApplicationController
|
||||
material.file.detach
|
||||
end
|
||||
material.save!
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: current_user)
|
||||
upsert_export_paths!(material)
|
||||
MaterialVersionRecorder.record!(material:, event_type: :update,
|
||||
created_by_user: current_user)
|
||||
@@ -240,6 +236,12 @@ class MaterialsController < ApplicationController
|
||||
nil
|
||||
end
|
||||
|
||||
def resolve_material_tag! tag_name_raw
|
||||
tag_name = TagName.find_undiscard_or_create_by!(name: tag_name_raw)
|
||||
tag = tag_name.tag
|
||||
tag || Tag.create!(tag_name:, category: :material)
|
||||
end
|
||||
|
||||
def material_index_needs_tag_name? filters
|
||||
filters[:q].present? || filters[:sort] == 'tag_name'
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ class TagImplication < ApplicationRecord
|
||||
validates :parent_tag_id, presence: true
|
||||
|
||||
validate :parent_tag_mustnt_be_itself
|
||||
validate :parent_tag_mustnt_create_cycle
|
||||
|
||||
private
|
||||
|
||||
@@ -14,4 +15,27 @@ class TagImplication < ApplicationRecord
|
||||
errors.add :parent_tag_id, '親タグは子タグと同一であってはなりません.'
|
||||
end
|
||||
end
|
||||
|
||||
def parent_tag_mustnt_create_cycle
|
||||
return if tag_id.blank? || parent_tag_id.blank?
|
||||
return if errors[:parent_tag_id].present?
|
||||
|
||||
seen = { }
|
||||
stack = [parent_tag_id]
|
||||
|
||||
until stack.empty?
|
||||
current_id = stack.pop
|
||||
next if seen[current_id]
|
||||
|
||||
seen[current_id] = true
|
||||
|
||||
if current_id == tag_id
|
||||
errors.add :parent_tag_id, '親タグに子孫タグを指定すると循環します.'
|
||||
errors.add :base, 'タグの親子関係が循環します.'
|
||||
return
|
||||
end
|
||||
|
||||
stack.concat(TagImplication.where(tag_id: current_id).pluck(:parent_tag_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagImplication, type: :model do
|
||||
it 'rejects a parent tag that would create a cycle' do
|
||||
child = create(:tag, name: 'tag_implication_cycle_child')
|
||||
parent = create(:tag, name: 'tag_implication_cycle_parent')
|
||||
|
||||
described_class.create!(tag: child, parent_tag: parent)
|
||||
|
||||
implication = described_class.new(tag: parent, parent_tag: child)
|
||||
|
||||
expect(implication).not_to be_valid
|
||||
expect(implication.errors[:parent_tag_id]).to include(
|
||||
'親タグに子孫タグを指定すると循環します.'
|
||||
)
|
||||
expect(implication.errors[:base]).to be_present
|
||||
end
|
||||
|
||||
it 'terminates even when existing data already contains a cycle' do
|
||||
child = create(:tag, name: 'tag_implication_existing_cycle_child')
|
||||
parent = create(:tag, name: 'tag_implication_existing_cycle_parent')
|
||||
ancestor = create(:tag, name: 'tag_implication_existing_cycle_ancestor')
|
||||
|
||||
described_class.create!(tag: parent, parent_tag: ancestor)
|
||||
described_class.insert_all!(
|
||||
[
|
||||
{ tag_id: ancestor.id, parent_tag_id: parent.id,
|
||||
created_at: Time.current, updated_at: Time.current }
|
||||
]
|
||||
)
|
||||
|
||||
implication = described_class.new(tag: child, parent_tag: parent)
|
||||
|
||||
expect(implication).to be_valid
|
||||
end
|
||||
end
|
||||
@@ -54,7 +54,17 @@ RSpec.describe Tag, type: :model do
|
||||
first = create(:tag, name: 'expand_cycle_first')
|
||||
second = create(:tag, name: 'expand_cycle_second')
|
||||
TagImplication.create!(tag: first, parent_tag: second)
|
||||
TagImplication.create!(tag: second, parent_tag: first)
|
||||
now = Time.current
|
||||
TagImplication.insert_all!(
|
||||
[
|
||||
{
|
||||
tag_id: second.id,
|
||||
parent_tag_id: first.id,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
expect(described_class.expand_parent_tags([first])).to contain_exactly(first, second)
|
||||
end
|
||||
|
||||
@@ -287,6 +287,25 @@ RSpec.describe 'Materials API', type: :request do
|
||||
expect(json.dig('export_paths', 'legacy_drive')).to eq('伊地知ニジカ/created.png')
|
||||
end
|
||||
|
||||
it 'creates a create tag_version for a newly created material tag' do
|
||||
expect do
|
||||
post '/materials', params: {
|
||||
tag: 'material_create_versioned_tag',
|
||||
file: dummy_upload(filename: 'created.png')
|
||||
}
|
||||
end.to change(TagVersion, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
|
||||
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_create_versioned_tag' })
|
||||
version = tag.tag_versions.order(:version_no).last
|
||||
|
||||
expect(version.event_type).to eq('create')
|
||||
expect(version.name).to eq('material_create_versioned_tag')
|
||||
expect(version.category).to eq('material')
|
||||
expect(version.created_by_user).to eq(member_user)
|
||||
end
|
||||
|
||||
it 'snapshots attached file metadata and sha256' do
|
||||
post '/materials', params: {
|
||||
tag: 'material_create_file_version',
|
||||
@@ -466,6 +485,73 @@ RSpec.describe 'Materials API', type: :request do
|
||||
expect(json.dig('tag', 'name')).to eq('material_update_new')
|
||||
end
|
||||
|
||||
it 'creates a create tag_version when update creates a new tag' do
|
||||
expect do
|
||||
put "/materials/#{ material.id }", params: {
|
||||
tag: 'material_update_versioned_tag',
|
||||
file: dummy_upload(filename: 'updated.png')
|
||||
}
|
||||
end.to change(Tag, :count).by(1)
|
||||
.and change(TagName, :count).by(1)
|
||||
.and change(TagVersion, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: 'material_update_versioned_tag' })
|
||||
version = tag.tag_versions.order(:version_no).last
|
||||
|
||||
expect(version.event_type).to eq('create')
|
||||
expect(version.name).to eq('material_update_versioned_tag')
|
||||
expect(version.category).to eq('material')
|
||||
expect(version.created_by_user).to eq(member_user)
|
||||
end
|
||||
|
||||
it 'backfills a create tag_version for an existing material tag without history' do
|
||||
existing_tag =
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_update_existing_no_history'),
|
||||
category: :material)
|
||||
|
||||
expect(existing_tag.tag_versions).to be_empty
|
||||
|
||||
expect do
|
||||
put "/materials/#{ material.id }", params: {
|
||||
tag: 'material_update_existing_no_history',
|
||||
file: dummy_upload(filename: 'updated.png')
|
||||
}
|
||||
end.to change(TagVersion, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
version = existing_tag.reload.tag_versions.order(:version_no).last
|
||||
expect(version.event_type).to eq('create')
|
||||
expect(version.name).to eq('material_update_existing_no_history')
|
||||
expect(version.category).to eq('material')
|
||||
expect(version.created_by_user).to eq(member_user)
|
||||
end
|
||||
|
||||
it 'backfills a create tag_version for an existing character tag without history' do
|
||||
existing_tag =
|
||||
Tag.create!(tag_name: TagName.create!(name: 'material_update_character_no_history'),
|
||||
category: :character)
|
||||
|
||||
expect(existing_tag.tag_versions).to be_empty
|
||||
|
||||
expect do
|
||||
put "/materials/#{ material.id }", params: {
|
||||
tag: 'material_update_character_no_history',
|
||||
file: dummy_upload(filename: 'updated.png')
|
||||
}
|
||||
end.to change(TagVersion, :count).by(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
||||
version = existing_tag.reload.tag_versions.order(:version_no).last
|
||||
expect(version.event_type).to eq('create')
|
||||
expect(version.name).to eq('material_update_character_no_history')
|
||||
expect(version.category).to eq('character')
|
||||
expect(version.created_by_user).to eq(member_user)
|
||||
end
|
||||
|
||||
it 'detaches the existing file without purging blob when url replaces file' do
|
||||
old_blob_id = material.file.blob.id
|
||||
|
||||
@@ -494,6 +580,7 @@ RSpec.describe 'Materials API', type: :request do
|
||||
it 'does not increase version for the same snapshot update' do
|
||||
MaterialVersionRecorder.record!(material:, event_type: :create,
|
||||
created_by_user: member_user)
|
||||
TagVersioning.ensure_snapshot!(tag, created_by_user: member_user)
|
||||
|
||||
expect do
|
||||
put "/materials/#{ material.id }", params: {
|
||||
@@ -501,6 +588,8 @@ RSpec.describe 'Materials API', type: :request do
|
||||
}
|
||||
end.not_to change(MaterialVersion, :count)
|
||||
|
||||
expect(tag.reload.tag_versions.count).to eq(1)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(material.reload.version_no).to eq(1)
|
||||
end
|
||||
|
||||
@@ -56,6 +56,17 @@ RSpec.describe "TagChildren", type: :request do
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
|
||||
it 'returns 422 and does not create relation when the new link makes a cycle' do
|
||||
TagImplication.create!(tag: parent, parent_tag: child)
|
||||
|
||||
expect {
|
||||
do_request
|
||||
}.not_to change(TagImplication, :count)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(TagImplication.where(tag: child, parent_tag: parent)).not_to exist
|
||||
end
|
||||
end
|
||||
|
||||
context "when Tag.find raises (invalid ids)" do
|
||||
|
||||
@@ -797,7 +797,17 @@ RSpec.describe 'Tags API', type: :request do
|
||||
)
|
||||
TagImplication.create!(tag: first, parent_tag: root_material)
|
||||
TagImplication.create!(tag: second, parent_tag: first)
|
||||
TagImplication.create!(tag: first, parent_tag: second)
|
||||
now = Time.current
|
||||
TagImplication.insert_all!(
|
||||
[
|
||||
{
|
||||
tag_id: first.id,
|
||||
parent_tag_id: second.id,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
get '/tags/with-depth', params: { parent: root_material.id }
|
||||
|
||||
@@ -1364,8 +1374,6 @@ RSpec.describe 'Tags API', type: :request do
|
||||
end
|
||||
|
||||
it 'parent_tags に指定すると循環する tag は 422 にする' do
|
||||
pending '#332 で対応予定'
|
||||
|
||||
child = Tag.create!(
|
||||
tag_name: TagName.create!(name: 'put_cycle_child'),
|
||||
category: :general
|
||||
|
||||
新しい課題から参照
ユーザをブロックする