このコミットが含まれているのは:
2026-06-22 21:24:55 +09:00
コミット d3af4563ca
12個のファイルの変更309行の追加47行の削除
+19 -11
ファイルの表示
@@ -45,7 +45,9 @@ class PostsController < ApplicationController
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id") .joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all")) .reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
.preload(:uploaded_user, :parents, :children, .preload(:uploaded_user, :parents, :children,
tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) active_post_tags: [:sections,
{ tag: [:deerjikists, :materials,
{ tag_name: :wiki_page }] }])
.with_attached_thumbnail .with_attached_thumbnail
q = q.where('posts.url LIKE ?', "%#{ url }%") if url q = q.where('posts.url LIKE ?', "%#{ url }%") if url
@@ -97,7 +99,9 @@ class PostsController < ApplicationController
def random def random
post = filtered_posts.preload(:uploaded_user, :parents, :children, post = filtered_posts.preload(:uploaded_user, :parents, :children,
tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) active_post_tags: [:sections,
{ tag: [:deerjikists, :materials,
{ tag_name: :wiki_page }] }])
.with_attached_thumbnail .with_attached_thumbnail
.order('RAND()') .order('RAND()')
.first .first
@@ -110,7 +114,9 @@ class PostsController < ApplicationController
post = post =
Post Post
.includes(:uploaded_user, :parents, :children, .includes(:uploaded_user, :parents, :children,
tags: [:deerjikists, :materials, { tag_name: :wiki_page }]) active_post_tags: [:sections,
{ tag: [:deerjikists, :materials,
{ tag_name: :wiki_page }] }])
.with_attached_thumbnail .with_attached_thumbnail
.find_by(id: params[:id]) .find_by(id: params[:id])
return head :not_found unless post return head :not_found unless post
@@ -168,6 +174,8 @@ class PostsController < ApplicationController
render_validation_error fields: { tags: 'ニコニコ・タグは直接指定できません.' } render_validation_error fields: { tags: 'ニコニコ・タグは直接指定できません.' }
rescue Tag::DeprecatedTagNormalisationError rescue Tag::DeprecatedTagNormalisationError
render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags
rescue Tag::SectionLiteralParseError
render_validation_error fields: { tags: ['タグ区間の記法が不正です.'] }
rescue ArgumentError => e rescue ArgumentError => e
render_validation_error fields: { parent_post_ids: [e.message] } render_validation_error fields: { parent_post_ids: [e.message] }
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
@@ -260,6 +268,8 @@ class PostsController < ApplicationController
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] } render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
rescue Tag::DeprecatedTagNormalisationError rescue Tag::DeprecatedTagNormalisationError
render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags
rescue Tag::SectionLiteralParseError
render_validation_error fields: { tags: ['タグ区間の記法が不正です.'] }
rescue ArgumentError => e rescue ArgumentError => e
render_validation_error fields: { parent_post_ids: [e.message] } render_validation_error fields: { parent_post_ids: [e.message] }
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
@@ -390,7 +400,8 @@ class PostsController < ApplicationController
end end
def build_tag_tree_for post def build_tag_tree_for post
tags = post.tags.reject(&:deprecated?).to_a post_tags = post.active_post_tags.reject { |post_tag| post_tag.tag.deprecated? }
tags = post_tags.map(&:tag)
tag_ids = tags.map(&:id) tag_ids = tags.map(&:id)
implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids) implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids)
@@ -405,11 +416,9 @@ class PostsController < ApplicationController
root_ids = tag_ids - child_ids root_ids = tag_ids - child_ids
tags_by_id = tags.index_by(&:id) tags_by_id = tags.index_by(&:id)
sections_by_tag_id = sections_by_tag_id = post_tags.to_h { |post_tag|
PostTagSection [post_tag.tag_id, post_tag.sections.as_json(only: [:begin_ms, :end_ms])]
.where(post_id: post.id, tag_id: tag_ids) }
.order(:begin_ms)
.group_by(&:tag_id)
memo = { } memo = { }
@@ -418,7 +427,6 @@ class PostsController < ApplicationController
return nil unless tag return nil unless tag
sections = sections_by_tag_id.fetch(tag_id, []) sections = sections_by_tag_id.fetch(tag_id, [])
.as_json(only: [:begin_ms, :end_ms])
if path.include?(tag_id) if path.include?(tag_id)
return TagRepr.inline(tag).merge(children: [], sections:) return TagRepr.inline(tag).merge(children: [], sections:)
@@ -578,7 +586,7 @@ class PostsController < ApplicationController
end end
def section_literal section def section_literal section
"[#{ Post.ms_to_time(section[0]) }-#{ Post.ms_to_time(section[1]) }]" "[#{ Post.ms_to_time(section[0]) }-#{ section[1] ? Post.ms_to_time(section[1]) : '' }]"
end end
def post_conflict_json post:, base_version_no:, base_snapshot:, def post_conflict_json post:, base_version_no:, base_snapshot:,
+1 -1
ファイルの表示
@@ -86,7 +86,7 @@ class Post < ApplicationRecord
end end
def self.section_literal section def self.section_literal section
"[#{ Post.ms_to_time(section.begin_ms) }-#{ Post.ms_to_time(section.end_ms) }]" "[#{ Post.ms_to_time(section.begin_ms) }-#{ section.end_ms ? Post.ms_to_time(section.end_ms) : '' }]"
end end
def self.ms_to_time ms def self.ms_to_time ms
+2 -2
ファイルの表示
@@ -15,6 +15,6 @@ class PostTagSection < ApplicationRecord
validates :begin_ms, presence: true, validates :begin_ms, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :end_ms, presence: true, validates :end_ms, numericality: { only_integer: true, greater_than: :begin_ms },
numericality: { only_integer: true, greater_than: :begin_ms } allow_nil: true
end end
+81 -22
ファイルの表示
@@ -17,6 +17,16 @@ class Tag < ApplicationRecord
end end
end end
class SectionLiteralParseError < ArgumentError
attr_reader :tag_name, :literal
def initialize tag_name, literal
@tag_name = tag_name
@literal = literal
super("invalid section literal for tag #{ tag_name }: #{ literal }")
end
end
has_many :post_tags, inverse_of: :tag has_many :post_tags, inverse_of: :tag
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag' has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
@@ -113,20 +123,22 @@ class Tag < ApplicationRecord
sections = { } sections = { }
tags = tag_names.map do |name| tags = tag_names.map do |name|
raw_name = name
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil] pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil]
name = name.sub(/\A#{ pf }/i, '') name = name.sub(/\A#{ pf }/i, '')
sections_by_tag = [] sections_by_tag = []
while n = name.sub!(/^(\S*?)\[([0-9:.]*?)-([0-9:.]*?)\](\S*?)$/, '\1\4 \2 \3') while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
name, *section_raw = n.split name = "#{ match[1] }#{ match[4] }"
sections_by_tag << normalise_section_range!(
begin_raw: match[2],
end_raw: match[3],
tag_name: name)
end
begin_ms, end_ms = section_raw.map { time_to_ms(_1) } if name.include?('[') || name.include?(']')
next if begin_ms == end_ms raise SectionLiteralParseError.new(raw_name, raw_name)
begin_ms, end_ms = end_ms, begin_ms if begin_ms > end_ms
sections_by_tag << [begin_ms, end_ms]
end end
name = TagName.canonicalise(name).first name = TagName.canonicalise(name).first
@@ -141,6 +153,7 @@ class Tag < ApplicationRecord
sections[tag.id] ||= [] sections[tag.id] ||= []
sections[tag.id].concat(sections_by_tag) sections[tag.id].concat(sections_by_tag)
sections[tag.id] = merge_section_ranges(sections[tag.id])
end end
end end
@@ -178,6 +191,45 @@ class Tag < ApplicationRecord
(result + tags).uniq { |t| t.id } (result + tags).uniq { |t| t.id }
end end
def self.normalise_section_range! begin_raw:, end_raw:, tag_name:
begin_ms = begin_raw.empty? ? 0 : time_to_ms!(begin_raw, tag_name:)
end_ms = end_raw.empty? ? nil : time_to_ms!(end_raw, tag_name:)
if end_ms
begin_ms, end_ms = end_ms, begin_ms if begin_ms > end_ms
end_ms = begin_ms + 1 if begin_ms == end_ms
end
[begin_ms, end_ms]
end
def self.merge_section_ranges ranges
sorted_ranges = ranges.sort_by { |begin_ms, end_ms| [begin_ms, end_ms || Float::INFINITY] }
merged = []
sorted_ranges.each do |begin_ms, end_ms|
if merged.empty?
merged << [begin_ms, end_ms]
next
end
last_begin_ms, last_end_ms = merged[-1]
if last_end_ms.nil? || begin_ms <= last_end_ms
merged[-1] = [last_begin_ms, merge_section_end(last_end_ms, end_ms)]
else
merged << [begin_ms, end_ms]
end
end
merged
end
def self.merge_section_end left_end_ms, right_end_ms
return nil if left_end_ms.nil? || right_end_ms.nil?
[left_end_ms, right_end_ms].max
end
def self.find_or_create_by_tag_name! name, category: def self.find_or_create_by_tag_name! name, category:
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip) tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
tn = tn.canonical if tn.canonical_id? tn = tn.canonical if tn.canonical_id?
@@ -274,23 +326,30 @@ class Tag < ApplicationRecord
end end
end end
def self.time_to_ms str def self.time_to_ms! str, tag_name:
parts = str.split(':') match =
case str
when /\A(?<seconds>\d+)(?:\.(?<ms>\d{1,3}))?\z/
{ hours: nil, minutes: nil, seconds: Regexp.last_match[:seconds],
ms: Regexp.last_match[:ms] }
when /\A(?<minutes>\d+):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
{ hours: nil, minutes: Regexp.last_match[:minutes],
seconds: Regexp.last_match[:seconds],
ms: Regexp.last_match[:ms] }
when /\A(?<hours>\d+):(?<minutes>[0-5]?\d):(?<seconds>[0-5]?\d)(?:\.(?<ms>\d{1,3}))?\z/
{ hours: Regexp.last_match[:hours],
minutes: Regexp.last_match[:minutes],
seconds: Regexp.last_match[:seconds],
ms: Regexp.last_match[:ms] }
end
s_part = parts.pop raise SectionLiteralParseError.new(tag_name, str) unless match
s, ms = s_part.split('.')
total_s = s.to_i total_s = match[:seconds].to_i
total_s += match[:minutes].to_i * 60 if match[:minutes]
total_s += match[:hours].to_i * 3_600 if match[:hours]
if parts.length >= 1 total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
total_s += parts.pop.to_i * 60
end
if parts.length >= 1
total_s += parts.pop.to_i * 3_600
end
total_s * 1_000 + ms.to_s.ljust(3, '0')[0, 3].to_i
end end
def nico_tags_cannot_be_deprecated def nico_tags_cannot_be_deprecated
+11 -3
ファイルの表示
@@ -18,7 +18,7 @@ module PostRepr
def base post, current_user = nil def base post, current_user = nil
json = common(post) json = common(post)
json['tags'] = tag_json(post.tags) json['tags'] = tag_json(post)
json['uploaded_user'] = post.uploaded_user && UserRepr.base(post.uploaded_user) json['uploaded_user'] = post.uploaded_user && UserRepr.base(post.uploaded_user)
json['viewed'] = current_user ? current_user.viewed?(post) : false json['viewed'] = current_user ? current_user.viewed?(post) : false
json json
@@ -52,8 +52,16 @@ module PostRepr
.merge('thumbnail' => thumbnail_url(post)) .merge('thumbnail' => thumbnail_url(post))
end end
def tag_json tags def tag_json post
tags.reject(&:deprecated?).map { |tag| TagRepr.inline(tag) } post
.active_post_tags
.reject { _1.tag.deprecated? }
.sort_by { _1.tag.name }
.map { |post_tag|
TagRepr.inline(post_tag.tag).merge(
'children' => [],
'sections' => post_tag.sections.as_json(only: [:begin_ms, :end_ms]))
}
end end
def thumbnail_url post def thumbnail_url post
@@ -4,7 +4,7 @@ class CreatePostTagSections < ActiveRecord::Migration[8.0]
t.references :post, null: false, foreign_key: true, index: false t.references :post, null: false, foreign_key: true, index: false
t.references :tag, null: false, foreign_key: true, index: false t.references :tag, null: false, foreign_key: true, index: false
t.integer :begin_ms, null: false t.integer :begin_ms, null: false
t.integer :end_ms, null: false t.integer :end_ms, null: true
t.timestamps t.timestamps
t.index [:post_id, :begin_ms], name: 'idx_post_tag_sections_post_id_begin_ms' t.index [:post_id, :begin_ms], name: 'idx_post_tag_sections_post_id_begin_ms'
@@ -12,7 +12,7 @@ class CreatePostTagSections < ActiveRecord::Migration[8.0]
t.check_constraint 'begin_ms >= 0', t.check_constraint 'begin_ms >= 0',
name: 'chk_post_tag_sections_begin_ms_natural' name: 'chk_post_tag_sections_begin_ms_natural'
t.check_constraint 'begin_ms < end_ms', t.check_constraint 'end_ms IS NULL OR begin_ms < end_ms',
name: 'chk_post_tag_sections_end_ms_after_begin_ms' name: 'chk_post_tag_sections_end_ms_after_begin_ms'
end end
end end
生成ファイル
+3 -4
ファイルの表示
@@ -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_06_21_000000) do ActiveRecord::Schema[8.0].define(version: 2026_06_22_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
@@ -214,12 +214,12 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_21_000000) do
t.bigint "post_id", null: false t.bigint "post_id", null: false
t.bigint "tag_id", null: false t.bigint "tag_id", null: false
t.integer "begin_ms", null: false t.integer "begin_ms", null: false
t.integer "end_ms", null: false t.integer "end_ms"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["post_id", "begin_ms"], name: "idx_post_tag_sections_post_id_begin_ms" t.index ["post_id", "begin_ms"], name: "idx_post_tag_sections_post_id_begin_ms"
t.index ["tag_id"], name: "fk_rails_8be3847903" t.index ["tag_id"], name: "fk_rails_8be3847903"
t.check_constraint "`begin_ms` < `end_ms`", name: "chk_post_tag_sections_end_ms_after_begin_ms" t.check_constraint "(`end_ms` is null) or (`begin_ms` < `end_ms`)", name: "chk_post_tag_sections_end_ms_after_begin_ms"
t.check_constraint "`begin_ms` >= 0", name: "chk_post_tag_sections_begin_ms_natural" t.check_constraint "`begin_ms` >= 0", name: "chk_post_tag_sections_begin_ms_natural"
end end
@@ -432,7 +432,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_21_000000) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["expires_at"], name: "index_theatre_watching_users_on_expires_at" t.index ["expires_at"], name: "index_theatre_watching_users_on_expires_at"
t.index ["theatre_id", "expires_at"], name: "idx_on_theatre_id_skip_expires_at_4c8de1dd42"
t.index ["theatre_id", "expires_at"], name: "index_theatre_watching_users_on_theatre_id_and_expires_at" t.index ["theatre_id", "expires_at"], name: "index_theatre_watching_users_on_theatre_id_and_expires_at"
t.index ["theatre_id"], name: "index_theatre_watching_users_on_theatre_id" t.index ["theatre_id"], name: "index_theatre_watching_users_on_theatre_id"
t.index ["user_id"], name: "index_theatre_watching_users_on_user_id" t.index ["user_id"], name: "index_theatre_watching_users_on_user_id"
+12
ファイルの表示
@@ -25,5 +25,17 @@ RSpec.describe PostTag, type: :model do
expect(post_tag.sections).to be_empty expect(post_tag.sections).to be_empty
end end
it 'allows open-ended sections' do
post_tag = create(:post_tag)
section = create(:post_tag_section,
post: post_tag.post,
tag: post_tag.tag,
begin_ms: 1000,
end_ms: nil)
expect(section).to be_valid
expect(post_tag.sections).to contain_exactly(section)
end
end end
end end
+79
ファイルの表示
@@ -19,6 +19,85 @@ RSpec.describe Tag, type: :model do
expect(error.tag_names).to eq([deprecated_tag.name]) expect(error.tag_names).to eq([deprecated_tag.name])
} }
end end
it 'rejects invalid section literals instead of treating them as zero' do
expect {
described_class.normalise_tags!(
['normalise_invalid_section[1:aa-2:00]'],
with_sections: true
)
}.to raise_error(Tag::SectionLiteralParseError)
end
it 'parses open-ended section literals' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-]'],
with_sections: true
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
it 'parses omitted begin as zero' do
result = described_class.normalise_tags!(
['伊地知ニジカ[-1:00]'],
with_sections: true
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, 60_000]])
end
it 'parses fully open section literals as zero to end-of-video' do
result = described_class.normalise_tags!(
['伊地知ニジカ[-]'],
with_sections: true
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[0, nil]])
end
it 'expands zero-width sections to one millisecond' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-1:00]'],
with_sections: true
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 60_001]])
end
it 'swaps reversed section boundaries' do
result = described_class.normalise_tags!(
['伊地知ニジカ[2:00-1:00]'],
with_sections: true
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, 120_000]])
end
it 'merges open-ended sections over later bounded sections' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-][2:00-3:00]'],
with_sections: true
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
it 'merges adjacent bounded and open-ended sections' do
result = described_class.normalise_tags!(
['伊地知ニジカ[1:00-3:00][3:00-]'],
with_sections: true
)
tag = result.fetch(:tags).find { _1.name == '伊地知ニジカ' }
expect(result.fetch(:sections).fetch(tag.id)).to eq([[60_000, nil]])
end
end end
describe '.expand_parent_tags' do describe '.expand_parent_tags' do
+97
ファイルの表示
@@ -127,6 +127,22 @@ RSpec.describe 'Posts API', type: :request do
expect(all_tag_names).to include("spec_tag") expect(all_tag_names).to include("spec_tag")
end end
it 'keeps children and sections keys in non-detail tag responses' do
PostTagSection.create!(post: hit_post, tag:, begin_ms: 1_000, end_ms: nil)
get '/posts'
expect(response).to have_http_status(:ok)
hit_json = json.fetch('posts').find { |post| post['id'] == hit_post.id }
tag_json = hit_json.fetch('tags').find { |item| item['name'] == 'spec_tag' }
expect(tag_json.fetch('children')).to eq([])
expect(tag_json.fetch('sections')).to eq([
{ 'begin_ms' => 1_000, 'end_ms' => nil }
])
end
context "when q is provided" do context "when q is provided" do
it "filters posts by q (hit case)" do it "filters posts by q (hit case)" do
get "/posts", params: { tags: "spec_tag" } get "/posts", params: { tags: "spec_tag" }
@@ -767,6 +783,87 @@ RSpec.describe 'Posts API', type: :request do
expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent') expect(saved_names).not_to include('deprecated_parent', 'deprecated_grandparent')
end end
it 'returns validation error for an invalid section literal' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'invalid section literal',
url: 'https://example.com/invalid-section-literal',
tags: 'spec_tag[1:aa-2:00]',
thumbnail: dummy_upload
)
expect(response).to have_http_status(:unprocessable_entity)
expect(json).to include(
'type' => 'validation_error',
'message' => '入力内容を確認してください.'
)
expect(json.fetch('errors')).to include(
'tags' => ['タグ区間の記法が不正です.']
)
end
it 'saves open-ended sections with end_ms NULL' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'open ended section literal',
url: 'https://example.com/open-ended-section-literal',
tags: '伊地知ニジカ[1:00-]',
thumbnail: dummy_upload
)
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
section = PostTagSection.find_by!(post: created_post, tag:)
expect(section.begin_ms).to eq(60_000)
expect(section.end_ms).to be_nil
end
it 'treats [-] as [0:00-] and saves end_ms NULL' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'fully open section literal',
url: 'https://example.com/fully-open-section-literal',
tags: '伊地知ニジカ[-]',
thumbnail: dummy_upload
)
expect(response).to have_http_status(:created)
created_post = Post.find(json.fetch('id'))
tag = Tag.joins(:tag_name).find_by!(tag_names: { name: '伊地知ニジカ' })
section = PostTagSection.find_by!(post: created_post, tag:)
expect(section.begin_ms).to eq(0)
expect(section.end_ms).to be_nil
end
it 'returns end_ms null for open-ended sections in show response' do
sign_in_as(member)
post '/posts', params: post_write_params(
title: 'show open ended section literal',
url: 'https://example.com/show-open-ended-section-literal',
tags: '伊地知ニジカ[1:00-]',
thumbnail: dummy_upload
)
expect(response).to have_http_status(:created)
get "/posts/#{ json.fetch('id') }"
expect(response).to have_http_status(:ok)
tag_json = json.fetch('tags').find { |item| item['name'] == '伊地知ニジカ' }
expect(tag_json.fetch('sections')).to eq([
{ 'begin_ms' => 60_000, 'end_ms' => nil }
])
end
context "when nico tag already exists in tags" do context "when nico tag already exists in tags" do
before do before do
Tag.find_undiscard_or_create_by!( Tag.find_undiscard_or_create_by!(
+1 -1
ファイルの表示
@@ -33,7 +33,7 @@ const tagsToStr = (tags: TagWithSections[]): string => {
return [...(new Set (result.map (t => return [...(new Set (result.map (t =>
`${ t.name }${ t.sections `${ t.name }${ t.sections
.map (s => `[${ msToTime (s.beginMs) }-${ msToTime (s.endMs) }]`) .map (s => `[${ msToTime (s.beginMs) }-${ s.endMs == null ? '' : msToTime (s.endMs) }]`)
.join ('') }`)))].join (' ') .join ('') }`)))].join (' ')
} }
+1 -1
ファイルの表示
@@ -221,7 +221,7 @@ export type TagVersion = {
createdByUser: { id: number; name: string | null } | null } createdByUser: { id: number; name: string | null } | null }
export type TagWithSections = Tag & { sections: { beginMs: number export type TagWithSections = Tag & { sections: { beginMs: number
endMs: number }[] endMs: number | null }[]
children: TagWithSections[] } children: TagWithSections[] }
export type Theatre = { export type Theatre = {