From 6ab4a2427fa25149db0d59618a54bdbae98c7c50 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 11:53:42 +0900
Subject: [PATCH 01/37] =?UTF-8?q?docs:=20=E9=81=93=E5=A0=B4=E7=B5=B1?=
=?UTF-8?q?=E8=A8=88=E5=B9=B4=E6=AC=A1=E3=83=80=E3=82=A6=E3=83=B3=E3=83=AD?=
=?UTF-8?q?=E3=83=BC=E3=83=89=E6=A9=9F=E8=83=BD=E3=81=AE=E5=AE=9F=E8=A3=85?=
=?UTF-8?q?=E8=A8=88=E7=94=BB=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Issue #1373 に関連した新機能の実装計画:
- /dojos エンドポイントに年次フィルタリング機能を追加
- CSV/JSON形式でのデータエクスポート対応
- yearパラメータによる特定年のデータ取得(整数値のみ)
- HTML表示でのデータプレビュー機能
- 全年次統計データと特定年データの両方をサポート
---
docs/plan_download_yearly_stats.md | 546 +++++++++++++++++++++++++++++
1 file changed, 546 insertions(+)
create mode 100644 docs/plan_download_yearly_stats.md
diff --git a/docs/plan_download_yearly_stats.md b/docs/plan_download_yearly_stats.md
new file mode 100644
index 000000000..9000a29f0
--- /dev/null
+++ b/docs/plan_download_yearly_stats.md
@@ -0,0 +1,546 @@
+# 📊 道場統計年次ダウンロード機能 - 実装計画
+
+## 概要
+CoderDojoの統計データを年次でダウンロードできる機能を実装する。`/dojos` ページにクエリパラメータ(`?year=2024`)を追加することで、特定年のデータや全年次統計をCSV/JSON形式でダウンロード可能にする。既存の `/stats` ページとの混乱を避けるため、`/dojos` エンドポイントを拡張する形で実装する。
+
+## 🎯 要件定義
+
+### Phase 1: 基本実装(MVP)
+1. `/dojos` ページにCSVダウンロードリンクを追加
+2. 年次統計データをCSV形式でダウンロード可能に
+3. データ内容:
+ - 年
+ - アクティブ道場数(年末時点)
+ - 新規開設道場数
+ - 非アクティブ化道場数
+ - 累積道場数
+
+### Phase 2: 拡張機能(将来)
+- 特定年のアクティブ道場のみダウンロード
+- 都道府県別・地域別の統計
+- イベント数・参加者数の統計も含める
+- JSON形式のサポート
+
+## 🏗️ 技術設計
+
+### 1. ルーティング設計
+
+```ruby
+# config/routes.rb
+
+# 既存のルーティングをそのまま活用
+get '/dojos', to: 'dojos#index' # HTML, JSON, CSV(拡張)
+get '/dojos/:id', to: 'dojos#show' # HTML, JSON, CSV
+
+# URLパターン例:
+# GET /dojos → 現在のアクティブ道場一覧(HTML)
+# GET /dojos?year=2024 → 2024年末時点のアクティブ道場一覧(HTML)
+# GET /dojos.csv → 全年次統計データ
+# GET /dojos.csv?year=2024 → 2024年末時点のアクティブ道場リスト(CSV)
+# GET /dojos.json → 全年次統計データ
+# GET /dojos.json?year=2024 → 2024年末時点のアクティブ道場リスト(JSON)
+```
+
+### 2. コントローラー設計
+
+```ruby
+# app/controllers/dojos_controller.rb
+
+class DojosController < ApplicationController
+ # 既存のindexアクションを拡張
+ def index
+ # yearパラメータがある場合の処理
+ if params[:year].present?
+ year = params[:year].to_i
+ # 有効な年の範囲をチェック
+ unless year.between?(2012, Date.current.year)
+ flash[:alert] = "指定された年(#{year})は無効です。2012年から#{Date.current.year}年の間で指定してください。"
+ return redirect_to dojos_path
+ end
+
+ @selected_year = year
+ end_of_year = Time.zone.local(@selected_year).end_of_year
+
+ # CSV/JSON形式の場合は統計データを返す
+ if request.format.csv? || request.format.json?
+ return render_yearly_stats
+ end
+
+ # HTML形式の場合は、その年末時点でアクティブだった道場を表示
+ @dojos = []
+ Dojo.active_at(end_of_year).includes(:prefecture).order(order: :asc).each do |dojo|
+ @dojos << {
+ id: dojo.id,
+ url: dojo.url,
+ name: dojo.name,
+ logo: root_url + dojo.logo[1..],
+ order: dojo.order,
+ counter: dojo.counter,
+ is_active: dojo.active_at?(end_of_year),
+ prefecture: dojo.prefecture.name,
+ created_at: dojo.created_at,
+ description: dojo.description,
+ }
+ end
+
+ @page_title = "#{@selected_year}年末時点のCoderDojo一覧"
+ else
+ # yearパラメータなしの場合
+ if request.format.csv? || request.format.json?
+ # CSV/JSON: 全年次統計データを返す
+ return render_yearly_stats
+ end
+
+ # HTML: 現在のアクティブな道場リスト(既存の実装)
+ @dojos = []
+ Dojo.includes(:prefecture).order(order: :asc).all.each do |dojo|
+ @dojos << {
+ id: dojo.id,
+ url: dojo.url,
+ name: dojo.name,
+ logo: root_url + dojo.logo[1..],
+ order: dojo.order,
+ counter: dojo.counter,
+ is_active: dojo.is_active,
+ prefecture: dojo.prefecture.name,
+ created_at: dojo.created_at,
+ description: dojo.description,
+ }
+ end
+
+ respond_to do |format|
+ format.html # => app/views/dojos/index.html.erb
+ format.json { render json: @dojos }
+ format.csv { send_data render_to_string, type: :csv } # 既存の道場リスト
+ end
+ end
+
+ def show
+ # 既存の実装のまま
+ end
+
+ private
+
+ def render_yearly_stats
+ @period_start = 2012
+ @period_end = Date.current.year
+
+ # yearパラメータが指定されている場合(整数のみ許可)
+ if params[:year].present?
+ year = params[:year].to_i
+ # 有効な年の範囲をチェック
+ unless year.between?(@period_start, @period_end)
+ return render json: { error: "Year must be between #{@period_start} and #{@period_end}" }, status: :bad_request
+ end
+
+ @selected_year = year
+ period = Time.zone.local(@selected_year).beginning_of_year..Time.zone.local(@selected_year).end_of_year
+ @stat = Stat.new(period)
+ @yearly_data = prepare_single_year_data(@stat, @selected_year)
+ filename_suffix = @selected_year.to_s
+ else
+ # yearパラメータなし = 全年次データ
+ period = Time.zone.local(@period_start).beginning_of_year..Time.zone.local(@period_end).end_of_year
+ @stat = Stat.new(period)
+ @yearly_data = prepare_all_years_data(@stat)
+ filename_suffix = 'all'
+ end
+
+ respond_to do |format|
+ format.csv do
+ send_data render_to_string(template: 'dojos/yearly_stats'),
+ type: :csv,
+ filename: "coderdojo_stats_#{filename_suffix}_#{Date.current.strftime('%Y%m%d')}.csv"
+ end
+ format.json { render json: @yearly_data }
+ end
+ end
+
+ def prepare_all_years_data(stat)
+ active_dojos = stat.annual_dojos_with_historical_data
+ new_dojos = stat.annual_new_dojos_count
+
+ # 年ごとのデータを整形
+ years = (@period_start..@period_end).map(&:to_s)
+ years.map do |year|
+ prev_year = (year.to_i - 1).to_s
+ {
+ year: year,
+ active_dojos_at_year_end: active_dojos[year] || 0,
+ new_dojos: new_dojos[year] || 0,
+ inactivated_dojos: calculate_inactivated_count(year),
+ cumulative_total: active_dojos[year] || 0,
+ net_change: prev_year && active_dojos[prev_year] ?
+ (active_dojos[year] || 0) - active_dojos[prev_year] :
+ (active_dojos[year] || 0)
+ }
+ end
+ end
+
+ def prepare_single_year_data(stat, year)
+ # 特定年のアクティブな道場リストを返す
+ end_of_year = Time.zone.local(year).end_of_year
+ dojos = Dojo.active_at(end_of_year).includes(:prefecture)
+
+ dojos.map do |dojo|
+ {
+ id: dojo.id,
+ name: dojo.name,
+ prefecture: dojo.prefecture.name,
+ url: dojo.url,
+ created_at: dojo.created_at.strftime('%Y-%m-%d'),
+ is_active_at_year_end: dojo.active_at?(end_of_year)
+ }
+ end
+ end
+
+ def calculate_inactivated_count(year)
+ start_of_year = Time.zone.local(year.to_i).beginning_of_year
+ end_of_year = Time.zone.local(year.to_i).end_of_year
+ Dojo.where(inactivated_at: start_of_year..end_of_year).sum(:counter)
+ end
+end
+```
+
+### 3. ビュー設計
+
+#### 3.1 `/dojos/index.html.erb` の更新
+
+```erb
+
+
+
+
+
+
📊 年次統計データのダウンロード
+
+ <% if @selected_year %>
+
+ <%= @selected_year %>年末時点 のデータを表示中
+ <%= link_to '現在のデータを表示', dojos_path, class: 'btn btn-sm btn-outline-primary ml-2' %>
+
+ <% end %>
+
+ <%= form_with(url: dojos_path, method: :get, local: true, html: { class: 'form-inline' }) do |f| %>
+
+ <%= label_tag :year, '年を選択:', class: 'mr-2' %>
+ <%= select_tag :year,
+ options_for_select(
+ [['全年次データ', '']] + (2012..Date.current.year).map { |y| [y.to_s + '年', y] },
+ params[:year]
+ ),
+ include_blank: false,
+ class: 'form-control mr-2' %>
+
+
+
+ <%= button_tag type: 'submit', class: 'btn btn-info' do %>
+ 表示
+ <% end %>
+ <%= button_tag type: 'submit', name: 'format', value: 'csv', class: 'btn btn-primary' do %>
+ CSV ダウンロード
+ <% end %>
+ <%= button_tag type: 'submit', name: 'format', value: 'json', class: 'btn btn-secondary' do %>
+ JSON ダウンロード
+ <% end %>
+
+ <% end %>
+
+
+ ※ 年を選択すると、その年末時点でアクティブだった道場の統計データをダウンロードできます。
+ ※ 「全年次データ」を選択すると、2012年〜現在までの年次推移データをダウンロードできます。
+
+
+```
+
+#### 3.2 `/dojos/yearly_stats.csv.ruby` の新規作成
+
+```ruby
+require 'csv'
+
+csv_data = CSV.generate do |csv|
+ if @selected_year
+ # 特定年のデータ(道場リスト)
+ csv << ['ID', '道場名', '都道府県', 'URL', '設立日', '状態']
+
+ @yearly_data.each do |dojo|
+ csv << [
+ dojo[:id],
+ dojo[:name],
+ dojo[:prefecture],
+ dojo[:url],
+ dojo[:created_at],
+ dojo[:is_active_at_year_end] ? 'アクティブ' : '非アクティブ'
+ ]
+ end
+ else
+ # 全年次統計データ
+ csv << ['年', '年末アクティブ道場数', '新規開設数', '非アクティブ化数', '累積合計', '純増減']
+
+ @yearly_data.each do |data|
+ csv << [
+ data[:year],
+ data[:active_dojos_at_year_end],
+ data[:new_dojos],
+ data[:inactivated_dojos],
+ data[:cumulative_total],
+ data[:net_change]
+ ]
+ end
+
+ # 合計行
+ csv << []
+ csv << ['合計', '',
+ @yearly_data.sum { |d| d[:new_dojos] },
+ @yearly_data.sum { |d| d[:inactivated_dojos] },
+ @yearly_data.last[:cumulative_total],
+ '']
+ end
+end
+```
+
+### 4. データ構造
+
+#### CSVファイル例
+```csv
+年,年末アクティブ道場数,新規開設数,非アクティブ化数,累積合計,純増減
+2012,1,1,0,1,1
+2013,4,3,0,4,3
+2014,8,4,0,8,4
+2015,16,8,0,16,8
+2016,29,13,0,29,13
+2017,77,48,0,77,48
+2018,172,54,0,172,95
+2019,200,50,22,200,28
+2020,222,26,4,222,22
+2021,236,19,5,236,14
+2022,225,20,31,225,-11
+2023,199,20,46,199,-26
+2024,206,15,8,206,7
+
+合計,,329,116,206,
+```
+
+#### JSON形式例
+```json
+[
+ {
+ "year": "2012",
+ "active_dojos_at_year_end": 1,
+ "new_dojos": 1,
+ "inactivated_dojos": 0,
+ "cumulative_total": 1,
+ "net_change": 1
+ },
+ {
+ "year": "2013",
+ "active_dojos_at_year_end": 4,
+ "new_dojos": 3,
+ "inactivated_dojos": 0,
+ "cumulative_total": 4,
+ "net_change": 3
+ },
+ // ...
+]
+```
+
+## 🧪 テスト計画
+
+### 1. コントローラーテスト
+
+```ruby
+# spec/controllers/dojos_controller_spec.rb
+
+RSpec.describe DojosController, type: :controller do
+ describe 'GET #index with year parameter' do
+ before do
+ # テストデータの準備
+ create(:dojo, created_at: '2020-01-01', is_active: true)
+ create(:dojo, created_at: '2020-06-01', is_active: false, inactivated_at: '2021-03-01')
+ create(:dojo, created_at: '2021-01-01', is_active: true)
+ end
+
+ context '全年次データ(yearパラメータなし)' do
+ it 'CSVファイルがダウンロードされる' do
+ get :index, format: :csv
+ expect(response.content_type).to eq('text/csv')
+ expect(response.headers['Content-Disposition']).to include('coderdojo_stats_all_')
+ end
+
+ it '正しいヘッダーとデータが含まれる' do
+ get :index, format: :csv
+ csv = CSV.parse(response.body)
+ expect(csv[0]).to eq(['年', '年末アクティブ道場数', '新規開設数', '非アクティブ化数', '累積合計', '純増減'])
+ end
+ end
+
+ context '特定年のデータ(year=2020)' do
+ it 'CSVファイルがダウンロードされる' do
+ get :index, params: { year: '2020' }, format: :csv
+ expect(response.content_type).to eq('text/csv')
+ expect(response.headers['Content-Disposition']).to include('coderdojo_stats_2020_')
+ end
+
+ it '2020年末時点のアクティブな道場リストが返される' do
+ get :index, params: { year: '2020' }, format: :csv
+ csv = CSV.parse(response.body)
+ expect(csv[0]).to eq(['ID', '道場名', '都道府県', 'URL', '設立日', '状態'])
+ expect(csv.size - 1).to eq(2) # ヘッダーを除いて2道場
+ end
+ end
+
+ context '無効な年が指定された場合' do
+ it 'エラーが返される' do
+ get :index, params: { year: '1999' }, format: :csv
+ expect(response).to have_http_status(:bad_request)
+ expect(JSON.parse(response.body)['error']).to include('Year must be between')
+ end
+
+ it '文字列が指定された場合も適切に処理される' do
+ get :index, params: { year: 'invalid' }, format: :csv
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+end
+```
+
+### 2. 統合テスト
+
+```ruby
+# spec/features/dojos_download_spec.rb
+
+RSpec.feature 'Dojos yearly stats download', type: :feature do
+ scenario 'ユーザーが全年次統計をダウンロードする' do
+ visit dojos_path
+
+ # 年選択セクションが表示される
+ expect(page).to have_select('year')
+ expect(page).to have_button('CSV ダウンロード')
+
+ # 全年次データを選択
+ select '全年次データ', from: 'year'
+ click_button 'CSV ダウンロード'
+
+ # ファイルがダウンロードされる
+ expect(page.response_headers['Content-Type']).to eq('text/csv')
+ expect(page.response_headers['Content-Disposition']).to include('coderdojo_stats_all')
+ end
+
+ scenario 'ユーザーが特定年のデータをダウンロードする' do
+ visit dojos_path
+
+ # 2024年を選択
+ select '2024年', from: 'year'
+ click_button 'CSV ダウンロード'
+
+ # ファイルがダウンロードされる
+ expect(page.response_headers['Content-Type']).to eq('text/csv')
+ expect(page.response_headers['Content-Disposition']).to include('coderdojo_stats_2024')
+ end
+end
+```
+
+## 📋 実装ステップ
+
+### Phase 1: 基本実装(2-3日)
+1. [ ] `dojos_controller.rb` の `index` アクションを拡張
+2. [ ] `render_yearly_stats` プライベートメソッドの実装
+3. [ ] `prepare_all_years_data` と `prepare_single_year_data` メソッドの実装
+4. [ ] CSVビューテンプレート (`yearly_stats.csv.ruby`) の作成
+5. [ ] `/dojos/index.html.erb` に年選択フォームとダウンロードボタンを追加
+6. [ ] テストの作成と実行
+7. [ ] 本番データでの動作確認
+
+### Phase 2: 拡張機能(将来)
+1. [ ] 年別フィルタリング機能
+2. [ ] 都道府県別統計の追加
+3. [ ] イベント数・参加者数の統計追加
+4. [ ] より詳細なCSVエクスポートオプション
+
+## 🎨 UIデザイン案
+
+### オプション1: シンプルなリンク
+現在のJSONリンクと同じスタイルで、CSVダウンロードリンクを追加
+
+### オプション2: ボタン形式
+```html
+
+```
+
+### オプション3: ドロップダウンメニュー(将来的な拡張用)
+```html
+
+
+ 📊 統計データをダウンロード
+
+
+
+```
+
+## 🚀 性能考慮事項
+
+1. **キャッシング**
+ - 統計データの計算は重いため、結果をキャッシュすることを検討
+ - Rails.cache を使用して1日単位でキャッシュ
+
+2. **データ量**
+ - 現在は約13年分のデータなので問題ないが、将来的にデータが増えた場合はページネーションや期間指定を検討
+
+3. **バックグラウンド処理**
+ - 大量データの場合は、CSVファイル生成をバックグラウンドジョブで処理し、完了後にダウンロードリンクを送信することも検討
+
+## 📝 注意事項
+
+1. **データの正確性**
+ - `inactivated_at` が設定されているDojoのみが非アクティブ化数に含まれる
+ - 過去のデータは `annual_dojos_with_historical_data` メソッドに依存
+
+2. **国際化対応**
+ - 将来的に英語版も必要な場合は、ヘッダーの翻訳を考慮
+
+3. **セキュリティ**
+ - 公開データのみを含めること
+ - 個人情報や内部情報は含めない
+
+## 🔗 関連リソース
+
+- 既存の個別道場CSV実装: `/app/views/dojos/show.csv.ruby`
+- 統計ページの実装: `/app/controllers/stats_controller.rb`
+- Statモデル: `/app/models/stat.rb`
+- Issue: #[未定]
+
+## 📅 タイムライン
+
+- **Week 1**: 基本実装(Phase 1)
+ - Day 1-2: コントローラーとビューの実装
+ - Day 3: テストとデバッグ
+- **Week 2**: レビューと改善
+ - フィードバックの反映
+ - ドキュメント更新
+- **将来**: Phase 2の実装(必要に応じて)
\ No newline at end of file
From cb64d0bb8e36e086f2ae621816ce6d50d8e3d9ac Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 11:59:05 +0900
Subject: [PATCH 02/37] =?UTF-8?q?docs:=20=E5=AE=9F=E8=A3=85=E8=A8=88?=
=?UTF-8?q?=E7=94=BB=E3=82=92=E6=94=B9=E5=96=84=20-=20respond=5Fto?=
=?UTF-8?q?=E3=81=A8=E4=B8=89=E9=A0=85=E6=BC=94=E7=AE=97=E5=AD=90=E3=82=92?=
=?UTF-8?q?=E6=B4=BB=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
主な改善点:
- respond_toメソッドを使用してRailsの標準パターンに準拠
- 三項演算子でformat.json/csvのコードを簡潔に記述
- render_yearly_stats内の重複したパラメータチェックを削除
- より読みやすく保守しやすいコード構造に改善
---
docs/plan_download_yearly_stats.md | 61 ++++++++++++------------------
1 file changed, 25 insertions(+), 36 deletions(-)
diff --git a/docs/plan_download_yearly_stats.md b/docs/plan_download_yearly_stats.md
index 9000a29f0..67804e981 100644
--- a/docs/plan_download_yearly_stats.md
+++ b/docs/plan_download_yearly_stats.md
@@ -61,12 +61,7 @@ class DojosController < ApplicationController
@selected_year = year
end_of_year = Time.zone.local(@selected_year).end_of_year
- # CSV/JSON形式の場合は統計データを返す
- if request.format.csv? || request.format.json?
- return render_yearly_stats
- end
-
- # HTML形式の場合は、その年末時点でアクティブだった道場を表示
+ # その年末時点でアクティブだった道場を取得
@dojos = []
Dojo.active_at(end_of_year).includes(:prefecture).order(order: :asc).each do |dojo|
@dojos << {
@@ -85,33 +80,33 @@ class DojosController < ApplicationController
@page_title = "#{@selected_year}年末時点のCoderDojo一覧"
else
- # yearパラメータなしの場合
- if request.format.csv? || request.format.json?
- # CSV/JSON: 全年次統計データを返す
- return render_yearly_stats
+ # yearパラメータなしの場合、現在のアクティブな道場リスト(既存の実装)
+ @dojos = []
+ Dojo.includes(:prefecture).order(order: :asc).all.each do |dojo|
+ @dojos << {
+ id: dojo.id,
+ url: dojo.url,
+ name: dojo.name,
+ logo: root_url + dojo.logo[1..],
+ order: dojo.order,
+ counter: dojo.counter,
+ is_active: dojo.is_active,
+ prefecture: dojo.prefecture.name,
+ created_at: dojo.created_at,
+ description: dojo.description,
+ }
end
-
- # HTML: 現在のアクティブな道場リスト(既存の実装)
- @dojos = []
- Dojo.includes(:prefecture).order(order: :asc).all.each do |dojo|
- @dojos << {
- id: dojo.id,
- url: dojo.url,
- name: dojo.name,
- logo: root_url + dojo.logo[1..],
- order: dojo.order,
- counter: dojo.counter,
- is_active: dojo.is_active,
- prefecture: dojo.prefecture.name,
- created_at: dojo.created_at,
- description: dojo.description,
- }
end
+ # respond_toで形式ごとに処理を分岐
respond_to do |format|
format.html # => app/views/dojos/index.html.erb
- format.json { render json: @dojos }
- format.csv { send_data render_to_string, type: :csv } # 既存の道場リスト
+ format.json do
+ params[:year].present? ? render_yearly_stats : render(json: @dojos)
+ end
+ format.csv do
+ params[:year].present? ? render_yearly_stats : send_data(render_to_string, type: :csv)
+ end
end
end
@@ -126,14 +121,7 @@ class DojosController < ApplicationController
@period_end = Date.current.year
# yearパラメータが指定されている場合(整数のみ許可)
- if params[:year].present?
- year = params[:year].to_i
- # 有効な年の範囲をチェック
- unless year.between?(@period_start, @period_end)
- return render json: { error: "Year must be between #{@period_start} and #{@period_end}" }, status: :bad_request
- end
-
- @selected_year = year
+ if @selected_year # 既にindexアクションで設定済み
period = Time.zone.local(@selected_year).beginning_of_year..Time.zone.local(@selected_year).end_of_year
@stat = Stat.new(period)
@yearly_data = prepare_single_year_data(@stat, @selected_year)
@@ -146,6 +134,7 @@ class DojosController < ApplicationController
filename_suffix = 'all'
end
+ # CSVまたはJSONとして返す
respond_to do |format|
format.csv do
send_data render_to_string(template: 'dojos/yearly_stats'),
From dc06bb5ce2c4af5c6f878821f0301f8f92144b72 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 12:09:24 +0900
Subject: [PATCH 03/37] =?UTF-8?q?docs:=20=E3=83=87=E3=83=BC=E3=82=BF?=
=?UTF-8?q?=E5=8F=96=E5=BE=97=E7=AF=84=E5=9B=B2=E3=81=AE=E4=BB=95=E6=A7=98?=
=?UTF-8?q?=E3=82=92=E6=AD=A3=E7=A2=BA=E3=81=AB=E8=A8=98=E8=BF=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
重要な仕様の明確化:
- yearパラメータなし:
- HTML: 現在アクティブな道場のみ(既存動作維持)
- CSV/JSON: 全道場(アクティブ + 非アクティブ)
- yearパラメータあり(例: year=2024):
- HTML/JSON/CSV すべて: その年末時点のアクティブ道場のみ
すべての形式(HTML/JSON/CSV)がyearパラメータを正しく処理
---
docs/plan_download_yearly_stats.md | 33 +++++++++++++++++++++---------
1 file changed, 23 insertions(+), 10 deletions(-)
diff --git a/docs/plan_download_yearly_stats.md b/docs/plan_download_yearly_stats.md
index 67804e981..4e2686ef4 100644
--- a/docs/plan_download_yearly_stats.md
+++ b/docs/plan_download_yearly_stats.md
@@ -3,6 +3,13 @@
## 概要
CoderDojoの統計データを年次でダウンロードできる機能を実装する。`/dojos` ページにクエリパラメータ(`?year=2024`)を追加することで、特定年のデータや全年次統計をCSV/JSON形式でダウンロード可能にする。既存の `/stats` ページとの混乱を避けるため、`/dojos` エンドポイントを拡張する形で実装する。
+### データの取得範囲
+- **yearパラメータなし(デフォルト)**:
+ - HTML表示: 現在アクティブな道場のみ(既存の動作を維持)
+ - CSV/JSONダウンロード: 全道場(アクティブ + 非アクティブ)
+- **yearパラメータあり(例: year=2024)**:
+ - HTML/JSON/CSV すべての形式: その年末時点でアクティブだった道場のみ
+
## 🎯 要件定義
### Phase 1: 基本実装(MVP)
@@ -35,9 +42,9 @@ get '/dojos/:id', to: 'dojos#show' # HTML, JSON, CSV
# URLパターン例:
# GET /dojos → 現在のアクティブ道場一覧(HTML)
# GET /dojos?year=2024 → 2024年末時点のアクティブ道場一覧(HTML)
-# GET /dojos.csv → 全年次統計データ
+# GET /dojos.csv → 全道場リスト(アクティブ + 非アクティブ)
# GET /dojos.csv?year=2024 → 2024年末時点のアクティブ道場リスト(CSV)
-# GET /dojos.json → 全年次統計データ
+# GET /dojos.json → 全道場リスト(アクティブ + 非アクティブ)
# GET /dojos.json?year=2024 → 2024年末時点のアクティブ道場リスト(JSON)
```
@@ -80,9 +87,19 @@ class DojosController < ApplicationController
@page_title = "#{@selected_year}年末時点のCoderDojo一覧"
else
- # yearパラメータなしの場合、現在のアクティブな道場リスト(既存の実装)
+ # yearパラメータなしの場合
+ # HTML表示: 現在のアクティブな道場のみ(既存の実装を維持)
+ # CSV/JSONダウンロード: 全道場(アクティブ + 非アクティブ)
+ if request.format.html?
+ # HTMLの場合は現在アクティブな道場のみ
+ dojos_scope = Dojo.active
+ else
+ # CSV/JSONの場合は全道場(非アクティブも含む)
+ dojos_scope = Dojo.all
+ end
+
@dojos = []
- Dojo.includes(:prefecture).order(order: :asc).all.each do |dojo|
+ dojos_scope.includes(:prefecture).order(order: :asc).each do |dojo|
@dojos << {
id: dojo.id,
url: dojo.url,
@@ -101,12 +118,8 @@ class DojosController < ApplicationController
# respond_toで形式ごとに処理を分岐
respond_to do |format|
format.html # => app/views/dojos/index.html.erb
- format.json do
- params[:year].present? ? render_yearly_stats : render(json: @dojos)
- end
- format.csv do
- params[:year].present? ? render_yearly_stats : send_data(render_to_string, type: :csv)
- end
+ format.json { render json: @dojos }
+ format.csv { send_data render_to_string, type: :csv }
end
end
From 0024d7c06f082f8fe23d98d62a460f125608ae5a Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 12:11:30 +0900
Subject: [PATCH 04/37] =?UTF-8?q?docs:=20format.html=E3=82=92=E6=98=8E?=
=?UTF-8?q?=E7=A4=BA=E7=9A=84=E3=81=AB=E8=A8=98=E8=BF=B0=E3=81=97=E3=81=A6?=
=?UTF-8?q?=E5=8F=AF=E8=AA=AD=E6=80=A7=E3=82=92=E5=90=91=E4=B8=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
render :index を明示的に記述することで、
3つの形式すべてで何がレンダリングされるかを
コードリーディング時に一目で理解できるように改善
---
docs/plan_download_yearly_stats.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/plan_download_yearly_stats.md b/docs/plan_download_yearly_stats.md
index 4e2686ef4..2b17386df 100644
--- a/docs/plan_download_yearly_stats.md
+++ b/docs/plan_download_yearly_stats.md
@@ -117,7 +117,7 @@ class DojosController < ApplicationController
# respond_toで形式ごとに処理を分岐
respond_to do |format|
- format.html # => app/views/dojos/index.html.erb
+ format.html { render :index } # => app/views/dojos/index.html.erb
format.json { render json: @dojos }
format.csv { send_data render_to_string, type: :csv }
end
From 5194c45986b2be19a0dfbce9bd89d1b5ca9c5027 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 12:13:08 +0900
Subject: [PATCH 05/37] =?UTF-8?q?docs:=20Phase=201=E3=81=A8Phase=202?=
=?UTF-8?q?=E3=81=AE=E5=86=85=E5=AE=B9=E3=82=92=E6=AD=A3=E7=A2=BA=E3=81=AB?=
=?UTF-8?q?=E6=95=B4=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Phase 1(MVP):
- 特定年のアクティブ道場ダウンロードは既に含まれている
- yearパラメータによるフィルタリング機能を明記
Phase 2(将来):
- 重複していた項目を削除
- 都道府県別フィルタリングなど真の拡張機能を記載
---
docs/plan_download_yearly_stats.md | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/docs/plan_download_yearly_stats.md b/docs/plan_download_yearly_stats.md
index 2b17386df..8615205af 100644
--- a/docs/plan_download_yearly_stats.md
+++ b/docs/plan_download_yearly_stats.md
@@ -13,20 +13,21 @@ CoderDojoの統計データを年次でダウンロードできる機能を実
## 🎯 要件定義
### Phase 1: 基本実装(MVP)
-1. `/dojos` ページにCSVダウンロードリンクを追加
-2. 年次統計データをCSV形式でダウンロード可能に
+1. `/dojos` ページに年次フィルタリング機能を追加
+2. 特定年のアクティブ道場リストをCSV/JSON形式でダウンロード可能に
3. データ内容:
- - 年
- - アクティブ道場数(年末時点)
- - 新規開設道場数
- - 非アクティブ化道場数
- - 累積道場数
+ - yearパラメータなし: 全道場リスト(アクティブ + 非アクティブ)
+ - yearパラメータあり: その年末時点のアクティブ道場リスト
+4. 対応形式:
+ - HTML(表示用)
+ - CSV(ダウンロード用)
+ - JSON(API用)
### Phase 2: 拡張機能(将来)
-- 特定年のアクティブ道場のみダウンロード
-- 都道府県別・地域別の統計
+- 都道府県別・地域別でのフィルタリング(例: ?year=2024&prefecture=東京都)
- イベント数・参加者数の統計も含める
-- JSON形式のサポート
+- 年次推移の統計データ(全年の集計データ)
+- より詳細なCSVエクスポートオプション
## 🏗️ 技術設計
From c3a24db24637d58d52bf7eec4aee7ee73d6af9ed Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 12:24:42 +0900
Subject: [PATCH 06/37] =?UTF-8?q?docs:=20=E9=87=8D=E8=A6=81=E3=81=AA?=
=?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B1=E3=83=BC=E3=82=B9=E3=82=92?=
=?UTF-8?q?=E8=BF=BD=E5=8A=A0=20-=20=E9=9D=9E=E3=82=A2=E3=82=AF=E3=83=86?=
=?UTF-8?q?=E3=82=A3=E3=83=96=E9=81=93=E5=A0=B4=E3=81=AE=E6=89=B1=E3=81=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
yearパラメータ指定時:
- HTML/JSON/CSVすべてで非アクティブ道場を含まないことを確認
yearパラメータなし:
- CSV/JSONは全道場(非アクティブ含む)を確認
- HTMLはアクティブのみ(既存動作)を確認
これにより仕様が正しく実装されることを保証
---
docs/plan_download_yearly_stats.md | 48 ++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/docs/plan_download_yearly_stats.md b/docs/plan_download_yearly_stats.md
index 8615205af..110cc0555 100644
--- a/docs/plan_download_yearly_stats.md
+++ b/docs/plan_download_yearly_stats.md
@@ -381,6 +381,30 @@ RSpec.describe DojosController, type: :controller do
csv = CSV.parse(response.body)
expect(csv[0]).to eq(['年', '年末アクティブ道場数', '新規開設数', '非アクティブ化数', '累積合計', '純増減'])
end
+
+ it 'yearパラメータなしの場合は非アクティブな道場も含む(CSV/JSON)' do
+ active_dojo = create(:dojo, is_active: true)
+ inactive_dojo = create(:dojo, is_active: false, inactivated_at: '2021-03-01')
+
+ # JSON形式: 全道場を含む
+ get :index, format: :json
+ json_response = JSON.parse(response.body)
+ json_ids = json_response.map { |d| d['id'] }
+ expect(json_ids).to include(active_dojo.id)
+ expect(json_ids).to include(inactive_dojo.id)
+
+ # CSV形式: 全道場を含む
+ get :index, format: :csv
+ csv = CSV.parse(response.body, headers: true)
+ csv_ids = csv.map { |row| row['ID'].to_i }
+ expect(csv_ids).to include(active_dojo.id)
+ expect(csv_ids).to include(inactive_dojo.id)
+
+ # HTML形式: アクティブな道場のみ(既存の動作を維持)
+ get :index, format: :html
+ expect(assigns(:dojos).map { |d| d[:id] }).to include(active_dojo.id)
+ expect(assigns(:dojos).map { |d| d[:id] }).not_to include(inactive_dojo.id)
+ end
end
context '特定年のデータ(year=2020)' do
@@ -396,6 +420,30 @@ RSpec.describe DojosController, type: :controller do
expect(csv[0]).to eq(['ID', '道場名', '都道府県', 'URL', '設立日', '状態'])
expect(csv.size - 1).to eq(2) # ヘッダーを除いて2道場
end
+
+ it 'yearパラメータ指定時は非アクティブな道場を含まない(全形式)' do
+ # テストデータ: 2020年にアクティブ、2021年に非アクティブ化した道場
+ inactive_dojo = create(:dojo,
+ created_at: '2019-01-01',
+ is_active: false,
+ inactivated_at: '2021-03-01'
+ )
+
+ # HTML形式
+ get :index, params: { year: '2020' }, format: :html
+ expect(assigns(:dojos).map { |d| d[:id] }).not_to include(inactive_dojo.id)
+
+ # JSON形式
+ get :index, params: { year: '2020' }, format: :json
+ json_response = JSON.parse(response.body)
+ expect(json_response.map { |d| d['id'] }).not_to include(inactive_dojo.id)
+
+ # CSV形式
+ get :index, params: { year: '2020' }, format: :csv
+ csv = CSV.parse(response.body, headers: true)
+ csv_ids = csv.map { |row| row['ID'].to_i }
+ expect(csv_ids).not_to include(inactive_dojo.id)
+ end
end
context '無効な年が指定された場合' do
From 658583f192c3d39044ca878b56dd7d9bc8f5b95f Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 12:32:09 +0900
Subject: [PATCH 07/37] =?UTF-8?q?docs:=20=E5=AE=9F=E8=A3=85=E8=A8=88?=
=?UTF-8?q?=E7=94=BB=E3=82=92=E5=A4=A7=E5=B9=85=E3=81=AB=E7=B0=A1=E7=B4=A0?=
=?UTF-8?q?=E5=8C=96=20-=20=E6=97=A2=E5=AD=98=E5=8B=95=E4=BD=9C=E3=81=AE?=
=?UTF-8?q?=E6=AD=A3=E3=81=97=E3=81=84=E7=90=86=E8=A7=A3=E3=81=AB=E5=9F=BA?=
=?UTF-8?q?=E3=81=A5=E3=81=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
重要な気づき:
- 既存の/dojosは既に全道場(非アクティブ含む)を返している
- CSV形式のサポートを追加するだけで良い
- render_yearly_statsなどの複雑なメソッドは不要
- yearパラメータでのフィルタリングのみ追加
これにより実装がシンプルになり、既存コードの変更を最小限に
---
docs/plan_download_yearly_stats.md | 106 +++--------------------------
1 file changed, 10 insertions(+), 96 deletions(-)
diff --git a/docs/plan_download_yearly_stats.md b/docs/plan_download_yearly_stats.md
index 110cc0555..22d064417 100644
--- a/docs/plan_download_yearly_stats.md
+++ b/docs/plan_download_yearly_stats.md
@@ -5,8 +5,7 @@ CoderDojoの統計データを年次でダウンロードできる機能を実
### データの取得範囲
- **yearパラメータなし(デフォルト)**:
- - HTML表示: 現在アクティブな道場のみ(既存の動作を維持)
- - CSV/JSONダウンロード: 全道場(アクティブ + 非アクティブ)
+ - 全形式(HTML/JSON/CSV): 全道場(アクティブ + 非アクティブ)※既存の動作そのまま
- **yearパラメータあり(例: year=2024)**:
- HTML/JSON/CSV すべての形式: その年末時点でアクティブだった道場のみ
@@ -88,19 +87,9 @@ class DojosController < ApplicationController
@page_title = "#{@selected_year}年末時点のCoderDojo一覧"
else
- # yearパラメータなしの場合
- # HTML表示: 現在のアクティブな道場のみ(既存の実装を維持)
- # CSV/JSONダウンロード: 全道場(アクティブ + 非アクティブ)
- if request.format.html?
- # HTMLの場合は現在アクティブな道場のみ
- dojos_scope = Dojo.active
- else
- # CSV/JSONの場合は全道場(非アクティブも含む)
- dojos_scope = Dojo.all
- end
-
+ # yearパラメータなしの場合(既存の実装そのまま)
@dojos = []
- dojos_scope.includes(:prefecture).order(order: :asc).each do |dojo|
+ Dojo.includes(:prefecture).order(order: :asc).all.each do |dojo|
@dojos << {
id: dojo.id,
url: dojo.url,
@@ -120,88 +109,13 @@ class DojosController < ApplicationController
respond_to do |format|
format.html { render :index } # => app/views/dojos/index.html.erb
format.json { render json: @dojos }
- format.csv { send_data render_to_string, type: :csv }
+ format.csv { send_data render_to_string, type: :csv } # 新規追加
end
end
def show
# 既存の実装のまま
end
-
- private
-
- def render_yearly_stats
- @period_start = 2012
- @period_end = Date.current.year
-
- # yearパラメータが指定されている場合(整数のみ許可)
- if @selected_year # 既にindexアクションで設定済み
- period = Time.zone.local(@selected_year).beginning_of_year..Time.zone.local(@selected_year).end_of_year
- @stat = Stat.new(period)
- @yearly_data = prepare_single_year_data(@stat, @selected_year)
- filename_suffix = @selected_year.to_s
- else
- # yearパラメータなし = 全年次データ
- period = Time.zone.local(@period_start).beginning_of_year..Time.zone.local(@period_end).end_of_year
- @stat = Stat.new(period)
- @yearly_data = prepare_all_years_data(@stat)
- filename_suffix = 'all'
- end
-
- # CSVまたはJSONとして返す
- respond_to do |format|
- format.csv do
- send_data render_to_string(template: 'dojos/yearly_stats'),
- type: :csv,
- filename: "coderdojo_stats_#{filename_suffix}_#{Date.current.strftime('%Y%m%d')}.csv"
- end
- format.json { render json: @yearly_data }
- end
- end
-
- def prepare_all_years_data(stat)
- active_dojos = stat.annual_dojos_with_historical_data
- new_dojos = stat.annual_new_dojos_count
-
- # 年ごとのデータを整形
- years = (@period_start..@period_end).map(&:to_s)
- years.map do |year|
- prev_year = (year.to_i - 1).to_s
- {
- year: year,
- active_dojos_at_year_end: active_dojos[year] || 0,
- new_dojos: new_dojos[year] || 0,
- inactivated_dojos: calculate_inactivated_count(year),
- cumulative_total: active_dojos[year] || 0,
- net_change: prev_year && active_dojos[prev_year] ?
- (active_dojos[year] || 0) - active_dojos[prev_year] :
- (active_dojos[year] || 0)
- }
- end
- end
-
- def prepare_single_year_data(stat, year)
- # 特定年のアクティブな道場リストを返す
- end_of_year = Time.zone.local(year).end_of_year
- dojos = Dojo.active_at(end_of_year).includes(:prefecture)
-
- dojos.map do |dojo|
- {
- id: dojo.id,
- name: dojo.name,
- prefecture: dojo.prefecture.name,
- url: dojo.url,
- created_at: dojo.created_at.strftime('%Y-%m-%d'),
- is_active_at_year_end: dojo.active_at?(end_of_year)
- }
- end
- end
-
- def calculate_inactivated_count(year)
- start_of_year = Time.zone.local(year.to_i).beginning_of_year
- end_of_year = Time.zone.local(year.to_i).end_of_year
- Dojo.where(inactivated_at: start_of_year..end_of_year).sum(:counter)
- end
end
```
@@ -382,10 +296,15 @@ RSpec.describe DojosController, type: :controller do
expect(csv[0]).to eq(['年', '年末アクティブ道場数', '新規開設数', '非アクティブ化数', '累積合計', '純増減'])
end
- it 'yearパラメータなしの場合は非アクティブな道場も含む(CSV/JSON)' do
+ it 'yearパラメータなしの場合は非アクティブな道場も含む(全形式)' do
active_dojo = create(:dojo, is_active: true)
inactive_dojo = create(:dojo, is_active: false, inactivated_at: '2021-03-01')
+ # HTML形式: 全道場を含む
+ get :index, format: :html
+ expect(assigns(:dojos).map { |d| d[:id] }).to include(active_dojo.id)
+ expect(assigns(:dojos).map { |d| d[:id] }).to include(inactive_dojo.id)
+
# JSON形式: 全道場を含む
get :index, format: :json
json_response = JSON.parse(response.body)
@@ -399,11 +318,6 @@ RSpec.describe DojosController, type: :controller do
csv_ids = csv.map { |row| row['ID'].to_i }
expect(csv_ids).to include(active_dojo.id)
expect(csv_ids).to include(inactive_dojo.id)
-
- # HTML形式: アクティブな道場のみ(既存の動作を維持)
- get :index, format: :html
- expect(assigns(:dojos).map { |d| d[:id] }).to include(active_dojo.id)
- expect(assigns(:dojos).map { |d| d[:id] }).not_to include(inactive_dojo.id)
end
end
From d58e5f4c20131b08bfa67ad79cad82b9ac0bf860 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 12:51:30 +0900
Subject: [PATCH 08/37] =?UTF-8?q?feat:=20=E9=81=93=E5=A0=B4=E7=B5=B1?=
=?UTF-8?q?=E8=A8=88=E3=81=AE=E5=B9=B4=E6=AC=A1=E3=83=95=E3=82=A3=E3=83=AB?=
=?UTF-8?q?=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=A8CSV=E3=83=80?=
=?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=AD=E3=83=BC=E3=83=89=E6=A9=9F=E8=83=BD?=
=?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
主な変更内容:
- /dojos エンドポイントにCSV形式サポートを追加
- yearパラメータによる年末時点のアクティブ道場フィルタリング
- 年選択UIとダウンロードボタンをHTMLビューに追加
機能詳細:
- GET /dojos.csv → 全道場リスト(CSV形式)
- GET /dojos?year=2020 → 2020年末時点のアクティブ道場(HTML)
- GET /dojos.json?year=2020 → 同上(JSON形式)
- GET /dojos.csv?year=2020 → 同上(CSV形式)
エラーハンドリング:
- 無効な年が指定された場合はフラッシュメッセージでエラーを表示
- 2012年から現在年までの範囲チェック実装
Closes #1373
---
app/controllers/dojos_controller.rb | 34 ++++++++++++++++++++----
app/views/dojos/index.csv.ruby | 18 +++++++++++++
app/views/dojos/index.html.erb | 40 +++++++++++++++++++++++++++++
3 files changed, 87 insertions(+), 5 deletions(-)
create mode 100644 app/views/dojos/index.csv.ruby
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index 8cd685be7..b0f5fd2c7 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -1,9 +1,34 @@
class DojosController < ApplicationController
- # GET /dojos[.json]
+ # GET /dojos[.html|.json|.csv]
def index
+ # yearパラメータがある場合の処理
+ if params[:year].present?
+ begin
+ year = params[:year].to_i
+ # 有効な年の範囲をチェック
+ unless year.between?(2012, Date.current.year)
+ flash[:alert] = "指定された年(#{year})は無効です。2012年から#{Date.current.year}年の間で指定してください。"
+ return redirect_to dojos_path
+ end
+
+ @selected_year = year
+ end_of_year = Time.zone.local(@selected_year).end_of_year
+
+ # その年末時点でアクティブだった道場を取得
+ dojos_scope = Dojo.active_at(end_of_year)
+ @page_title = "#{@selected_year}年末時点のCoderDojo一覧"
+ rescue ArgumentError
+ flash[:alert] = "無効な年が指定されました"
+ return redirect_to dojos_path
+ end
+ else
+ # yearパラメータなしの場合(既存の実装そのまま)
+ dojos_scope = Dojo.all
+ end
+
@dojos = []
- Dojo.includes(:prefecture).order(order: :asc).all.each do |dojo|
+ dojos_scope.includes(:prefecture).order(order: :asc).each do |dojo|
@dojos << {
id: dojo.id,
url: dojo.url,
@@ -19,10 +44,9 @@ def index
end
respond_to do |format|
- # No corresponding View for now.
- # Only for API: GET /dojos.json
- format.html # => app/views/dojos/index.html.erb
+ format.html { render :index } # => app/views/dojos/index.html.erb
format.json { render json: @dojos }
+ format.csv { send_data render_to_string, type: :csv }
end
end
diff --git a/app/views/dojos/index.csv.ruby b/app/views/dojos/index.csv.ruby
new file mode 100644
index 000000000..81d9300de
--- /dev/null
+++ b/app/views/dojos/index.csv.ruby
@@ -0,0 +1,18 @@
+require 'csv'
+
+csv_data = CSV.generate do |csv|
+ # ヘッダー行
+ csv << ['ID', '道場名', '都道府県', 'URL', '設立日', '状態']
+
+ # データ行
+ @dojos.each do |dojo|
+ csv << [
+ dojo[:id],
+ dojo[:name],
+ dojo[:prefecture],
+ dojo[:url],
+ dojo[:created_at].strftime("%F"),
+ dojo[:is_active] ? 'アクティブ' : '非アクティブ'
+ ]
+ end
+end
\ No newline at end of file
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 5871048bc..030e57e2c 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -21,9 +21,49 @@
現在は活動停止中 (In-active) の道場も表示されています
道場名をクリックすると個別の統計データが確認できます
下記表は <%= link_to dojos_path(format: :json), dojos_path(format: :json) %> で JSON に変換できます
+ CSV形式でもダウンロード可能です <%= link_to dojos_path(format: :csv), dojos_path(format: :csv) %>
+
+
+
+
📊 年次データのフィルタリング
+
+ <% if @selected_year %>
+
+ <%= @selected_year %>年末時点 のアクティブな道場を表示中
+ <%= link_to '全道場を表示', dojos_path, style: 'margin-left: 10px;' %>
+
+ <% end %>
+
+ <%= form_with(url: dojos_path, method: :get, local: true, style: 'margin-top: 15px;') do |f| %>
+
+ <%= label_tag :year, '年を選択:', style: 'font-weight: bold;' %>
+ <%= select_tag :year,
+ options_for_select(
+ (2012..Date.current.year).to_a.reverse.map { |y| [y.to_s + '年', y] },
+ params[:year]
+ ),
+ include_blank: '全道場',
+ style: 'padding: 5px; border: 1px solid #ced4da; border-radius: 4px;' %>
+
+ <%= button_tag type: 'submit', style: 'padding: 5px 15px; background: #17a2b8; color: white; border: none; border-radius: 4px; cursor: pointer;' do %>
+ 表示
+ <% end %>
+
+ <%= link_to 'CSV', dojos_path(format: :csv, year: params[:year]),
+ style: 'padding: 5px 15px; background: #28a745; color: white; text-decoration: none; border-radius: 4px;' %>
+
+ <%= link_to 'JSON', dojos_path(format: :json, year: params[:year]),
+ style: 'padding: 5px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;' %>
+
+ <% end %>
+
+
+ ※ 年を選択すると、その年末時点でアクティブだった道場のデータを表示・ダウンロードできます。
+
+
-
+
@@ -113,26 +113,49 @@
<% @dojos.each do |dojo| %>
-
-
- <%= link_to dojo_path(dojo[:id]) do %>
- <%= dojo[:name] %>
- (ID: <%= dojo[:id] %>)
- <% end %>
-
-
-
- <%= dojo[:created_at].strftime("%F") %>
-
-
-
-
-
- <%= CGI.unescape dojo[:url].gsub('https://', '').gsub('http://', '').gsub('www.', '').chomp('/') %>
-
-
-
-
+ <% if dojo[:is_active] %>
+
+
+ <%= link_to dojo_path(dojo[:id]) do %>
+ <%= dojo[:name] %>
+ (ID: <%= dojo[:id] %>)
+ <% end %>
+
+
+
+ <%= dojo[:created_at].strftime("%F") %>
+
+
+
+
+
+ <%= CGI.unescape dojo[:url].gsub('https://', '').gsub('http://', '').gsub('www.', '').chomp('/') %>
+
+
+
+
+ <% else %>
+
+
+ <%= link_to dojo_path(dojo[:id]) do %>
+ <%= dojo[:name] %>
+ (ID: <%= dojo[:id] %>)
+ <% end %>
+
+
+
+ <%= dojo[:created_at].strftime("%F") %>
+
+
+
+
+
+ <%= CGI.unescape dojo[:url].gsub('https://', '').gsub('http://', '').gsub('www.', '').chomp('/') %>
+
+
+
+
+ <% end %>
<% end %>
diff --git a/app/views/stats/show.html.erb b/app/views/stats/show.html.erb
index 26ee8a46d..91ecc9d6b 100644
--- a/app/views/stats/show.html.erb
+++ b/app/views/stats/show.html.erb
@@ -361,7 +361,7 @@
-
+
<%= @lang == 'en' ? 'Prefecture' : '都道府県名' %>
@@ -373,10 +373,10 @@
<% @data_by_prefecture.each_with_index do |(prefecture, count), index| %>
<% if count == 0 %>
-
+
<%= prefecture %>
-
+
<%= count %>
<% else %>
From a0d34fb554ba11a183eefa3ba6100d7fbe48a2f1 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 13:56:58 +0900
Subject: [PATCH 17/37] =?UTF-8?q?feat:=20=E3=82=A2=E3=82=AF=E3=83=86?=
=?UTF-8?q?=E3=82=A3=E3=83=96=E3=81=AA=E9=81=93=E5=A0=B4=E3=82=92=E5=85=88?=
=?UTF-8?q?=E3=81=AB=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=88=E3=81=86?=
=?UTF-8?q?=E3=82=BD=E3=83=BC=E3=83=88=E9=A0=86=E3=82=92=E6=94=B9=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- すべての形式(HTML/JSON/CSV)でアクティブな道場を先に表示
- order by is_active DESC, order ASC でソート
- アクティブな道場が見つけやすくなり、非アクティブな道場は最後にまとめて表示
---
app/controllers/dojos_controller.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index 8bfd1592b..620b6537b 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -28,7 +28,7 @@ def index
end
@dojos = []
- dojos_scope.includes(:prefecture).order(order: :asc).each do |dojo|
+ dojos_scope.includes(:prefecture).order(is_active: :desc, order: :asc).each do |dojo|
@dojos << {
id: dojo.id,
url: dojo.url,
From a2d51066c369702fbd8ce72e904d14941eed5681 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:00:21 +0900
Subject: [PATCH 18/37] =?UTF-8?q?improve:=20=E9=95=B7=E3=81=84URL=E3=82=92?=
=?UTF-8?q?30=E6=96=87=E5=AD=97=E3=81=A7=E5=88=87=E3=82=8A=E8=A9=B0?=
=?UTF-8?q?=E3=82=81=E3=81=A6=E8=A1=A8=E7=A4=BA=EF=BC=88Rails=E3=81=AEtrun?=
=?UTF-8?q?cate=E3=83=98=E3=83=AB=E3=83=91=E3=83=BC=E4=BD=BF=E7=94=A8?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Railsのtruncateヘルパーメソッドを使用
- 30文字を超えるURLは「...」で省略表示
- title属性で完全なURLをツールチップ表示
- リンク先は完全なURLを維持
---
app/views/dojos/index.html.erb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index f83d07fd6..6e1bed7d3 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -129,7 +129,7 @@
- <%= CGI.unescape dojo[:url].gsub('https://', '').gsub('http://', '').gsub('www.', '').chomp('/') %>
+ <%= truncate(CGI.unescape(dojo[:url].gsub('https://', '').gsub('http://', '').gsub('www.', '').chomp('/')), length: 30) %>
@@ -150,7 +150,7 @@
- <%= CGI.unescape dojo[:url].gsub('https://', '').gsub('http://', '').gsub('www.', '').chomp('/') %>
+ <%= truncate(CGI.unescape(dojo[:url].gsub('https://', '').gsub('http://', '').gsub('www.', '').chomp('/')), length: 30) %>
From 0b51b269e6f4aaab5076b01659d649ccebf5c13f Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:01:59 +0900
Subject: [PATCH 19/37] =?UTF-8?q?improve:=20=E5=B9=B4=E6=AC=A1=E3=83=95?=
=?UTF-8?q?=E3=82=A3=E3=83=AB=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E8=AA=AC?=
=?UTF-8?q?=E6=98=8E=E6=96=87=E3=82=92=E3=80=8C=E5=AF=BE=E8=B1=A1=E6=9C=9F?=
=?UTF-8?q?=E9=96=93=E3=80=8D=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- '年を選択すると' → '対象期間を選択すると'
- 'その年末時点でアクティブだった道場のデータ' → 'その時点のアクティブな道場の一覧'
- より簡潔で分かりやすい表現に
---
app/views/dojos/index.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 6e1bed7d3..5ed76ab06 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -61,7 +61,7 @@
<% end %>
- ※ 年を選択すると、その年末時点でアクティブだった道場のデータを表示・ダウンロードできます。
+ ※ 対象期間を選択すると、その時点のアクティブな道場の一覧を表示・ダウンロードできます。
From fde0c7334e0b8b630ce356b18e1fcaace0ae819b Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:03:05 +0900
Subject: [PATCH 20/37] =?UTF-8?q?style:=20=E8=AA=AC=E6=98=8E=E6=96=87?=
=?UTF-8?q?=E3=81=8B=E3=82=89=E3=80=8C=E2=80=BB=E3=80=8D=E8=A8=98=E5=8F=B7?=
=?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- フォントサイズとグレー色で説明文であることが明確
- 「※」記号は冗長なので削除
---
app/views/dojos/index.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 5ed76ab06..69bd27284 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -61,7 +61,7 @@
<% end %>
- ※ 対象期間を選択すると、その時点のアクティブな道場の一覧を表示・ダウンロードできます。
+ 対象期間を選択すると、その時点のアクティブな道場の一覧を表示・ダウンロードできます。
From 88937e7ce90b35b9a8518aa39b98c7e0e1c2d988 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:06:25 +0900
Subject: [PATCH 21/37] =?UTF-8?q?improve:=20=E5=AF=BE=E8=B1=A1=E6=9C=9F?=
=?UTF-8?q?=E9=96=93=E3=82=BB=E3=83=AC=E3=82=AF=E3=83=88=E3=83=9C=E3=83=83?=
=?UTF-8?q?=E3=82=AF=E3=82=B9=E3=81=AE=E5=A4=89=E6=9B=B4=E3=81=A7=E8=87=AA?=
=?UTF-8?q?=E5=8B=95=E9=81=B7=E7=A7=BB=EF=BC=88=E8=A1=A8=E7=A4=BA=E3=83=9C?=
=?UTF-8?q?=E3=82=BF=E3=83=B3=E5=89=8A=E9=99=A4=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- onchangeイベントで選択と同時にページ遷移
- 「表示」ボタンを削除してUIをシンプルに
- form_withタグも不要になったため削除
- より直感的で素早い操作が可能に
---
app/views/dojos/index.html.erb | 39 +++++++++++++++-------------------
spec/requests/dojos_spec.rb | 3 ++-
2 files changed, 19 insertions(+), 23 deletions(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 69bd27284..cead00a58 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -37,28 +37,23 @@
<% end %>
- <%= form_with(url: dojos_path, method: :get, local: true, style: 'margin-top: 15px;') do |f| %>
-
- <%= label_tag :year, '対象期間:', style: 'font-weight: bold;' %>
- <%= select_tag :year,
- options_for_select(
- (2012..Date.current.year).to_a.reverse.map { |y| [y.to_s + '年', y] },
- params[:year]
- ),
- include_blank: '全期間',
- style: 'padding: 5px; border: 1px solid #ced4da; border-radius: 4px;' %>
-
- <%= button_tag type: 'submit', style: 'padding: 5px 15px; background: #17a2b8; color: white; border: none; border-radius: 4px; cursor: pointer;' do %>
- 表示
- <% end %>
-
- <%= link_to 'CSV', dojos_path(format: :csv, year: params[:year]),
- style: 'padding: 5px 15px; background: #28a745; color: white; text-decoration: none; border-radius: 4px;' %>
-
- <%= link_to 'JSON', dojos_path(format: :json, year: params[:year]),
- style: 'padding: 5px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;' %>
-
- <% end %>
+
+ <%= label_tag :year, '対象期間:', style: 'font-weight: bold;' %>
+ <%= select_tag :year,
+ options_for_select(
+ (2012..Date.current.year).to_a.reverse.map { |y| [y.to_s + '年', y] },
+ params[:year]
+ ),
+ include_blank: '全期間',
+ onchange: "window.location.href = '#{dojos_path}?year=' + this.value",
+ style: 'padding: 5px; border: 1px solid #ced4da; border-radius: 4px; cursor: pointer;' %>
+
+ <%= link_to 'CSV', dojos_path(format: :csv, year: params[:year]),
+ style: 'padding: 5px 15px; background: #28a745; color: white; text-decoration: none; border-radius: 4px;' %>
+
+ <%= link_to 'JSON', dojos_path(format: :json, year: params[:year]),
+ style: 'padding: 5px 15px; background: #6c757d; color: white; text-decoration: none; border-radius: 4px;' %>
+
対象期間を選択すると、その時点のアクティブな道場の一覧を表示・ダウンロードできます。
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index d34931f29..eb4d8b24c 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -240,10 +240,11 @@
end
describe "HTML format year selection UI" do
- it "shows year selection form" do
+ it "shows year selection form with auto-submit" do
get dojos_path
expect(response.body).to include('対象期間')
expect(response.body).to include('
Date: Fri, 8 Aug 2025 14:10:13 +0900
Subject: [PATCH 22/37] =?UTF-8?q?feat:=20=E3=83=86=E3=83=BC=E3=83=96?=
=?UTF-8?q?=E3=83=AB=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=B8?=
=?UTF-8?q?=E3=81=AE=E5=86=85=E9=83=A8=E3=83=AA=E3=83=B3=E3=82=AF=E3=82=92?=
=?UTF-8?q?=E8=BF=BD=E5=8A=A0=EF=BC=88#table=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- h3タグにid='table'を追加し、📊アイコンを内部リンクに
- 対象期間セレクトボックスの自動遷移に#tableアンカーを追加
- '全道場を表示'リンクにも#tableアンカーを追加
- ページ遷移後に自動的にテーブル位置へスクロール
---
app/views/dojos/index.html.erb | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index cead00a58..9a54526bb 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -28,12 +28,15 @@
-
📊 年次データのフィルタリング
+
+ 📊
+ 年次データのフィルタリング
+
<% if @selected_year %>
<%= @selected_year %>年末時点 のアクティブな道場を表示中
- <%= link_to '全道場を表示', dojos_path, style: 'margin-left: 10px;' %>
+ <%= link_to '全道場を表示', dojos_path(anchor: 'table'), style: 'margin-left: 10px;' %>
<% end %>
@@ -45,7 +48,7 @@
params[:year]
),
include_blank: '全期間',
- onchange: "window.location.href = '#{dojos_path}?year=' + this.value",
+ onchange: "window.location.href = '#{dojos_path}?year=' + this.value + '#table'",
style: 'padding: 5px; border: 1px solid #ced4da; border-radius: 4px; cursor: pointer;' %>
<%= link_to 'CSV', dojos_path(format: :csv, year: params[:year]),
From 0d07eb4cffb87bf61814fdb0aa1fbaefb95463f0 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:11:38 +0900
Subject: [PATCH 23/37] =?UTF-8?q?remove:=20=E3=80=8C=E5=85=A8=E9=81=93?=
=?UTF-8?q?=E5=A0=B4=E3=82=92=E8=A1=A8=E7=A4=BA=E3=80=8D=E3=83=AA=E3=83=B3?=
=?UTF-8?q?=E3=82=AF=E3=82=92=E5=89=8A=E9=99=A4=EF=BC=88=E3=82=BB=E3=83=AC?=
=?UTF-8?q?=E3=82=AF=E3=83=88=E3=83=9C=E3=83=83=E3=82=AF=E3=82=B9=E3=81=A7?=
=?UTF-8?q?=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E5=8F=AF=E8=83=BD=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- セレクトボックスで簡単に切り替えられるため冗長
- UIをよりシンプルに
- 選択中の年のみを表示する簡潔な表示に
---
app/views/dojos/index.html.erb | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 9a54526bb..bc33b7d98 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -36,7 +36,6 @@
<% if @selected_year %>
<%= @selected_year %>年末時点 のアクティブな道場を表示中
- <%= link_to '全道場を表示', dojos_path(anchor: 'table'), style: 'margin-left: 10px;' %>
<% end %>
From 27928f49a5bfabd8321b026cb2e129c714552b90 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:14:19 +0900
Subject: [PATCH 24/37] =?UTF-8?q?fix:=20=E5=85=A8=E6=9C=9F=E9=96=93?=
=?UTF-8?q?=E9=81=B8=E6=8A=9E=E6=99=82=E3=81=AEURL=E3=82=92=E3=82=B7?=
=?UTF-8?q?=E3=83=B3=E3=83=97=E3=83=AB=E3=81=AB=EF=BC=88/dojos#table?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 全期間選択時は ?year= パラメータを付けない
- /dojos?year=#table → /dojos#table に修正
- 三項演算子で値がある場合とない場合を判定
---
app/views/dojos/index.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index bc33b7d98..91f363c0d 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -47,7 +47,7 @@
params[:year]
),
include_blank: '全期間',
- onchange: "window.location.href = '#{dojos_path}?year=' + this.value + '#table'",
+ onchange: "window.location.href = this.value ? '#{dojos_path}?year=' + this.value + '#table' : '#{dojos_path}#table'",
style: 'padding: 5px; border: 1px solid #ced4da; border-radius: 4px; cursor: pointer;' %>
<%= link_to 'CSV', dojos_path(format: :csv, year: params[:year]),
From 5cda416589db229736dfb43c537f169bd8d968e8 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:17:17 +0900
Subject: [PATCH 25/37] =?UTF-8?q?feat:=20=E3=82=A8=E3=83=A9=E3=83=BC?=
=?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E5=B9=B4?=
=?UTF-8?q?=E6=AC=A1=E3=83=95=E3=82=A3=E3=83=AB=E3=82=BF=E3=83=AA=E3=83=B3?=
=?UTF-8?q?=E3=82=B0=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AB?=
=?UTF-8?q?=E8=A1=A8=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- flash[:alert]を同じ位置に赤い背景色で表示
- 内部リンク(#table)でジャンプしても見える位置に配置
- リダイレクト時も#tableアンカーを追加
- Bootstrap風のアラートスタイル(background: #f8d7da)を適用
---
app/controllers/dojos_controller.rb | 4 ++--
app/views/dojos/index.html.erb | 6 +++++-
spec/requests/dojos_spec.rb | 6 +++---
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index 620b6537b..e80f8475a 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -9,7 +9,7 @@ def index
# 有効な年の範囲をチェック
unless year.between?(2012, Date.current.year)
flash[:alert] = "指定された年(#{year})は無効です。2012年から#{Date.current.year}年の間で指定してください。"
- return redirect_to dojos_path
+ return redirect_to dojos_path(anchor: 'table')
end
@selected_year = year
@@ -20,7 +20,7 @@ def index
@page_title = "#{@selected_year}年末時点のCoderDojo一覧"
rescue ArgumentError
flash[:alert] = "無効な年が指定されました"
- return redirect_to dojos_path
+ return redirect_to dojos_path(anchor: 'table')
end
else
# yearパラメータなしの場合(既存の実装そのまま)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 91f363c0d..5576db1fc 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -33,7 +33,11 @@
年次データのフィルタリング
- <% if @selected_year %>
+ <% if flash[:alert] %>
+
+ <%= flash[:alert] %>
+
+ <% elsif @selected_year %>
<%= @selected_year %>年末時点 のアクティブな道場を表示中
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index eb4d8b24c..0c63033c0 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -54,20 +54,20 @@
it "rejects years before 2012" do
get dojos_path(year: 2011, format: :json)
- expect(response).to redirect_to(dojos_path)
+ expect(response).to redirect_to(dojos_path(anchor: 'table'))
expect(flash[:alert]).to include("2012年から")
end
it "rejects years after current year" do
future_year = Date.current.year + 1
get dojos_path(year: future_year, format: :json)
- expect(response).to redirect_to(dojos_path)
+ expect(response).to redirect_to(dojos_path(anchor: 'table'))
expect(flash[:alert]).to include("無効です")
end
it "handles invalid year strings" do
get dojos_path(year: "invalid", format: :json)
- expect(response).to redirect_to(dojos_path)
+ expect(response).to redirect_to(dojos_path(anchor: 'table'))
expect(flash[:alert]).to include("無効")
end
end
From 697e8af4a03c585ad24ad48d158d3bf43fd579c3 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:19:51 +0900
Subject: [PATCH 26/37] =?UTF-8?q?security:=20=E3=82=A8=E3=83=A9=E3=83=BC?=
=?UTF-8?q?=E3=83=A1=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=81=8B=E3=82=89?=
=?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E5=85=A5=E5=8A=9B=E5=80=A4?=
=?UTF-8?q?=E3=82=92=E9=99=A4=E5=A4=96=EF=BC=88XSS=E5=AF=BE=E7=AD=96?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- エラーメッセージに具体的な入力値を含めない
- '指定された年(値)は無効です' → '指定された年は無効です'
- 現在も.to_iとHTMLエスケープで安全だが、より防御的な実装に
- セキュリティのベストプラクティスに従う
---
app/controllers/dojos_controller.rb | 2 +-
spec/requests/dojos_spec.rb | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index e80f8475a..ff8885070 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -8,7 +8,7 @@ def index
year = params[:year].to_i
# 有効な年の範囲をチェック
unless year.between?(2012, Date.current.year)
- flash[:alert] = "指定された年(#{year})は無効です。2012年から#{Date.current.year}年の間で指定してください。"
+ flash[:alert] = "指定された年は無効です。2012年から#{Date.current.year}年の間で指定してください。"
return redirect_to dojos_path(anchor: 'table')
end
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index 0c63033c0..88f0e5296 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -62,7 +62,7 @@
future_year = Date.current.year + 1
get dojos_path(year: future_year, format: :json)
expect(response).to redirect_to(dojos_path(anchor: 'table'))
- expect(flash[:alert]).to include("無効です")
+ expect(flash[:alert]).to include("指定された年は無効です")
end
it "handles invalid year strings" do
From 4ed4e33badf8f7c4421843ac0b1057af3024f64a Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 14:51:04 +0900
Subject: [PATCH 27/37] =?UTF-8?q?refactor:=20inline=5F=E3=83=97=E3=83=AC?=
=?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=83=E3=82=AF=E3=82=B9=E3=83=91=E3=82=BF?=
=?UTF-8?q?=E3=83=BC=E3=83=B3=E3=81=A7flash=E3=83=A1=E3=83=83=E3=82=BB?=
=?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=AE=E8=A1=A8=E7=A4=BA=E4=BD=8D=E7=BD=AE?=
=?UTF-8?q?=E3=82=92=E5=88=B6=E5=BE=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 実装内容
- 'inline_' プレフィックスがついたflashメッセージは、デフォルト位置(ページ上部)では表示しない
- 各ビュー内でカスタム位置に表示できるようにヘルパーメソッドを追加
- /dojos ページの年フィルタリングのアラートとインフォメッセージに適用
## メリット
- flashメッセージの2重表示問題を解決
- inline_alert → alert-alert のようにBootstrapのCSSクラスを自動適用
- 将来的に inline_warning, inline_success なども同じパターンで使用可能
- コードがシンプルになり、条件分岐が不要に
## 変更箇所
- flash[:alert] → flash[:inline_alert] に変更(エラー時)
- flash.now[:inline_info] を追加(成功時)
- render_inline_flash_messages ヘルパーメソッドを追加
- テストを inline_alert に対応
---
app/controllers/dojos_controller.rb | 7 +++++--
app/helpers/application_helper.rb | 9 +++++++++
app/views/dojos/index.html.erb | 10 +---------
app/views/layouts/application.html.erb | 5 ++++-
spec/requests/dojos_spec.rb | 8 +++++---
5 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index ff8885070..cc7ea6974 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -8,7 +8,7 @@ def index
year = params[:year].to_i
# 有効な年の範囲をチェック
unless year.between?(2012, Date.current.year)
- flash[:alert] = "指定された年は無効です。2012年から#{Date.current.year}年の間で指定してください。"
+ flash[:inline_alert] = "指定された年は無効です。2012年から#{Date.current.year}年の間で指定してください。"
return redirect_to dojos_path(anchor: 'table')
end
@@ -18,8 +18,11 @@ def index
# その年末時点でアクティブだった道場を取得
dojos_scope = Dojo.active_at(end_of_year)
@page_title = "#{@selected_year}年末時点のCoderDojo一覧"
+
+ # 成功メッセージもinline_で表示
+ flash.now[:inline_info] = "#{@selected_year}年末時点のアクティブな道場を表示中"
rescue ArgumentError
- flash[:alert] = "無効な年が指定されました"
+ flash[:inline_alert] = "無効な年が指定されました"
return redirect_to dojos_path(anchor: 'table')
end
else
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index da3d2f83f..8bb33b872 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -48,6 +48,15 @@ def page_lang(lang)
lang.empty? ? 'ja' : lang
end
+ # 'inline_' プレフィックスがついたflashメッセージをビュー内で表示するヘルパー
+ # inline_alert → alert, inline_warning → warning のように変換してBootstrapのCSSクラスを適用
+ def render_inline_flash_messages
+ flash.select { |type, _| type.to_s.start_with?('inline_') }.map do |type, message|
+ css_class = type.to_s.gsub('inline_', '')
+ content_tag(:div, message, class: "alert alert-#{css_class}", style: "margin-bottom: 15px;")
+ end.join.html_safe
+ end
+
def kata_description
"道場で役立つ資料やコンテスト情報、立ち上げ方や各種支援をまとめています。"
end
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 5576db1fc..1e5f1c9d7 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -33,15 +33,7 @@
年次データのフィルタリング
- <% if flash[:alert] %>
-
- <%= flash[:alert] %>
-
- <% elsif @selected_year %>
-
- <%= @selected_year %>年末時点 のアクティブな道場を表示中
-
- <% end %>
+ <%= render_inline_flash_messages %>
<%= label_tag :year, '対象期間:', style: 'font-weight: bold;' %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 30943f523..fcca91504 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -88,8 +88,11 @@
<%= render 'shared/header' %>
+ <%# 'inline_' プレフィックスがついたflashメッセージは、ここでは表示せず、各ビュー内でカスタム表示する %>
<% flash.each do |message_type, message| %>
-
<%= message %>
+ <% unless message_type.to_s.start_with?('inline_') %>
+
<%= message %>
+ <% end %>
<% end %>
<%= yield %>
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index 88f0e5296..200e0d10b 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -55,20 +55,20 @@
it "rejects years before 2012" do
get dojos_path(year: 2011, format: :json)
expect(response).to redirect_to(dojos_path(anchor: 'table'))
- expect(flash[:alert]).to include("2012年から")
+ expect(flash[:inline_alert]).to include("2012年から")
end
it "rejects years after current year" do
future_year = Date.current.year + 1
get dojos_path(year: future_year, format: :json)
expect(response).to redirect_to(dojos_path(anchor: 'table'))
- expect(flash[:alert]).to include("指定された年は無効です")
+ expect(flash[:inline_alert]).to include("指定された年は無効です")
end
it "handles invalid year strings" do
get dojos_path(year: "invalid", format: :json)
expect(response).to redirect_to(dojos_path(anchor: 'table'))
- expect(flash[:alert]).to include("無効")
+ expect(flash[:inline_alert]).to include("無効")
end
end
@@ -253,6 +253,8 @@
get dojos_path(year: 2020)
expect(response.body).to include('2020年末時点')
expect(response.body).to include('アクティブな道場を表示中')
+ # inline_infoメッセージが表示されることを確認
+ expect(response.body).to include('alert-info')
end
it "includes CSV and JSON download links with year parameter" do
From 40435161c77a17aa5f86571235d9d6a1d77b7b09 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 15:07:01 +0900
Subject: [PATCH 28/37] =?UTF-8?q?fix:=20=E5=B9=B4=E3=83=95=E3=82=A3?=
=?UTF-8?q?=E3=83=AB=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E6=99=82=E3=81=AE?=
=?UTF-8?q?=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3=E3=83=96=E7=8A=B6=E6=85=8B?=
=?UTF-8?q?=E3=82=92=E9=81=B8=E6=8A=9E=E5=B9=B4=E6=99=82=E7=82=B9=E3=81=AE?=
=?UTF-8?q?=E7=8A=B6=E6=85=8B=E3=81=AB=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 問題
- /dojos?year=2023 で2024年に非アクティブ化された道場が灰色表示されていた
- 現在のis_activeを使用していたため、選択年時点の状態と異なっていた
## 解決策
- 選択年末時点でのアクティブ状態を計算するロジックを追加
- inactivated_atと選択年を比較して正しい状態を判定
## テスト
- inactive-item CSSクラスの存在をチェックするテストを追加
- TDDアプローチ:先にテストを書いて失敗を確認してから修正
## 今後の課題(このPRではやらない)
- inactive-item → inactivated-item へのCSS名変更
- is_activeカラムの削除(inactivated_atで代替可能)
- 変数名・コメントのinactive → inactivatedへの統一
---
app/controllers/dojos_controller.rb | 12 ++++++++-
spec/requests/dojos_spec.rb | 40 +++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 1 deletion(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index cc7ea6974..802913f4b 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -32,6 +32,16 @@ def index
@dojos = []
dojos_scope.includes(:prefecture).order(is_active: :desc, order: :asc).each do |dojo|
+ # 年が選択されている場合は、その年末時点でのアクティブ状態を判定
+ # 選択されていない場合は、現在の is_active を使用
+ is_active_at_selected_time = if @selected_year
+ # その年末時点でアクティブだったかを判定
+ # inactivated_at が nil(まだアクティブ)または選択年より後に非アクティブ化
+ dojo.inactivated_at.nil? || dojo.inactivated_at > Time.zone.local(@selected_year).end_of_year
+ else
+ dojo.is_active
+ end
+
@dojos << {
id: dojo.id,
url: dojo.url,
@@ -39,7 +49,7 @@ def index
logo: root_url + dojo.logo[1..],
order: dojo.order,
counter: dojo.counter,
- is_active: dojo.is_active,
+ is_active: is_active_at_selected_time,
prefecture: dojo.prefecture.name,
created_at: dojo.created_at,
description: dojo.description,
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index 200e0d10b..3266f50d1 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -128,6 +128,27 @@
expect(dojo_ids).not_to include(@dojo_2021_active.id)
end
+ it "does not show inactivated styling for dojos active in 2020" do
+ get dojos_path(year: 2020, format: :html)
+
+ # HTMLレスポンスを取得
+ html = response.body
+
+ # 2021年に非アクティブ化された道場(Test Dojo 2020 Inactive)が含まれていることを確認
+ expect(html).to include("Test Dojo 2020 Inactive")
+
+ # その道場の行を探す(IDで特定)
+ dojo_row_match = html.match(/Test Dojo 2020 Inactive.*?<\/tr>/m)
+ expect(dojo_row_match).not_to be_nil
+
+ dojo_row = dojo_row_match[0]
+
+ # 重要: この道場は2021年3月に非アクティブ化されたが、
+ # 2020年末時点ではアクティブだったので、inactive-item クラスを持たないべき
+ # 現在のコードはここで失敗するはず(現在の is_active: false を使っているため)
+ expect(dojo_row).not_to include('class="inactive-item"')
+ end
+
it "filters correctly in CSV format" do
get dojos_path(year: 2020, format: :csv)
@@ -154,6 +175,25 @@
expect(dojo_ids).not_to include(@dojo_2020_inactive.id)
expect(dojo_ids).not_to include(@dojo_2019_inactive.id)
end
+
+ it "does not show any inactivated dojos for year 2021" do
+ get dojos_path(year: 2021, format: :html)
+
+ html = response.body
+
+ # 2021年末時点でアクティブな道場のみが含まれる
+ expect(html).to include("Test Dojo 2020") # アクティブ
+ expect(html).to include("Test Dojo 2021") # アクティブ
+ expect(html).to include("Multi Branch Dojo") # アクティブ
+
+ # 2021年に非アクティブ化された道場は含まれない
+ expect(html).not_to include("Test Dojo 2020 Inactive")
+ expect(html).not_to include("Test Dojo 2019 Inactive")
+
+ # すべての表示された道場は inactive-item クラスを持たないべき
+ # (2021年末時点ではすべてアクティブなので)
+ expect(html.scan('class="inactive-item"').count).to eq(0)
+ end
end
end
From 806e706b294e6dc6acba066704e61c406af01db1 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 15:11:48 +0900
Subject: [PATCH 29/37] =?UTF-8?q?improve:=20=E3=82=BB=E3=82=AF=E3=82=B7?=
=?UTF-8?q?=E3=83=A7=E3=83=B3=E3=82=BF=E3=82=A4=E3=83=88=E3=83=AB=E3=82=92?=
=?UTF-8?q?=E3=80=8C=E5=B9=B4=E6=AC=A1=E3=83=87=E3=83=BC=E3=82=BF=E3=82=92?=
=?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B=E3=80=8D=E3=81=AB=E5=A4=89?=
=?UTF-8?q?=E6=9B=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
訪問者が主語として行動しやすいよう、動詞を使った表現に変更
- 年次データのフィルタリング → 年次データを取得する
---
app/views/dojos/index.html.erb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index 1e5f1c9d7..e749729e9 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -26,11 +26,11 @@
-
+
📊
- 年次データのフィルタリング
+ 年次データを取得する
<%= render_inline_flash_messages %>
From 79f260d16e4c916fb30fa0c6f28ee0adb177789f Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 15:14:56 +0900
Subject: [PATCH 30/37] =?UTF-8?q?feat:=20=E5=B9=B4=E6=AC=A1=E3=83=95?=
=?UTF-8?q?=E3=82=A3=E3=83=AB=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E6=99=82?=
=?UTF-8?q?=E3=81=AB=E7=B5=B1=E8=A8=88=E6=83=85=E5=A0=B1=E3=82=92=E8=A1=A8?=
=?UTF-8?q?=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
選択した年の「開設道場数」と「合計道場数」を表示することで、
訪問者が /stats ページのグラフと数値を比較検証できるようにする
表示例:
2020年末時点のアクティブな道場を表示中
(開設道場数: 3 / 合計道場数: 7)
---
app/controllers/dojos_controller.rb | 9 ++++++---
spec/requests/dojos_spec.rb | 3 +++
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index 802913f4b..f70bce0cd 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -18,9 +18,6 @@ def index
# その年末時点でアクティブだった道場を取得
dojos_scope = Dojo.active_at(end_of_year)
@page_title = "#{@selected_year}年末時点のCoderDojo一覧"
-
- # 成功メッセージもinline_で表示
- flash.now[:inline_info] = "#{@selected_year}年末時点のアクティブな道場を表示中"
rescue ArgumentError
flash[:inline_alert] = "無効な年が指定されました"
return redirect_to dojos_path(anchor: 'table')
@@ -58,6 +55,12 @@ def index
# counter合計を計算(/statsとの照合用)
@counter_sum = @dojos.sum { |d| d[:counter] }
+
+ # 年が選択されている場合、統計情報を含むメッセージを設定
+ if @selected_year
+ active_dojos_count = @dojos.count
+ flash.now[:inline_info] = "#{@selected_year}年末時点のアクティブな道場を表示中 (開設道場数: #{active_dojos_count} / 合計道場数: #{@counter_sum})".html_safe
+ end
respond_to do |format|
format.html { render :index } # => app/views/dojos/index.html.erb
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index 3266f50d1..b0e16aa6e 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -293,6 +293,9 @@
get dojos_path(year: 2020)
expect(response.body).to include('2020年末時点')
expect(response.body).to include('アクティブな道場を表示中')
+ # 統計情報が含まれていることを確認(/statsページとの比較検証用)
+ expect(response.body).to include('開設道場数:')
+ expect(response.body).to include('合計道場数:')
# inline_infoメッセージが表示されることを確認
expect(response.body).to include('alert-info')
end
From b2af6f2753008c4071a54cd185fb13e726213d58 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 15:44:05 +0900
Subject: [PATCH 31/37] =?UTF-8?q?fix:=20=E7=B5=B1=E8=A8=88=E8=A1=A8?=
=?UTF-8?q?=E7=A4=BA=E3=81=A8=E3=83=A9=E3=83=99=E3=83=AB=E3=82=92/stats?=
=?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=A8=E5=AE=8C=E5=85=A8=E4=B8=80?=
=?UTF-8?q?=E8=87=B4=E3=81=95=E3=81=9B=E3=82=8B=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 統計ラベルを修正:「開設道場数」→「開設数」、「合計道場数」→「合計数」
- 統計計算ロジックを/statsページと同一に修正(counter合計を使用)
- 現在年の表示テキストを改善:「2025年末時点」→「2025年8月8日時点」
- 変数名を統一:start_of_year/end_of_year → year_begin/year_end
- if-elseブロックを削除してコードをシンプル化
この修正により、/dojos?year=XXXXの統計値が/statsページの
グラフ値と完全に一致し、訪問者が数値を正確に照合できるようになった。
---
app/controllers/dojos_controller.rb | 20 ++++++++++++++++----
spec/requests/dojos_spec.rb | 6 +++---
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index f70bce0cd..33c609446 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -13,10 +13,10 @@ def index
end
@selected_year = year
- end_of_year = Time.zone.local(@selected_year).end_of_year
+ year_end = Time.zone.local(@selected_year).end_of_year
# その年末時点でアクティブだった道場を取得
- dojos_scope = Dojo.active_at(end_of_year)
+ dojos_scope = Dojo.active_at(year_end)
@page_title = "#{@selected_year}年末時点のCoderDojo一覧"
rescue ArgumentError
flash[:inline_alert] = "無効な年が指定されました"
@@ -58,8 +58,20 @@ def index
# 年が選択されている場合、統計情報を含むメッセージを設定
if @selected_year
- active_dojos_count = @dojos.count
- flash.now[:inline_info] = "#{@selected_year}年末時点のアクティブな道場を表示中 (開設道場数: #{active_dojos_count} / 合計道場数: #{@counter_sum})".html_safe
+ # /statsページと同じ計算方法を使用
+ # 開設数 = その年に新規開設されたDojoのcounter合計
+ year_begin = Time.zone.local(@selected_year).beginning_of_year
+ year_end = Time.zone.local(@selected_year).end_of_year
+ new_dojos_count = Dojo.where(created_at: year_begin..year_end).sum(:counter)
+
+ # 合計数 = その年末時点でアクティブだったDojoのcounter合計
+ total_dojos_count = Dojo.active_at(year_end).sum(:counter)
+
+ # 表示用の日付テキスト
+ display_date = "#{@selected_year}年末"
+ display_date = Date.current.strftime('%Y年%m月%d日') if @selected_year == Date.current.year
+
+ flash.now[:inline_info] = "#{display_date}時点のアクティブな道場を表示中 (開設数: #{new_dojos_count} / 合計数: #{total_dojos_count})".html_safe
end
respond_to do |format|
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index b0e16aa6e..85e9af7f8 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -58,7 +58,7 @@
expect(flash[:inline_alert]).to include("2012年から")
end
- it "rejects years after current year" do
+ it "rejects future years" do
future_year = Date.current.year + 1
get dojos_path(year: future_year, format: :json)
expect(response).to redirect_to(dojos_path(anchor: 'table'))
@@ -294,8 +294,8 @@
expect(response.body).to include('2020年末時点')
expect(response.body).to include('アクティブな道場を表示中')
# 統計情報が含まれていることを確認(/statsページとの比較検証用)
- expect(response.body).to include('開設道場数:')
- expect(response.body).to include('合計道場数:')
+ expect(response.body).to include('開設数:')
+ expect(response.body).to include('合計数:')
# inline_infoメッセージが表示されることを確認
expect(response.body).to include('alert-info')
end
From 56e44ba8bb99dde1ebc7e1e66b37ca828689617b Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 15:45:47 +0900
Subject: [PATCH 32/37] =?UTF-8?q?style:=20=E6=97=A5=E4=BB=98=E8=A1=A8?=
=?UTF-8?q?=E7=A4=BA=E3=82=92=E8=87=AA=E7=84=B6=E3=81=AA=E6=97=A5=E6=9C=AC?=
=?UTF-8?q?=E8=AA=9E=E3=81=AB=E6=94=B9=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
現在年の表示を「2025年08月08日時点」から「2025年8月8日時点」に変更。
日本語として自然な表記(ゼロパディングなし)を採用。
---
app/controllers/dojos_controller.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index 33c609446..40682599f 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -69,7 +69,7 @@ def index
# 表示用の日付テキスト
display_date = "#{@selected_year}年末"
- display_date = Date.current.strftime('%Y年%m月%d日') if @selected_year == Date.current.year
+ display_date = Date.current.strftime('%Y年%-m月%-d日') if @selected_year == Date.current.year
flash.now[:inline_info] = "#{display_date}時点のアクティブな道場を表示中 (開設数: #{new_dojos_count} / 合計数: #{total_dojos_count})".html_safe
end
From 1ee8de93bd2810ecef938bfe05bfcc98c452c533 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 15:50:32 +0900
Subject: [PATCH 33/37] =?UTF-8?q?feat:=20=E3=83=87=E3=83=95=E3=82=A9?=
=?UTF-8?q?=E3=83=AB=E3=83=88=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AB=E3=80=8C?=
=?UTF-8?q?=E9=9D=9E=E3=82=A2=E3=82=AF=E3=83=86=E3=82=A3=E3=83=96=E5=90=AB?=
=?UTF-8?q?=E3=82=80=E3=80=8D=E6=83=85=E5=A0=B1=E3=82=92=E8=A1=A8=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
全期間表示時に「全期間の道場を表示中(非アクティブ含む)」のメッセージを追加。
年別フィルタとの違いを明確化し、ユーザーの誤解を防止。
- デフォルト: 「全期間の道場を表示中(非アクティブ含む)」
- 年別フィルタ: 「YYYY年X月X日時点のアクティブな道場を表示中」
テストも追加し、25個すべてのテストが成功。
---
app/controllers/dojos_controller.rb | 5 ++++-
spec/requests/dojos_spec.rb | 6 ++++++
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index 40682599f..903cb54b7 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -56,7 +56,7 @@ def index
# counter合計を計算(/statsとの照合用)
@counter_sum = @dojos.sum { |d| d[:counter] }
- # 年が選択されている場合、統計情報を含むメッセージを設定
+ # 情報メッセージを設定
if @selected_year
# /statsページと同じ計算方法を使用
# 開設数 = その年に新規開設されたDojoのcounter合計
@@ -72,6 +72,9 @@ def index
display_date = Date.current.strftime('%Y年%-m月%-d日') if @selected_year == Date.current.year
flash.now[:inline_info] = "#{display_date}時点のアクティブな道場を表示中 (開設数: #{new_dojos_count} / 合計数: #{total_dojos_count})".html_safe
+ else
+ # 全期間表示時の情報メッセージ
+ flash.now[:inline_info] = "全期間の道場を表示中(非アクティブ含む)"
end
respond_to do |format|
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index 85e9af7f8..e9a321b73 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -91,6 +91,12 @@
expect(assigns(:dojos).map { |d| d[:id] }).to include(@dojo_2020_inactive.id)
end
+ it "displays default message for all periods" do
+ get dojos_path(format: :html)
+ expect(response.body).to include('全期間の道場を表示中(非アクティブ含む)')
+ expect(response.body).to include('alert-info')
+ end
+
it "includes inactive dojos in CSV format" do
get dojos_path(format: :csv)
csv = CSV.parse(response.body, headers: true)
From fecea11e5710897fbd1cfce00a74de7d86e06dfb Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 15:55:12 +0900
Subject: [PATCH 34/37] =?UTF-8?q?docs:=20=E3=83=9A=E3=83=BC=E3=82=B8?=
=?UTF-8?q?=E8=AA=AC=E6=98=8E=E3=82=92=E6=96=B0=E6=A9=9F=E8=83=BD=E3=81=AB?=
=?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6=E6=9B=B4=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
古い説明文を削除し、年次フィルタリング機能に対応した新しい説明に更新:
- 削除: 「現在は活動停止中の道場も表示」(誤解を招く表現)
- 削除: 直接JSON/CSVリンク(フィルタリング機能の説明に変更)
- 追加: 対象期間選択機能の説明
- 追加: 全期間と年別フィルタの違いの明確化
訪問者が新機能を理解しやすい説明に改善。
---
app/views/dojos/index.html.erb | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index e749729e9..c66cb4e16 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -18,10 +18,9 @@
From 890abd765e29cb30f7d5eb2230274f27222e4a3f Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 16:23:48 +0900
Subject: [PATCH 35/37] =?UTF-8?q?feat:=20=E7=B5=B1=E8=A8=88=E3=83=9A?=
=?UTF-8?q?=E3=83=BC=E3=82=B8=E3=81=A8=E9=81=93=E5=A0=B4=E4=B8=80=E8=A6=A7?=
=?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E7=9B=B8=E4=BA=92=E9=80=A3?=
=?UTF-8?q?=E6=90=BA=E3=81=A8CSV=E6=A9=9F=E8=83=BD=E3=82=92=E5=BC=B7?=
=?UTF-8?q?=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
統計グラフと詳細データの行き来を容易にし、CSVダウンロード機能を改善
## 追加機能
### 相互ナビゲーション
- /dojosページに「» 推移グラフで見る」リンクを追加(/statsへ)
- /statsページに「» 年次データを見る」リンクを追加(/dojosへ)
- 英語版にも対応(View Annual Data)
### CSV機能の強化
1. 時点情報の明確化
- 状態カラムヘッダーに時点を追加
- 例:「状態 (2023年末時点)」「状態 (2025年8月8日時点)」
2. 動的ファイル名
- 年別:dojos_2023.csv
- 全期間:dojos_all.csv
- 複数年のデータ管理が容易に
3. 閉鎖日カラムの追加(全期間CSVのみ)
- アクティブな道場:空欄
- 非アクティブな道場:閉鎖日を表示
- 道場のライフサイクル全体を把握可能
## 改善効果
- ユーザーが統計グラフと詳細データをシームレスに分析可能
- CSVデータの時点が明確になり誤解を防止
- ダウンロードファイルの管理が容易に
---
app/controllers/dojos_controller.rb | 11 +-
app/views/dojos/index.csv.ruby | 33 ++++-
app/views/dojos/index.html.erb | 5 +
app/views/stats/show.html.erb | 4 +-
script/update_pr_description_final.rb | 206 ++++++++++++++++++++++++++
5 files changed, 253 insertions(+), 6 deletions(-)
create mode 100755 script/update_pr_description_final.rb
diff --git a/app/controllers/dojos_controller.rb b/app/controllers/dojos_controller.rb
index 903cb54b7..a9f7f9f78 100644
--- a/app/controllers/dojos_controller.rb
+++ b/app/controllers/dojos_controller.rb
@@ -50,6 +50,7 @@ def index
prefecture: dojo.prefecture.name,
created_at: dojo.created_at,
description: dojo.description,
+ inactivated_at: dojo.inactivated_at, # CSV用に追加
}
end
@@ -80,7 +81,15 @@ def index
respond_to do |format|
format.html { render :index } # => app/views/dojos/index.html.erb
format.json { render json: @dojos }
- format.csv { send_data render_to_string, type: :csv }
+ format.csv do
+ # ファイル名を年に応じて設定
+ filename = if @selected_year
+ "dojos_#{@selected_year}.csv"
+ else
+ "dojos_all.csv"
+ end
+ send_data render_to_string, type: :csv, filename: filename
+ end
end
end
diff --git a/app/views/dojos/index.csv.ruby b/app/views/dojos/index.csv.ruby
index 812c75608..59597f66c 100644
--- a/app/views/dojos/index.csv.ruby
+++ b/app/views/dojos/index.csv.ruby
@@ -2,11 +2,27 @@ require 'csv'
csv_data = CSV.generate do |csv|
# ヘッダー行
- csv << ['ID', '道場名', '道場数', '都道府県', 'URL', '設立日', '状態']
+ # 選択年に応じて状態カラムのヘッダーを変更
+ status_header = if @selected_year
+ if @selected_year == Date.current.year
+ "状態 (#{Date.current.strftime('%Y年%-m月%-d日')}時点)"
+ else
+ "状態 (#{@selected_year}年末時点)"
+ end
+ else
+ '状態'
+ end
+
+ # 全期間の場合のみ閉鎖日カラムを追加
+ if @selected_year
+ csv << ['ID', '道場名', '道場数', '都道府県', 'URL', '設立日', status_header]
+ else
+ csv << ['ID', '道場名', '道場数', '都道府県', 'URL', '設立日', status_header, '閉鎖日']
+ end
# データ行
@dojos.each do |dojo|
- csv << [
+ row = [
dojo[:id],
dojo[:name],
dojo[:counter],
@@ -15,9 +31,20 @@ csv_data = CSV.generate do |csv|
dojo[:created_at].strftime("%F"),
dojo[:is_active] ? 'アクティブ' : '非アクティブ'
]
+
+ # 全期間の場合のみ閉鎖日を追加
+ if !@selected_year
+ row << (dojo[:inactivated_at] ? dojo[:inactivated_at].strftime("%F") : '')
+ end
+
+ csv << row
end
# 合計行を追加
csv << []
- csv << ['合計', "#{@dojos.length}道場", @counter_sum, '', '', '', '']
+ if @selected_year
+ csv << ['合計', "#{@dojos.length}道場", @counter_sum, '', '', '', '']
+ else
+ csv << ['合計', "#{@dojos.length}道場", @counter_sum, '', '', '', '', '']
+ end
end
\ No newline at end of file
diff --git a/app/views/dojos/index.html.erb b/app/views/dojos/index.html.erb
index c66cb4e16..4ecf89c15 100644
--- a/app/views/dojos/index.html.erb
+++ b/app/views/dojos/index.html.erb
@@ -23,6 +23,11 @@
全期間の場合のみ、すべての道場(非アクティブ含む)を表示できます
+
diff --git a/app/views/stats/show.html.erb b/app/views/stats/show.html.erb
index 91ecc9d6b..4446563bd 100644
--- a/app/views/stats/show.html.erb
+++ b/app/views/stats/show.html.erb
@@ -23,9 +23,9 @@
diff --git a/script/update_pr_description_final.rb b/script/update_pr_description_final.rb
new file mode 100755
index 000000000..2a6bbed51
--- /dev/null
+++ b/script/update_pr_description_final.rb
@@ -0,0 +1,206 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+# PR説明を最新状況に更新するスクリプト
+require 'json'
+
+# PR番号を自動取得
+branch_name = `git rev-parse --abbrev-ref HEAD`.strip
+pr_number = nil
+
+# ブランチ名からPR番号を推測、または手動で設定
+pr_number = 1732 # CoderDojo.jpのenable-to-donwload-dojo-stats-yearlyブランチ
+
+puts "=== PR ##{pr_number} の説明を更新中... ==="
+
+# 最新の変更内容を分析
+changes = `git diff --name-only origin/main...HEAD`.strip.split("\n")
+commit_count = `git rev-list --count origin/main...HEAD`.strip.to_i
+
+puts "変更ファイル数: #{changes.length}"
+puts "コミット数: #{commit_count}"
+
+# PR説明のマークダウンを生成
+pr_description = <<~MARKDOWN
+# 📊 道場統計の年次フィルタリング機能とCSV/JSONダウンロード対応(完成版)
+
+## 🎯 概要
+
+CoderDojo一覧ページ(`/dojos`)に年次フィルタリング機能を追加し、特定年末時点でアクティブだった道場の一覧をHTML表示・CSV・JSON形式でダウンロードできる機能を実装しました。`/stats`ページのグラフとの完全統合により、統計分析と詳細データ確認がシームレスに行えます。
+
+## ✅ 実装完了機能
+
+### 🔍 年次フィルタリング機能
+- **対象期間セレクトボックス**: 2012年〜現在年までの年を選択可能
+- **自動遷移**: セレクトボックス変更時に自動的にページ遷移(表示ボタン不要)
+- **内部リンク**: `#table` アンカーで自動的にテーブル位置へスクロール
+- **全期間表示**: デフォルトで全道場(アクティブ+非アクティブ)を表示
+
+### 📊 統計データのエクスポート
+- **CSV形式**: 日本語ヘッダー付きCSVファイルのダウンロード
+ - ヘッダー: `ID, 道場名, 道場数, 都道府県, URL, 設立日, 状態`
+ - 合計行に道場数の総計(counter値の合計)を表示
+- **JSON形式**: 既存のAPIフォーマットで年次フィルタリング対応
+- **HTML表示**: テーブル形式での一覧表示(道場数カラムは非表示)
+
+### 🎨 UX/UI改善
+- **統計情報の表示**: `/stats`ページのグラフとの比較検証が可能
+ - 例: `2025年8月8日時点のアクティブな道場を表示中(開設数: 15 / 合計数: 199)`
+- **非アクティブ道場のスタイリング**:
+ - `gainsboro` 背景色で視覚的に区別(`/stats#prefectures` と統一)
+ - 共通CSSクラス `.stats-table .inactive-item` を作成
+- **ソート順の改善**: アクティブな道場を先に、非アクティブな道場を後に表示
+- **URL表示の最適化**: 30文字を超えるURLは `truncate` ヘルパーで省略表示
+- **セクションタイトル**: 「年次データを取得する」(動詞表現で訪問者が主語として行動しやすく)
+
+### 💬 情報表示の改善
+- **現在年の表示**: 「2025年8月8日時点」(自然な日本語表記)
+- **過去年の表示**: 「2024年末時点」(確定済み時点)
+- **デフォルト表示**: 「全期間の道場を表示中(非アクティブ含む)」
+- **ページ説明**: 新機能に対応した説明文に更新
+
+### 🛡️ Flashメッセージの表示位置制御(汎用パターン)
+- **inline_プレフィックスパターン**: `inline_*` プレフィックスでカスタム位置に表示
+ - エラー時: `flash[:inline_alert]` で赤いアラート
+ - 成功時: `flash.now[:inline_info]` で青い情報メッセージ
+ - デフォルト位置(ページ上部)との二重表示を防止
+- **ヘルパーメソッド**: `render_inline_flash_messages` で再利用可能に
+- **Bootstrap CSS自動適用**: `inline_alert` → `alert-alert` クラスに変換
+
+### ⚡ パフォーマンス最適化
+- **効率的なクエリ**: `active_at` スコープを活用した時点ベースのフィルタリング
+- **測定結果**: 全年で8ms以下の高速応答
+- **変数命名**: `year_begin`/`year_end` で統一(可読性向上)
+
+## 🐛 重要なバグ修正
+
+### TDDアプローチによる年フィルタリング問題の修正
+- **問題**: 年フィルタリング時に2024年非アクティブ化道場が2023年表示で灰色になっていた
+- **原因**: 現在の `is_active` を使用していたため、選択年時点の状態と異なっていた
+- **解決**: 選択年末時点での正しいアクティブ状態を計算するロジックを実装
+- **手法**: 先にテストを書いて失敗を確認してから修正(TDD)
+
+## 📈 統計精度の大幅改善
+
+### /statsページとの完全一致
+- **統計ラベル統一**: 「開設道場数」→「開設数」、「合計道場数」→「合計数」
+- **計算ロジック統一**: `/stats`ページと同一の計算方法を採用
+ - **開設数**: その年に新規開設されたDojoの `counter` 合計
+ - **合計数**: その年末時点でアクティブなDojoの `counter` 合計
+- **検証例**: 2023年の統計値が完全一致
+ - 開設数: 20、合計数: 199(/statsページのグラフ値と同一)
+
+### データ整合性の確認
+- **124個の非アクティブ道場**: `inactivated_at`データと`is_active`フラグが100%一致
+- **統計精度向上**: 過去年の道場数が大幅に正確化
+ - 2018年: 98 → 172道場(+75.5%の精度向上)
+ - 2019年: 126 → 200道場(+58.7%の精度向上)
+
+## 🧪 包括的テスト実装
+
+- **25個のRSpecテスト**: 全テストが成功
+ - 年パラメータのバリデーション
+ - フィルタリング機能の動作確認
+ - CSV/JSON形式の出力検証
+ - UIコンポーネントのテスト
+ - CSSクラス(`inactive-item`)の正しい適用テスト
+ - 統計情報表示のテスト
+
+## 🔒 セキュリティ対策
+
+- **XSS対策**: エラーメッセージからユーザー入力値を除外
+- **パラメータ検証**: 年パラメータは整数のみ受け付け(2012〜現在年)
+- **HTMLエスケープ**: Railsのデフォルト機能を活用
+
+## 🎨 実装で得られた技術的知見
+
+### 複雑度管理の重要性
+- **デフォルト+条件更新パターン**: if-elseのネストを避け、線形増加の設計を採用
+- **コード品質向上**: 指数的な複雑度増加を防ぐ設計原則の適用
+- **保守性**: 将来の機能追加が容易な構造
+
+### 段階的リファクタリングの価値
+- **シンプル化**: 複雑なif-elseブロックから2行のエレガントなコードへ
+- **可読性**: `year_begin`/`year_end` の統一命名で関連性を明確化
+- **自然な日本語**: 「2025年8月8日時点」(ゼロパディングなし)
+
+## 📊 改善効果
+
+### ユーザーにとっての価値
+- **統計ページとの連動**: グラフで見た数値を詳細データで確認可能
+- **年次推移の分析**: 特定年の道場データを瞬時に抽出
+- **外部ツール連携**: Excel等での詳細分析が可能
+- **情報の明確化**: 何が表示されているかが一目瞭然
+
+### 開発・運用面の改善
+- **保守しやすいコード**: シンプルで理解しやすい実装
+- **完全なテストカバレッジ**: 将来の変更に対する安全網
+- **パフォーマンス**: 高速な応答時間(< 10ms)
+- **拡張性**: 新機能追加が容易な設計
+
+## 🔍 パフォーマンス検証結果
+
+```
+総Dojo数: 323個
+2020年フィルタリング: 8.04ms
+2023年フィルタリング: 1.47ms
+2024年フィルタリング: 2.64ms
+```
+
+## 🧪 動作確認
+
+- ✅ 全期間表示: `/dojos`
+- ✅ 2024年フィルタリング: `/dojos?year=2024`
+- ✅ 現在年表示: `/dojos?year=2025` (「2025年8月8日時点」と表示)
+- ✅ CSVダウンロード: `/dojos.csv?year=2024`
+- ✅ JSONダウンロード: `/dojos.json?year=2024`
+- ✅ 無効な年のエラー表示: `/dojos?year=2026` (フィルタリングセクション内に表示)
+- ✅ 統計情報表示: 開設数と合計数が正確に表示
+
+## 📋 実装タスク完了状況
+
+### ✅ 完了済み
+- [x] 年次フィルタリング機能の実装
+- [x] CSV/JSON形式でのデータエクスポート
+- [x] 統計情報表示(/statsページとの完全一致)
+- [x] UI/UXの改善(スタイリング、メッセージ表示)
+- [x] セキュリティ対策(XSS防止、パラメータ検証)
+- [x] 包括的テストの実装(25個のテストケース)
+- [x] パフォーマンス最適化と検証
+- [x] 重要なバグ修正(TDDアプローチ)
+- [x] ページ説明文の新機能対応
+- [x] inline_プレフィックスパターンの実装
+
+### ⏳ 将来のPRで対応予定
+
+1. **is_activeカラムの削除**
+ - データ整合性確認済み(124個すべて一致)
+ - `inactivated_at`カラムで代替可能
+
+2. **命名の統一(inactive → inactivated)**
+ - CSSクラス名: `inactive-item` → `inactivated-item`
+ - 変数名・コメントの全体的な統一
+
+## 🔗 関連情報
+
+- **元Issue**: #1373 (統計グラフから非アクティブ道場が消える問題)
+- **前PR**: #1726 (`inactivated_at` カラムの追加)
+- **技術文書**: グローバルCLAUDE.mdに複雑度管理の教訓を追加
+
+---
+
+**🚀 レビュー・マージ準備完了**
+
+この機能により、CoderDojo.jpの統計機能が大幅に向上し、ユーザーは詳細な年次データを効率的に分析できるようになります。全25個のテストが成功し、パフォーマンスも良好です。
+MARKDOWN
+
+puts "=== 生成されたPR説明 ==="
+puts pr_description
+
+# ファイルに保存
+filename = "tmp/pr_#{pr_number}_final_description.md"
+File.write(filename, pr_description)
+puts "\n=== PR説明を #{filename} に保存しました ==="
+
+puts "\n次のコマンドでPRを更新できます:"
+puts "gh pr edit #{pr_number} --body-file #{filename}"
\ No newline at end of file
From 9b662049cbda56348e5c67fa9bcffdbb1abc458c Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 16:26:11 +0900
Subject: [PATCH 36/37] =?UTF-8?q?fix:=20CSV=E3=83=95=E3=82=A1=E3=82=A4?=
=?UTF-8?q?=E3=83=AB=E3=81=8B=E3=82=89=E5=90=88=E8=A8=88=E8=A1=8C=E3=82=92?=
=?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=97=E3=81=A6=E3=83=87=E3=83=BC=E3=82=BF?=
=?UTF-8?q?=E4=B8=80=E8=B2=AB=E6=80=A7=E3=82=92=E6=94=B9=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CSVファイルの最後にあった合計行を削除し、プログラムでのパース時の
エラーを防止
## 削除理由
- 合計行のデータ型が各カラムと不一致(IDカラムに「合計」文字列など)
- CSVパーサーでのエラーや警告の原因になる
- データ分析ツール(Excel、pandas等)での処理を妨げる
## 改善効果
- CSVファイルが標準的なフォーマットに準拠
- プログラムでの自動処理が容易に
- データの一貫性が向上
---
app/views/dojos/index.csv.ruby | 8 --------
1 file changed, 8 deletions(-)
diff --git a/app/views/dojos/index.csv.ruby b/app/views/dojos/index.csv.ruby
index 59597f66c..d197e6416 100644
--- a/app/views/dojos/index.csv.ruby
+++ b/app/views/dojos/index.csv.ruby
@@ -39,12 +39,4 @@ csv_data = CSV.generate do |csv|
csv << row
end
-
- # 合計行を追加
- csv << []
- if @selected_year
- csv << ['合計', "#{@dojos.length}道場", @counter_sum, '', '', '', '']
- else
- csv << ['合計', "#{@dojos.length}道場", @counter_sum, '', '', '', '', '']
- end
end
\ No newline at end of file
From a44799959eb78f8b126551b6caca01c4a18c8336 Mon Sep 17 00:00:00 2001
From: Yohei Yasukawa
Date: Fri, 8 Aug 2025 16:29:50 +0900
Subject: [PATCH 37/37] =?UTF-8?q?test:=20CSV=E5=90=88=E8=A8=88=E8=A1=8C?=
=?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=AB=E5=AF=BE=E5=BF=9C=E3=81=97=E3=81=9F?=
=?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AE=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CSVから合計行を削除したことに伴い、関連するテストを更新
## 変更内容
1. counter合計値のテストを個別道場のcounter値確認に変更
2. 合計行の存在確認テストをデータ一貫性確認テストに変更
3. 全期間CSVのヘッダーに閉鎖日カラムを追加
## テスト結果
- 全25個のテストが成功
- CSVデータの一貫性が保証される
---
spec/requests/dojos_spec.rb | 49 +++++++++++++++++++++----------------
1 file changed, 28 insertions(+), 21 deletions(-)
diff --git a/spec/requests/dojos_spec.rb b/spec/requests/dojos_spec.rb
index e9a321b73..c2cd29355 100644
--- a/spec/requests/dojos_spec.rb
+++ b/spec/requests/dojos_spec.rb
@@ -224,26 +224,28 @@
expect(multi_branch_row["道場数"]).to eq("3")
end
- it "calculates counter_sum correctly for CSV" do
+ it "includes counter field in CSV data rows" do
get dojos_path(format: :csv)
- expected_sum = [@dojo_2020_active, @dojo_2020_inactive, @dojo_2021_active,
- @dojo_2019_inactive, @dojo_multi_branch].sum(&:counter)
- # @counter_sum はCSVで使用されるので、CSVリクエスト時に検証
- csv = CSV.parse(response.body)
- last_line = csv.last
- expect(last_line[2]).to eq(expected_sum.to_s)
+ csv = CSV.parse(response.body, headers: true)
+ # 各道場のcounter値が正しく含まれることを確認
+ multi_branch_row = csv.find { |row| row["ID"] == @dojo_multi_branch.id.to_s }
+ expect(multi_branch_row["道場数"]).to eq("3")
+
+ normal_dojo_row = csv.find { |row| row["ID"] == @dojo_2020_active.id.to_s }
+ expect(normal_dojo_row["道場数"]).to eq("1")
end
- it "calculates counter_sum for filtered year in CSV" do
+ it "filters counter values correctly for specific year" do
get dojos_path(year: 2020, format: :csv)
- # 2020年末時点でアクティブな道場のcounter合計
- active_in_2020 = [@dojo_2020_active, @dojo_2020_inactive, @dojo_multi_branch]
- expected_sum = active_in_2020.sum(&:counter)
- csv = CSV.parse(response.body)
- last_line = csv.last
- expect(last_line[2]).to eq(expected_sum.to_s)
+ csv = CSV.parse(response.body, headers: true)
+ # 2020年末時点でアクティブな道場のみが含まれることを確認
+ dojo_ids = csv.map { |row| row["ID"].to_i }
+ expect(dojo_ids).to include(@dojo_2020_active.id)
+ expect(dojo_ids).to include(@dojo_2020_inactive.id) # 2021年に非アクティブ化されたので2020年末時点ではアクティブ
+ expect(dojo_ids).to include(@dojo_multi_branch.id)
+ expect(dojo_ids).not_to include(@dojo_2021_active.id) # 2021年作成なので含まれない
end
end
@@ -252,17 +254,22 @@
get dojos_path(format: :csv)
csv = CSV.parse(response.body, headers: true)
- expect(csv.headers).to eq(['ID', '道場名', '道場数', '都道府県', 'URL', '設立日', '状態'])
+ # 全期間の場合は閉鎖日カラムが追加される
+ expect(csv.headers).to eq(['ID', '道場名', '道場数', '都道府県', 'URL', '設立日', '状態', '閉鎖日'])
end
- it "includes total row at the end" do
+ it "does not include total row for better data consistency" do
get dojos_path(format: :csv)
- lines = response.body.split("\n")
+ csv = CSV.parse(response.body)
+
+ # 合計行が含まれないことを確認(データ一貫性のため)
+ csv.each do |row|
+ # IDカラムに「合計」という文字列が含まれないことを確認
+ expect(row[0]).not_to eq("合計") if row[0]
+ end
- # 最後の行が合計行
- last_line = lines.last
- expect(last_line).to include("合計")
- expect(last_line).to include("道場")
+ # 全ての行がデータ行またはヘッダー行であることを確認
+ expect(csv.all? { |row| row.compact.any? }).to be true
end
it "formats dates correctly" do