| @@ -97,6 +97,8 @@ class PostsController < ApplicationController | |||
| tags = Tag.normalise_tags(tag_names) | |||
| tags = Tag.expand_parent_tags(tags) | |||
| sync_post_tags!(post, tags) | |||
| post.reload | |||
| render json: post.as_json(include: { tags: { only: [:id, :category, :post_count], | |||
| methods: [:name, :has_wiki] } }), | |||
| status: :created | |||
| @@ -136,6 +138,8 @@ class PostsController < ApplicationController | |||
| Tag.normalise_tags(tag_names, with_tagme: false) | |||
| tags = Tag.expand_parent_tags(tags) | |||
| sync_post_tags!(post, tags) | |||
| post.reload | |||
| json = post.as_json | |||
| json['tags'] = build_tag_tree_for(post.tags) | |||
| render json:, status: :ok | |||
| @@ -197,6 +201,8 @@ class PostsController < ApplicationController | |||
| end | |||
| def filter_posts_by_tags tag_names, match_type | |||
| tag_names = TagName.canonicalise(tag_names) | |||
| posts = Post.joins(tags: :tag_name) | |||
| if match_type == 'any' | |||
| @@ -18,13 +18,41 @@ class TagsController < ApplicationController | |||
| with_nico = !(params[:nico].to_s.strip.downcase.in?(['0', 'false', 'off', 'no'])) | |||
| tags = (Tag.joins(:tag_name).includes(:tag_name) | |||
| .where(((with_nico ? '(tags.category = ? AND tag_names.name LIKE ?) OR ' : '') + | |||
| 'tag_names.name LIKE ?'), | |||
| *(with_nico ? ['nico', "nico:#{ q }%"] : []), "#{ q }%") | |||
| .order(Arel.sql('post_count DESC, tag_names.name ASC')) | |||
| .limit(20)) | |||
| render json: tags.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki]) | |||
| alias_rows = | |||
| TagName | |||
| .where('name LIKE ?', "#{ q }%") | |||
| .where.not(canonical_id: nil) | |||
| .pluck(:canonical_id, :name) | |||
| matched_alias_by_tag_name_id = { } | |||
| canonical_ids = [] | |||
| alias_rows.each do |canonical_id, alias_name| | |||
| canonical_ids << canonical_id | |||
| matched_alias_by_tag_name_id[canonical_id] ||= alias_name | |||
| end | |||
| base = Tag.joins(:tag_name).includes(:tag_name) | |||
| canonical_hit = | |||
| base | |||
| .where(((with_nico ? '(tags.category = ? AND tag_names.name LIKE ?) OR ' : '') + | |||
| 'tag_names.name LIKE ?'), | |||
| *(with_nico ? ['nico', "nico:#{ q }%"] : []), "#{ q }%") | |||
| tags = | |||
| if canonical_ids.present? | |||
| canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq)) | |||
| else | |||
| canonical_hit | |||
| end | |||
| tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a | |||
| render json: tags.map { |tag| | |||
| tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki]) | |||
| .merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id]) | |||
| } | |||
| end | |||
| def show | |||
| @@ -9,8 +9,8 @@ class Post < ApplicationRecord | |||
| has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag' | |||
| has_many :tags, through: :active_post_tags | |||
| has_many :user_post_views, dependent: :destroy | |||
| has_many :post_similarities, dependent: :destroy | |||
| has_many :user_post_views, dependent: :delete_all | |||
| has_many :post_similarities, dependent: :delete_all | |||
| has_one_attached :thumbnail | |||
| @@ -22,6 +22,8 @@ class Tag < ApplicationRecord | |||
| class_name: 'TagImplication', foreign_key: :tag_id, dependent: :destroy | |||
| has_many :parents, through: :reversed_tag_implications, source: :parent_tag | |||
| has_many :tag_similarities, dependent: :delete_all | |||
| belongs_to :tag_name | |||
| delegate :name, to: :tag_name, allow_nil: true | |||
| validates :tag_name, presence: true | |||
| @@ -72,7 +74,7 @@ class Tag < ApplicationRecord | |||
| tags = tag_names.map do |name| | |||
| pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil] | |||
| name = name.delete_prefix(pf) | |||
| name = TagName.canonicalise(name.delete_prefix(pf)).first | |||
| find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag| | |||
| if cat && tag.category != cat | |||
| tag.update!(category: cat) | |||
| @@ -109,6 +111,8 @@ class Tag < ApplicationRecord | |||
| def self.find_or_create_by_tag_name! name, category: | |||
| tn = TagName.find_or_create_by!(name: name.to_s.strip) | |||
| tn = tn.canonical if tn.canonical_id? | |||
| Tag.find_or_create_by!(tag_name_id: tn.id) do |t| | |||
| t.category = category | |||
| end | |||
| @@ -127,7 +131,7 @@ class Tag < ApplicationRecord | |||
| end | |||
| def tag_name_must_be_canonical | |||
| if tag_name&.canonical_id | |||
| if tag_name&.canonical_id? | |||
| errors.add :tag_name, 'tag_names へは実体を示す必要があります.' | |||
| end | |||
| end | |||
| @@ -9,6 +9,16 @@ class TagName < ApplicationRecord | |||
| validate :canonical_must_be_canonical | |||
| validate :alias_name_must_not_have_prefix | |||
| validate :canonical_must_not_be_present_with_tag_or_wiki_page | |||
| def self.canonicalise names | |||
| names = Array(names).map { |n| n.to_s.strip }.reject(&:blank?) | |||
| return [] if names.blank? | |||
| tns = TagName.includes(:canonical).where(name: names).index_by(&:name) | |||
| names.map { |name| tns[name]&.canonical&.name || name }.uniq | |||
| end | |||
| private | |||
| @@ -23,4 +33,10 @@ class TagName < ApplicationRecord | |||
| errors.add :name, 'エーリアス名にプレフィクスを含むことはできません.' | |||
| end | |||
| end | |||
| def canonical_must_not_be_present_with_tag_or_wiki_page | |||
| if canonical_id? && (tag || wiki_page) | |||
| errors.add :canonical, 'タグもしくは Wiki の参照がある名前はエーリアスになれません.' | |||
| end | |||
| end | |||
| end | |||
| @@ -155,6 +155,7 @@ RSpec.describe 'Posts API', type: :request do | |||
| describe 'POST /posts' do | |||
| let(:member) { create(:user, :member) } | |||
| let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) } | |||
| it '401 when not logged in' do | |||
| sign_out | |||
| @@ -201,10 +202,9 @@ RSpec.describe 'Posts API', type: :request do | |||
| expect(json).to include('id', 'title', 'url') | |||
| # tags が name を含むこと(API 側の serialization が正しいこと) | |||
| expect(json).to have_key('tags') | |||
| expect(json['tags']).to be_an(Array) | |||
| expect(json['tags'][0]).to have_key('name') | |||
| expect(json['tags'][0]['name']).to eq('spec_tag') | |||
| names = json.fetch('tags').map { |t| t['name'] } | |||
| expect(names).to include('spec_tag') | |||
| expect(names).not_to include('manko') | |||
| end | |||
| context "when nico tag already exists in tags" do | |||