feat: URL 正規化(#208) #226
@@ -17,7 +17,12 @@ class Post < ApplicationRecord
|
|||||||
foreign_key: :target_post_id
|
foreign_key: :target_post_id
|
||||||
has_one_attached :thumbnail
|
has_one_attached :thumbnail
|
||||||
|
|
||||||
|
before_validation :normalise_url
|
||||||
|
|
||||||
|
validates :url, presence: true, uniqueness: true
|
||||||
|
|
||||||
validate :validate_original_created_range
|
validate :validate_original_created_range
|
||||||
|
validate :url_must_be_http_url
|
||||||
|
|
||||||
def as_json options = { }
|
def as_json options = { }
|
||||||
super(options).merge({ thumbnail: thumbnail.attached? ?
|
super(options).merge({ thumbnail: thumbnail.attached? ?
|
||||||
@@ -69,4 +74,33 @@ class Post < ApplicationRecord
|
|||||||
errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.'
|
errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def url_must_be_http_url
|
||||||
|
begin
|
||||||
|
u = URI.parse(url)
|
||||||
|
rescue URI::InvalidURIError
|
||||||
|
errors.add(:url, 'URL が不正です.')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if !(u in URI::HTTP) || u.host.blank?
|
||||||
|
errors.add(:url, 'URL が不正です.')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalise_url
|
||||||
|
return if url.blank?
|
||||||
|
|
||||||
|
self.url = url.strip
|
||||||
|
|
||||||
|
u = URI.parse(url)
|
||||||
|
return unless u in URI::HTTP
|
||||||
|
|
||||||
|
u.host = u.host.downcase if u.host
|
||||||
|
u.path = u.path.sub(/\/\Z/, '') if u.path.present?
|
||||||
|
self.url = u.to_s
|
||||||
|
rescue URI::InvalidURIError
|
||||||
|
;
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,4 +28,8 @@
|
|||||||
# enabled: "ON"
|
# enabled: "ON"
|
||||||
|
|
||||||
en:
|
en:
|
||||||
hello: "Hello world"
|
activerecord:
|
||||||
|
errors:
|
||||||
|
messages:
|
||||||
|
record_invalid: "Validation failed: %{errors}"
|
||||||
|
taken: 'イキスギ!'
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
class AddUniqueIndexToUrlInPosts < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
change_column :posts, :url, :string, limit: 768
|
||||||
|
add_index :posts, :url, unique: true, name: 'index_posts_on_url'
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+3
-2
@@ -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_12_111800) do
|
ActiveRecord::Schema[8.0].define(version: 2026_01_18_144400) 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
|
||||||
@@ -85,7 +85,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_12_111800) do
|
|||||||
|
|
||||||
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "title"
|
t.string "title"
|
||||||
t.string "url", limit: 2000, null: false
|
t.string "url", limit: 768, null: false
|
||||||
t.string "thumbnail_base", limit: 2000
|
t.string "thumbnail_base", limit: 2000
|
||||||
t.bigint "parent_id"
|
t.bigint "parent_id"
|
||||||
t.bigint "uploaded_user_id"
|
t.bigint "uploaded_user_id"
|
||||||
@@ -95,6 +95,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_12_111800) do
|
|||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["parent_id"], name: "index_posts_on_parent_id"
|
t.index ["parent_id"], name: "index_posts_on_parent_id"
|
||||||
t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id"
|
t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id"
|
||||||
|
t.index ["url"], name: "index_posts_on_url", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "settings", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
|||||||
@@ -33,14 +33,14 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
|
|
||||||
let!(:hit_post) do
|
let!(:hit_post) do
|
||||||
Post.create!(uploaded_user: user, title: "hello spec world",
|
Post.create!(uploaded_user: user, title: "hello spec world",
|
||||||
url: 'https://example.com/spec').tap do |p|
|
url: 'https://example.com/spec2').tap do |p|
|
||||||
PostTag.create!(post: p, tag:)
|
PostTag.create!(post: p, tag:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
let!(:miss_post) do
|
let!(:miss_post) do
|
||||||
Post.create!(uploaded_user: user, title: "unrelated title",
|
Post.create!(uploaded_user: user, title: "unrelated title",
|
||||||
url: 'https://example.com/spec2').tap do |p|
|
url: 'https://example.com/spec3').tap do |p|
|
||||||
PostTag.create!(post: p, tag: tag2)
|
PostTag.create!(post: p, tag: tag2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -158,6 +158,36 @@ RSpec.describe 'Posts API', type: :request do
|
|||||||
expect(json['tags']).to be_an(Array)
|
expect(json['tags']).to be_an(Array)
|
||||||
expect(json['tags'][0]).to have_key('name')
|
expect(json['tags'][0]).to have_key('name')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when url is blank' do
|
||||||
|
it 'returns 422' do
|
||||||
|
sign_in_as(member)
|
||||||
|
|
||||||
|
post '/posts', params: {
|
||||||
|
title: 'new post',
|
||||||
|
url: ' ',
|
||||||
|
tags: 'spec_tag', # 既存タグ名を投げる
|
||||||
|
thumbnail: dummy_upload
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when url is invalid' do
|
||||||
|
it 'returns 422' do
|
||||||
|
sign_in_as(member)
|
||||||
|
|
||||||
|
post '/posts', params: {
|
||||||
|
title: 'new post',
|
||||||
|
url: 'ぼざクリタグ広場',
|
||||||
|
tags: 'spec_tag', # 既存タグ名を投げる
|
||||||
|
thumbnail: dummy_upload
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT /posts/:id' do
|
describe 'PUT /posts/:id' do
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ RSpec.describe 'Wiki API', type: :request do
|
|||||||
|
|
||||||
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
|
let!(:tn) { TagName.create!(name: 'spec_wiki_title') }
|
||||||
let!(:page) do
|
let!(:page) do
|
||||||
WikiPage.create!(tag_name: tn, created_user: user, updated_user: user)
|
WikiPage.create!(tag_name: tn, created_user: user, updated_user: user).tap do |p|
|
||||||
|
Wiki::Commit.content!(page: p, body: 'init', created_user: user, message: 'init')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /wiki' do
|
describe 'GET /wiki' do
|
||||||
|
|||||||
Reference in New Issue
Block a user