#353 を再開できなかったので. Reviewed-on: #380 Co-authored-by: miteruzo <miteruzo@naver.com> Co-committed-by: miteruzo <miteruzo@naver.com>
このコミットはPull リクエスト #380 でマージされました.
このコミットが含まれているのは:
@@ -25,6 +25,7 @@ class PostVersionsController < ApplicationController
|
||||
SQL
|
||||
.select('post_versions.*', 'prev.title AS prev_title', 'prev.url AS prev_url',
|
||||
'prev.thumbnail_base AS prev_thumbnail_base', 'prev.tags AS prev_tags',
|
||||
'prev.video_ms AS prev_video_ms',
|
||||
'prev.original_created_from AS prev_original_created_from',
|
||||
'prev.original_created_before AS prev_original_created_before')
|
||||
q = q.where('post_versions.post_id = ?', post_id) if post_id
|
||||
@@ -74,6 +75,10 @@ class PostVersionsController < ApplicationController
|
||||
current: row.thumbnail_base,
|
||||
prev: row.attributes['prev_thumbnail_base']
|
||||
},
|
||||
video_ms: {
|
||||
current: row.video_ms,
|
||||
prev: row.attributes['prev_video_ms']
|
||||
},
|
||||
tags: build_version_tags(cur_tags, prev_tags),
|
||||
original_created_from: {
|
||||
current: row.original_created_from&.iso8601,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
class PostsController < ApplicationController
|
||||
Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)
|
||||
|
||||
class VideoMsParseError < ArgumentError
|
||||
;
|
||||
end
|
||||
|
||||
def index
|
||||
url = params[:url].presence
|
||||
title = params[:title].presence
|
||||
@@ -45,7 +49,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 +103,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 +118,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
|
||||
@@ -125,7 +135,7 @@ class PostsController < ApplicationController
|
||||
child_posts:,
|
||||
sibling_posts:,
|
||||
related:)
|
||||
.merge(tags: build_tag_tree_for(post.tags))
|
||||
.merge(tags: build_tag_tree_for(post))
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -149,11 +159,15 @@ class PostsController < ApplicationController
|
||||
ApplicationRecord.transaction do
|
||||
post.save!
|
||||
|
||||
tags = Tag.normalise_tags!(tag_names, deny_deprecated: true)
|
||||
Tag.normalise_tags!(tag_names, deny_deprecated: true, with_sections: true) =>
|
||||
{ tags:, sections: }
|
||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user)
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
sync_post_tags!(post, tags)
|
||||
post.video_ms = normalise_video_ms(tags)
|
||||
validate_video_sections!(post.video_ms, sections)
|
||||
post.save!
|
||||
sync_post_tags!(post, tags, sections)
|
||||
|
||||
sync_parent_posts!(post, parent_post_ids)
|
||||
|
||||
@@ -166,6 +180,10 @@ 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 VideoMsParseError
|
||||
render_validation_error fields: { video_ms: ['動画時間の記法が不正です.'] }
|
||||
rescue MiniMagick::Error
|
||||
render_validation_error fields: { thumbnail: ['サムネイル画像の変換に失敗しました.'] }
|
||||
rescue ArgumentError => e
|
||||
@@ -224,6 +242,8 @@ class PostsController < ApplicationController
|
||||
original_created_from:,
|
||||
original_created_before:,
|
||||
tag_names:,
|
||||
video_ms_param: params[:video_ms],
|
||||
duration_param: params[:duration],
|
||||
parent_post_ids:)
|
||||
|
||||
snapshot_to_apply =
|
||||
@@ -254,12 +274,16 @@ class PostsController < ApplicationController
|
||||
|
||||
post.reload
|
||||
json = PostRepr.base(post, current_user)
|
||||
json['tags'] = build_tag_tree_for(post.tags)
|
||||
json['tags'] = build_tag_tree_for(post)
|
||||
render json:, status: :ok
|
||||
rescue Tag::NicoTagNormalisationError
|
||||
render_validation_error fields: { tags: ['ニコニコ・タグは直接指定できません.'] }
|
||||
rescue Tag::DeprecatedTagNormalisationError
|
||||
render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags
|
||||
rescue Tag::SectionLiteralParseError
|
||||
render_validation_error fields: { tags: ['タグ区間の記法が不正です.'] }
|
||||
rescue VideoMsParseError
|
||||
render_validation_error fields: { video_ms: ['動画時間の記法が不正です.'] }
|
||||
rescue ArgumentError => e
|
||||
render_validation_error fields: { parent_post_ids: [e.message] }
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@@ -358,7 +382,7 @@ class PostsController < ApplicationController
|
||||
def tagged_post_ids_for(name) =
|
||||
Post.joins(tags: :tag_name).where(tag_names: { name: }).select(:id)
|
||||
|
||||
def sync_post_tags! post, desired_tags
|
||||
def sync_post_tags! post, desired_tags, sections
|
||||
desired_tags.each do |t|
|
||||
t.save! if t.new_record?
|
||||
end
|
||||
@@ -377,13 +401,21 @@ class PostsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
PostTagSection.where(post_id: post.id).destroy_all
|
||||
sections.each do |tag_id, ranges|
|
||||
ranges.each do |begin_ms, end_ms|
|
||||
PostTagSection.create!(post_id: post.id, tag_id:, begin_ms:, end_ms:)
|
||||
end
|
||||
end
|
||||
|
||||
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
|
||||
pt.discard_by!(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
def build_tag_tree_for tags
|
||||
tags = tags.reject(&:deprecated?).to_a
|
||||
def build_tag_tree_for post
|
||||
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)
|
||||
@@ -398,6 +430,9 @@ class PostsController < ApplicationController
|
||||
root_ids = tag_ids - child_ids
|
||||
|
||||
tags_by_id = tags.index_by(&: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 = { }
|
||||
|
||||
@@ -405,8 +440,10 @@ class PostsController < ApplicationController
|
||||
tag = tags_by_id[tag_id]
|
||||
return nil unless tag
|
||||
|
||||
sections = sections_by_tag_id.fetch(tag_id, [])
|
||||
|
||||
if path.include?(tag_id)
|
||||
return TagRepr.inline(tag).merge(children: [])
|
||||
return TagRepr.inline(tag).merge(children: [], sections:)
|
||||
end
|
||||
|
||||
if memo.key?(tag_id)
|
||||
@@ -418,7 +455,7 @@ class PostsController < ApplicationController
|
||||
|
||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
||||
|
||||
memo[tag_id] = TagRepr.inline(tag).merge(children:)
|
||||
memo[tag_id] = TagRepr.inline(tag).merge(children:, sections:)
|
||||
end
|
||||
|
||||
root_ids.filter_map { |id| build_node.call(id, []) }
|
||||
@@ -487,6 +524,7 @@ class PostsController < ApplicationController
|
||||
|
||||
def post_snapshot_from_version version
|
||||
{ title: version.title,
|
||||
video_ms: version.respond_to?(:video_ms) ? version.video_ms : nil,
|
||||
original_created_from: snapshot_time(version.original_created_from),
|
||||
original_created_before: snapshot_time(version.original_created_before),
|
||||
tag_names: editable_tag_names_from_version(version),
|
||||
@@ -499,6 +537,7 @@ class PostsController < ApplicationController
|
||||
|
||||
def post_snapshot_from_record post
|
||||
{ title: post.title,
|
||||
video_ms: post.video_ms,
|
||||
original_created_from: snapshot_time(post.original_created_from),
|
||||
original_created_before: snapshot_time(post.original_created_before),
|
||||
tag_names: editable_tag_names_from_post(post),
|
||||
@@ -506,16 +545,41 @@ class PostsController < ApplicationController
|
||||
end
|
||||
|
||||
def editable_tag_names_from_post post
|
||||
post.tags.not_nico.where(deprecated_at: nil)
|
||||
.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
post
|
||||
.post_tags
|
||||
.kept
|
||||
.joins(tag: :tag_name)
|
||||
.merge(Tag.not_nico)
|
||||
.merge(Tag.where(deprecated_at: nil))
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.order('tag_names.name')
|
||||
.map do |post_tag|
|
||||
name = post_tag.tag.tag_name.name
|
||||
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||
|
||||
next name if sections.empty?
|
||||
|
||||
"#{ name }#{ sections.map { Post.section_literal(_1) }.join }"
|
||||
end
|
||||
end
|
||||
|
||||
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
||||
tag_names:, parent_post_ids:
|
||||
tag_names:, video_ms_param:, duration_param:, parent_post_ids:
|
||||
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||
with_sections: true) =>
|
||||
{ tags:, sections: }
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
video_ms = normalise_video_ms(tags, video_ms_param:, duration_param:)
|
||||
validate_video_sections!(video_ms, sections)
|
||||
|
||||
{ title:,
|
||||
video_ms:,
|
||||
original_created_from: snapshot_time(original_created_from),
|
||||
original_created_before: snapshot_time(original_created_before),
|
||||
tag_names: incoming_tag_names_for_snapshot(tag_names),
|
||||
tag_names: tags.uniq(&:id).map { |tag|
|
||||
"#{ tag.name }#{ sections[tag.id].to_a.map { section_literal(_1) }.join }"
|
||||
}.sort,
|
||||
parent_post_ids: parent_post_ids.sort }
|
||||
end
|
||||
|
||||
@@ -538,11 +602,8 @@ class PostsController < ApplicationController
|
||||
value.to_s
|
||||
end
|
||||
|
||||
def incoming_tag_names_for_snapshot raw_tag_names
|
||||
tags = Tag.normalise_tags!(raw_tag_names, with_tagme: false,
|
||||
deny_deprecated: true)
|
||||
|
||||
Tag.expand_parent_tags(tags).reject(&:deprecated?).map(&:name).uniq.sort
|
||||
def section_literal section
|
||||
"[#{ 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:,
|
||||
@@ -563,6 +624,8 @@ class PostsController < ApplicationController
|
||||
def post_snapshot_changes base_snapshot, current_snapshot, incoming_snapshot
|
||||
[scalar_snapshot_change(:title, 'タイトル',
|
||||
base_snapshot, current_snapshot, incoming_snapshot),
|
||||
scalar_snapshot_change(:video_ms, '動画時間',
|
||||
base_snapshot, current_snapshot, incoming_snapshot),
|
||||
scalar_snapshot_change(:original_created_from, 'オリジナルの作成日時(以降)',
|
||||
base_snapshot, current_snapshot, incoming_snapshot),
|
||||
scalar_snapshot_change(:original_created_before, 'オリジナルの作成日時(より前)',
|
||||
@@ -626,11 +689,14 @@ class PostsController < ApplicationController
|
||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
||||
|
||||
post.update!(title: snapshot[:title],
|
||||
video_ms: snapshot[:video_ms],
|
||||
original_created_from: snapshot[:original_created_from],
|
||||
original_created_before: snapshot[:original_created_before])
|
||||
|
||||
editable_tags = Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false,
|
||||
deny_deprecated: true)
|
||||
Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false,
|
||||
deny_deprecated: true,
|
||||
with_sections: true) =>
|
||||
{ tags: editable_tags, sections: }
|
||||
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
|
||||
|
||||
readonly_tags = post.tags.nico.to_a
|
||||
@@ -638,14 +704,17 @@ class PostsController < ApplicationController
|
||||
tags = readonly_tags + editable_tags
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
|
||||
sync_post_tags!(post, tags)
|
||||
post.video_ms = tags.any? { _1.id == Tag.video.id } ? snapshot[:video_ms] : nil
|
||||
validate_video_sections!(post.video_ms, sections)
|
||||
post.save!
|
||||
sync_post_tags!(post, tags, sections)
|
||||
sync_parent_posts!(post, snapshot[:parent_post_ids])
|
||||
|
||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
|
||||
end
|
||||
|
||||
def merge_post_snapshots base_snapshot, current_snapshot, incoming_snapshot
|
||||
[:title, :original_created_from, :original_created_before].map {
|
||||
[:title, :video_ms, :original_created_from, :original_created_before].map {
|
||||
[_1, merge_scalar_snapshot_value(base_snapshot[_1],
|
||||
current_snapshot[_1],
|
||||
incoming_snapshot[_1])]
|
||||
@@ -689,4 +758,44 @@ class PostsController < ApplicationController
|
||||
render_validation_error record
|
||||
end
|
||||
end
|
||||
|
||||
def normalise_video_ms tags, video_ms_param: params[:video_ms], duration_param: params[:duration]
|
||||
return nil unless tags.any? { _1.id == Tag.video.id }
|
||||
|
||||
if video_ms_param.present?
|
||||
video_ms = Integer(video_ms_param, exception: false)
|
||||
raise VideoMsParseError unless video_ms&.positive?
|
||||
|
||||
return video_ms
|
||||
end
|
||||
|
||||
return nil if duration_param.blank?
|
||||
|
||||
video_ms = Tag.time_to_ms!(duration_param.to_s, tag_name: '動画時間')
|
||||
raise VideoMsParseError unless video_ms.positive?
|
||||
|
||||
video_ms
|
||||
rescue Tag::SectionLiteralParseError
|
||||
raise VideoMsParseError
|
||||
end
|
||||
|
||||
def validate_video_sections! video_ms, sections
|
||||
return unless video_ms
|
||||
|
||||
sections.each_value do |ranges|
|
||||
ranges.each do |begin_ms, end_ms|
|
||||
if begin_ms >= video_ms
|
||||
post = Post.new
|
||||
post.errors.add :video_ms, 'タグ区間の開始が動画時間以上です.'
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
|
||||
if end_ms && end_ms > video_ms
|
||||
post = Post.new
|
||||
post.errors.add :video_ms, 'タグ区間の終端が動画時間を超えてゐます.'
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,6 +60,7 @@ class Post < ApplicationRecord
|
||||
before_validation :normalise_url
|
||||
|
||||
validates :url, presence: true, uniqueness: true
|
||||
validates :video_ms, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
||||
|
||||
validate :validate_original_created_range
|
||||
validate :url_must_be_http_url
|
||||
@@ -83,7 +84,42 @@ class Post < ApplicationRecord
|
||||
super(options).merge(thumbnail: nil)
|
||||
end
|
||||
|
||||
def snapshot_tag_names = tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||
def snapshot_tag_names
|
||||
post_tags
|
||||
.kept
|
||||
.joins(tag: :tag_name)
|
||||
.includes(:sections, tag: :tag_name)
|
||||
.order('tag_names.name')
|
||||
.map do |post_tag|
|
||||
name = post_tag.tag.tag_name.name
|
||||
sections = post_tag.sections.sort_by(&:begin_ms)
|
||||
|
||||
next name if sections.empty?
|
||||
|
||||
"#{ name }#{ sections.map { Post.section_literal(_1) }.join }"
|
||||
end
|
||||
end
|
||||
|
||||
def self.section_literal section
|
||||
"[#{ Post.ms_to_time(section.begin_ms) }-#{ section.end_ms ? Post.ms_to_time(section.end_ms) : '' }]"
|
||||
end
|
||||
|
||||
def self.ms_to_time ms
|
||||
total_s = ms / 1_000
|
||||
s = total_s % 60
|
||||
min = (total_s / 60) % 60
|
||||
h = total_s / 3_600
|
||||
remainder_ms = ms % 1_000
|
||||
|
||||
base =
|
||||
if h.positive?
|
||||
'%d:%02d:%02d' % [h, min, s]
|
||||
else
|
||||
'%d:%02d' % [min, s]
|
||||
end
|
||||
|
||||
remainder_ms.positive? ? "#{ base }.#{ remainder_ms.to_s.rjust(3, '0') }" : base
|
||||
end
|
||||
|
||||
def snapshot_parent_post_ids = parents.order(:id).pluck(:id)
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ class PostTag < ApplicationRecord
|
||||
belongs_to :created_user, class_name: 'User', optional: true
|
||||
belongs_to :deleted_user, class_name: 'User', optional: true
|
||||
|
||||
has_many :sections, -> { order(:begin_ms) }, class_name: 'PostTagSection',
|
||||
foreign_key: [:post_id, :tag_id],
|
||||
primary_key: [:post_id, :tag_id],
|
||||
dependent: :delete_all,
|
||||
inverse_of: :post_tag
|
||||
|
||||
validates :post_id, presence: true
|
||||
validates :tag_id, presence: true
|
||||
validates :post_id, uniqueness: {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
class PostTagSection < ApplicationRecord
|
||||
self.primary_key = :post_id, :tag_id, :begin_ms
|
||||
|
||||
belongs_to :post
|
||||
belongs_to :tag
|
||||
|
||||
belongs_to :post_tag, -> { kept }, foreign_key: [:post_id, :tag_id],
|
||||
primary_key: [:post_id, :tag_id],
|
||||
inverse_of: :sections,
|
||||
optional: true
|
||||
|
||||
validates :post_id, presence: true
|
||||
validates :tag_id, presence: true
|
||||
|
||||
validates :begin_ms, presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
||||
validates :end_ms, numericality: { only_integer: true, greater_than: :begin_ms },
|
||||
allow_nil: true
|
||||
end
|
||||
@@ -5,6 +5,7 @@ class PostVersion < ApplicationRecord
|
||||
belongs_to :parent, class_name: 'Post', optional: true
|
||||
|
||||
validates :url, presence: true
|
||||
validates :video_ms, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
||||
|
||||
validate :validate_original_created_range
|
||||
|
||||
|
||||
+112
-3
@@ -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'
|
||||
@@ -105,26 +115,60 @@ class Tag < ApplicationRecord
|
||||
def self.normalise_tags! tag_names, with_tagme: true,
|
||||
with_no_deerjikist: true,
|
||||
deny_nico: true,
|
||||
deny_deprecated: false
|
||||
deny_deprecated: false,
|
||||
with_sections: false
|
||||
if deny_nico && tag_names.any? { |n| n.downcase.start_with?('nico:') }
|
||||
raise NicoTagNormalisationError
|
||||
end
|
||||
|
||||
sections = { }
|
||||
tags = tag_names.map do |name|
|
||||
raw_name = name
|
||||
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil]
|
||||
name = TagName.canonicalise(name.sub(/\A#{ pf }/i, '')).first
|
||||
|
||||
name = name.sub(/\A#{ pf }/i, '')
|
||||
|
||||
sections_by_tag = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
sections_by_tag << normalise_section_range!(
|
||||
begin_raw: match[2],
|
||||
end_raw: match[3],
|
||||
tag_name: name)
|
||||
end
|
||||
|
||||
if name.include?('[') || name.include?(']')
|
||||
raise SectionLiteralParseError.new(raw_name, raw_name)
|
||||
end
|
||||
|
||||
name = TagName.canonicalise(name).first
|
||||
|
||||
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
|
||||
if deny_deprecated && tag.deprecated?
|
||||
raise DeprecatedTagNormalisationError, [tag.name]
|
||||
end
|
||||
|
||||
tag.update!(category: cat) if cat && tag.category != cat
|
||||
next if sections_by_tag.empty?
|
||||
|
||||
sections[tag.id] ||= []
|
||||
sections[tag.id].concat(sections_by_tag)
|
||||
sections[tag.id] = merge_section_ranges(sections[tag.id])
|
||||
sections.delete(tag.id) if sections[tag.id] == [[0, nil]]
|
||||
end
|
||||
end
|
||||
|
||||
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||
tags << Tag.no_deerjikist if with_no_deerjikist && tags.all? { |t| !(t.deerjikist?) }
|
||||
tags.uniq(&:id)
|
||||
tags.uniq!(&:id)
|
||||
|
||||
if with_sections
|
||||
{ tags:, sections: }
|
||||
else
|
||||
tags
|
||||
end
|
||||
end
|
||||
|
||||
def self.expand_parent_tags tags
|
||||
@@ -150,6 +194,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?
|
||||
@@ -246,6 +329,32 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
raise SectionLiteralParseError.new(tag_name, str) unless match
|
||||
|
||||
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]
|
||||
|
||||
total_s * 1_000 + match[:ms].to_s.ljust(3, '0')[0, 3].to_i
|
||||
end
|
||||
|
||||
def nico_tags_cannot_be_deprecated
|
||||
if nico? && deprecated_at.present?
|
||||
errors.add :deprecated_at, 'ニコタグは廃止できません.'
|
||||
|
||||
@@ -8,6 +8,7 @@ module PostRepr
|
||||
:url,
|
||||
:title,
|
||||
:thumbnail_base,
|
||||
:video_ms,
|
||||
:original_created_from,
|
||||
:original_created_before,
|
||||
:created_at,
|
||||
@@ -18,7 +19,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 +53,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
|
||||
|
||||
@@ -23,6 +23,7 @@ class PostVersionRecorder < VersionRecorder
|
||||
{ title: @record.title,
|
||||
url: @record.url,
|
||||
thumbnail_base: @record.thumbnail_base,
|
||||
video_ms: @record.video_ms,
|
||||
tags: @record.snapshot_tag_names.join(' '),
|
||||
parent_post_ids: @record.snapshot_parent_post_ids.join(' '),
|
||||
original_created_from: @record.original_created_from,
|
||||
|
||||
新しい課題から参照
ユーザをブロックする