コミットを比較
5 コミット
main
...
8c89984ac8
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 8c89984ac8 | |||
| beb9e11aeb | |||
| 5145db250d | |||
| adb354db12 | |||
| be2ad31d2c |
@@ -1,9 +1,13 @@
|
|||||||
class NicoTagsController < ApplicationController
|
class NicoTagsController < ApplicationController
|
||||||
|
TAG_JSON = { only: [:id, :category, :post_count], methods: [:name, :has_wiki] }.freeze
|
||||||
|
|
||||||
def index
|
def index
|
||||||
limit = (params[:limit] || 20).to_i
|
limit = (params[:limit] || 20).to_i
|
||||||
cursor = params[:cursor].presence
|
cursor = params[:cursor].presence
|
||||||
|
|
||||||
q = Tag.nico_tags.includes(:linked_tags).order(updated_at: :desc)
|
q = Tag.nico_tags
|
||||||
|
.includes(:tag_name, linked_tags: :tag_name)
|
||||||
|
.order(updated_at: :desc)
|
||||||
q = q.where('tags.updated_at < ?', Time.iso8601(cursor)) if cursor
|
q = q.where('tags.updated_at < ?', Time.iso8601(cursor)) if cursor
|
||||||
|
|
||||||
tags = q.limit(limit + 1)
|
tags = q.limit(limit + 1)
|
||||||
@@ -15,7 +19,9 @@ class NicoTagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: { tags: tags.map { |tag|
|
render json: { tags: tags.map { |tag|
|
||||||
tag.as_json(include: :linked_tags)
|
tag.as_json(TAG_JSON).merge(linked_tags: tag.linked_tags.map { |lt|
|
||||||
|
lt.as_json(TAG_JSON)
|
||||||
|
})
|
||||||
}, next_cursor: }
|
}, next_cursor: }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,12 +36,11 @@ class NicoTagsController < ApplicationController
|
|||||||
|
|
||||||
linked_tag_names = params[:tags].to_s.split(' ')
|
linked_tag_names = params[:tags].to_s.split(' ')
|
||||||
linked_tags = Tag.normalise_tags(linked_tag_names, with_tagme: false)
|
linked_tags = Tag.normalise_tags(linked_tag_names, with_tagme: false)
|
||||||
return head :bad_request if linked_tags.filter { |t| t.category == 'nico' }.present?
|
return head :bad_request if linked_tags.any? { |t| t.category == 'nico' }
|
||||||
|
|
||||||
tag.linked_tags = linked_tags
|
tag.linked_tags = linked_tags
|
||||||
tag.updated_at = Time.now
|
|
||||||
tag.save!
|
tag.save!
|
||||||
|
|
||||||
render json: tag.linked_tags, status: :ok
|
render json: tag.linked_tags.map { |t| t.as_json(TAG_JSON) }, status: :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
class PostsController < ApplicationController
|
class PostsController < ApplicationController
|
||||||
Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)
|
Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)
|
||||||
|
|
||||||
# GET /posts
|
|
||||||
def index
|
def index
|
||||||
page = (params[:page].presence || 1).to_i
|
page = (params[:page].presence || 1).to_i
|
||||||
limit = (params[:limit].presence || 20).to_i
|
limit = (params[:limit].presence || 20).to_i
|
||||||
@@ -18,7 +17,7 @@ class PostsController < ApplicationController
|
|||||||
'posts.created_at)'
|
'posts.created_at)'
|
||||||
q =
|
q =
|
||||||
filtered_posts
|
filtered_posts
|
||||||
.preload(:tags)
|
.preload(tags: :tag_name)
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
.select("posts.*, #{ sort_sql } AS sort_ts")
|
.select("posts.*, #{ sort_sql } AS sort_ts")
|
||||||
.order(Arel.sql("#{ sort_sql } DESC"))
|
.order(Arel.sql("#{ sort_sql } DESC"))
|
||||||
@@ -36,7 +35,8 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: { posts: posts.map { |post|
|
render json: { posts: posts.map { |post|
|
||||||
post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }).tap do |json|
|
post.as_json(include: { tags: { only: [:id, :category, :post_count],
|
||||||
|
methods: [:name] } }).tap do |json|
|
||||||
json['thumbnail'] =
|
json['thumbnail'] =
|
||||||
if post.thumbnail.attached?
|
if post.thumbnail.attached?
|
||||||
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
||||||
@@ -48,19 +48,19 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def random
|
def random
|
||||||
post = filtered_posts.order('RAND()').first
|
post = filtered_posts.preload(tags: :tag_name).order('RAND()').first
|
||||||
return head :not_found unless post
|
return head :not_found unless post
|
||||||
|
|
||||||
viewed = current_user&.viewed?(post) || false
|
viewed = current_user&.viewed?(post) || false
|
||||||
|
|
||||||
render json: (post
|
render json: (post
|
||||||
.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
|
.as_json(include: { tags: { only: [:id, :category, :post_count],
|
||||||
|
methods: [:name] } })
|
||||||
.merge(viewed:))
|
.merge(viewed:))
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /posts/1
|
|
||||||
def show
|
def show
|
||||||
post = Post.includes(:tags).find(params[:id])
|
post = Post.includes(tags: :tag_name).find(params[:id])
|
||||||
return head :not_found unless post
|
return head :not_found unless post
|
||||||
|
|
||||||
viewed = current_user&.viewed?(post) || false
|
viewed = current_user&.viewed?(post) || false
|
||||||
@@ -73,7 +73,6 @@ class PostsController < ApplicationController
|
|||||||
render json:
|
render json:
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /posts
|
|
||||||
def create
|
def create
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.member?
|
return head :forbidden unless current_user.member?
|
||||||
@@ -96,7 +95,8 @@ class PostsController < ApplicationController
|
|||||||
tags = Tag.normalise_tags(tag_names)
|
tags = Tag.normalise_tags(tag_names)
|
||||||
tags = Tag.expand_parent_tags(tags)
|
tags = Tag.expand_parent_tags(tags)
|
||||||
sync_post_tags!(post, tags)
|
sync_post_tags!(post, tags)
|
||||||
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
|
render json: post.as_json(include: { tags: { only: [:id, :category, :post_count],
|
||||||
|
methods: [:name] } }),
|
||||||
status: :created
|
status: :created
|
||||||
else
|
else
|
||||||
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
|
||||||
@@ -117,7 +117,6 @@ class PostsController < ApplicationController
|
|||||||
head :no_content
|
head :no_content
|
||||||
end
|
end
|
||||||
|
|
||||||
# PATCH/PUT /posts/1
|
|
||||||
def update
|
def update
|
||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.member?
|
return head :forbidden unless current_user.member?
|
||||||
@@ -153,13 +152,13 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
pts = PostTag.with_discarded
|
pts = PostTag.with_discarded
|
||||||
pts = pts.where(post_id: id) if id.present?
|
pts = pts.where(post_id: id) if id.present?
|
||||||
pts = pts.includes(:post, :tag, :created_user, :deleted_user)
|
pts = pts.includes(:post, { tag: :tag_name }, :created_user, :deleted_user)
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
pts.each do |pt|
|
pts.each do |pt|
|
||||||
events << Event.new(
|
events << Event.new(
|
||||||
post: pt.post,
|
post: pt.post,
|
||||||
tag: pt.tag,
|
tag: pt.tag.as_json(only: [:id, :category], methods: [:name]),
|
||||||
user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
|
user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
|
||||||
change_type: 'add',
|
change_type: 'add',
|
||||||
timestamp: pt.created_at)
|
timestamp: pt.created_at)
|
||||||
@@ -167,7 +166,7 @@ class PostsController < ApplicationController
|
|||||||
if pt.discarded_at
|
if pt.discarded_at
|
||||||
events << Event.new(
|
events << Event.new(
|
||||||
post: pt.post,
|
post: pt.post,
|
||||||
tag: pt.tag,
|
tag: pt.tag.as_json(only: [:id, :category], methods: [:name]),
|
||||||
user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
|
user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
|
||||||
change_type: 'remove',
|
change_type: 'remove',
|
||||||
timestamp: pt.discarded_at)
|
timestamp: pt.discarded_at)
|
||||||
@@ -192,16 +191,16 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_posts_by_tags tag_names, match_type
|
def filter_posts_by_tags tag_names, match_type
|
||||||
posts = Post.joins(:tags)
|
posts = Post.joins(tags: :tag_name)
|
||||||
|
|
||||||
if match_type == 'any'
|
if match_type == 'any'
|
||||||
posts = posts.where(tags: { name: tag_names }).distinct
|
posts.where(tag_names: { name: tag_names }).distinct
|
||||||
else
|
else
|
||||||
tag_names.each do |tag|
|
posts.where(tag_names: { name: tag_names })
|
||||||
posts = posts.where(id: Post.joins(:tags).where(tags: { name: tag }))
|
.group('posts.id')
|
||||||
|
.having('COUNT(DISTINCT tag_names.id) = ?', tag_names.uniq.size)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
posts.distinct
|
|
||||||
end
|
|
||||||
|
|
||||||
def sync_post_tags! post, desired_tags
|
def sync_post_tags! post, desired_tags
|
||||||
desired_tags.each do |t|
|
desired_tags.each do |t|
|
||||||
@@ -251,7 +250,8 @@ class PostsController < ApplicationController
|
|||||||
return nil unless tag
|
return nil unless tag
|
||||||
|
|
||||||
if path.include?(tag_id)
|
if path.include?(tag_id)
|
||||||
return tag.as_json(only: [:id, :name, :category, :post_count]).merge(children: [])
|
return tag.as_json(only: [:id, :category, :post_count],
|
||||||
|
methods: [:name]).merge(children: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
if memo.key?(tag_id)
|
if memo.key?(tag_id)
|
||||||
@@ -263,7 +263,8 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
||||||
|
|
||||||
memo[tag_id] = tag.as_json(only: [:id, :name, :category, :post_count]).merge(children:)
|
memo[tag_id] = tag.as_json(only: [:id, :category, :post_count],
|
||||||
|
methods: [:name]).merge(children:)
|
||||||
end
|
end
|
||||||
|
|
||||||
root_ids.filter_map { |id| build_node.call(id, []) }
|
root_ids.filter_map { |id| build_node.call(id, []) }
|
||||||
|
|||||||
@@ -1,35 +1,41 @@
|
|||||||
class TagsController < ApplicationController
|
class TagsController < ApplicationController
|
||||||
def index
|
def index
|
||||||
post_id = params[:post]
|
post_id = params[:post]
|
||||||
tags = if post_id.present?
|
|
||||||
|
tags =
|
||||||
|
if post_id.present?
|
||||||
Tag.joins(:posts).where(posts: { id: post_id })
|
Tag.joins(:posts).where(posts: { id: post_id })
|
||||||
else
|
else
|
||||||
Tag.all
|
Tag.all
|
||||||
end
|
end
|
||||||
render json: tags
|
|
||||||
|
render json: tags.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
||||||
end
|
end
|
||||||
|
|
||||||
def autocomplete
|
def autocomplete
|
||||||
q = params[:q].to_s.strip
|
q = params[:q].to_s.strip
|
||||||
return render json: [] if q.blank?
|
return render json: [] if q.blank?
|
||||||
|
|
||||||
tags = (Tag
|
tags = (Tag.joins(:tag_name).includes(:tag_name)
|
||||||
.where('(category = ? AND name LIKE ?) OR name LIKE ?',
|
.where('(tags.category = ? AND tag_names.name LIKE ?) OR tag_names.name LIKE ?',
|
||||||
'nico', "nico:#{ q }%", "#{ q }%")
|
'nico', "nico:#{ q }%", "#{ q }%")
|
||||||
.order('post_count DESC, name ASC')
|
.order(Arel.sql('post_count DESC, tag_names.name ASC'))
|
||||||
.limit(20))
|
.limit(20))
|
||||||
render json: tags
|
render json: tags.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
tag = Tag.find(params[:id])
|
tag = Tag.find_by(id: params[:id])
|
||||||
render json: tag
|
render json: tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_by_name
|
def show_by_name
|
||||||
tag = Tag.find_by(name: params[:name])
|
name = params[:name].to_s.strip
|
||||||
|
return head :bad_request if name.blank?
|
||||||
|
|
||||||
|
tag = Tag.joins(:tag_name).includes(:tag_name).find_by(tag_names: { name: })
|
||||||
if tag
|
if tag
|
||||||
render json: tag
|
render json: tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
||||||
else
|
else
|
||||||
head :not_found
|
head :not_found
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,19 +2,25 @@ class WikiPagesController < ApplicationController
|
|||||||
rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict
|
rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: WikiPage.all
|
pages = WikiPage.all
|
||||||
|
render json: pages.as_json(methods: [:title])
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render_wiki_page_or_404 WikiPage.find(params[:id])
|
page = WikiPage.find_by(id: params[:id])
|
||||||
|
render_wiki_page_or_404 page
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_by_title
|
def show_by_title
|
||||||
render_wiki_page_or_404 WikiPage.find_by(title: params[:title])
|
title = params[:title].to_s.strip
|
||||||
|
page = WikiPage.joins(:tag_name)
|
||||||
|
.includes(:tag_name)
|
||||||
|
.find_by(tag_names: { name: title })
|
||||||
|
render_wiki_page_or_404 page
|
||||||
end
|
end
|
||||||
|
|
||||||
def exists
|
def exists
|
||||||
if WikiPage.exists?(params[:id])
|
if WikiPage.exists?(id: params[:id])
|
||||||
head :no_content
|
head :no_content
|
||||||
else
|
else
|
||||||
head :not_found
|
head :not_found
|
||||||
@@ -22,7 +28,8 @@ class WikiPagesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def exists_by_title
|
def exists_by_title
|
||||||
if WikiPage.exists?(title: params[:title])
|
title = params[:title].to_s.strip
|
||||||
|
if WikiPage.joins(:tag_name).exists?(tag_names: { name: title })
|
||||||
head :no_content
|
head :no_content
|
||||||
else
|
else
|
||||||
head :not_found
|
head :not_found
|
||||||
@@ -81,7 +88,7 @@ class WikiPagesController < ApplicationController
|
|||||||
message = params[:message].presence
|
message = params[:message].presence
|
||||||
Wiki::Commit.content!(page:, body:, created_user: current_user, message:)
|
Wiki::Commit.content!(page:, body:, created_user: current_user, message:)
|
||||||
|
|
||||||
render json: page, status: :created
|
render json: page.as_json(methods: [:title]), status: :created
|
||||||
else
|
else
|
||||||
render json: { errors: page.errors.full_messages },
|
render json: { errors: page.errors.full_messages },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
@@ -115,14 +122,14 @@ class WikiPagesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
title = params[:title]&.strip
|
title = params[:title].to_s.strip
|
||||||
|
|
||||||
q = WikiPage.all
|
q = WikiPage.joins(:tag_name).includes(:tag_name)
|
||||||
if title.present?
|
if title.present?
|
||||||
q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
|
q = q.where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: q.limit(20)
|
render json: q.limit(20).as_json(methods: [:title])
|
||||||
end
|
end
|
||||||
|
|
||||||
def changes
|
def changes
|
||||||
@@ -162,12 +169,14 @@ class WikiPagesController < ApplicationController
|
|||||||
pred = page.pred_revision_id(revision_id)
|
pred = page.pred_revision_id(revision_id)
|
||||||
succ = page.succ_revision_id(revision_id)
|
succ = page.succ_revision_id(revision_id)
|
||||||
|
|
||||||
return render json: page.as_json.merge(body:, revision_id:, pred:, succ:)
|
return render json: page.as_json(methods: [:title])
|
||||||
|
.merge(body:, revision_id:, pred:, succ:)
|
||||||
end
|
end
|
||||||
|
|
||||||
rev = page.current_revision
|
rev = page.current_revision
|
||||||
unless rev
|
unless rev
|
||||||
return render json: page.as_json.merge(body: nil, revision_id: nil, pred: nil, succ: nil)
|
return render json: page.as_json(methods: [:title])
|
||||||
|
.merge(body: nil, revision_id: nil, pred: nil, succ: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
if rev.redirect?
|
if rev.redirect?
|
||||||
@@ -181,7 +190,7 @@ class WikiPagesController < ApplicationController
|
|||||||
pred = page.pred_revision_id(revision_id)
|
pred = page.pred_revision_id(revision_id)
|
||||||
succ = page.succ_revision_id(revision_id)
|
succ = page.succ_revision_id(revision_id)
|
||||||
|
|
||||||
render json: page.as_json.merge(body:, revision_id:, pred:, succ:)
|
render json: page.as_json(methods: [:title]).merge(body:, revision_id:, pred:, succ:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_wiki_conflict err
|
def render_wiki_conflict err
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
class PostTag < ApplicationRecord
|
class PostTag < ApplicationRecord
|
||||||
include Discard::Model
|
include Discard::Model
|
||||||
|
|
||||||
|
before_destroy do
|
||||||
|
raise ActiveRecord::ReadOnlyRecord, '消さないでください.'
|
||||||
|
end
|
||||||
|
|
||||||
belongs_to :post
|
belongs_to :post
|
||||||
belongs_to :tag, counter_cache: :post_count
|
belongs_to :tag, counter_cache: :post_count
|
||||||
belongs_to :created_user, class_name: 'User', optional: true
|
belongs_to :created_user, class_name: 'User', optional: true
|
||||||
|
|||||||
+28
-13
@@ -21,6 +21,10 @@ class Tag < ApplicationRecord
|
|||||||
dependent: :destroy
|
dependent: :destroy
|
||||||
has_many :parents, through: :reversed_tag_implications, source: :parent_tag
|
has_many :parents, through: :reversed_tag_implications, source: :parent_tag
|
||||||
|
|
||||||
|
belongs_to :tag_name
|
||||||
|
delegate :name, to: :tag_name, allow_nil: true
|
||||||
|
validates :tag_name, presence: true
|
||||||
|
|
||||||
enum :category, { deerjikist: 'deerjikist',
|
enum :category, { deerjikist: 'deerjikist',
|
||||||
meme: 'meme',
|
meme: 'meme',
|
||||||
character: 'character',
|
character: 'character',
|
||||||
@@ -29,7 +33,6 @@ class Tag < ApplicationRecord
|
|||||||
nico: 'nico',
|
nico: 'nico',
|
||||||
meta: 'meta' }
|
meta: 'meta' }
|
||||||
|
|
||||||
validates :name, presence: true, length: { maximum: 255 }
|
|
||||||
validates :category, presence: true, inclusion: { in: Tag.categories.keys }
|
validates :category, presence: true, inclusion: { in: Tag.categories.keys }
|
||||||
|
|
||||||
validate :nico_tag_name_must_start_with_nico
|
validate :nico_tag_name_must_start_with_nico
|
||||||
@@ -44,31 +47,35 @@ class Tag < ApplicationRecord
|
|||||||
'mtr:' => 'material',
|
'mtr:' => 'material',
|
||||||
'meta:' => 'meta' }.freeze
|
'meta:' => 'meta' }.freeze
|
||||||
|
|
||||||
def self.tagme
|
def name= val
|
||||||
@tagme ||= Tag.find_or_create_by!(name: 'タグ希望') do |tag|
|
(self.tag_name ||= build_tag_name).name = val
|
||||||
tag.category = 'meta'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_wiki
|
||||||
|
tag_name&.wiki_page.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.tagme
|
||||||
|
@tagme ||= find_or_create_by_tag_name!('タグ希望', category: 'meta')
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.bot
|
def self.bot
|
||||||
@bot ||= Tag.find_or_create_by!(name: 'bot操作') do |tag|
|
@bot ||= find_or_create_by_tag_name!('bot操作', category: 'meta')
|
||||||
tag.category = 'meta'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.normalise_tags tag_names, with_tagme: true
|
def self.normalise_tags tag_names, with_tagme: true
|
||||||
tags = tag_names.map do |name|
|
tags = tag_names.map do |name|
|
||||||
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil]
|
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil]
|
||||||
name.delete_prefix!(pf)
|
name.delete_prefix!(pf)
|
||||||
Tag.find_or_initialize_by(name:).tap do |tag|
|
find_or_create_by_tag_name!(name, category: (cat || 'general')).tap do |tag|
|
||||||
if cat && tag.category != cat
|
if cat && tag.category != cat
|
||||||
tag.category = cat
|
tag.update!(category: cat)
|
||||||
tag.save!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||||
tags.uniq
|
tags.uniq(&:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.expand_parent_tags tags
|
def self.expand_parent_tags tags
|
||||||
@@ -94,11 +101,19 @@ class Tag < ApplicationRecord
|
|||||||
(result + tags).uniq { |t| t.id }
|
(result + tags).uniq { |t| t.id }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.find_or_create_by_tag_name!(name, category:)
|
||||||
|
tn = TagName.find_or_create_by!(name: name.to_s.strip)
|
||||||
|
Tag.find_or_create_by!(tag_name_id: tn.id) do |t|
|
||||||
|
t.category = category
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def nico_tag_name_must_start_with_nico
|
def nico_tag_name_must_start_with_nico
|
||||||
if ((category == 'nico' && !(name.start_with?('nico:'))) ||
|
n = name.to_s
|
||||||
(category != 'nico' && name.start_with?('nico:')))
|
if ((category == 'nico' && !(n.start_with?('nico:'))) ||
|
||||||
|
(category != 'nico' && n.start_with?('nico:')))
|
||||||
errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.'
|
errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class TagName < ApplicationRecord
|
||||||
|
has_one :tag
|
||||||
|
has_one :wiki_page
|
||||||
|
|
||||||
|
belongs_to :canonical, class_name: 'TagName', optional: true
|
||||||
|
has_many :aliases, class_name: 'TagName', foreign_key: :canonical_id
|
||||||
|
|
||||||
|
validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
|
||||||
|
end
|
||||||
@@ -11,7 +11,16 @@ class WikiPage < ApplicationRecord
|
|||||||
foreign_key: :redirect_page_id,
|
foreign_key: :redirect_page_id,
|
||||||
dependent: :nullify
|
dependent: :nullify
|
||||||
|
|
||||||
validates :title, presence: true, length: { maximum: 255 }, uniqueness: true
|
belongs_to :tag_name
|
||||||
|
validates :tag_name, presence: true
|
||||||
|
|
||||||
|
def title
|
||||||
|
tag_name.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def title= val
|
||||||
|
(self.tag_name ||= build_tag_name).name = val
|
||||||
|
end
|
||||||
|
|
||||||
def current_revision
|
def current_revision
|
||||||
wiki_revisions.order(id: :desc).first
|
wiki_revisions.order(id: :desc).first
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
class CreateTagNames < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :tag_names do |t|
|
||||||
|
t.string :name, limit: 255, null: false
|
||||||
|
t.references :canonical, null: true, foreign_key: { to_table: :tag_names }, index: true
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :tag_names, :name, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
class AddTagNameToTags < ActiveRecord::Migration[7.0]
|
||||||
|
class Tag < ApplicationRecord
|
||||||
|
self.table_name = 'tags'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagName < ApplicationRecord
|
||||||
|
self.table_name = 'tag_names'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_reference :tags, :tag_name, null: true, foreign_key: true, index: false, after: :id
|
||||||
|
add_index :tags, :tag_name_id, unique: true
|
||||||
|
|
||||||
|
Tag.find_each do |tag|
|
||||||
|
name = tag.read_attribute(:name)
|
||||||
|
tn = TagName.find_or_create_by!(name:) do |r|
|
||||||
|
r.canonical_id = nil
|
||||||
|
end
|
||||||
|
tag.update_columns(tag_name_id: tn.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :tags, :tag_name_id, false
|
||||||
|
|
||||||
|
remove_column :tags, :name
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_column :tags, :name, :string, limit: 255, null: true, after: :id
|
||||||
|
|
||||||
|
Tag.find_each do |tag|
|
||||||
|
tag_name_id = tag.read_attribute(:tag_name_id)
|
||||||
|
name = TagName.find(tag_name_id).read_attribute(:name)
|
||||||
|
tag.update_columns(name:)
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :tags, :name, false
|
||||||
|
|
||||||
|
remove_foreign_key :tags, column: :tag_name_id
|
||||||
|
remove_index :tags, :tag_name_id
|
||||||
|
remove_column :tags, :tag_name_id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
class AddTagNameToWikiPages < ActiveRecord::Migration[7.0]
|
||||||
|
class WikiPage < ApplicationRecord
|
||||||
|
self.table_name = 'wiki_pages'
|
||||||
|
end
|
||||||
|
|
||||||
|
class TagName < ApplicationRecord
|
||||||
|
self.table_name = 'tag_names'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_reference :wiki_pages, :tag_name, null: true, foreign_key: true, index: false, after: :id
|
||||||
|
add_index :wiki_pages, :tag_name_id, unique: true
|
||||||
|
|
||||||
|
WikiPage.find_each do |page|
|
||||||
|
name = page.read_attribute(:title)
|
||||||
|
tn = TagName.find_or_create_by!(name:) do |r|
|
||||||
|
r.canonical_id = nil
|
||||||
|
end
|
||||||
|
page.update_columns(tag_name_id: tn.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :wiki_pages, :tag_name_id, false
|
||||||
|
|
||||||
|
remove_column :wiki_pages, :title
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
add_column :wiki_pages, :title, :string, limit: 255, null: true, after: :id
|
||||||
|
|
||||||
|
WikiPage.find_each do |page|
|
||||||
|
tag_name_id = page.read_attribute(:tag_name_id)
|
||||||
|
title = TagName.find(tag_name_id).read_attribute(:name)
|
||||||
|
page.update_columns(title:)
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :wiki_pages, :title, false
|
||||||
|
|
||||||
|
remove_foreign_key :wiki_pages, column: :tag_name_id
|
||||||
|
remove_index :wiki_pages, :tag_name_id
|
||||||
|
remove_column :wiki_pages, :tag_name_id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
class DropTagAliases < ActiveRecord::Migration[7.0]
|
||||||
|
def up
|
||||||
|
drop_table :tag_aliases
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration, '戻せません.'
|
||||||
|
end
|
||||||
|
end
|
||||||
生成ファイル
+18
-13
@@ -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: 2025_12_30_143400) do
|
ActiveRecord::Schema[8.0].define(version: 2026_01_12_111800) 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
|
||||||
@@ -84,7 +84,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_143400) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "title", null: false
|
t.string "title"
|
||||||
t.string "url", limit: 2000, null: false
|
t.string "url", limit: 2000, null: false
|
||||||
t.string "thumbnail_base", limit: 2000
|
t.string "thumbnail_base", limit: 2000
|
||||||
t.bigint "parent_id"
|
t.bigint "parent_id"
|
||||||
@@ -106,14 +106,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_143400) do
|
|||||||
t.index ["user_id"], name: "index_settings_on_user_id"
|
t.index ["user_id"], name: "index_settings_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tag_aliases", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.bigint "tag_id", null: false
|
|
||||||
t.string "name", null: false
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["tag_id"], name: "index_tag_aliases_on_tag_id"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "parent_tag_id", null: false
|
t.bigint "parent_tag_id", null: false
|
||||||
@@ -124,6 +116,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_143400) do
|
|||||||
t.index ["tag_id"], name: "index_tag_implications_on_tag_id"
|
t.index ["tag_id"], name: "index_tag_implications_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "tag_names", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.string "name", null: false
|
||||||
|
t.bigint "canonical_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["canonical_id"], name: "index_tag_names_on_canonical_id"
|
||||||
|
t.index ["name"], name: "index_tag_names_on_name", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "target_tag_id", null: false
|
t.bigint "target_tag_id", null: false
|
||||||
@@ -133,11 +134,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_143400) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tags", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.bigint "tag_name_id", null: false
|
||||||
t.string "category", default: "general", null: false
|
t.string "category", default: "general", null: false
|
||||||
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.integer "post_count", default: 0, null: false
|
t.integer "post_count", default: 0, null: false
|
||||||
|
t.index ["tag_name_id"], name: "index_tags_on_tag_name_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "user_ips", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "user_ips", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
@@ -176,12 +178,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_143400) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "title", null: false
|
t.bigint "tag_name_id", null: false
|
||||||
t.bigint "created_user_id", null: false
|
t.bigint "created_user_id", null: false
|
||||||
t.bigint "updated_user_id", null: false
|
t.bigint "updated_user_id", null: false
|
||||||
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 ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
|
||||||
|
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
|
||||||
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -228,15 +231,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_30_143400) do
|
|||||||
add_foreign_key "posts", "posts", column: "parent_id"
|
add_foreign_key "posts", "posts", column: "parent_id"
|
||||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||||
add_foreign_key "settings", "users"
|
add_foreign_key "settings", "users"
|
||||||
add_foreign_key "tag_aliases", "tags"
|
|
||||||
add_foreign_key "tag_implications", "tags"
|
add_foreign_key "tag_implications", "tags"
|
||||||
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
||||||
|
add_foreign_key "tag_names", "tag_names", column: "canonical_id"
|
||||||
add_foreign_key "tag_similarities", "tags"
|
add_foreign_key "tag_similarities", "tags"
|
||||||
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
||||||
|
add_foreign_key "tags", "tag_names"
|
||||||
add_foreign_key "user_ips", "ip_addresses"
|
add_foreign_key "user_ips", "ip_addresses"
|
||||||
add_foreign_key "user_ips", "users"
|
add_foreign_key "user_ips", "users"
|
||||||
add_foreign_key "user_post_views", "posts"
|
add_foreign_key "user_post_views", "posts"
|
||||||
add_foreign_key "user_post_views", "users"
|
add_foreign_key "user_post_views", "users"
|
||||||
|
add_foreign_key "wiki_pages", "tag_names"
|
||||||
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
add_foreign_key "wiki_pages", "users", column: "created_user_id"
|
||||||
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
add_foreign_key "wiki_pages", "users", column: "updated_user_id"
|
||||||
add_foreign_key "wiki_revision_lines", "wiki_lines"
|
add_foreign_key "wiki_revision_lines", "wiki_lines"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace :nico do
|
|||||||
require 'open3'
|
require 'open3'
|
||||||
require 'open-uri'
|
require 'open-uri'
|
||||||
require 'nokogiri'
|
require 'nokogiri'
|
||||||
|
require 'set'
|
||||||
|
|
||||||
fetch_thumbnail = -> url do
|
fetch_thumbnail = -> url do
|
||||||
html = URI.open(url, read_timeout: 60, 'User-Agent' => 'Mozilla/5.0').read
|
html = URI.open(url, read_timeout: 60, 'User-Agent' => 'Mozilla/5.0').read
|
||||||
@@ -12,9 +13,9 @@ namespace :nico do
|
|||||||
doc.at('meta[name="thumbnail"]')&.[]('content').presence
|
doc.at('meta[name="thumbnail"]')&.[]('content').presence
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_post_tags! post, desired_tag_ids
|
def sync_post_tags! post, desired_tag_ids, current_ids: nil
|
||||||
|
current_ids ||= PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
|
||||||
desired_ids = desired_tag_ids.compact.to_set
|
desired_ids = desired_tag_ids.compact.to_set
|
||||||
current_ids = post.tags.pluck(:id).to_set
|
|
||||||
|
|
||||||
to_add = desired_ids - current_ids
|
to_add = desired_ids - current_ids
|
||||||
to_remove = current_ids - desired_ids
|
to_remove = current_ids - desired_ids
|
||||||
@@ -43,12 +44,12 @@ namespace :nico do
|
|||||||
|
|
||||||
data = JSON.parse(stdout)
|
data = JSON.parse(stdout)
|
||||||
data.each do |datum|
|
data.each do |datum|
|
||||||
post = Post.where('url LIKE ?', '%nicovideo.jp%').find { |post|
|
code = datum['code']
|
||||||
post.url =~ %r{#{ Regexp.escape(datum['code']) }(?!\d)}
|
post = Post.where('url REGEXP ?', "nicovideo\\.jp/watch/#{ Regexp.escape(code) }([^0-9]|$)")
|
||||||
}
|
.first
|
||||||
unless post
|
unless post
|
||||||
title = datum['title']
|
title = datum['title']
|
||||||
url = "https://www.nicovideo.jp/watch/#{ datum['code'] }"
|
url = "https://www.nicovideo.jp/watch/#{ code }"
|
||||||
thumbnail_base = fetch_thumbnail.(url) rescue nil
|
thumbnail_base = fetch_thumbnail.(url) rescue nil
|
||||||
post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil)
|
post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil)
|
||||||
if thumbnail_base.present?
|
if thumbnail_base.present?
|
||||||
@@ -62,21 +63,19 @@ namespace :nico do
|
|||||||
sync_post_tags!(post, [Tag.tagme.id])
|
sync_post_tags!(post, [Tag.tagme.id])
|
||||||
end
|
end
|
||||||
|
|
||||||
kept_tags = post.tags.reload
|
kept_ids = PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
|
||||||
kept_non_nico_ids = kept_tags.where.not(category: 'nico').pluck(:id).to_set
|
kept_non_nico_ids = post.tags.where.not(category: 'nico').pluck(:id).to_set
|
||||||
|
|
||||||
desired_nico_ids = []
|
desired_nico_ids = []
|
||||||
desired_non_nico_ids = []
|
desired_non_nico_ids = []
|
||||||
datum['tags'].each do |raw|
|
datum['tags'].each do |raw|
|
||||||
name = "nico:#{ raw }"
|
name = "nico:#{ raw }"
|
||||||
tag = Tag.find_or_initialize_by(name:) do |t|
|
tag = Tag.find_or_create_by_tag_name!(name, category: 'nico')
|
||||||
t.category = 'nico'
|
|
||||||
end
|
|
||||||
tag.save! if tag.new_record?
|
|
||||||
desired_nico_ids << tag.id
|
desired_nico_ids << tag.id
|
||||||
unless tag.in?(kept_tags)
|
unless tag.id.in?(kept_ids)
|
||||||
desired_non_nico_ids.concat(tag.linked_tags.pluck(:id))
|
linked_ids = tag.linked_tags.pluck(:id)
|
||||||
desired_nico_ids.concat(tag.linked_tags.pluck(:id))
|
desired_non_nico_ids.concat(linked_ids)
|
||||||
|
desired_nico_ids.concat(linked_ids)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
desired_nico_ids.uniq!
|
desired_nico_ids.uniq!
|
||||||
@@ -89,7 +88,7 @@ namespace :nico do
|
|||||||
end
|
end
|
||||||
desired_all_ids.uniq!
|
desired_all_ids.uniq!
|
||||||
|
|
||||||
sync_post_tags!(post, desired_all_ids)
|
sync_post_tags!(post, desired_all_ids, current_ids: kept_ids)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする