コミットを比較

...

10 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 409498729a Merge remote-tracking branch 'origin/main' into feature/206 2026-03-08 23:12:35 +09:00
みてるぞ 9e3cbd2469 投稿検索ページ(#206) (#274)
#206 エラー修正

#206 updated_at の並び順修正

Merge remote-tracking branch 'origin/main' into feature/206

Merge branch 'main' into feature/206

Merge branch 'main' into feature/206

Merge branch 'main' into feature/206

#206

#206

#206

#206

#206

#206 タグ補完追加

#206

#206

#206

#206

#206

Merge remote-tracking branch 'origin/main' into feature/206

#206

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #274
2026-03-08 23:12:16 +09:00
みてるぞ f7af5efaf9 #206 エラー修正 2026-03-08 23:06:10 +09:00
みてるぞ 4235903b49 #206 updated_at の並び順修正 2026-03-08 22:58:22 +09:00
みてるぞ 17ff0e8114 Merge remote-tracking branch 'origin/main' into feature/206 2026-03-08 16:30:56 +09:00
みてるぞ 16e9b8ca49 タグの合併処理追加(#282) (#284)
#282

#282

#282

#282

#282

#282

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #284
2026-03-08 15:46:05 +09:00
みてるぞ 6b1079fc1a Merge branch 'main' into feature/206 2026-03-07 13:58:59 +09:00
みてるぞ 7885f6dfb9 feat: “ニジラー情報不詳” タグの自動付与(#106) (#196)
#106

#106 エラー対応

#106

Merge branch 'main' into '#106'

Merge branch 'main' into '#106'

Merge remote-tracking branch 'origin/main' into '#106'

#106 誤字

Merge remote-tracking branch 'origin/main' into '#106'

#106 ニジラー情報なし

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #196
2026-03-07 13:58:43 +09:00
みてるぞ a66a73e004 Merge branch 'main' into feature/206 2026-03-05 23:10:45 +09:00
みてるぞ 98330b00bb ニコニコ同期にてニジラー情報自動記載(#276) (#277)
#276

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #277
2026-03-05 23:10:37 +09:00
14個のファイルの変更198行の追加81行の削除
+2 -2
ファイルの表示
@@ -17,7 +17,7 @@ class DeerjikistsController < ApplicationController
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.gte_member?
platform = params[:platform].to_s.strip platform = params[:platform].to_s.strip
code = params[:code].to_s.strip code = params[:code].to_s.strip
@@ -34,7 +34,7 @@ class DeerjikistsController < ApplicationController
def destroy def destroy
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.gte_member?
platform = params[:platform].to_s.strip platform = params[:platform].to_s.strip
code = params[:code].to_s.strip code = params[:code].to_s.strip
+1 -1
ファイルの表示
@@ -25,7 +25,7 @@ class NicoTagsController < ApplicationController
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.gte_member?
id = params[:id].to_i id = params[:id].to_i
+12 -7
ファイルの表示
@@ -65,15 +65,20 @@ class PostsController < ApplicationController
end end
sort_sql = sort_sql =
if order[0] == 'original_created_at' case order[0]
when 'original_created_at'
'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' + 'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' +
'posts.original_created_from,' + 'posts.original_created_from,' +
'posts.created_at) ' + 'posts.created_at) '
order[1] when 'updated_at'
updated_at_all_sql
else else
"posts.#{ order[0] } #{ order[1] }" "posts.#{ order[0] }"
end end
posts = q.order(Arel.sql("#{ sort_sql }")).limit(limit).offset(offset).to_a posts = q.order(Arel.sql("#{ sort_sql } #{ order[1] }, id #{ order[1] }"))
.limit(limit)
.offset(offset)
.to_a
q = q.except(:select, :order) q = q.except(:select, :order)
@@ -116,7 +121,7 @@ class PostsController < ApplicationController
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.gte_member?
# TODO: サイトに応じて thumbnail_base 設定 # TODO: サイトに応じて thumbnail_base 設定
title = params[:title].presence title = params[:title].presence
@@ -160,7 +165,7 @@ class PostsController < ApplicationController
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.gte_member?
title = params[:title].presence title = params[:title].presence
tag_names = params[:tags].to_s.split tag_names = params[:tags].to_s.split
+1 -1
ファイルの表示
@@ -109,7 +109,7 @@ class TagsController < ApplicationController
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.gte_member?
name = params[:name].presence name = params[:name].presence
category = params[:category].presence category = params[:category].presence
+2 -2
ファイルの表示
@@ -83,7 +83,7 @@ class WikiPagesController < ApplicationController
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.gte_member?
name = params[:title]&.strip name = params[:title]&.strip
body = params[:body].to_s body = params[:body].to_s
@@ -105,7 +105,7 @@ class WikiPagesController < ApplicationController
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.gte_member?
title = params[:title]&.strip title = params[:title]&.strip
body = params[:body].to_s body = params[:body].to_s
+44 -9
ファイルの表示
@@ -23,6 +23,8 @@ class Tag < ApplicationRecord
has_many :parents, through: :reversed_tag_implications, source: :parent_tag has_many :parents, through: :reversed_tag_implications, source: :parent_tag
has_many :tag_similarities, dependent: :delete_all has_many :tag_similarities, dependent: :delete_all
has_many :tag_similarities_as_target,
class_name: 'TagSimilarity', foreign_key: :target_tag_id, dependent: :delete_all
has_many :deerjikists, dependent: :delete_all has_many :deerjikists, dependent: :delete_all
@@ -46,7 +48,7 @@ class Tag < ApplicationRecord
validate :tag_name_must_be_canonical validate :tag_name_must_be_canonical
validate :category_must_be_deerjikist_with_deerjikists validate :category_must_be_deerjikist_with_deerjikists
scope :nico_tags, -> { where(category: :nico) } scope :nico_tags, -> { nico }
CATEGORY_PREFIXES = { CATEGORY_PREFIXES = {
'general:' => :general, 'general:' => :general,
@@ -64,9 +66,7 @@ class Tag < ApplicationRecord
(self.tag_name ||= build_tag_name).name = val (self.tag_name ||= build_tag_name).name = val
end end
def has_wiki def has_wiki = wiki_page.present?
wiki_page.present?
end
def self.tagme def self.tagme
@tagme ||= find_or_create_by_tag_name!('タグ希望', category: :meta) @tagme ||= find_or_create_by_tag_name!('タグ希望', category: :meta)
@@ -76,6 +76,10 @@ class Tag < ApplicationRecord
@bot ||= find_or_create_by_tag_name!('bot操作', category: :meta) @bot ||= find_or_create_by_tag_name!('bot操作', category: :meta)
end end
def self.no_deerjikist
@no_deerjikist ||= find_or_create_by_tag_name!('ニジラー情報不詳', category: :meta)
end
def self.video def self.video
@video ||= find_or_create_by_tag_name!('動画', category: :meta) @video ||= find_or_create_by_tag_name!('動画', category: :meta)
end end
@@ -93,13 +97,12 @@ class Tag < ApplicationRecord
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 = TagName.canonicalise(name.sub(/\A#{ pf }/i, '')).first name = TagName.canonicalise(name.sub(/\A#{ pf }/i, '')).first
find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag| find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
if cat && tag.category != cat tag.update!(category: cat) if cat && tag.category != cat
tag.update!(category: cat)
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 << Tag.no_deerjikist if tags.all? { |t| !(t.deerjikist?) }
tags.uniq(&:id) tags.uniq(&:id)
end end
@@ -137,12 +140,44 @@ class Tag < ApplicationRecord
retry retry
end end
def self.merge_tags! target_tag, source_tags
target_tag => Tag
Tag.transaction do
Array(source_tags).compact.uniq.each do |st|
st => Tag
next if st == target_tag
st.post_tags.find_each do |pt|
if PostTag.kept.exists?(post_id: pt.post_id, tag_id: target_tag.id)
pt.discard_by!(nil)
# discard 後の update! は禁止なので DB を直に更新
pt.update_columns(tag_id: target_tag.id, updated_at: Time.current)
else
pt.update!(tag: target_tag)
end
end
tag_name = st.tag_name
st.destroy!
tag_name.reload
tag_name.update!(canonical: target_tag.tag_name)
end
# 投稿件数を再集計
target_tag.update_columns(post_count: PostTag.kept.where(tag: target_tag).count)
end
target_tag.reload
end
private private
def nico_tag_name_must_start_with_nico def nico_tag_name_must_start_with_nico
n = name.to_s n = name.to_s
if ((category == 'nico' && !(n.downcase.start_with?('nico:'))) || if ((nico? && !(n.downcase.start_with?('nico:'))) ||
(category != 'nico' && n.downcase.start_with?('nico:'))) (!(nico?) && n.downcase.start_with?('nico:')))
errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.' errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.'
end end
end end
+9 -15
ファイルの表示
@@ -1,29 +1,23 @@
class User < ApplicationRecord class User < ApplicationRecord
enum :role, { guest: 'guest', member: 'member', admin: 'admin' } enum :role, guest: 'guest', member: 'member', admin: 'admin'
validates :name, length: { maximum: 255 } validates :name, length: { maximum: 255 }
validates :inheritance_code, presence: true, length: { maximum: 64 } validates :inheritance_code, presence: true, length: { maximum: 64 }
validates :role, presence: true, inclusion: { in: roles.keys } validates :role, presence: true, inclusion: { in: roles.keys }
validates :banned, inclusion: { in: [true, false] } validates :banned, inclusion: { in: [true, false] }
has_many :posts has_many :created_posts,
class_name: 'Post', foreign_key: :uploaded_user_id, dependent: :nullify
has_many :settings has_many :settings
has_many :user_ips, dependent: :destroy has_many :user_ips, dependent: :destroy
has_many :ip_addresses, through: :user_ips has_many :ip_addresses, through: :user_ips
has_many :user_post_views, dependent: :destroy has_many :user_post_views, dependent: :destroy
has_many :viewed_posts, through: :user_post_views, source: :post has_many :viewed_posts, through: :user_post_views, source: :post
has_many :created_wiki_pages, class_name: 'WikiPage', foreign_key: 'created_user_id', dependent: :nullify has_many :created_wiki_pages,
has_many :updated_wiki_pages, class_name: 'WikiPage', foreign_key: 'updated_user_id', dependent: :nullify class_name: 'WikiPage', foreign_key: :created_user_id, dependent: :nullify
has_many :updated_wiki_pages,
class_name: 'WikiPage', foreign_key: :updated_user_id, dependent: :nullify
def viewed? post def viewed?(post) = user_post_views.exists?(post_id: post.id)
user_post_views.exists? post_id: post.id def gte_member? = member? || admin?
end
def member?
['member', 'admin'].include?(role)
end
def admin?
role == 'admin'
end
end end
+1 -1
ファイルの表示
@@ -8,7 +8,7 @@ class WikiLine < ApplicationRecord
sha = Digest::SHA256.hexdigest(body) sha = Digest::SHA256.hexdigest(body)
now = Time.current now = Time.current
upsert({ sha256: sha, body:, created_at: now, updated_at: now }) upsert(sha256: sha, body:, created_at: now, updated_at: now)
find_by!(sha256: sha) find_by!(sha256: sha)
end end
+4 -11
ファイルの表示
@@ -14,17 +14,13 @@ class WikiPage < ApplicationRecord
belongs_to :tag_name belongs_to :tag_name
validates :tag_name, presence: true validates :tag_name, presence: true
def title def title = tag_name.name
tag_name.name
end
def title= val def title= val
(self.tag_name ||= build_tag_name).name = val (self.tag_name ||= build_tag_name).name = val
end end
def current_revision def current_revision = wiki_revisions.order(id: :desc).first
wiki_revisions.order(id: :desc).first
end
def body def body
rev = current_revision rev = current_revision
@@ -49,11 +45,8 @@ class WikiPage < ApplicationRecord
page page
end end
def pred_revision_id revision_id def pred_revision_id(revision_id) =
wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id) wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id)
end def succ_revision_id(revision_id) =
def succ_revision_id revision_id
wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id) wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id)
end
end end
+1 -1
ファイルの表示
@@ -7,7 +7,7 @@ class WikiRevision < ApplicationRecord
has_many :wiki_revision_lines, dependent: :delete_all has_many :wiki_revision_lines, dependent: :delete_all
has_many :wiki_lines, through: :wiki_revision_lines has_many :wiki_lines, through: :wiki_revision_lines
enum :kind, { content: 0, redirect: 1 } enum :kind, content: 0, redirect: 1
validates :kind, presence: true validates :kind, presence: true
validates :lines_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates :lines_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+46 -26
ファイルの表示
@@ -2,9 +2,9 @@ namespace :nico do
desc 'ニコニコ DB 同期' desc 'ニコニコ DB 同期'
task sync: :environment do task sync: :environment do
require 'json' require 'json'
require 'open3'
require 'open-uri'
require 'nokogiri' require 'nokogiri'
require 'open-uri'
require 'open3'
require 'set' require 'set'
require 'time' require 'time'
@@ -15,12 +15,12 @@ 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, current_ids: nil def sync_post_tags! post, desired_tag_ids, current_tag_ids: nil
current_ids ||= PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set current_tag_ids ||= PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
desired_ids = desired_tag_ids.compact.to_set desired_tag_ids = desired_tag_ids.compact.to_set
to_add = desired_ids - current_ids to_add = desired_tag_ids - current_tag_ids
to_remove = current_ids - desired_ids to_remove = current_tag_ids - desired_tag_ids
Tag.where(id: to_add.to_a).find_each do |tag| Tag.where(id: to_add.to_a).find_each do |tag|
begin begin
@@ -42,12 +42,14 @@ namespace :nico do
{ 'MYSQL_USER' => mysql_user, 'MYSQL_PASS' => mysql_pass }, { 'MYSQL_USER' => mysql_user, 'MYSQL_PASS' => mysql_pass },
'python3', "#{ nizika_nico_path }/get_videos.py") 'python3', "#{ nizika_nico_path }/get_videos.py")
abort unless status.success? unless status.success?
warn stderr
abort
end
data = JSON.parse(stdout) data = JSON.parse(stdout)
data.each do |datum| data.each do |datum|
code = datum['code'] code = datum['code']
post = post =
Post Post
.where('url REGEXP ?', "nicovideo\\.jp/watch/#{ Regexp.escape(code) }([^0-9]|$)") .where('url REGEXP ?', "nicovideo\\.jp/watch/#{ Regexp.escape(code) }([^0-9]|$)")
@@ -94,32 +96,50 @@ namespace :nico do
sync_post_tags!(post, [Tag.tagme.id, Tag.bot.id, Tag.niconico.id, Tag.video.id]) sync_post_tags!(post, [Tag.tagme.id, Tag.bot.id, Tag.niconico.id, Tag.video.id])
end end
kept_ids = PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set tags = post.tags
kept_non_nico_ids = post.tags.where.not(category: 'nico').pluck(:id).to_set # 既存のタグ Id. 集合
kept_tag_ids = tags.pluck(:id).to_set
# うち内部タグ Id. 集合
kept_non_nico_tag_ids = tags.not_nico.pluck(:id).to_set
# 記載すべき外部タグ Id. および連携される内部タグ Id. のリスト
desired_nico_tag_based_ids = []
# 記載すべき内部タグ Id. のリスト
desired_non_nico_tag_ids = []
desired_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_create_by_tag_name!(name, category: 'nico') tag = Tag.find_or_create_by_tag_name!(name, category: :nico)
desired_nico_ids << tag.id desired_nico_tag_based_ids << tag.id
unless tag.id.in?(kept_ids)
# 新たに記載される外部タグと連携される内部タグを記載
unless tag.id.in?(kept_tag_ids)
linked_ids = tag.linked_tags.pluck(:id) linked_ids = tag.linked_tags.pluck(:id)
desired_non_nico_ids.concat(linked_ids) desired_non_nico_tag_ids.concat(linked_ids)
desired_nico_ids.concat(linked_ids) desired_nico_tag_based_ids.concat(linked_ids)
end end
end end
desired_nico_ids.uniq!
desired_all_ids = kept_non_nico_ids.to_a + desired_nico_ids deerjikist = Deerjikist.find_by(platform: :nico, code: datum['user'])
desired_non_nico_ids.concat(kept_non_nico_ids.to_a) if deerjikist
desired_non_nico_ids.uniq! desired_non_nico_tag_ids << deerjikist.tag_id
if kept_non_nico_ids.to_set != desired_non_nico_ids.to_set desired_nico_tag_based_ids << deerjikist.tag_id
desired_all_ids << Tag.bot.id elsif !(Tag.where(id: kept_non_nico_tag_ids).where(category: :deerjikist).exists?)
desired_non_nico_tag_ids << Tag.no_deerjikist.id
desired_nico_tag_based_ids << Tag.no_deerjikist.id
end end
desired_all_ids.uniq!
sync_post_tags!(post, desired_all_ids, current_ids: kept_ids) desired_nico_tag_based_ids.uniq!
desired_all_tag_ids = kept_non_nico_tag_ids.to_a + desired_nico_tag_based_ids
desired_non_nico_tag_ids.concat(kept_non_nico_tag_ids.to_a)
desired_non_nico_tag_ids.uniq!
if kept_non_nico_tag_ids != desired_non_nico_tag_ids.to_set
desired_all_tag_ids << Tag.bot.id
end
desired_all_tag_ids.uniq!
sync_post_tags!(post, desired_all_tag_ids, current_tag_ids: kept_tag_ids)
end end
end end
end end
+71
ファイルの表示
@@ -0,0 +1,71 @@
require 'rails_helper'
RSpec.describe Tag, type: :model do
describe '.merge_tags!' do
let!(:target_tag) { create(:tag) }
let!(:source_tag) { create(:tag) }
let!(:post_record) { Post.create!(url: 'https://example.com/posts/1', title: 'test post') }
context 'when merging a simple source tag' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
it 'moves the post_tag, deletes the source tag, and aliases the source tag_name' do
described_class.merge_tags!(target_tag, [source_tag])
expect(source_post_tag.reload.tag_id).to eq(target_tag.id)
expect(Tag.exists?(source_tag.id)).to be(false)
expect(source_tag.tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
end
end
context 'when the target already has the same post_tag' do
let!(:target_post_tag) { PostTag.create!(post: post_record, tag: target_tag) }
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
it 'discards the duplicate source post_tag and keeps one active target post_tag' do
described_class.merge_tags!(target_tag, [source_tag])
active = PostTag.kept.where(post_id: post_record.id, tag_id: target_tag.id)
discarded_source = PostTag.with_discarded.find(source_post_tag.id)
expect(active.count).to eq(1)
expect(discarded_source.discarded_at).to be_present
expect(Tag.exists?(source_tag.id)).to be(false)
expect(source_tag.tag_name.reload.canonical_id).to eq(target_tag.tag_name_id)
end
end
context 'when source_tags includes the target itself' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
it 'ignores the target tag in source_tags' do
described_class.merge_tags!(target_tag, [source_tag, target_tag])
expect(Tag.exists?(target_tag.id)).to be(true)
expect(Tag.exists?(source_tag.id)).to be(false)
expect(source_post_tag.reload.tag_id).to eq(target_tag.id)
end
end
context 'when aliasing the source tag_name is invalid' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
let!(:wiki_page) do
WikiPage.create!(
tag_name: source_tag.tag_name,
created_user: create_admin_user!,
updated_user: create_admin_user!)
end
it 'rolls back the transaction' do
expect {
described_class.merge_tags!(target_tag, [source_tag])
}.to raise_error(ActiveRecord::RecordInvalid)
expect(Tag.exists?(source_tag.id)).to be(true)
expect(source_post_tag.reload.tag_id).to eq(source_tag.id)
expect(source_tag.tag_name.reload.canonical_id).to be_nil
end
end
end
end
-1
ファイルの表示
@@ -1,4 +1,3 @@
# spec/requests/tag_children_spec.rb
require "rails_helper" require "rails_helper"
RSpec.describe "TagChildren", type: :request do RSpec.describe "TagChildren", type: :request do
+2 -2
ファイルの表示
@@ -106,8 +106,8 @@ RSpec.describe 'Tags API', type: :request do
end end
before do before do
allow(member_user).to receive(:member?).and_return(true) allow(member_user).to receive(:gte_member?).and_return(true)
allow(non_member_user).to receive(:member?).and_return(false) allow(non_member_user).to receive(:gte_member?).and_return(false)
end end
describe "PATCH /tags/:id" do describe "PATCH /tags/:id" do