Browse Source

#99

feature/099
みてるぞ 2 weeks ago
parent
commit
c28326b941
10 changed files with 274 additions and 30 deletions
  1. +2
    -2
      backend/Gemfile
  2. +21
    -0
      backend/Gemfile.lock
  3. +94
    -0
      backend/app/controllers/materials_controller.rb
  4. +31
    -0
      backend/app/models/material.rb
  5. +17
    -0
      backend/app/representations/material_repr.rb
  6. +1
    -1
      backend/config/environments/production.rb
  7. +2
    -0
      backend/config/routes.rb
  8. +7
    -26
      backend/config/storage.yml
  9. +34
    -0
      backend/db/migrate/20260329034700_create_materials.rb
  10. +65
    -1
      backend/db/schema.rb

+ 2
- 2
backend/Gemfile View File

@@ -50,8 +50,6 @@ group :development, :test do
gem 'factory_bot_rails' gem 'factory_bot_rails'
end end




gem "mysql2", "~> 0.5.6" gem "mysql2", "~> 0.5.6"


gem "image_processing", "~> 1.14" gem "image_processing", "~> 1.14"
@@ -69,3 +67,5 @@ gem 'whenever', require: false
gem 'discard' gem 'discard'


gem "rspec-rails", "~> 8.0", :groups => [:development, :test] gem "rspec-rails", "~> 8.0", :groups => [:development, :test]

gem 'aws-sdk-s3', require: false

+ 21
- 0
backend/Gemfile.lock View File

@@ -73,6 +73,25 @@ GEM
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1) uri (>= 0.13.1)
ast (2.4.3) ast (2.4.3)
aws-eventstream (1.4.0)
aws-partitions (1.1231.0)
aws-sdk-core (3.244.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.123.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.217.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.2.0) base64 (0.2.0)
bcrypt_pbkdf (1.1.1) bcrypt_pbkdf (1.1.1)
bcrypt_pbkdf (1.1.1-arm64-darwin) bcrypt_pbkdf (1.1.1-arm64-darwin)
@@ -157,6 +176,7 @@ GEM
pp (>= 0.6.0) pp (>= 0.6.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2)
json (2.12.0) json (2.12.0)
jwt (2.10.1) jwt (2.10.1)
base64 base64
@@ -441,6 +461,7 @@ PLATFORMS
x86_64-linux-musl x86_64-linux-musl


DEPENDENCIES DEPENDENCIES
aws-sdk-s3
bootsnap bootsnap
brakeman brakeman
diff-lcs diff-lcs


+ 94
- 0
backend/app/controllers/materials_controller.rb View File

@@ -0,0 +1,94 @@
class MaterialsController < ApplicationController
def index
page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i

page = 1 if page < 1
limit = 1 if limit < 1

offset = (page - 1) * limit

tag_id = params[:tag_id].presence
parent_id = params[:parent_id].presence

q = Material.includes(:tag, :created_by_user).with_attached_file
q = q.where(tag_id:) if tag_id
q = q.where(parent_id:) if parent_id

count = q.count
materials = q.order(created_at: :desc, id: :desc).limit(limit).offset(offset)

render json: { materials: materials.map { |m| material_json(m) }, count: count }
end

def show
material = Material.includes(:tag, :created_by_user).with_attached_file.find_by(id: params[:id])
return head :not_found unless material

render json: material_json(material)
end

def create
return head :unauthorized unless current_user
return head :forbidden unless current_user.gte_member?

file = params[:file]
return head :bad_request if file.blank?

material = Material.new(
url: params[:url].presence,
parent_id: params[:parent_id].presence,
tag_id: params[:tag_id].presence,
created_by_user: current_user)
material.file.attach(file)

if material.save
render json: material_json(material), status: :created
else
render json: { errors: material.errors.full_messages }, status: :unprocessable_entity
end
end

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

material = Material.with_attached_file.find_by(id: params[:id])
return head :not_found unless material

material.assign_attributes(
url: params[:url].presence,
parent_id: params[:parent_id].presence,
tag_id: params[:tag_id].presence
)
material.file.attach(params[:file]) if params[:file].present?

if material.save
render json: material_json(material)
else
render json: { errors: material.errors.full_messages }, status: :unprocessable_entity
end
end

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

material = Material.find_by(id: params[:id])
return head :not_found unless material

material.discard
head :no_content
end

private

def material_json(material)
MaterialRepr.base(material).merge(
'filename' => material.file.attached? ? material.file.filename.to_s : nil,
'byte_size' => material.file.attached? ? material.file.byte_size : nil,
'content_type' => material.file.attached? ? material.file.content_type : nil,
'url' => material.file.attached? ? url_for(material.file) : nil
)
end
end

+ 31
- 0
backend/app/models/material.rb View File

@@ -0,0 +1,31 @@
class Material < ApplicationRecord
include MyDiscard

default_scope -> { kept }

belongs_to :parent, class_name: 'Material', optional: true
has_many :children, class_name: 'Material', foreign_key: :parent_id, dependent: :nullify

belongs_to :tag, optional: true
belongs_to :created_by_user, class_name: 'User', optional: true
belongs_to :updated_by_user, class_name: 'User', optional: true

has_one_attached :file, dependent: :purge

validate :file_must_be_attached
validate :tag_must_be_material_category

private

def file_must_be_attached
return if url.present? || file.attached?

errors.add(:url, 'URL かファイルのどちらかは必須です.')
end

def tag_must_be_material_category
return if tag.blank? || tag.material?

errors.add(:tag, '素材カテゴリのタグを指定してください.')
end
end

+ 17
- 0
backend/app/representations/material_repr.rb View File

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


module MaterialRepr
BASE = { only: [:id, :url, :parent_id, :created_at, :updated_at],
include: { created_by_user: UserRepr::BASE, tag: TagRepr::BASE } }.freeze

module_function

def base(material)
material.as_json(BASE)
end

def many(materials)
materials.map { |m| base(m) }
end
end

+ 1
- 1
backend/config/environments/production.rb View File

@@ -19,7 +19,7 @@ Rails.application.configure do
# config.asset_host = "http://assets.example.com" # config.asset_host = "http://assets.example.com"


# Store uploaded files on the local file system (see config/storage.yml for options). # Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
config.active_storage.service = :r2


# Assume all access to the app is happening through a SSL-terminating reverse proxy. # Assume all access to the app is happening through a SSL-terminating reverse proxy.
config.assume_ssl = true config.assume_ssl = true


+ 2
- 0
backend/config/routes.rb View File

@@ -81,4 +81,6 @@ Rails.application.routes.draw do


resources :comments, controller: :theatre_comments, only: [:index, :create] resources :comments, controller: :theatre_comments, only: [:index, :create]
end end

resources :materials, only: [:index, :show, :create, :update, :destroy]
end end

+ 7
- 26
backend/config/storage.yml View File

@@ -6,29 +6,10 @@ local:
service: Disk service: Disk
root: <%= Rails.root.join("storage") %> root: <%= Rails.root.join("storage") %>


# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket-<%= Rails.env %>

# Remember not to checkin your GCS keyfile to a repository
# google:
# service: GCS
# project: your_project
# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
# bucket: your_own_bucket-<%= Rails.env %>

# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
# service: AzureStorage
# storage_account_name: your_account_name
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
# container: your_container_name-<%= Rails.env %>

# mirror:
# service: Mirror
# primary: local
# mirrors: [ amazon, google, microsoft ]
r2:
service: S3
endpoint: <%= ENV['R2_ENDPOINT'] %>
access_key_id: <%= ENV['R2_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['R2_SECRET_ACCESS_KEY'] %>
bucket: <%= ENV['R2_BUCKET'] %>
region: auto

+ 34
- 0
backend/db/migrate/20260329034700_create_materials.rb View File

@@ -0,0 +1,34 @@
class CreateMaterials < ActiveRecord::Migration[8.0]
def change
create_table :materials do |t|
t.string :url
t.references :parent, index: true, foreign_key: { to_table: :materials }
t.references :tag, index: true, foreign_key: true
t.references :created_by_user, foreign_key: { to_table: :users }
t.references :updated_by_user, foreign_key: { to_table: :users }
t.timestamps
t.datetime :discarded_at, index: true
t.virtual :active_url, type: :string,
as: 'IF(discarded_at IS NULL, url, NULL)',
stored: false

t.index :active_url, unique: true
end

create_table :material_versions do |t|
t.references :material, null: false, foreign_key: true
t.integer :version_no, null: false
t.string :url, index: true
t.references :parent, index: true, foreign_key: { to_table: :materials }
t.references :tag, index: true, foreign_key: true
t.references :created_by_user, foreign_key: { to_table: :users }
t.references :updated_by_user, foreign_key: { to_table: :users }
t.timestamps
t.datetime :discarded_at, index: true

t.index [:material_id, :version_no],
unique: true,
name: 'index_material_versions_on_material_id_and_version_no'
end
end
end

+ 65
- 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_03_17_015000) do
ActiveRecord::Schema[8.0].define(version: 2026_03_29_034700) 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
@@ -56,6 +56,45 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_17_015000) do
t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true t.index ["ip_address"], name: "index_ip_addresses_on_ip_address", unique: true
end end


create_table "material_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "material_id", null: false
t.integer "version_no", null: false
t.string "url"
t.bigint "parent_id"
t.bigint "tag_id"
t.bigint "created_by_user_id"
t.bigint "updated_by_user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "discarded_at"
t.index ["created_by_user_id"], name: "index_material_versions_on_created_by_user_id"
t.index ["discarded_at"], name: "index_material_versions_on_discarded_at"
t.index ["material_id", "version_no"], name: "index_material_versions_on_material_id_and_version_no", unique: true
t.index ["material_id"], name: "index_material_versions_on_material_id"
t.index ["parent_id"], name: "index_material_versions_on_parent_id"
t.index ["tag_id"], name: "index_material_versions_on_tag_id"
t.index ["updated_by_user_id"], name: "index_material_versions_on_updated_by_user_id"
t.index ["url"], name: "index_material_versions_on_url"
end

create_table "materials", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "url"
t.bigint "parent_id"
t.bigint "tag_id"
t.bigint "created_by_user_id"
t.bigint "updated_by_user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "discarded_at"
t.virtual "active_url", type: :string, as: "if((`discarded_at` is null),`url`,NULL)"
t.index ["active_url"], name: "index_materials_on_active_url", unique: true
t.index ["created_by_user_id"], name: "index_materials_on_created_by_user_id"
t.index ["discarded_at"], name: "index_materials_on_discarded_at"
t.index ["parent_id"], name: "index_materials_on_parent_id"
t.index ["tag_id"], name: "index_materials_on_tag_id"
t.index ["updated_by_user_id"], name: "index_materials_on_updated_by_user_id"
end

create_table "nico_tag_relations", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "nico_tag_relations", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "nico_tag_id", null: false t.bigint "nico_tag_id", null: false
t.bigint "tag_id", null: false t.bigint "tag_id", null: false
@@ -239,6 +278,19 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_17_015000) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end


create_table "wiki_assets", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "wiki_page_id", null: false
t.integer "no", null: false
t.string "alt_text"
t.binary "sha256", limit: 32, null: false
t.bigint "created_by_user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["created_by_user_id"], name: "index_wiki_assets_on_created_by_user_id"
t.index ["wiki_page_id", "no"], name: "index_wiki_assets_on_wiki_page_id_and_no", unique: true
t.index ["wiki_page_id", "sha256"], name: "index_wiki_assets_on_wiki_page_id_and_sha256", unique: true
end

create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "sha256", limit: 64, null: false t.string "sha256", limit: 64, null: false
t.text "body", null: false t.text "body", null: false
@@ -254,6 +306,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_17_015000) do
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.datetime "discarded_at" t.datetime "discarded_at"
t.integer "next_asset_no", default: 1, 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 ["discarded_at"], name: "index_wiki_pages_on_discarded_at" t.index ["discarded_at"], name: "index_wiki_pages_on_discarded_at"
t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true t.index ["tag_name_id"], name: "index_wiki_pages_on_tag_name_id", unique: true
@@ -292,6 +345,15 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_17_015000) do


add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "material_versions", "materials"
add_foreign_key "material_versions", "materials", column: "parent_id"
add_foreign_key "material_versions", "tags"
add_foreign_key "material_versions", "users", column: "created_by_user_id"
add_foreign_key "material_versions", "users", column: "updated_by_user_id"
add_foreign_key "materials", "materials", column: "parent_id"
add_foreign_key "materials", "tags"
add_foreign_key "materials", "users", column: "created_by_user_id"
add_foreign_key "materials", "users", column: "updated_by_user_id"
add_foreign_key "nico_tag_relations", "tags" add_foreign_key "nico_tag_relations", "tags"
add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id" add_foreign_key "nico_tag_relations", "tags", column: "nico_tag_id"
add_foreign_key "post_similarities", "posts" add_foreign_key "post_similarities", "posts"
@@ -320,6 +382,8 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_17_015000) do
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_assets", "users", column: "created_by_user_id"
add_foreign_key "wiki_assets", "wiki_pages"
add_foreign_key "wiki_pages", "tag_names" 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"


Loading…
Cancel
Save