Compare commits

...

2 Commits

Author SHA1 Message Date
  みてるぞ df385d9768 #247 1 day ago
  みてるぞ a74696fc43 #247 1 day ago
12 changed files with 438 additions and 4 deletions
Unified View
  1. +47
    -0
      backend/app/controllers/deerjikists_controller.rb
  2. +21
    -0
      backend/app/controllers/tags_controller.rb
  3. +21
    -0
      backend/app/models/deerjikist.rb
  4. +9
    -0
      backend/app/models/tag.rb
  5. +16
    -0
      backend/app/representations/deerjikist_repr.rb
  6. +0
    -0
      backend/app/representations/wiki_page_repr.rb
  7. +19
    -1
      backend/config/routes.rb
  8. +10
    -0
      backend/db/migrate/20260303122700_create_deerjikists.rb
  9. +10
    -1
      backend/db/schema.rb
  10. +2
    -2
      backend/spec/factories/tags.rb
  11. +181
    -0
      backend/spec/requests/deerjikists_spec.rb
  12. +102
    -0
      backend/spec/requests/tags_deerjikists_spec.rb

+ 47
- 0
backend/app/controllers/deerjikists_controller.rb View File

@@ -0,0 +1,47 @@
class DeerjikistsController < ApplicationController
def show
platform = params[:platform].to_s.strip
code = params[:code].to_s.strip
return head :bad_request if platform.blank? || code.blank?

deerjikist = Deerjikist
.joins(:tag)
.includes(tag: :tag_name)
.find_by(platform:, code:)
if deerjikist
render json: DeerjikistRepr.base(deerjikist)
else
head :not_found
end
end

def update
return head :unauthorized unless current_user
return head :forbidden unless current_user.member?

platform = params[:platform].to_s.strip
code = params[:code].to_s.strip
tag_id = params[:tag_id].to_i
return head :bad_request if platform.blank? || code.blank? || tag_id <= 0

deerjikist = Deerjikist.find_or_initialize_by(platform:, code:).tap do |d|
d.tag_id = tag_id
d.save!
end

render json: DeerjikistRepr.base(deerjikist)
end

def destroy
return head :unauthorized unless current_user
return head :forbidden unless current_user.member?

platform = params[:platform].to_s.strip
code = params[:code].to_s.strip
return head :bad_request if platform.blank? || code.blank?

Deerjikist.find([platform, code]).destroy!

head :no_content
end
end

+ 21
- 0
backend/app/controllers/tags_controller.rb View File

@@ -86,6 +86,27 @@ class TagsController < ApplicationController
end end
end end


def deerjikists
tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(id: params[:id])
return head :not_found unless tag

render json: DeerjikistRepr.many(tag.deerjikists)
end

def deerjikists_by_name
name = params[:name].to_s.strip
return head :bad_request if name.blank?

tag = Tag.joins(:tag_name)
.includes(:tag_name, tag_name: :wiki_page)
.find_by(tag_names: { name: })
return head :not_found unless tag

render json: DeerjikistRepr.many(tag.deerjikists)
end

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?


+ 21
- 0
backend/app/models/deerjikist.rb View File

@@ -0,0 +1,21 @@
class Deerjikist < ApplicationRecord
self.primary_key = :platform, :code

belongs_to :tag

validates :platform, presence: true
validates :code, presence: true
validates :tag_id, presence: true

validate :tag_must_be_deerjikist

enum :platform, nico: 'nico', youtube: 'youtube'

private

def tag_must_be_deerjikist
if tag && !(tag.deerjikist?)
errors.add :tag, 'タグはニジラー・カテゴリである必要があります.'
end
end
end

+ 9
- 0
backend/app/models/tag.rb View File

@@ -24,6 +24,8 @@ class Tag < ApplicationRecord


has_many :tag_similarities, dependent: :delete_all has_many :tag_similarities, dependent: :delete_all


has_many :deerjikists, dependent: :delete_all

belongs_to :tag_name belongs_to :tag_name
delegate :wiki_page, to: :tag_name delegate :wiki_page, to: :tag_name


@@ -42,6 +44,7 @@ class Tag < ApplicationRecord


validate :nico_tag_name_must_start_with_nico validate :nico_tag_name_must_start_with_nico
validate :tag_name_must_be_canonical validate :tag_name_must_be_canonical
validate :category_must_be_deerjikist_with_deerjikists


scope :nico_tags, -> { where(category: :nico) } scope :nico_tags, -> { where(category: :nico) }


@@ -149,4 +152,10 @@ class Tag < ApplicationRecord
errors.add :tag_name, 'tag_names へは実体を示す必要があります.' errors.add :tag_name, 'tag_names へは実体を示す必要があります.'
end end
end end

def category_must_be_deerjikist_with_deerjikists
if !(deerjikist?) && deerjikists.exists?
errors.add :category, 'ニジラーと紐づいてゐるタグはニジラー・カテゴリである必要があります.'
end
end
end end

+ 16
- 0
backend/app/representations/deerjikist_repr.rb View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true


module DeerjikistRepr
BASE = { only: [:platform, :code], include: { tag: TagRepr::BASE } }.freeze

module_function

def base deerjikist
deerjikist.as_json(BASE)
end

def many deerjikists
deerjikists.map { |d| base(d) }
end
end

backend/app/representations/WikiPageRepr.rb → backend/app/representations/wiki_page_repr.rb View File


+ 19
- 1
backend/config/routes.rb View File

@@ -9,7 +9,15 @@ Rails.application.routes.draw do
resources :tags, only: [:index, :show, :update] do resources :tags, only: [:index, :show, :update] do
collection do collection do
get :autocomplete get :autocomplete
get 'name/:name', action: :show_by_name

scope :name do
get ':name/deerjikists', action: :deerjikists_by_name
get ':name', action: :show_by_name
end
end

member do
get :deerjikists
end end
end end


@@ -54,4 +62,14 @@ Rails.application.routes.draw do
post 'code/renew', action: :renew post 'code/renew', action: :renew
end end
end end

resources :deerjikists, only: [] do
collection do
scope ':platform/:code' do
get '', action: :show
put '', action: :update
delete '', action: :destroy
end
end
end
end end

+ 10
- 0
backend/db/migrate/20260303122700_create_deerjikists.rb View File

@@ -0,0 +1,10 @@
class CreateDeerjikists < ActiveRecord::Migration[8.0]
def change
create_table :deerjikists, primary_key: [:platform, :code] do |t|
t.string :platform, null: false, limit: 16
t.string :code, null: false
t.references :tag, null: false
t.timestamps
end
end
end

+ 10
- 1
backend/db/schema.rb View File

@@ -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: 2026_01_27_005300) do
ActiveRecord::Schema[8.0].define(version: 2026_03_03_122700) 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
@@ -39,6 +39,15 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_27_005300) do
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end end


create_table "deerjikists", primary_key: ["platform", "code"], charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "platform", limit: 16, null: false
t.string "code", null: false
t.bigint "tag_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["tag_id"], name: "index_deerjikists_on_tag_id"
end

create_table "ip_addresses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "ip_addresses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.binary "ip_address", limit: 16, null: false t.binary "ip_address", limit: 16, null: false
t.boolean "banned", default: false, null: false t.boolean "banned", default: false, null: false


+ 2
- 2
backend/spec/factories/tags.rb View File

@@ -1,11 +1,11 @@
FactoryBot.define do FactoryBot.define do
factory :tag do factory :tag do
category { 'general' }
category { :general }
post_count { 0 } post_count { 0 }
association :tag_name association :tag_name


trait :nico do trait :nico do
category { 'nico' }
category { :nico }
tag_name { association(:tag_name, name: "nico:#{ SecureRandom.hex(4) }") } tag_name { association(:tag_name, name: "nico:#{ SecureRandom.hex(4) }") }
end end
end end


+ 181
- 0
backend/spec/requests/deerjikists_spec.rb View File

@@ -0,0 +1,181 @@
require 'rails_helper'

RSpec.describe 'Deerjikists API', type: :request do
let(:platform) { 'nico' }
let(:code) { 'deerjika-bot' }

let!(:tag1) { create(:tag, category: :deerjikist) }
let!(:tag2) { create(:tag, category: :deerjikist) }

let(:member) { create(:user, :member) }
let(:guest) { create(:user, role: :guest) }

describe 'GET /deerjikists/:platform/:code' do
subject(:do_request) do
get "/deerjikists/#{ platform }/#{ code }"
end

context 'when deerjikist exists' do
before do
Deerjikist.create!(platform:, code:, tag: tag1)
end

it 'returns 200 and deerjikist json' do
do_request
expect(response).to have_http_status(:ok)

expect(json).to be_a(Hash)
expect(json['platform']).to eq(platform)
expect(json['code']).to eq(code)

expect(json['tag']).to be_a(Hash)
expect(json['tag']['id']).to eq(tag1.id)
expect(json['tag']['name']).to eq(tag1.name)
end
end

context 'when deerjikist does not exist' do
it 'returns 404' do
do_request
expect(response).to have_http_status(:not_found)
end
end

context 'when platform or code become blank after strip' do
it 'returns 400' do
get '/deerjikists/%20/%20'
expect(response).to have_http_status(:bad_request)
end
end
end

describe 'PUT /deerjikists/:platform/:code' do
subject(:do_request) do
put "/deerjikists/#{ platform }/#{ code }", params: payload
end

let(:payload) { { tag_id: tag1.id } }

context 'when not legged in' do
it 'returns 401' do
sign_out
do_request
expect(response).to have_http_status(:unauthorized)
end
end

context 'when logged in but not member' do
it 'returns 403' do
sign_in_as guest
do_request
expect(response).to have_http_status(:forbidden)
end
end

context 'when member' do
before do
sign_in_as member
end

context 'when params invalid' do
it 'returns 400 when tag_id is missing or invalid' do
put "/deerjikists/#{ platform }/#{ code }", params: { tag_id: 0 }
expect(response).to have_http_status(:bad_request)
end

it 'returns 400 when platform or code blank after strip' do
put '/deerjikists/%20/%20', params: { tag_id: tag1.id }
expect(response).to have_http_status(:bad_request)
end
end

context 'when creating new deerjikist' do
it 'creates and returns 200 with json' do
expect { do_request }.to change { Deerjikist.count }.by(1)
expect(response).to have_http_status(:ok)

d = Deerjikist.find_by(platform:, code:)
expect(d).to be_present
expect(d.tag_id).to eq(tag1.id)

expect(json['platform']).to eq(platform)
expect(json['code']).to eq(code)
end
end

context 'when updating existing deerjikist' do
before do
Deerjikist.create!(platform:, code:, tag: tag1)
end

let(:payload) { { tag_id: tag2.id } }

it 'updates tag_id and returns 200' do
expect { do_request }.not_to change { Deerjikist.count }
expect(response).to have_http_status(:ok)

d = Deerjikist.find_by(platform:, code:)
expect(d.tag_id).to eq(tag2.id)

expect(json['platform']).to eq(platform)
expect(json['code']).to eq(code)
end
end
end
end

describe 'DELETE /deerjikists/:platform/:code' do
subject(:do_request) do
delete "/deerjikists/#{ platform }/#{ code }"
end

context 'when not logged in' do
it 'returns 401' do
sign_out
do_request
expect(response).to have_http_status(:unauthorized)
end
end

context 'when logged in but not member' do
it 'returns 403' do
sign_in_as guest
do_request
expect(response).to have_http_status(:forbidden)
end
end

context 'when member' do
before do
sign_in_as member
end

it 'returns 400 when platform/code blank after strip' do
delete '/deerjikists/%20/%20'
expect(response).to have_http_status(:bad_request)
end

context 'when deerjikist exists' do
before do
Deerjikist.create!(platform: platform, code: code, tag: tag1)
end

it 'destroys and returns 204' do
expect {
do_request
}.to change { Deerjikist.exists?(platform: platform, code: code) }
.from(true).to(false)

expect(response).to have_http_status(:no_content)
end
end

context 'when deerjikist does not exist' do
it 'returns 404' do
do_request
expect(response).to have_http_status(:not_found)
end
end
end
end
end

+ 102
- 0
backend/spec/requests/tags_deerjikists_spec.rb View File

@@ -0,0 +1,102 @@
require 'rails_helper'

RSpec.describe 'Tags deerjikists API', type: :request do
let(:platform1) { 'nico' }
let(:code1) { 'deerjika-bot' }
let(:platform2) { 'youtube' }
let(:code2) { 'deerjika-bot.bsky.social' }

let!(:tag) { create(:tag, category: :deerjikist) }

before do
# show_by_name / deerjikists_by_name 用に名前を固定
tag.tag_name.update!(name: 'deerjika')
end

describe 'GET /tags/:id/deerjikists' do
subject(:do_request) do
get "/tags/#{ tag_id }/deerjikists"
end

let(:tag_id) { tag.id }

context 'when tag exists and has no deerjikists' do
it 'returns 200 and empty array' do
do_request
expect(response).to have_http_status(:ok)
expect(json).to eq([])
end
end

context 'when tag exists and has deerjikists' do
before do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
Deerjikist.create!(platform: platform2, code: code2, tag: tag)
end

it 'returns 200 and deerjikists array' do
do_request
expect(response).to have_http_status(:ok)

expect(json).to be_a(Array)
expect(json.size).to eq(2)

expect(json.map { |h| [h['platform'], h['code']] }).to contain_exactly(
[platform1, code1],
[platform2, code2],
)
end
end

context 'when tag does not exist' do
let(:tag_id) { 9_999_999 }

it 'returns 404' do
do_request
expect(response).to have_http_status(:not_found)
end
end
end

describe 'GET /tags/name/:name/deerjikists' do
subject(:do_request) do
get "/tags/name/#{ name }/deerjikists"
end

let(:name) { 'deerjika' }

context 'when name is blank after strip' do
let(:name) { '%20' }

it 'returns 400' do
do_request
expect(response).to have_http_status(:bad_request)
end
end

context 'when tag does not exist for name' do
let(:name) { 'no-such-tag' }

it 'returns 404' do
do_request
expect(response).to have_http_status(:not_found)
end
end

context 'when tag exists and has deerjikists' do
before do
Deerjikist.create!(platform: platform1, code: code1, tag: tag)
end

it 'returns 200 and deerjikists array' do
do_request
expect(response).to have_http_status(:ok)

expect(json).to be_a(Array)
expect(json.size).to eq(1)
expect(json[0]['platform']).to eq(platform1)
expect(json[0]['code']).to eq(code1)
end
end
end
end

Loading…
Cancel
Save