コミットを比較

..

102 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 817cd6ae73 #399 2026-07-18 23:06:12 +09:00
みてるぞ dc7353304f #399 2026-07-18 22:53:36 +09:00
みてるぞ 5ff3fc9441 #399 2026-07-18 22:32:34 +09:00
みてるぞ a847d93d2f #399 2026-07-18 22:18:20 +09:00
みてるぞ 2f87669699 #399 2026-07-18 21:59:22 +09:00
みてるぞ 1c906d7432 #399 2026-07-18 21:19:53 +09:00
みてるぞ f662dc9dc0 #399 2026-07-18 20:22:53 +09:00
みてるぞ 206c6bc0a0 #399 2026-07-18 20:10:15 +09:00
みてるぞ 8f66ee8059 #399 2026-07-18 20:03:26 +09:00
みてるぞ 83e3db3314 #399 2026-07-18 20:00:40 +09:00
みてるぞ eae6c30064 #399 2026-07-18 19:26:27 +09:00
みてるぞ 240e078f0b #399 2026-07-18 18:33:11 +09:00
みてるぞ 74b1ada0dd #399 2026-07-18 17:44:56 +09:00
みてるぞ 23d8adf65d #399 2026-07-18 17:35:28 +09:00
みてるぞ e0debed94e #399 2026-07-18 17:31:57 +09:00
みてるぞ 583ce22a7e #399 2026-07-18 17:07:56 +09:00
みてるぞ 7c808a6f76 #399 2026-07-18 15:15:10 +09:00
みてるぞ a5ae7c6f2d #399 2026-07-18 14:55:11 +09:00
みてるぞ 03dc4d0661 #399 2026-07-18 13:32:22 +09:00
みてるぞ 06b9c1cb50 #399 2026-07-18 13:07:49 +09:00
みてるぞ 688b4af575 #399 2026-07-18 12:53:50 +09:00
みてるぞ 040cc3f25d #399 2026-07-18 12:15:32 +09:00
みてるぞ b3e67d8cca #399 2026-07-18 11:55:09 +09:00
みてるぞ b00a62a0ac #399 2026-07-18 09:11:11 +09:00
みてるぞ e6c3c635b8 #399 2026-07-18 02:29:55 +09:00
みてるぞ 9552081133 #399 2026-07-18 02:08:20 +09:00
みてるぞ 2d2a9b4bd6 #399 2026-07-18 01:37:11 +09:00
みてるぞ 09ac2576bb #399 2026-07-18 01:28:22 +09:00
みてるぞ 9eca670934 #399 2026-07-18 01:12:55 +09:00
みてるぞ c31d84115d #399 2026-07-18 00:31:02 +09:00
みてるぞ ff970f8171 #399 2026-07-18 00:01:12 +09:00
みてるぞ 0ae41b6266 #399 2026-07-17 23:14:37 +09:00
みてるぞ eb4bf5e35c #399 2026-07-17 22:21:18 +09:00
みてるぞ e6b7e33b83 #399 2026-07-17 21:53:24 +09:00
みてるぞ 3820d3d4d5 #399 2026-07-17 21:06:26 +09:00
みてるぞ dd2d199d04 #399 2026-07-16 23:39:13 +09:00
みてるぞ 0a8ffc38b8 #399 2026-07-16 23:23:32 +09:00
みてるぞ 3f75994bd4 #399 2026-07-16 23:12:14 +09:00
みてるぞ f91b78bd47 #399 2026-07-16 22:31:32 +09:00
みてるぞ 7f8cce39bc #399 2026-07-16 22:05:40 +09:00
みてるぞ 5c5a9fa1b0 #399 2026-07-16 22:00:16 +09:00
みてるぞ 66c738cbff #399 2026-07-16 21:45:02 +09:00
みてるぞ cde0a2deae #399 2026-07-16 20:54:42 +09:00
みてるぞ 43a3772976 #399 2026-07-16 20:35:07 +09:00
みてるぞ 90d8d3ff08 #399 2026-07-16 20:05:09 +09:00
みてるぞ d1de631eed #399 2026-07-16 19:35:21 +09:00
みてるぞ 6f3ccb0ffa #399 2026-07-16 19:18:35 +09:00
みてるぞ 95c3f08a44 #399 2026-07-16 18:52:07 +09:00
みてるぞ ef95b20a7e #399 2026-07-16 18:33:45 +09:00
みてるぞ f76fbe6711 #399 2026-07-16 18:16:20 +09:00
みてるぞ 0224c4d2f4 #399 2026-07-16 12:39:35 +09:00
みてるぞ ead825e3ee #399 2026-07-16 07:56:45 +09:00
みてるぞ 3d88fdecb1 #399 2026-07-16 01:19:36 +09:00
みてるぞ 2e1b4449ba #399 2026-07-16 00:44:11 +09:00
みてるぞ 4eb483c049 #399 2026-07-16 00:24:24 +09:00
みてるぞ e197f27e9a #399 2026-07-15 23:59:01 +09:00
みてるぞ 34f81325d2 #399 2026-07-15 23:42:05 +09:00
みてるぞ 9af4670c6c #399 2026-07-15 23:29:19 +09:00
みてるぞ 5183b0b354 #399 2026-07-15 23:06:28 +09:00
みてるぞ d0914fce3b #399 2026-07-15 22:51:01 +09:00
みてるぞ 0ac7332458 #399 2026-07-15 22:41:06 +09:00
みてるぞ 07ce19e32d #399 2026-07-15 22:26:58 +09:00
みてるぞ 5f1d619139 #399 2026-07-15 22:04:47 +09:00
みてるぞ 3cd22855b3 #399 2026-07-15 21:42:00 +09:00
みてるぞ 55c69866a7 #399 2026-07-15 21:37:03 +09:00
みてるぞ 04d2f0f5a9 #399 2026-07-15 20:59:00 +09:00
みてるぞ 025f49cbcb #399 2026-07-15 20:51:24 +09:00
みてるぞ d2f1b1ed48 #399 2026-07-15 20:25:51 +09:00
みてるぞ 6e5aa1e30f #399 2026-07-15 19:57:09 +09:00
みてるぞ 21d6c1408a #399 2026-07-15 19:43:39 +09:00
みてるぞ eebbd5d069 #399 2026-07-15 19:33:18 +09:00
みてるぞ 58828597d7 #399 2026-07-15 08:03:25 +09:00
みてるぞ b0c24f319a #399 2026-07-14 20:03:11 +09:00
みてるぞ f9463f383f #399 2026-07-14 19:37:04 +09:00
みてるぞ 6d037192c4 #399 2026-07-14 19:12:23 +09:00
みてるぞ d035da99ad #399 2026-07-14 18:39:24 +09:00
みてるぞ bc660676ef Merge remote-tracking branch 'origin/main' into feature/399 2026-07-14 12:40:32 +09:00
みてるぞ 97b132e5ab #399 2026-07-13 01:31:45 +09:00
みてるぞ be68841bd3 #399 2026-07-13 01:17:11 +09:00
みてるぞ ac82adc6b3 #399 2026-07-13 00:58:02 +09:00
みてるぞ aa96ec95d1 #399 2026-07-12 14:03:05 +09:00
みてるぞ df17f20907 #399 2026-07-12 13:14:36 +09:00
みてるぞ 769966648b #399 2026-07-12 12:43:31 +09:00
みてるぞ 7bcb76516c #399 2026-07-12 12:23:53 +09:00
みてるぞ 8970edc59f #399 2026-07-12 11:17:23 +09:00
みてるぞ 9b1ea56e36 #399 2026-07-12 10:27:13 +09:00
みてるぞ 155edfe018 #399 2026-07-12 02:22:36 +09:00
みてるぞ 23f1ffb04b #399 2026-07-12 02:08:32 +09:00
みてるぞ ccfe65a6a4 #399 2026-07-12 02:01:25 +09:00
みてるぞ 06d6e512e4 #399 2026-07-12 01:56:24 +09:00
みてるぞ 5bd097bcfe #399 2026-07-12 01:47:34 +09:00
みてるぞ f636d2a177 #399 2026-07-12 01:39:31 +09:00
みてるぞ 10dc776313 #399 2026-07-12 01:17:45 +09:00
みてるぞ a9e16735f8 #399 2026-07-12 01:11:07 +09:00
みてるぞ 440a6c9961 #399 2026-07-12 00:54:21 +09:00
みてるぞ 4535a9d260 #399 2026-07-12 00:46:53 +09:00
みてるぞ 440d3d38be #399 2026-07-11 23:21:59 +09:00
みてるぞ cb33d9ca25 #399 2026-07-11 23:08:03 +09:00
みてるぞ 19bf24432a #399 2026-07-11 23:07:51 +09:00
みてるぞ ac41385962 Merge remote-tracking branch 'origin/main' into feature/399 2026-07-11 22:36:03 +09:00
みてるぞ 08bf92ff79 #399 2026-07-11 21:46:10 +09:00
みてるぞ c51d7b98ba #399 2026-07-11 21:11:36 +09:00
6個のファイルの変更15行の追加230行の削除
+2 -26
ファイルの表示
@@ -574,32 +574,8 @@ and layout reuse, follow `frontend/AGENTS.md`.
wording and placement before implementing it. wording and placement before implementing it.
- Do not invent replacement copy when removing unrequested wording. - Do not invent replacement copy when removing unrequested wording.
- Do not create, modify, or run tests unless the user explicitly asks for - Do not create, modify, or run tests unless the user explicitly asks for
test work. When the user asks for tests, keep working within the permitted test work. When the user asks for tests, keep working and rerun them until
test-file scope and rerun them until they pass or the remaining failure is they pass or the remaining failure is clearly blocked.
clearly blocked.
- Test-only work includes adding, updating, deleting, reorganising, or fixing
SyntaxError in tests. During test-only work, do not modify production code.
- During test-only work, do not change production constants, behaviour, API
contracts, validation, routes, authentication, permissions, UI, copy,
dependencies, limits, thresholds, defaults, migrations, schema, or
environment settings to satisfy tests.
- Do not make production code match failing tests, mock assumptions, fixtures,
snapshots, old expectations, or stale setup. This includes changing
production constants merely because a test expects a different value.
- If test work reveals a production bug, spec mismatch, or missing behaviour,
stop without modifying production code and report: the failing test or
discovered issue, the related production file, the actual behaviour, the
expected behaviour, and why a production change appears necessary.
- Modify production code for test failures only when the user explicitly asks
for that production change. Do not expand a test task into a production task
on your own authority.
- If the user explicitly asks for both production implementation and test
updates, implement production code to the confirmed specification first,
then add or update tests to verify that specification. Never roll production
behaviour back to satisfy old tests.
- If it is unclear whether the test or the production implementation is stale,
or a test cannot be corrected without changing production code, ask the user
instead of guessing.
## Backend rules ## Backend rules
+7 -115
ファイルの表示
@@ -200,45 +200,14 @@ class TagsController < ApplicationController
.find_by(id: params[:id]) .find_by(id: params[:id])
return head :not_found unless tag return head :not_found unless tag
rows = normalise_deerjikist_rows(tag)
return if performed?
ApplicationRecord.transaction do ApplicationRecord.transaction do
tag.lock! tag.deerjikists = []
params[:_json].each.with_index do |item, i|
requested_keys = rows.map { |row| [row[:platform], row[:code]] }.uniq platform = item[:platform]
row_indexes_by_key = rows_by_key(rows) code = normalise_deerjikist_code(platform, item[:code])
locked_deerjikists = lock_deerjikists_for_tag_update(tag.id, requested_keys) deerjikist = Deerjikist.find_or_initialize_by(platform:, code:)
current_deerjikists = locked_deerjikists.filter { |deerjikist| deerjikist.tag = tag
deerjikist.tag_id == tag.id render_deerjikist_form_record_invalid(deerjikist, i) unless deerjikist.save
}
requested_deerjikists = locked_deerjikists.filter { |deerjikist|
row_indexes_by_key.key?([deerjikist.platform, deerjikist.code])
}
render_deerjikist_conflicts(requested_deerjikists, row_indexes_by_key, tag)
raise ActiveRecord::Rollback if performed?
requested_keys_set = requested_keys.to_set
current_deerjikists.each do |deerjikist|
key = [deerjikist.platform, deerjikist.code]
deerjikist.destroy! unless requested_keys_set.include?(key)
end
existing_keys = requested_deerjikists.to_h { |deerjikist|
[[deerjikist.platform, deerjikist.code], true]
}
requested_keys.each do |platform, code|
next if existing_keys[[platform, code]]
deerjikist = Deerjikist.new(platform:, code:, tag:)
row_index = row_indexes_by_key[[platform, code]].first
begin
render_deerjikist_form_record_invalid(deerjikist, row_index) unless deerjikist.save
rescue ActiveRecord::RecordNotUnique
conflicts = lock_deerjikists_for_tag_update(tag.id, [[platform, code]])
render_deerjikist_conflicts(conflicts, row_indexes_by_key, tag)
end
raise ActiveRecord::Rollback if performed? raise ActiveRecord::Rollback if performed?
end end
end end
@@ -684,7 +653,6 @@ class TagsController < ApplicationController
end end
def normalise_deerjikist_code platform, code def normalise_deerjikist_code platform, code
code = code.to_s
return code if platform != 'youtube' || code[0] != '@' return code if platform != 'youtube' || code[0] != '@'
url = "https://www.youtube.com/#{ code }" url = "https://www.youtube.com/#{ code }"
@@ -701,82 +669,6 @@ class TagsController < ApplicationController
nil nil
end end
def normalise_deerjikist_rows tag
rows = []
params[:_json].each.with_index do |item, index|
platform = item[:platform]
unless Deerjikist.platforms.key?(platform)
render_deerjikist_platform_invalid(index)
return rows
end
code = normalise_deerjikist_code(platform, item[:code])
deerjikist = Deerjikist.new(platform:, code:, tag:)
unless deerjikist.valid?
render_deerjikist_form_record_invalid(deerjikist, index)
return rows
end
rows << { index:, platform:, code: }
end
rows
end
def lock_deerjikists_for_tag_update tag_id, keys
clauses = ['tag_id = ?']
values = [tag_id]
keys.each do |platform, code|
clauses << '(platform = ? AND code = ?)'
values << platform << code
end
Deerjikist
.where(clauses.join(' OR '), *values)
.order(:platform, :code)
.lock
.to_a
end
def render_deerjikist_conflicts deerjikists, row_indexes_by_key, tag
conflicts = deerjikists.filter { |deerjikist| deerjikist.tag_id != tag.id }
return if conflicts.empty?
tag_names_by_id = Tag
.joins(:tag_name)
.where(id: conflicts.map(&:tag_id).uniq)
.pluck('tags.id', 'tag_names.name')
.to_h
fields = { }
conflicts.each do |deerjikist|
message = "この情報は既に「#{ tag_names_by_id[deerjikist.tag_id] }」に紐づいてゐます."
row_indexes_by_key[[deerjikist.platform, deerjikist.code]].each do |index|
field = :"deerjikists.#{ index }.code"
fields[field] ||= []
fields[field] << message
end
end
render_validation_error fields:
end
def render_deerjikist_platform_invalid index
render_validation_error fields: {
:"deerjikists.#{ index }.platform" => ['値が不正です.'],
}
end
def rows_by_key rows
rows.each_with_object({ }) do |row, result|
key = [row[:platform], row[:code]]
result[key] ||= []
result[key] << row[:index]
end
end
def render_deerjikist_form_record_invalid deerjikist, index def render_deerjikist_form_record_invalid deerjikist, index
fields = { } fields = { }
-83
ファイルの表示
@@ -226,14 +226,6 @@ RSpec.describe 'Tags deerjikists API', type: :request do
[platform2, code2], [platform2, code2],
) )
end end
it 'locks the tag before replacing the complete list' do
expect_any_instance_of(Tag).to receive(:lock!).and_call_original
do_request
expect(response).to have_http_status(:ok)
end
end end
context 'when tag already has deerjikists' do context 'when tag already has deerjikists' do
@@ -307,81 +299,6 @@ RSpec.describe 'Tags deerjikists API', type: :request do
end end
end end
context 'when platform is outside the enum' do
let(:payload) do
[
{ platform: 'invalid', code: code1 },
]
end
it 'returns 422 with an indexed platform error without changing the list' do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
expect {
do_request
}.not_to change { Deerjikist.where(tag: tag).map { |d| [d.platform, d.code] } }
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'deerjikists.0.platform' => [be_present],
)
end
end
context 'when a requested deerjikist belongs to another tag' do
let!(:other_tag) { create(:tag, category: :deerjikist) }
let!(:owned_deerjikist) do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
end
let!(:conflicting_deerjikist) do
Deerjikist.create!(platform: platform2, code: code2, tag: other_tag)
end
let(:payload) do
[
{ platform: 'nico', code: 'new-code' },
{ platform: platform2, code: code2 },
]
end
before do
other_tag.tag_name.update!(name: 'existing-deerjikist')
end
it 'returns an indexed 422 error and rolls back the complete replacement' do
expect {
do_request
}.not_to change { Deerjikist.order(:platform, :code).pluck(:platform, :code, :tag_id) }
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'deerjikists.1.code' => [include('existing-deerjikist')],
)
expect(owned_deerjikist.reload.tag_id).to eq(tag.id)
expect(conflicting_deerjikist.reload.tag_id).to eq(other_tag.id)
end
end
context 'when a requested deerjikist already belongs to the same tag' do
let!(:existing_deerjikist) do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
end
let(:payload) do
[
{ platform: platform1, code: code1 },
]
end
it 'keeps the existing row' do
expect {
do_request
}.not_to change { existing_deerjikist.reload.created_at }
expect(response).to have_http_status(:ok)
expect(Deerjikist.where(tag: tag).pluck(:platform, :code))
.to eq([[platform1, code1]])
end
end
context 'when youtube code is handle' do context 'when youtube code is handle' do
let(:channel_id) { 'UCabcdefghijklmnopqrstuv' } let(:channel_id) { 'UCabcdefghijklmnopqrstuv' }
let(:payload) do let(:payload) do
+4 -4
ファイルの表示
@@ -29,15 +29,15 @@ describe ('post new review URL state', () => {
.toEqual (['one', 'two']) .toEqual (['one', 'two'])
}) })
it ('allows at most a 6 143 byte request target', () => { it ('allows at most a 4095-byte request target', () => {
const baseUrl = 'https://example.com/' const baseUrl = 'https://example.com/'
const baseLength = postNewReviewPathByteLength ([baseUrl]) const baseLength = postNewReviewPathByteLength ([baseUrl])
const allowed = `${ baseUrl }${ 'a'.repeat (6_143 - baseLength) }` const allowed = `${ baseUrl }${ 'a'.repeat (4_095 - baseLength) }`
const denied = `${ allowed }a` const denied = `${ allowed }a`
expect (postNewReviewPathByteLength ([allowed])).toBe (6_143) expect (postNewReviewPathByteLength ([allowed])).toBe (4_095)
expect (isPostNewReviewPathWithinLimit ([allowed])).toBe (true) expect (isPostNewReviewPathWithinLimit ([allowed])).toBe (true)
expect (postNewReviewPathByteLength ([denied])).toBe (6_144) expect (postNewReviewPathByteLength ([denied])).toBe (4_096)
expect (isPostNewReviewPathWithinLimit ([denied])).toBe (false) expect (isPostNewReviewPathWithinLimit ([denied])).toBe (false)
}) })
}) })
+1 -1
ファイルの表示
@@ -1,5 +1,5 @@
const POST_NEW_REVIEW_PATH_PREFIX = '/posts/new?urls=' const POST_NEW_REVIEW_PATH_PREFIX = '/posts/new?urls='
const MAX_POST_NEW_REVIEW_TARGET_BYTES = 6_144 const MAX_POST_NEW_REVIEW_TARGET_BYTES = 4_096
const textEncoder = new TextEncoder () const textEncoder = new TextEncoder ()
+1 -1
ファイルの表示
@@ -60,7 +60,7 @@ describe ('PostImportSourcePage', () => {
const input = screen.getByRole ('textbox', { name: '' }) const input = screen.getByRole ('textbox', { name: '' })
fireEvent.change (input, { fireEvent.change (input, {
target: { value: `https://example.com/${ 'a'.repeat (6_200) }` } }) target: { value: `https://example.com/${ 'a'.repeat (4_100) }` } })
expect (screen.getByRole ('button', { name: '次へ' })).toBeDisabled () expect (screen.getByRole ('button', { name: '次へ' })).toBeDisabled ()
}) })