これまでこのブログで何度かお知らせしていますが、熊本地震の災害支援のために2016年4月25日から2016年5月31日まで「Everyday Rails - RSpecによるRailsテスト入門」のチャリティセールを実施しています。
今回のエントリでは、昨日(2016年5月15日)時点での収益(=募金額)と、収益を集計するRailsアプリの紹介をします。
ありがたいことに、昨日の時点で募金額はなんと899.55ドル(約9万7000円)になりました。

上のグラフを見てもらえれば分かるとおり、日によって多少の違いはあるものの、今のところ毎日売上げが発生しています。
購入してくださったみなさん、どうもありがとうございました。
この調子でいくと当初の目標額だった500ドルの2倍、1000ドル(約10万8000円)も夢じゃないかもしれません。
みなさん、引き続きご協力よろしくお願いします!
ところで、上のグラフは僕が作ったRailsアプリケーションで表示したものです。
「お金の話だけじゃなくて技術的なネタも読みたい!」と思っているエンジニアさん向けに、このRailsアプリの仕組みを説明します。
このRailsアプリはこんな感じで処理しています。
まあ特に変わったところはない、非常にオーソドックスな流れですね。
LeanpubのAPIからは次のようなJSONデータが返ってきます。
author_royaltiesの1.5(ドル)が収益です。
{"cause_royalty_percentage":"0.0","author_royalty_percentage":"5.0","author_royalties":"1.5","cause_royalties":"0.0","author_paid_out_at":null,"cause_paid_out_at":null,"created_at":"2016-04-21T15:58:45.000Z","royalty_days_hold":45,"publisher_royalties":"0.0","publisher_paid_out_at":null,"author_username":"foo_bar","publisher_slug":"","user_email":"","purchase_uuid":"HlKg4GLtj-fVwM4UqcRj2","invoice_id":"DimXToR5rhc2z_g8A34LM","date_purchased":"2016-04-21T15:58:45.000Z"}
上のJSONは1レコード分なので、実際は複数レコードが配列になって返ってきます。
あとはRails側でAPIを呼び出して、配列をぐるぐるループさせながらJSONデータ(Ruby内ではハッシュとして扱う)を保存していけば終わりです。
実際のコードとは若干異なりますが、イメージ的には以下のようなコードになります。
slug ='everydayrailsrspec-jp'api_key ='(api key)'url ="https://leanpub.com/#{slug}/individual_purchases.json?api_key=#{api_key}"# APIを叩いて売上データを取得hash_array =JSON.load(open(url))hash_array.eachdo |hash|# 1件ずつ売上データをデータベースに保存 purchase_uuid, author_username = hash.values_at('purchase_uuid','author_username') sale =Sale.find_or_initialize_by(purchase_uuid: purchase_uuid,author_username: author_username) hash.eachdo |key,value| sale.send("#{key}=", value)end sale.save!end
JSONをパースしてデータベースに保存するところはRSpecでテストも書いています。
(Everyday RailsはそもそもRSpecの本ですからね!)
RSpec.describeSale,type::modeldo describe'::save_records_from_hash_array!'do let(:json_text)do <<-'JSON'[ { "cause_royalty_percentage":"0.0", "author_royalty_percentage":"5.0", "author_royalties":"1.5", "cause_royalties":"0.0", "author_paid_out_at":null, "cause_paid_out_at":null, "created_at":"2016-04-21T15:58:45.000Z", "royalty_days_hold":45, "publisher_royalties":"0.0", "publisher_paid_out_at":null, "author_username":"foo_bar", "publisher_slug":"", "user_email":"", "purchase_uuid":"HlKg4GLtj-fVwM4UqcRj2", "invoice_id":"DimXToR5rhc2z_g8A34LM", "date_purchased":"2016-04-21T15:58:45.000Z" }]JSONend it'creates record'do# メソッドを呼び出すとレコードが増えることを確認 expect {Sale.save_records_from_hash_array!(JSON.parse(json_text)) }.to change(Sale,:count).from(0).to(1)# JSONの各値がちゃんと保存されていることを確認 sale =Sale.first expect(sale).to have_attributes(cause_royalty_percentage: BigDecimal('0.0'),author_royalty_percentage: BigDecimal('5.0'),author_royalties: BigDecimal('1.5'),cause_royalties: BigDecimal('0.0'),author_paid_out_at:nil,cause_paid_out_at:nil,royalty_days_hold:45,publisher_royalties: BigDecimal('0.0'),publisher_paid_out_at:nil,author_username:'foo_bar',publisher_slug:'',user_email:'',purchase_uuid:'HlKg4GLtj-fVwM4UqcRj2',invoice_id:'DimXToR5rhc2z_g8A34LM',date_purchased:'2016-04-21T15:58:45.000Z'.to_time )endendend
データの集計はがっつりSQLを書いて集計しています。
classSale <ActiveRecord::Basedefself.sum_by_date sql = <<-SQLWITH jst_sales AS ( SELECT *, date_purchased + INTERVAL '9 hours' AS jst_date_purchased FROM sales), sum_by_date AS ( SELECT to_char(jst_date_purchased, 'YYYY/MM/DD') AS purchased_on, count(DISTINCT purchase_uuid) AS purchase_count, sum(author_royalties) AS royalties FROM jst_sales GROUP BY purchased_on )SELECT *, SUM(royalties) OVER ( ORDER BY purchased_on) AS cum_royaltiesFROM sum_by_dateORDER BY purchased_on DESCSQL find_by_sql(sql)endend
"WITH jst_sales AS ( )"の部分はCTE(共通テーブル式)というPostgreSQLの機能を使っています。
CTEはSQL内だけで有効なビュー(Railsではなく、データベースのVIEW)を作り出す機能と考えてもらえばOKです。
まずここで購入日時を日本時間に変換しています。
"sum_by_date AS ( )"の部分もCTEです。
ここは普通に日付単位で収益を集計しています。
一番最後に出てくるSELECT文が最終的に返却するデータのSELECT文になります。
ポイントは "SUM( ) OVER ( ORDER BY )" の部分です。
ここはPostgreSQLのWindow関数という機能を使って、収益の累積値を計算しています。
このSQLを実行すると以下のような結果が返ってきます。
| 日付 | 購入数 | 収益 | 累計額 |
|---|---|---|---|
| 2016/05/15 | 5 | $55.31 | $899.55 |
| 2016/05/14 | 1 | $7.60 | $844.24 |
| ... | ... | ... | ... |
| 2016/04/28 | 3 | $22.80 | $427.29 |
| 2016/04/27 | 16 | $151.94 | $404.49 |
| 2016/04/26 | 17 | $167.93 | $252.55 |
| 2016/04/25 | 9 | $84.62 | $84.62 |
グラフの表示はFlot というjQuery用ライブラリを使っています。
RailsからJavaScriptには直接データを渡せないので、divタグのdata属性にFlot用に加工したデータを埋め込みます。
<divdata-cum-royalties="[ [1463238000000, 899.55], [1463151600000, 844.24], ... , [1461510000000, 84.62]]" ...id="placeholder"></div>
"1463238000000"は日付を表すタイムスタンプで、"899.55"が値(ここでは収益の累積値)です。
最後にJavaScript側でデータを読み取って Flot に渡せばおしまいです。
$(function (){// 売上冊数var purchaseCount = $('#placeholder').data().purchaseCount;// 収益の累積値var cumRoyalties = $('#placeholder').data().cumRoyalties;// 目標値var goalData = $('#placeholder').data().goalData;var data =[{ data: purchaseCount},{ data: cumRoyalties},{ data: goalData}];var options ={ legend:{position:"ne"}, colors:["#eb941f","#55a868","#c60c30"], grid:{ hoverable:true}};// グラフを表示 $.plot("#placeholder", data, options);});
このRailsアプリのソースコードはGitHubに置いています。
上で説明したコードはブログ用に簡略化してる部分も多いので、処理の詳細を知りたい方はGitHubのコードを参照してください。
(自分用に作ったRailsアプリなので、公開するには恥ずかしい部分もいろいろあります。悪しからず・・・)
というわけで、このエントリではEveryday Railsチャリティセールの経過報告と集計用Railsアプリの紹介をしました。
Everyday Railsのチャリティセールは5月31日まで続きます。
引き続きみなさんのご協力をお願いします!
Everyday Rails - RSpecによるRailsテスト入門
チャリティセールの詳細や購入方法についてはこちらのエントリをご覧ください。
id:JunichiItoはてなブログPro株式会社ソニックガーデンで働くRubyプログラマ。
プログラミングスクール「フィヨルドブートキャンプ」のメンター、および「プロを目指す人のためのRuby入門」の著者。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。