このコミットが含まれているのは:
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする