#282 #282 #282 #282 #282 #282 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/284feature/068
| @@ -17,7 +17,7 @@ class DeerjikistsController < ApplicationController | |||
| def update | |||
| 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 | |||
| code = params[:code].to_s.strip | |||
| @@ -34,7 +34,7 @@ class DeerjikistsController < ApplicationController | |||
| def destroy | |||
| 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 | |||
| code = params[:code].to_s.strip | |||
| @@ -25,7 +25,7 @@ class NicoTagsController < ApplicationController | |||
| def update | |||
| 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 | |||
| @@ -78,7 +78,7 @@ class PostsController < ApplicationController | |||
| def create | |||
| return head :unauthorized unless current_user | |||
| return head :forbidden unless current_user.member? | |||
| return head :forbidden unless current_user.gte_member? | |||
| # TODO: サイトに応じて thumbnail_base 設定 | |||
| title = params[:title].presence | |||
| @@ -122,7 +122,7 @@ class PostsController < ApplicationController | |||
| def update | |||
| 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 | |||
| tag_names = params[:tags].to_s.split(' ') | |||
| @@ -109,7 +109,7 @@ class TagsController < ApplicationController | |||
| def update | |||
| 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 | |||
| category = params[:category].presence | |||
| @@ -83,7 +83,7 @@ class WikiPagesController < ApplicationController | |||
| def create | |||
| 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 | |||
| body = params[:body].to_s | |||
| @@ -105,7 +105,7 @@ class WikiPagesController < ApplicationController | |||
| def update | |||
| 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 | |||
| body = params[:body].to_s | |||
| @@ -23,6 +23,8 @@ class Tag < ApplicationRecord | |||
| has_many :parents, through: :reversed_tag_implications, source: :parent_tag | |||
| 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 | |||
| @@ -46,7 +48,7 @@ class Tag < ApplicationRecord | |||
| validate :tag_name_must_be_canonical | |||
| validate :category_must_be_deerjikist_with_deerjikists | |||
| scope :nico_tags, -> { where(category: :nico) } | |||
| scope :nico_tags, -> { nico } | |||
| CATEGORY_PREFIXES = { | |||
| 'general:' => :general, | |||
| @@ -64,9 +66,7 @@ class Tag < ApplicationRecord | |||
| (self.tag_name ||= build_tag_name).name = val | |||
| end | |||
| def has_wiki | |||
| wiki_page.present? | |||
| end | |||
| def has_wiki = wiki_page.present? | |||
| def self.tagme | |||
| @tagme ||= find_or_create_by_tag_name!('タグ希望', category: :meta) | |||
| @@ -97,14 +97,12 @@ class Tag < ApplicationRecord | |||
| pf, cat = CATEGORY_PREFIXES.find { |p, _| name.downcase.start_with?(p) } || ['', nil] | |||
| name = TagName.canonicalise(name.sub(/\A#{ pf }/i, '')).first | |||
| find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag| | |||
| if cat && tag.category != cat | |||
| tag.update!(category: cat) | |||
| end | |||
| tag.update!(category: cat) if cat && tag.category != cat | |||
| end | |||
| end | |||
| tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme) | |||
| tags << Tag.no_deerjikist if tags.all? { |t| t.category != 'deerjikist' } | |||
| tags << Tag.no_deerjikist if tags.all? { |t| !(t.deerjikist?) } | |||
| tags.uniq(&:id) | |||
| end | |||
| @@ -142,12 +140,44 @@ class Tag < ApplicationRecord | |||
| retry | |||
| 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 | |||
| def nico_tag_name_must_start_with_nico | |||
| n = name.to_s | |||
| if ((category == 'nico' && !(n.downcase.start_with?('nico:'))) || | |||
| (category != 'nico' && n.downcase.start_with?('nico:'))) | |||
| if ((nico? && !(n.downcase.start_with?('nico:'))) || | |||
| (!(nico?) && n.downcase.start_with?('nico:'))) | |||
| errors.add :name, 'ニコニコ・タグの命名規則に反してゐます.' | |||
| end | |||
| end | |||
| @@ -1,29 +1,23 @@ | |||
| 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 :inheritance_code, presence: true, length: { maximum: 64 } | |||
| validates :role, presence: true, inclusion: { in: roles.keys } | |||
| 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 :user_ips, dependent: :destroy | |||
| has_many :ip_addresses, through: :user_ips | |||
| has_many :user_post_views, dependent: :destroy | |||
| 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 :updated_wiki_pages, class_name: 'WikiPage', foreign_key: 'updated_user_id', dependent: :nullify | |||
| has_many :created_wiki_pages, | |||
| 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 | |||
| user_post_views.exists? post_id: post.id | |||
| end | |||
| def member? | |||
| ['member', 'admin'].include?(role) | |||
| end | |||
| def admin? | |||
| role == 'admin' | |||
| end | |||
| def viewed?(post) = user_post_views.exists?(post_id: post.id) | |||
| def gte_member? = member? || admin? | |||
| end | |||
| @@ -8,7 +8,7 @@ class WikiLine < ApplicationRecord | |||
| sha = Digest::SHA256.hexdigest(body) | |||
| 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) | |||
| end | |||
| @@ -14,17 +14,13 @@ class WikiPage < ApplicationRecord | |||
| belongs_to :tag_name | |||
| validates :tag_name, presence: true | |||
| def title | |||
| tag_name.name | |||
| end | |||
| def title = tag_name.name | |||
| def title= val | |||
| (self.tag_name ||= build_tag_name).name = val | |||
| end | |||
| def current_revision | |||
| wiki_revisions.order(id: :desc).first | |||
| end | |||
| def current_revision = wiki_revisions.order(id: :desc).first | |||
| def body | |||
| rev = current_revision | |||
| @@ -49,11 +45,8 @@ class WikiPage < ApplicationRecord | |||
| page | |||
| end | |||
| def pred_revision_id revision_id | |||
| wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id) | |||
| end | |||
| def succ_revision_id revision_id | |||
| wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id) | |||
| end | |||
| def pred_revision_id(revision_id) = | |||
| wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id) | |||
| def succ_revision_id(revision_id) = | |||
| wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id) | |||
| end | |||
| @@ -7,7 +7,7 @@ class WikiRevision < ApplicationRecord | |||
| has_many :wiki_revision_lines, dependent: :delete_all | |||
| 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 :lines_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 } | |||
| @@ -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,4 +1,3 @@ | |||
| # spec/requests/tag_children_spec.rb | |||
| require "rails_helper" | |||
| RSpec.describe "TagChildren", type: :request do | |||
| @@ -106,8 +106,8 @@ RSpec.describe 'Tags API', type: :request do | |||
| end | |||
| before do | |||
| allow(member_user).to receive(:member?).and_return(true) | |||
| allow(non_member_user).to receive(:member?).and_return(false) | |||
| allow(member_user).to receive(:gte_member?).and_return(true) | |||
| allow(non_member_user).to receive(:gte_member?).and_return(false) | |||
| end | |||
| describe "PATCH /tags/:id" do | |||