このコミットが含まれているのは:
2026-07-18 22:32:34 +09:00
コミット 5ff3fc9441
9個のファイルの変更145行の追加82行の削除
+54
ファイルの表示
@@ -140,10 +140,22 @@ npm run preview
99 文字を超えるなら block 形式へ切り替へるか、message 定数化などで縮める。
- Ruby の method chain や call argument を折り返す際、call-site の `)`
block close のやうに独立させない。
- Ruby では、行末の `\` を用途を問はず一切使用しない。
- Ruby では、文字列連結、logger message、method call、条件式、SQL 断片、
正規表現その他すべての式で、行末バックスラッシュによる継続を禁止する。
- Ruby の block body は、その基準位置から 2 空白深くする。
- Ruby の wrapped expression、method argument、array element、hash pair などの
continuation indentation は、その statement の基準位置から 4 空白深くする。
- Ruby の continuation indentation を、行頭からの絶対空白数として扱はない。
- Ruby では、暗黙的に継続可能な構文を優先し、method call、array、Hash 及び
括弧内ではバックスラッシュなしで改行する。
- Ruby では、行長制限を守るために行末バックスラッシュを導入してはならない。
- Ruby で行末バックスラッシュが必要に見える場合は、括弧内で自然に改行する、
一つの文字列補間へまとめる、中間変数へ分ける、`format` を使ふ、heredoc を
使ふ、array 又は Hash を組み立ててから処理する、条件式全体を括弧で囲む、
method へ抽出する、のいづれかへ書き換へる。
- formatter 又は自動修正にも、Ruby の行末バックスラッシュを生成させない。
- 新規 code だけでなく、今回触れる Ruby code にも行末バックスラッシュを残さない。
- 例へば class body 内の array は、class body の 2 空白を基準に、更に 4 空白
深くするため、結果として行頭から 6 空白になる。
@@ -197,6 +209,48 @@ end
Bad:
```rb
Rails.logger.info(
"post_import_metadata_fetch_failure "\
"#{ payload.to_json }")
```
Good:
```rb
payload = {
error: e.class.name,
message: e.message }
Rails.logger.info(
"post_import_metadata_fetch_failure #{ payload.to_json }")
```
Bad:
```rb
message = "first "\
"second"
```
Good:
```rb
message = format(
'%<first>s %<second>s',
first: 'first',
second: 'second')
```
Bad:
```rb
result = first_value + \
second_value
```
Bad:
```rb
records.each {
do_work(_1) }
+4 -2
ファイルの表示
@@ -138,8 +138,10 @@ class Post < ApplicationRecord
end
def self.section_literal section
"[#{ Post.ms_to_time(section.begin_ms) }-"\
"#{ section.end_ms ? Post.ms_to_time(section.end_ms) : '' }]"
end_ms =
section.end_ms ? Post.ms_to_time(section.end_ms) : ''
"[#{ Post.ms_to_time(section.begin_ms) }-#{ end_ms }]"
end
def self.ms_to_time ms
+8 -5
ファイルの表示
@@ -106,11 +106,14 @@ module PostRepr
Rails.application.routes.url_helpers.rails_storage_proxy_url(post.thumbnail, **options)
rescue ActionController::UrlGenerationError, ArgumentError, URI::InvalidURIError => e
Rails.logger.warn(
"PostRepr.thumbnail_url failed post_id=#{post.id} " \
"attachment_id=#{post.thumbnail.attachment&.id} " \
"blob_id=#{post.thumbnail.blob&.id} " \
"error_class=#{e.class} message=#{e.message}")
payload = {
post_id: post.id,
attachment_id: post.thumbnail.attachment&.id,
blob_id: post.thumbnail.blob&.id,
error_class: e.class,
message: e.message }
Rails.logger.warn("PostRepr.thumbnail_url failed #{ payload.to_json }")
nil
end
end
+8 -8
ファイルの表示
@@ -225,15 +225,15 @@ class PostImportPreviewer
end
{ data:, warnings:, validation_errors: { } }
rescue Preview::UrlSafety::UnsafeUrl => e
payload = { error: e.class.name, message: e.message }
Rails.logger.info(
"post_import_metadata_fetch_unsafe_url "\
"#{ { error: e.class.name, message: e.message }.to_json }")
"post_import_metadata_fetch_unsafe_url #{ payload.to_json }")
{ data: { }, warnings: { }, validation_errors: { url: [e.message] } }
rescue Preview::HttpFetcher::FetchFailed,
Preview::HttpFetcher::ResponseTooLarge => e
payload = { error: e.class.name, message: e.message }
Rails.logger.info(
"post_import_metadata_fetch_failure "\
"#{ { error: e.class.name, message: e.message }.to_json }")
"post_import_metadata_fetch_failure #{ payload.to_json }")
{ data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } }
end
@@ -335,14 +335,14 @@ class PostImportPreviewer
def safe_fetch_metadata url
fetch_metadata(url)
rescue Preview::UrlSafety::UnsafeUrl => e
payload = { error: e.class.name, message: e.message }
Rails.logger.info(
"post_import_metadata_fetch_unsafe_url "\
"#{ { error: e.class.name, message: e.message }.to_json }")
"post_import_metadata_fetch_unsafe_url #{ payload.to_json }")
{ data: { }, warnings: { }, validation_errors: { url: [e.message] } }
rescue StandardError => e
payload = { error: e.class.name, message: e.message }
Rails.logger.error(
"post_import_metadata_fetch_unexpected_failure "\
"#{ { error: e.class.name, message: e.message }.to_json }")
"post_import_metadata_fetch_unexpected_failure #{ payload.to_json }")
{ data: { }, warnings: { 'url' => [METADATA_FETCH_WARNING] }, validation_errors: { } }
end
+3 -4
ファイルの表示
@@ -3,10 +3,9 @@ require 'date'
class PostMetadataFetcher
TIMESTAMP_PATTERN =
Regexp.new(
'\A(\d{4})-(\d{2})-(\d{2})T(\d{2})' \
'(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?' \
'(Z|[+-]\d{2}:?\d{2})?\z')
/\A(\d{4})-(\d{2})-(\d{2})T(\d{2})
(?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?
(Z|[+-]\d{2}:?\d{2})?\z/x
def self.fetch raw_url
uri, = Preview::UrlSafety.validate(raw_url)
+3 -3
ファイルの表示
@@ -1,4 +1,4 @@
require "active_support/core_ext/integer/time"
require 'active_support/core_ext/integer/time'
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -17,8 +17,8 @@ Rails.application.configure do
# Enable/disable Action Controller caching. By default Action Controller caching is disabled.
# Run rails dev:cache to toggle Action Controller caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" }
if Rails.root.join('tmp/caching-dev.txt').exist?
config.public_file_server.headers = { 'cache-control' => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = false
end
+14 -12
ファイルの表示
@@ -101,34 +101,34 @@ RSpec.describe 'Posts API', type: :request do
end
end
describe "GET /posts" do
describe 'GET /posts' do
let!(:user) { create_member_user! }
let!(:tag_name) { TagName.create!(name: "spec_tag") }
let!(:tag_name) { TagName.create!(name: 'spec_tag') }
let!(:tag) { Tag.create!(tag_name:, category: :general) }
let!(:tag_name2) { TagName.create!(name: 'unko') }
let!(:tag2) { Tag.create!(tag_name: tag_name2, category: :deerjikist) }
let!(:alias_tag_name) { TagName.create!(name: 'manko', canonical: tag_name) }
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/spec2').tap do |p|
PostTag.create!(post: p, tag:)
end
end
let!(:miss_post) do
Post.create!(uploaded_user: user, title: "unrelated title",
Post.create!(uploaded_user: user, title: 'unrelated title',
url: 'https://example.com/spec3').tap do |p|
PostTag.create!(post: p, tag: tag2)
end
end
it 'returns posts with tag name in JSON' do
get "/posts"
get '/posts'
expect(response).to have_http_status(:ok)
posts = json.fetch("posts")
posts = json.fetch('posts')
# 全postの全tagが name を含むこと
expect(posts).not_to be_empty
@@ -141,8 +141,8 @@ RSpec.describe 'Posts API', type: :request do
expect(json['count']).to be_an(Integer)
# spec_tag を含む投稿が存在すること
all_tag_names = posts.flat_map { |p| p["tags"].map { |t| t["name"] } }
expect(all_tag_names).to include("spec_tag")
all_tag_names = posts.flat_map { |p| p['tags'].map { |t| t['name'] } }
expect(all_tag_names).to include('spec_tag')
end
it 'keeps children and sections keys in non-detail tag responses' do
@@ -201,10 +201,10 @@ RSpec.describe 'Posts API', type: :request do
end
it 'returns empty posts when nothing matches' do
get "/posts", params: { tags: "no_such_keyword_12345" }
get '/posts', params: { tags: 'no_such_keyword_12345' }
expect(response).to have_http_status(:ok)
expect(json.fetch("posts")).to eq([])
expect(json.fetch('posts')).to eq([])
expect(json.fetch('count')).to eq(0)
end
end
@@ -1712,8 +1712,10 @@ RSpec.describe 'Posts API', type: :request do
expect(post_record.reload.title).to eq('updated by other user')
end
it 'returns 409 with mergeable true when stale tag changes'\
' do not conflict but merge is not requested' do
it(
'returns 409 with mergeable true when stale tag changes '
'do not conflict but merge is not requested'
) do
sign_in_as(member)
base_version = create_post_version_for!(post_record.reload)
+21 -21
ファイルの表示
@@ -1,10 +1,10 @@
require "rails_helper"
require 'rails_helper'
RSpec.describe "nico:sync" do
RSpec.describe 'nico:sync' do
def stub_python(json_array)
status = instance_double(Process::Status, success?: true)
allow(Open3).to receive(:capture3).and_return([json_array.to_json, "", status])
allow(Open3).to receive(:capture3).and_return([json_array.to_json, '', status])
end
def create_tag!(name, category:)
@@ -25,12 +25,12 @@ RSpec.describe "nico:sync" do
)
# 既存の非nicoタグ(kept_non_nico_ids)
kept_general = create_tag!("spec_kept", category: "general")
kept_general = create_tag!('spec_kept', category: 'general')
PostTag.create!(post: post, tag: kept_general)
# 追加される linked tag を準備(nico tag に紐付く一般タグ)
linked = create_tag!("spec_linked", category: "general")
nico = create_tag!("nico:AAA", category: "nico")
linked = create_tag!('spec_linked', category: 'general')
nico = create_tag!('nico:AAA', category: 'nico')
link_nico_to_tag!(nico, linked)
# bot / tagme は task 内で使うので作っておく(Tag.bot/tagme がある前提)
@@ -46,22 +46,22 @@ RSpec.describe "nico:sync" do
'deleted_at' => '2026-01-31 00:00:00' }])
# 外部HTTPは今回「既存 post なので呼ばれない」はずだが、念のため塞ぐ
allow(URI).to receive(:open).and_return(StringIO.new("<html></html>"))
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
run_rake_task("nico:sync")
run_rake_task('nico:sync')
post.reload
active_tag_names = post.tags.joins(:tag_name).pluck("tag_names.name")
active_tag_names = post.tags.joins(:tag_name).pluck('tag_names.name')
expect(active_tag_names).to include("spec_kept")
expect(active_tag_names).to include("nico:AAA")
expect(active_tag_names).to include("spec_linked")
expect(active_tag_names).to include('spec_kept')
expect(active_tag_names).to include('nico:AAA')
expect(active_tag_names).to include('spec_linked')
expect(post.original_created_from).to eq(Time.iso8601('2026-01-01T03:34:00Z'))
expect(post.original_created_before).to eq(Time.iso8601('2026-01-01T03:35:00Z'))
# 差分が出るので bot が付く(kept_non_nico_ids != desired_non_nico_ids)
expect(active_tag_names).to include("bot操作")
expect(active_tag_names).to include('bot操作')
end
it '既存 post のサムネール取得に共通 attach 経路を使ふ' do
@@ -116,21 +116,21 @@ RSpec.describe "nico:sync" do
)
# 旧nicoタグ(今回の同期結果に含まれない)
old_nico = create_tag!("nico:OLD", category: "nico")
old_nico = create_tag!('nico:OLD', category: 'nico')
old_pt = PostTag.create!(post: post, tag: old_nico)
expect(old_pt.discarded_at).to be_nil
# 今回は NEW のみ欲しい
new_nico = create_tag!("nico:NEW", category: "nico")
new_nico = create_tag!('nico:NEW', category: 'nico')
# bot/tagme 念のため
Tag.bot
Tag.tagme
stub_python([{ "code" => "sm9", "title" => "t", "tags" => ["NEW"] }])
allow(URI).to receive(:open).and_return(StringIO.new("<html></html>"))
stub_python([{ 'code' => 'sm9', 'title' => 't', 'tags' => ['NEW'] }])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
run_rake_task("nico:sync")
run_rake_task('nico:sync')
# OLD は active から外れる(discarded_at が入る)
old_pts = PostTag.where(post_id: post.id, tag_id: old_nico.id).order(:id).to_a
@@ -138,9 +138,9 @@ RSpec.describe "nico:sync" do
# NEW は active にいる
post.reload
active_names = post.tags.joins(:tag_name).pluck("tag_names.name")
expect(active_names).to include("nico:NEW")
expect(active_names).not_to include("nico:OLD")
active_names = post.tags.joins(:tag_name).pluck('tag_names.name')
expect(active_names).to include('nico:NEW')
expect(active_names).not_to include('nico:OLD')
end
def snapshot_tags(post)
+14 -11
ファイルの表示
@@ -13,7 +13,7 @@ import MainArea from '@/components/layout/MainArea'
import { SITE_TITLE } from '@/config'
import { fetchMaterials, parseMaterialFilter } from '@/lib/materials'
import { materialsKeys } from '@/lib/queryKeys'
import { dateString, inputClass } from '@/lib/utils'
import { cn, dateString, inputClass } from '@/lib/utils'
import type { FC, FormEvent } from 'react'
@@ -113,10 +113,11 @@ const clearedTagSelectionPath = (
const MaterialThumb: FC<{ material: Material }> = ({ material }) => (
<div
className={`flex aspect-square h-[180px] w-[180px] items-center justify-center
overflow-hidden rounded-lg border border-stone-200 bg-white text-center
text-stone-900 shadow-sm dark:border-stone-700 dark:bg-stone-900
dark:text-stone-100`}>
className={cn (
'flex aspect-square h-[180px] w-[180px] items-center justify-center',
'overflow-hidden rounded-lg border border-stone-200 bg-white text-center',
'text-stone-900 shadow-sm dark:border-stone-700 dark:bg-stone-900',
'dark:text-stone-100')}>
{material.thumbnail
? <img src={material.thumbnail} alt="" className="block h-full w-full object-cover"/>
: (
@@ -534,27 +535,29 @@ const MaterialListPage: FC = () => {
<button
type="button"
onClick={() => updateQuery ({ view: 'card' })}
className={`rounded-full border px-4 py-2 text-sm ${
className={cn (
'rounded-full border px-4 py-2 text-sm',
view === 'card'
? [
'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400',
'dark:bg-sky-950 dark:text-sky-100'].join (' ')
'dark:bg-sky-950 dark:text-sky-100']
: [
'border-stone-300 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}>
'dark:bg-stone-900 dark:text-stone-100'])}>
</button>
<button
type="button"
onClick={() => updateQuery ({ view: 'list' })}
className={`rounded-full border px-4 py-2 text-sm ${
className={cn (
'rounded-full border px-4 py-2 text-sm',
view === 'list'
? [
'border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400',
'dark:bg-sky-950 dark:text-sky-100'].join (' ')
'dark:bg-sky-950 dark:text-sky-100']
: [
'border-stone-300 bg-white text-stone-900 dark:border-stone-700',
'dark:bg-stone-900 dark:text-stone-100'].join (' ') }`}>
'dark:bg-stone-900 dark:text-stone-100'])}>
</button>
</div>