このコミットが含まれているのは:
@@ -45,7 +45,9 @@ class PostsController < ApplicationController
|
||||
.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"))
|
||||
.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
|
||||
|
||||
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
||||
@@ -97,7 +99,9 @@ class PostsController < ApplicationController
|
||||
|
||||
def random
|
||||
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
|
||||
.order('RAND()')
|
||||
.first
|
||||
@@ -110,7 +114,9 @@ class PostsController < ApplicationController
|
||||
post =
|
||||
Post
|
||||
.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
|
||||
.find_by(id: params[:id])
|
||||
return head :not_found unless post
|
||||
@@ -168,6 +174,8 @@ class PostsController < ApplicationController
|
||||
render_validation_error fields: { tags: 'ニコニコ・タグは直接指定できません.' }
|
||||
rescue Tag::DeprecatedTagNormalisationError
|
||||
render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags
|
||||
rescue Tag::SectionLiteralParseError
|
||||
render_validation_error fields: { tags: ['タグ区間の記法が不正です.'] }
|
||||
rescue ArgumentError => e
|
||||
render_validation_error fields: { parent_post_ids: [e.message] }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@@ -260,6 +268,8 @@ class PostsController < ApplicationController
|
||||
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
|
||||
rescue Tag::DeprecatedTagNormalisationError
|
||||
render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags
|
||||
rescue Tag::SectionLiteralParseError
|
||||
render_validation_error fields: { tags: ['タグ区間の記法が不正です.'] }
|
||||
rescue ArgumentError => e
|
||||
render_validation_error fields: { parent_post_ids: [e.message] }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@@ -390,7 +400,8 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
tags_by_id = tags.index_by(&:id)
|
||||
sections_by_tag_id =
|
||||
PostTagSection
|
||||
.where(post_id: post.id, tag_id: tag_ids)
|
||||
.order(:begin_ms)
|
||||
.group_by(&:tag_id)
|
||||
sections_by_tag_id = post_tags.to_h { |post_tag|
|
||||
[post_tag.tag_id, post_tag.sections.as_json(only: [:begin_ms, :end_ms])]
|
||||
}
|
||||
|
||||
memo = { }
|
||||
|
||||
@@ -418,7 +427,6 @@ class PostsController < ApplicationController
|
||||
return nil unless tag
|
||||
|
||||
sections = sections_by_tag_id.fetch(tag_id, [])
|
||||
.as_json(only: [:begin_ms, :end_ms])
|
||||
|
||||
if path.include?(tag_id)
|
||||
return TagRepr.inline(tag).merge(children: [], sections:)
|
||||
@@ -578,7 +586,7 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def post_conflict_json post:, base_version_no:, base_snapshot:,
|
||||
|
||||
@@ -86,7 +86,7 @@ class Post < ApplicationRecord
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def self.ms_to_time ms
|
||||
|
||||
@@ -15,6 +15,6 @@ class PostTagSection < ApplicationRecord
|
||||
validates :begin_ms, presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
||||
validates :end_ms, presence: true,
|
||||
numericality: { only_integer: true, greater_than: :begin_ms }
|
||||
validates :end_ms, numericality: { only_integer: true, greater_than: :begin_ms },
|
||||
allow_nil: true
|
||||
end
|
||||
|
||||
+81
-22
@@ -17,6 +17,16 @@ class Tag < ApplicationRecord
|
||||
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 :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag
|
||||
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
|
||||
@@ -113,20 +123,22 @@ class Tag < ApplicationRecord
|
||||
|
||||
sections = { }
|
||||
tags = tag_names.map do |name|
|
||||
raw_name = name
|
||||
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil]
|
||||
|
||||
name = name.sub(/\A#{ pf }/i, '')
|
||||
|
||||
sections_by_tag = []
|
||||
while n = name.sub!(/^(\S*?)\[([0-9:.]*?)-([0-9:.]*?)\](\S*?)$/, '\1\4 \2 \3')
|
||||
name, *section_raw = n.split
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
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) }
|
||||
next if begin_ms == end_ms
|
||||
|
||||
begin_ms, end_ms = end_ms, begin_ms if begin_ms > end_ms
|
||||
|
||||
sections_by_tag << [begin_ms, end_ms]
|
||||
if name.include?('[') || name.include?(']')
|
||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
name = TagName.canonicalise(name).first
|
||||
@@ -141,6 +153,7 @@ class Tag < ApplicationRecord
|
||||
|
||||
sections[tag.id] ||= []
|
||||
sections[tag.id].concat(sections_by_tag)
|
||||
sections[tag.id] = merge_section_ranges(sections[tag.id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -178,6 +191,45 @@ class Tag < ApplicationRecord
|
||||
(result + tags).uniq { |t| t.id }
|
||||
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:
|
||||
tn = TagName.find_undiscard_or_create_by!(name: name.to_s.strip)
|
||||
tn = tn.canonical if tn.canonical_id?
|
||||
@@ -274,23 +326,30 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def self.time_to_ms str
|
||||
parts = str.split(':')
|
||||
def self.time_to_ms! str, tag_name:
|
||||
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
|
||||
s, ms = s_part.split('.')
|
||||
raise SectionLiteralParseError.new(tag_name, str) unless match
|
||||
|
||||
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 += 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
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
def nico_tags_cannot_be_deprecated
|
||||
|
||||
@@ -18,7 +18,7 @@ module PostRepr
|
||||
|
||||
def base post, current_user = nil
|
||||
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['viewed'] = current_user ? current_user.viewed?(post) : false
|
||||
json
|
||||
@@ -52,8 +52,16 @@ module PostRepr
|
||||
.merge('thumbnail' => thumbnail_url(post))
|
||||
end
|
||||
|
||||
def tag_json tags
|
||||
tags.reject(&:deprecated?).map { |tag| TagRepr.inline(tag) }
|
||||
def tag_json post
|
||||
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
|
||||
|
||||
def thumbnail_url post
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ class CreatePostTagSections < ActiveRecord::Migration[8.0]
|
||||
t.references :post, 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 :end_ms, null: false
|
||||
t.integer :end_ms, null: true
|
||||
t.timestamps
|
||||
|
||||
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',
|
||||
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'
|
||||
end
|
||||
end
|
||||
生成ファイル
+3
-4
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# 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|
|
||||
t.string "name", 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 "tag_id", 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 "updated_at", null: false
|
||||
t.index ["post_id", "begin_ms"], name: "idx_post_tag_sections_post_id_begin_ms"
|
||||
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"
|
||||
end
|
||||
|
||||
@@ -432,7 +432,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_06_21_000000) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
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"], name: "index_theatre_watching_users_on_theatre_id"
|
||||
t.index ["user_id"], name: "index_theatre_watching_users_on_user_id"
|
||||
|
||||
@@ -25,5 +25,17 @@ RSpec.describe PostTag, type: :model do
|
||||
|
||||
expect(post_tag.sections).to be_empty
|
||||
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
|
||||
|
||||
@@ -19,6 +19,85 @@ RSpec.describe Tag, type: :model do
|
||||
expect(error.tag_names).to eq([deprecated_tag.name])
|
||||
}
|
||||
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
|
||||
|
||||
describe '.expand_parent_tags' do
|
||||
|
||||
@@ -127,6 +127,22 @@ RSpec.describe 'Posts API', type: :request do
|
||||
expect(all_tag_names).to include("spec_tag")
|
||||
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
|
||||
it "filters posts by q (hit case)" do
|
||||
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')
|
||||
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
|
||||
before do
|
||||
Tag.find_undiscard_or_create_by!(
|
||||
|
||||
@@ -33,7 +33,7 @@ const tagsToStr = (tags: TagWithSections[]): string => {
|
||||
|
||||
return [...(new Set (result.map (t =>
|
||||
`${ 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 (' ')
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ export type TagVersion = {
|
||||
createdByUser: { id: number; name: string | null } | null }
|
||||
|
||||
export type TagWithSections = Tag & { sections: { beginMs: number
|
||||
endMs: number }[]
|
||||
endMs: number | null }[]
|
||||
children: TagWithSections[] }
|
||||
|
||||
export type Theatre = {
|
||||
|
||||
新しい課題から参照
ユーザをブロックする