コミットを比較
5 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 360d3c2a9c | |||
| ff2954cc63 | |||
| 3a8f7d8d89 | |||
| 84fe36a405 | |||
| b19c24cb8e |
@@ -57,7 +57,7 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
attribute :version_no, :integer, default: 1
|
attribute :version_no, :integer, default: 1
|
||||||
|
|
||||||
before_validation :normalise_url, if: :will_save_change_to_url?
|
before_validation :normalise_url
|
||||||
|
|
||||||
validates :url, presence: true, uniqueness: true
|
validates :url, presence: true, uniqueness: true
|
||||||
validates :video_ms, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
validates :video_ms, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
||||||
@@ -180,7 +180,7 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
u.host = u.host.downcase if u.host
|
u.host = u.host.downcase if u.host
|
||||||
u.path = u.path.sub(/\/\Z/, '') if u.path.present?
|
u.path = u.path.sub(/\/\Z/, '') if u.path.present?
|
||||||
self.url = PostUrlSanitisationRule.sanitise(u.to_s)
|
self.url = u.to_s
|
||||||
rescue URI::InvalidURIError
|
rescue URI::InvalidURIError
|
||||||
;
|
;
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
class PostUrlSanitisationRule < ApplicationRecord
|
|
||||||
include Discard::Model
|
|
||||||
|
|
||||||
class UrlConflictError < StandardError
|
|
||||||
attr_reader :conflicts
|
|
||||||
|
|
||||||
def initialize(conflicts)
|
|
||||||
@conflicts = conflicts
|
|
||||||
urls = conflicts.map { _1.fetch(:url) }.uniq.join(', ')
|
|
||||||
super("post URL sanitisation conflicts detected for #{ urls }")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.primary_key = :priority
|
|
||||||
|
|
||||||
default_scope -> { kept }
|
|
||||||
|
|
||||||
validates :source_pattern, presence: true, uniqueness: true
|
|
||||||
|
|
||||||
validate :source_pattern_must_be_regexp
|
|
||||||
|
|
||||||
class << self
|
|
||||||
def sanitise(url) =
|
|
||||||
rules.reduce(url.dup) { |value, (pattern, replacement)| value.sub(pattern, replacement) }
|
|
||||||
|
|
||||||
def apply!
|
|
||||||
rewrites = Post.order(:id).pluck(:id, :url).map do |post_id, original_url|
|
|
||||||
{ post_id:,
|
|
||||||
original_url:,
|
|
||||||
sanitised_url: sanitise(original_url) }
|
|
||||||
end
|
|
||||||
|
|
||||||
conflicts = rewrites
|
|
||||||
.group_by { _1.fetch(:sanitised_url) }
|
|
||||||
.values
|
|
||||||
.filter { _1.size > 1 }
|
|
||||||
.flatten
|
|
||||||
.map { { url: _1.fetch(:sanitised_url),
|
|
||||||
post_id: _1.fetch(:post_id),
|
|
||||||
original_url: _1.fetch(:original_url) } }
|
|
||||||
|
|
||||||
raise UrlConflictError.new(conflicts) if conflicts.present?
|
|
||||||
|
|
||||||
changed = rewrites.filter { _1.fetch(:original_url) != _1.fetch(:sanitised_url) }
|
|
||||||
return if changed.empty?
|
|
||||||
|
|
||||||
token = SecureRandom.hex(6)
|
|
||||||
|
|
||||||
Post.transaction do
|
|
||||||
changed.each do |row|
|
|
||||||
Post.where(id: row.fetch(:post_id))
|
|
||||||
.update_all(url: temporary_url_for(row.fetch(:post_id), token))
|
|
||||||
end
|
|
||||||
|
|
||||||
changed.each do |row|
|
|
||||||
Post.where(id: row.fetch(:post_id))
|
|
||||||
.update_all(url: row.fetch(:sanitised_url))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def rules = kept.order(:priority).map { |r| [Regexp.new(r.source_pattern), r.replacement] }
|
|
||||||
|
|
||||||
def temporary_url_for(post_id, token) =
|
|
||||||
"https://post-url-sanitising.invalid/#{ token }/#{ post_id }"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def source_pattern_must_be_regexp
|
|
||||||
Regexp.new(source_pattern)
|
|
||||||
rescue RegexpError
|
|
||||||
errors.add :source_pattern, '変な正規表現だね〜(笑)'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
class CreatePostUrlSanitisationRules < ActiveRecord::Migration[8.0]
|
|
||||||
def up
|
|
||||||
create_table :post_url_sanitisation_rules,
|
|
||||||
id: :integer,
|
|
||||||
primary_key: :priority do |t|
|
|
||||||
t.string :source_pattern, null: false
|
|
||||||
t.string :replacement, null: false
|
|
||||||
t.timestamps
|
|
||||||
t.datetime :discarded_at
|
|
||||||
t.index :source_pattern, unique: true
|
|
||||||
t.index :discarded_at
|
|
||||||
end
|
|
||||||
|
|
||||||
now = ActiveRecord::Base.connection.quote(Time.current)
|
|
||||||
execute <<~SQL
|
|
||||||
INSERT INTO
|
|
||||||
post_url_sanitisation_rules(priority, source_pattern, replacement, created_at, updated_at)
|
|
||||||
VALUES
|
|
||||||
(10, '\\Ahttps?://youtu\\\\.be/([^/?#]+)(?:[?#].*)?\\z',
|
|
||||||
'https://www.youtube.com/watch?v=\\\\1',
|
|
||||||
#{ now }, #{ now })
|
|
||||||
, (20, '\\Ahttps?://(?:www\\\\.|m\\\\.)?youtube\\\\.com/live/([^/?#]+)(?:[?#].*)?\\z',
|
|
||||||
'https://www.youtube.com/watch?v=\\\\1',
|
|
||||||
#{ now }, #{ now })
|
|
||||||
, (30, '\\Ahttps?://(?:www\\\\.|m\\\\.)?youtube\\\\.com/shorts/([^/?#]+)(?:[?#].*)?\\z',
|
|
||||||
'https://www.youtube.com/watch?v=\\\\1',
|
|
||||||
#{ now }, #{ now })
|
|
||||||
, (40, '\\Ahttps?://(?:www\\\\.|m\\\\.)?youtube\\\\.com/embed/([^/?#]+)(?:[?#].*)?\\z',
|
|
||||||
'https://www.youtube.com/watch?v=\\\\1',
|
|
||||||
#{ now }, #{ now })
|
|
||||||
, (50, '\\Ahttps?://(?:www\\\\.|m\\\\.)?youtube\\\\.com/watch\\\\?(?:[^#&]+&)*v=([^&#]+)(?:[&#].*)?\\z',
|
|
||||||
'https://www.youtube.com/watch?v=\\\\1',
|
|
||||||
#{ now }, #{ now })
|
|
||||||
, (60, '\\Ahttps?://nico\\\\.ms/([^/?#]+)(?:[?#].*)?\\z',
|
|
||||||
'https://www.nicovideo.jp/watch/\\\\1',
|
|
||||||
#{ now }, #{ now })
|
|
||||||
, (70, '\\Ahttps?://(?:www\\\\.)?nicovideo\\\\.jp/watch/([^?#/]+)(?:[?#].*)?\\z',
|
|
||||||
'https://www.nicovideo.jp/watch/\\\\1',
|
|
||||||
#{ now }, #{ now })
|
|
||||||
;
|
|
||||||
SQL
|
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
|
||||||
drop_table :post_url_sanitisation_rules
|
|
||||||
end
|
|
||||||
end
|
|
||||||
生成ファイル
+1
-11
@@ -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_07_13_000000) do
|
ActiveRecord::Schema[8.0].define(version: 2026_07_05_000000) 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
|
||||||
@@ -331,16 +331,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_13_000000) do
|
|||||||
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "post_url_sanitisation_rules", primary_key: "priority", id: :integer, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.string "source_pattern", null: false
|
|
||||||
t.string "replacement", null: false
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.datetime "discarded_at"
|
|
||||||
t.index ["discarded_at"], name: "index_post_url_sanitisation_rules_on_discarded_at"
|
|
||||||
t.index ["source_pattern"], name: "index_post_url_sanitisation_rules_on_source_pattern", unique: true
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "post_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "post_versions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "post_id", null: false
|
t.bigint "post_id", null: false
|
||||||
t.integer "version_no", null: false
|
t.integer "version_no", null: false
|
||||||
|
|||||||
@@ -239,11 +239,6 @@ body
|
|||||||
background: var(--top-nav-submenu-bg);
|
background: var(--top-nav-submenu-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav-mobile-menu .top-nav-submenu
|
|
||||||
{
|
|
||||||
background: var(--top-nav-mobile-active-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-nav-mobile-menu
|
.top-nav-mobile-menu
|
||||||
{
|
{
|
||||||
background: var(--top-nav-mobile-menu-bg);
|
background: var(--top-nav-mobile-menu-bg);
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import { readFileSync } from 'node:fs'
|
|
||||||
import { resolve } from 'node:path'
|
|
||||||
|
|
||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
|
|
||||||
|
|
||||||
const indexCss = readFileSync (
|
|
||||||
resolve (process.cwd (), 'src/index.css'),
|
|
||||||
'utf8')
|
|
||||||
const compactIndexCss = indexCss.replace (/\s+/g, ' ')
|
|
||||||
const mobileActiveRule = '.top-nav-mobile-active {'
|
|
||||||
+ ' background: var(--top-nav-mobile-active-bg); }'
|
|
||||||
const mobileSubmenuRule = '.top-nav-mobile-menu .top-nav-submenu {'
|
|
||||||
+ ' background: var(--top-nav-mobile-active-bg); }'
|
|
||||||
|
|
||||||
|
|
||||||
describe ('TopNav mobile menu colours', () => {
|
|
||||||
it ('uses one background variable for active rows and expanded submenus', () => {
|
|
||||||
expect (compactIndexCss).toContain (mobileActiveRule)
|
|
||||||
expect (compactIndexCss).toContain (mobileSubmenuRule)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -89,12 +89,8 @@ describe ('settings', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it ('derives TopNav colours without mixing mobile active backgrounds', () => {
|
it ('derives TopNav colours without mixing mobile active backgrounds', () => {
|
||||||
const tokens = buildThemeTokens ('light', { })
|
expect (buildThemeTokens ('light', { }).topNavMobileActiveBackground).toBe (
|
||||||
|
|
||||||
expect (tokens.topNavMobileActiveBackground).toBe (
|
|
||||||
LIGHT_THEME_TOKENS.topNavRootBackgroundDesktop)
|
LIGHT_THEME_TOKENS.topNavRootBackgroundDesktop)
|
||||||
expect (tokens.topNavMobileActiveBackground).not.toBe (
|
|
||||||
tokens.topNavRootBackgroundMobile)
|
|
||||||
expect (buildThemeTokens ('dark', { }).topNavMobileActiveBackground).toBe (
|
expect (buildThemeTokens ('dark', { }).topNavMobileActiveBackground).toBe (
|
||||||
DARK_THEME_TOKENS.topNavActiveBackground)
|
DARK_THEME_TOKENS.topNavActiveBackground)
|
||||||
})
|
})
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする