Skip to content

Commit 11c3d41

Browse files
committed
feat: YAMLファイルサポートと統計ロジックを実装
Phase 2の実装: - dojos:update_db_by_yaml タスクに inactivated_at サポートを追加 - Git履歴から inactivated_at を抽出するRakeタスクを作成 - dojos:extract_inactivated_at_from_git: 全Dojo対象 - dojos:extract_inactivated_at_for_dojo: 特定Dojo対象 - 統計ロジックを更新して過去の活動履歴を含めるように変更 - annual_dojos_with_historical_data メソッドを追加 - 各年末時点でアクティブだったDojo数を正しく集計 - Statモデルのテストを追加 次のステップ: 1. rails dojos:extract_inactivated_at_from_git でYAMLに日付追加 2. rails dojos:update_db_by_yaml でDBに反映 3. ローカルで統計ページの動作確認
1 parent d16ae98 commit 11c3d41

File tree

4 files changed

+272
-15
lines changed

4 files changed

+272
-15
lines changed

app/models/stat.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,23 @@ def annual_sum_of_participants
3333

3434
def annual_dojos_chart(lang = 'ja')
3535
# MEMO: トップページの道場数と一致するように Active Dojo を集計対象としている
36-
HighChartsBuilder.build_annual_dojos(Dojo.active.annual_count(@period), lang)
36+
# inactivated_at 実装後は、各年の時点でアクティブだったDojoを集計
37+
if Dojo.column_names.include?('inactivated_at')
38+
data = annual_dojos_with_historical_data
39+
HighChartsBuilder.build_annual_dojos(data, lang)
40+
else
41+
HighChartsBuilder.build_annual_dojos(Dojo.active.annual_count(@period), lang)
42+
end
43+
end
44+
45+
# 各年末時点でアクティブだったDojo数を集計(過去の非アクティブDojoも含む)
46+
def annual_dojos_with_historical_data
47+
(@period.first.year..@period.last.year).each_with_object({}) do |year, hash|
48+
end_of_year = Time.zone.local(year).end_of_year
49+
# その年の終わりにアクティブだったDojoの数を集計
50+
count = Dojo.active_at(end_of_year).sum(:counter)
51+
hash[year.to_s] = count
52+
end
3753
end
3854

3955
def annual_event_histories_chart(lang = 'ja')

lib/tasks/dojos.rake

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,21 @@ namespace :dojos do
3434
raise_if_invalid_dojo(dojo)
3535

3636
d = Dojo.find_or_initialize_by(id: dojo['id'])
37-
d.name = dojo['name']
38-
d.counter = dojo['counter'] || 1
39-
d.email = ''
40-
d.description = dojo['description']
41-
d.logo = dojo['logo']
42-
d.tags = dojo['tags']
43-
d.note = dojo['note'] || '' # For internal comments for developers
44-
d.url = dojo['url']
45-
d.created_at = d.new_record? ? Time.zone.now : dojo['created_at'] || d.created_at
46-
d.updated_at = Time.zone.now
47-
d.prefecture_id = dojo['prefecture_id']
48-
d.order = dojo['order'] || search_order_number_by(dojo['name'])
49-
d.is_active = dojo['is_active'].nil? ? true : dojo['is_active']
50-
d.is_private = dojo['is_private'].nil? ? false : dojo['is_private']
37+
d.name = dojo['name']
38+
d.counter = dojo['counter'] || 1
39+
d.email = ''
40+
d.description = dojo['description']
41+
d.logo = dojo['logo']
42+
d.tags = dojo['tags']
43+
d.note = dojo['note'] || '' # For internal comments for developers
44+
d.url = dojo['url']
45+
d.prefecture_id = dojo['prefecture_id']
46+
d.order = dojo['order'] || search_order_number_by(dojo['name'])
47+
d.is_active = dojo['is_active'].nil? ? true : dojo['is_active']
48+
d.is_private = dojo['is_private'].nil? ? false : dojo['is_private']
49+
d.inactivated_at = dojo['inactivated_at'] ? Time.zone.parse(dojo['inactivated_at']) : nil
50+
d.created_at = d.new_record? ? Time.zone.now : dojo['created_at'] || d.created_at
51+
d.updated_at = Time.zone.now
5152

5253
d.save!
5354
end

lib/tasks/dojos_inactivated_at.rake

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
namespace :dojos do
2+
desc 'Git履歴からinactivated_at日付を抽出してYAMLファイルに反映'
3+
task extract_inactivated_at_from_git: :environment do
4+
require 'git'
5+
6+
yaml_path = Rails.root.join('db', 'dojos.yaml')
7+
git = Git.open(Rails.root)
8+
9+
# YAMLファイルの内容を行番号付きで読み込む
10+
yaml_lines = File.readlines(yaml_path)
11+
12+
# 非アクティブなDojoを取得
13+
inactive_dojos = Dojo.inactive.where(inactivated_at: nil)
14+
15+
puts "=== Git履歴から inactivated_at を抽出 ==="
16+
puts "対象となる非アクティブDojo数: #{inactive_dojos.count}"
17+
puts ""
18+
19+
updated_count = 0
20+
21+
inactive_dojos.each do |dojo|
22+
puts "処理中: #{dojo.name} (ID: #{dojo.id})"
23+
24+
# is_active: false が記載されている行を探す
25+
target_line_number = nil
26+
in_dojo_block = false
27+
28+
yaml_lines.each_with_index do |line, index|
29+
# Dojoブロックの開始を検出
30+
if line.match?(/^- id: #{dojo.id}$/)
31+
in_dojo_block = true
32+
elsif line.match?(/^- id: \d+$/)
33+
in_dojo_block = false
34+
end
35+
36+
# 該当Dojoブロック内で is_active: false を見つける
37+
if in_dojo_block && line.match?(/^\s*is_active: false/)
38+
target_line_number = index + 1 # git blameは1-indexedなので+1
39+
break
40+
end
41+
end
42+
43+
if target_line_number
44+
# git blame を使って該当行の最新コミット情報を取得
45+
# --porcelain で解析しやすい形式で出力
46+
blame_cmd = "git blame #{yaml_path} -L #{target_line_number},+1 --porcelain"
47+
blame_output = `#{blame_cmd}`.strip
48+
49+
# コミットIDを抽出(最初の行の最初の要素)
50+
commit_id = blame_output.lines[0].split.first
51+
52+
if commit_id && commit_id.match?(/^[0-9a-f]{40}$/)
53+
# コミット情報を取得
54+
commit = git.gcommit(commit_id)
55+
inactivated_date = commit.author_date
56+
57+
# YAMLファイルのDojoブロックを見つけて更新
58+
yaml_updated = false
59+
yaml_lines.each_with_index do |line, index|
60+
if line.match?(/^- id: #{dojo.id}$/)
61+
# 該当Dojoブロックの最後に inactivated_at を追加
62+
insert_index = index + 1
63+
while insert_index < yaml_lines.length && !yaml_lines[insert_index].match?(/^- id:/)
64+
# is_active: false の次の行に挿入したい
65+
if yaml_lines[insert_index - 1].match?(/is_active: false/)
66+
yaml_lines.insert(insert_index,
67+
" inactivated_at: '#{inactivated_date.strftime('%Y-%m-%d %H:%M:%S')}'\n")
68+
yaml_updated = true
69+
break
70+
end
71+
insert_index += 1
72+
end
73+
break if yaml_updated
74+
end
75+
end
76+
77+
if yaml_updated
78+
updated_count += 1
79+
puts " ✓ inactivated_at を追加: #{inactivated_date.strftime('%Y-%m-%d %H:%M:%S')}"
80+
puts " コミット: #{commit_id[0..7]} by #{commit.author.name}"
81+
else
82+
puts " ✗ YAMLファイルの更新に失敗"
83+
end
84+
else
85+
puts " ✗ コミット情報の取得に失敗"
86+
end
87+
else
88+
puts " ✗ YAMLファイル内で 'is_active: false' 行が見つかりません"
89+
end
90+
91+
puts ""
92+
end
93+
94+
if updated_count > 0
95+
# YAMLファイルを書き戻す
96+
File.write(yaml_path, yaml_lines.join)
97+
98+
puts "=== 完了 ==="
99+
puts "合計 #{updated_count} 個のDojoに inactivated_at を追加しました"
100+
puts ""
101+
puts "次のステップ:"
102+
puts "1. db/dojos.yaml の変更内容を確認"
103+
puts "2. rails dojos:update_db_by_yaml を実行してDBに反映"
104+
puts "3. 変更をコミット"
105+
else
106+
puts "=== 完了 ==="
107+
puts "更新対象のDojoはありませんでした"
108+
end
109+
end
110+
111+
desc '特定のDojoのinactivated_at日付をGit履歴から抽出'
112+
task :extract_inactivated_at_for_dojo, [:dojo_id] => :environment do |t, args|
113+
require 'git'
114+
115+
dojo = Dojo.find(args[:dojo_id])
116+
yaml_path = Rails.root.join('db', 'dojos.yaml')
117+
git = Git.open(Rails.root)
118+
119+
puts "対象Dojo: #{dojo.name} (ID: #{dojo.id})"
120+
121+
# YAMLファイルの内容を読み込む
122+
yaml_lines = File.readlines(yaml_path)
123+
124+
# is_active: false が記載されている行を探す
125+
target_line_number = nil
126+
in_dojo_block = false
127+
128+
yaml_lines.each_with_index do |line, index|
129+
if line.match?(/^- id: #{dojo.id}$/)
130+
in_dojo_block = true
131+
elsif line.match?(/^- id: \d+$/)
132+
in_dojo_block = false
133+
end
134+
135+
if in_dojo_block && line.match?(/^\s*is_active: false/)
136+
target_line_number = index + 1
137+
break
138+
end
139+
end
140+
141+
if target_line_number
142+
blame_cmd = "git blame #{yaml_path} -L #{target_line_number},+1 --porcelain"
143+
blame_output = `#{blame_cmd}`.strip
144+
commit_id = blame_output.lines[0].split.first
145+
146+
if commit_id && commit_id.match?(/^[0-9a-f]{40}$/)
147+
commit = git.gcommit(commit_id)
148+
inactivated_date = commit.author_date
149+
150+
puts "✓ is_active: false に設定された日時: #{inactivated_date.strftime('%Y-%m-%d %H:%M:%S')}"
151+
puts " コミット: #{commit_id[0..7]}"
152+
puts " 作者: #{commit.author.name}"
153+
puts " メッセージ: #{commit.message.lines.first.strip}"
154+
else
155+
puts "✗ コミット情報の取得に失敗しました"
156+
end
157+
else
158+
puts "✗ YAMLファイル内で 'is_active: false' 行が見つかりません"
159+
end
160+
end
161+
end

spec/models/stat_spec.rb

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe Stat, type: :model do
4+
describe '#annual_dojos_with_historical_data' do
5+
let(:period) { Date.new(2020, 1, 1)..Date.new(2023, 12, 31) }
6+
let(:stat) { Stat.new(period) }
7+
8+
before do
9+
# 2020年から活動開始、2022年に非アクティブ化
10+
@dojo1 = Dojo.create!(
11+
name: 'CoderDojo テスト1',
12+
13+
created_at: Time.zone.local(2020, 3, 1),
14+
prefecture_id: 13,
15+
is_active: false,
16+
inactivated_at: Time.zone.local(2022, 6, 15)
17+
)
18+
19+
# 2021年から活動開始、現在も活動中
20+
@dojo2 = Dojo.create!(
21+
name: 'CoderDojo テスト2',
22+
email: '[email protected]',
23+
created_at: Time.zone.local(2021, 1, 1),
24+
prefecture_id: 13,
25+
is_active: true,
26+
inactivated_at: nil
27+
)
28+
29+
# 2019年から活動開始、2020年に非アクティブ化
30+
@dojo3 = Dojo.create!(
31+
name: 'CoderDojo テスト3',
32+
33+
created_at: Time.zone.local(2019, 1, 1),
34+
prefecture_id: 13,
35+
is_active: false,
36+
inactivated_at: Time.zone.local(2020, 3, 1)
37+
)
38+
end
39+
40+
it '各年末時点でアクティブだったDojo数を正しく集計する' do
41+
result = stat.annual_dojos_with_historical_data
42+
43+
# 2020年末: dojo1(活動中) + dojo3(3月に非アクティブ化) = 1
44+
expect(result['2020']).to eq(1)
45+
46+
# 2021年末: dojo1(活動中) + dojo2(活動中) = 2
47+
expect(result['2021']).to eq(2)
48+
49+
# 2022年末: dojo1(6月に非アクティブ化) + dojo2(活動中) = 1
50+
expect(result['2022']).to eq(1)
51+
52+
# 2023年末: dojo2(活動中) = 1
53+
expect(result['2023']).to eq(1)
54+
end
55+
end
56+
57+
describe '#annual_dojos_chart' do
58+
let(:period) { Date.new(2020, 1, 1)..Date.new(2023, 12, 31) }
59+
let(:stat) { Stat.new(period) }
60+
61+
context 'inactivated_at カラムが存在する場合' do
62+
it '過去の活動履歴を含めた統計を生成する' do
63+
allow(Dojo).to receive(:column_names).and_return(['id', 'name', 'inactivated_at'])
64+
expect(stat).to receive(:annual_dojos_with_historical_data)
65+
66+
stat.annual_dojos_chart
67+
end
68+
end
69+
70+
context 'inactivated_at カラムが存在しない場合' do
71+
it '従来通りアクティブなDojoのみを集計する' do
72+
allow(Dojo).to receive(:column_names).and_return(['id', 'name'])
73+
expect(Dojo.active).to receive(:annual_count).with(period)
74+
75+
stat.annual_dojos_chart
76+
end
77+
end
78+
end
79+
end

0 commit comments

Comments
 (0)