このコミットが含まれているのは:
2026-07-16 12:39:35 +09:00
コミット 0224c4d2f4
14個のファイルの変更371行の追加130行の削除
+61 -8
ファイルの表示
@@ -2,6 +2,13 @@ class Post < ApplicationRecord
require 'mini_magick'
require 'stringio'
ORIGINAL_CREATED_INVALID_MESSAGE = 'オリジナルの作成日時の形式が不正です.'.freeze
ORIGINAL_CREATED_MINUTE_PRECISION_MESSAGE =
'オリジナルの作成日時は分単位で入力してください.'.freeze
ORIGINAL_CREATED_ORDER_MESSAGE = 'オリジナルの作成日時の順番がをかしぃです.'.freeze
ORIGINAL_CREATED_MINIMUM_RANGE_MESSAGE =
'オリジナルの作成日時の範囲は1分以上必要です.'.freeze
def self.resized_thumbnail_attachment(upload)
upload.rewind
image = MiniMagick::Image.read(upload.read)
@@ -143,16 +150,17 @@ class Post < ApplicationRecord
private
def validate_original_created_range
f = original_created_from
b = original_created_before
return if f.blank? || b.blank?
f = parse_original_created_value(:original_created_from)
b = parse_original_created_value(:original_created_before)
return if f.nil? || b.nil?
f = Time.zone.parse(f) if String === f
b = Time.zone.parse(b) if String === b
return if !(f) || !(b)
if b <= f
errors.add :original_created_at, ORIGINAL_CREATED_ORDER_MESSAGE
return
end
if f >= b
errors.add :original_created_at, 'オリジナルの作成日時の順番がをかしぃです.'
if b - f < 1.minute
errors.add :original_created_at, ORIGINAL_CREATED_MINIMUM_RANGE_MESSAGE
end
end
@@ -175,4 +183,49 @@ class Post < ApplicationRecord
self.url = PostUrlNormaliser.normalise(url) || url.strip
end
def parse_original_created_value field
raw_value = public_send("#{ field }_before_type_cast")
value = public_send(field)
return nil if raw_value.blank? && value.blank?
time =
case raw_value
when String
parse_original_created_string(raw_value)
when Time, ActiveSupport::TimeWithZone
raw_value.in_time_zone
else
value&.in_time_zone
end
if time.nil?
errors.add field, ORIGINAL_CREATED_INVALID_MESSAGE
return nil
end
unless minute_precision_time?(time)
errors.add field, ORIGINAL_CREATED_MINUTE_PRECISION_MESSAGE
end
time
end
def parse_original_created_string raw_value
value = raw_value.to_s.strip
return nil if value.blank?
if value.match?(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}\z/)
return Time.zone.local(value[0, 4].to_i,
value[5, 2].to_i,
value[8, 2].to_i,
value[11, 2].to_i,
value[14, 2].to_i)
end
Time.iso8601(value).in_time_zone
rescue ArgumentError, TypeError
nil
end
def minute_precision_time? value
value.sec.zero? && value.nsec.zero?
end
end
+1 -2
ファイルの表示
@@ -250,8 +250,7 @@ class PostImportPreviewer
def sanitise_metadata_time value
return nil unless value.is_a?(String)
time = Time.iso8601(value)
time.nsec.zero? ? time.iso8601 : time.iso8601(9)
Time.iso8601(value).in_time_zone.change(sec: 0, nsec: 0).iso8601
rescue ArgumentError, TypeError
nil
end
+6 -23
ファイルの表示
@@ -81,26 +81,16 @@ class PostMetadataFetcher
day = match[3].to_i
hour = match[4].to_i
minute = match[5]&.to_i || 0
second = match[6]&.to_i || 0
fraction = match[7]
offset = match[8]
whole_second = second + fractional_seconds(fraction)
from =
timestamp =
if offset.present?
Time.new(year, month, day, hour, minute, whole_second, parse_offset(offset)).in_time_zone
Time.new(year, month, day, hour, minute, 0, parse_offset(offset)).in_time_zone
else
Time.zone.local(year, month, day, hour, minute, whole_second)
end
before =
if fraction.present?
from + (10**(-fraction.length))
elsif match[6].present?
from + 1.second
elsif match[5].present?
from + 1.minute
else
from + 1.hour
Time.zone.local(year, month, day, hour, minute)
end
from = timestamp.change(sec: 0, nsec: 0)
before = from + 1.minute
[from, before]
end
@@ -110,12 +100,6 @@ class PostMetadataFetcher
value.match?(/\A[+-]\d{2}:\d{2}\z/) ? value : "#{ value[0, 3] }:#{ value[3, 2] }"
end
def self.fractional_seconds value
return 0 if value.blank?
Rational(value.to_i, 10**value.length)
end
def self.serialise_time value
return nil if value.nil?
@@ -126,6 +110,5 @@ class PostMetadataFetcher
:original_created_range,
:parse_timestamp_range,
:parse_offset,
:fractional_seconds,
:serialise_time
end
+26
ファイルの表示
@@ -84,6 +84,32 @@ RSpec.describe 'Post imports API', type: :request do
expect(response).to have_http_status(:bad_request)
expect(json.fetch('message')).to eq('取込行の形式が不正です.')
end
it 'returns original created datetime validation errors for minute precision' do
sign_in_as(member)
post '/posts/import/validate', params: {
rows: [{
sourceRow: 1,
url: 'https://example.com/post',
metadataUrl: 'https://example.com/post',
attributes: {
originalCreatedFrom: '2020-01-01T00:00:30Z',
originalCreatedBefore: '2020-01-01T00:01Z' },
provenance: {
url: 'manual',
originalCreatedFrom: 'manual',
originalCreatedBefore: 'manual' },
tagSources: { automatic: '', manual: '' }
}],
changed_row: -1
}
expect(response).to have_http_status(:ok)
expect(json.fetch('rows').first.fetch('validation_errors')).to include(
'original_created_from' => ['オリジナルの作成日時は分単位で入力してください.']
)
end
end
describe 'POST /posts/import' do
+83
ファイルの表示
@@ -2206,6 +2206,89 @@ RSpec.describe 'Posts API', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
end
it 'rejects second-bearing original created timestamps on POST /posts' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'invalid original created from',
url: 'https://example.com/invalid-original-created-from',
tags: 'spec_tag',
thumbnail: dummy_upload,
original_created_from: '2020-01-01T00:00:01Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'original_created_from' => ['オリジナルの作成日時は分単位で入力してください.']
)
end
it 'rejects fractional-second original created timestamps on POST /posts' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'invalid original created before',
url: 'https://example.com/invalid-original-created-before',
tags: 'spec_tag',
thumbnail: dummy_upload,
original_created_before: '2020-01-01T00:00:00.123Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'original_created_before' => ['オリジナルの作成日時は分単位で入力してください.']
)
end
it 'rejects original created ranges shorter than one minute on POST /posts' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'too short original created range',
url: 'https://example.com/too-short-original-created-range',
tags: 'spec_tag',
thumbnail: dummy_upload,
original_created_from: '2020-01-01T00:00Z',
original_created_before: '2020-01-01T00:00:30Z')
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'original_created_at' => ['オリジナルの作成日時の範囲は1分以上必要です.']
)
end
it 'accepts original created ranges that are exactly one minute on POST /posts' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'valid original created range',
url: 'https://example.com/valid-original-created-range',
tags: 'spec_tag',
thumbnail: dummy_upload,
original_created_from: '2020-01-01T00:00Z',
original_created_before: '2020-01-01T00:01Z')
expect(response).to have_http_status(:created)
expect(Time.iso8601(json.fetch('original_created_from')))
.to eq(Time.iso8601('2020-01-01T00:00Z'))
expect(Time.iso8601(json.fetch('original_created_before')))
.to eq(Time.iso8601('2020-01-01T00:01Z'))
end
it 'rejects unparseable original created timestamps on PUT /posts/:id' do
sign_in_as(member)
base_version = create_post_version_for!(post_record)
put "/posts/#{post_record.id}", params: post_write_params(
base_version_no: base_version.version_no,
title: 'updated title',
tags: 'spec_tag',
original_created_from: 'not-a-time')
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'original_created_from' => ['オリジナルの作成日時の形式が不正です.']
)
end
end
describe 'tag versioning from post write actions' do
+4 -4
ファイルの表示
@@ -37,16 +37,16 @@ RSpec.describe PostMetadataFetcher do
)
end
it 'preserves an input offset and fractional-second precision' do
it 'rounds second and fractional-second timestamps down to one-minute ranges' do
result = fetch_with_published_time('2024-02-03T12:34:56.123+02:30')
expect(Time.iso8601(result.fetch(:original_created_from)))
.to eq(Time.iso8601('2024-02-03T12:34:56.123+02:30'))
.to eq(Time.iso8601('2024-02-03T12:34:00+02:30'))
expect(
Time.iso8601(result.fetch(:original_created_before)) -
Time.iso8601(result.fetch(:original_created_from))
).to eq(0.001)
expect(result.fetch(:original_created_from)).to include('.123000000')
).to eq(60)
expect(result.fetch(:original_created_from)).not_to include('.')
end
it 'adds platform tags for known video URLs' do