Skip to content

Commit f3a83d3

Browse files
authored
Merge pull request #128 from nalabjp/statistics
Implementation to aggregate statistical information #12
2 parents 22486ae + d392c68 commit f3a83d3

20 files changed

+440
-4
lines changed

.travis.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,11 @@ deploy:
1212
run:
1313
- "bundle exec rails db:migrate"
1414
- "bundle exec rails dojos:update_db_by_yaml"
15-
15+
cache:
16+
- bundler
17+
script:
18+
- bundle exec rake db:migrate --trace
19+
- bundle exec rake
20+
env:
21+
global:
22+
- TZ='Asia/Tokyo'

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ gem 'secure_headers'
2626
# Rendering legal documents
2727
gem 'kramdown'
2828

29+
gem 'faraday'
2930
# TODO: Delete this if the following issue is fixed
3031
# https://github.com/bundler/bundler/issues/5332
3132
gem 'faraday_middleware', '0.10'

Gemfile.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ DEPENDENCIES
284284
bootstrap-sass
285285
capybara
286286
coffee-rails
287+
faraday
287288
faraday_middleware (= 0.10)
288289
font-awesome-rails
289290
jbuilder
@@ -315,4 +316,4 @@ RUBY VERSION
315316
ruby 2.4.0p0
316317

317318
BUNDLED WITH
318-
1.13.7
319+
1.15.3

app/models/dojo.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ class Dojo < ApplicationRecord
33
NUM_OF_WHOLE_DOJOS = "1,250"
44
NUM_OF_JAPAN_DOJOS = Dojo.count.to_s
55

6+
has_one :dojo_event_service
7+
has_many :event_histories
8+
69
serialize :tags
710
default_scope -> { order(order: :asc) }
811
before_save { self.email = self.email.downcase }

app/models/dojo_event_service.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class DojoEventService < ApplicationRecord
2+
belongs_to :dojo
3+
enum name: %i( connpass doorkeeper )
4+
end

app/models/event_history.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class EventHistory < ApplicationRecord
2+
belongs_to :dojo
3+
4+
validates :dojo_name, presence: true
5+
validates :service_name, presence: true, uniqueness: { scope: :event_id }
6+
validates :service_group_id, presence: true
7+
validates :event_id, presence: true
8+
validates :event_url, presence: true, uniqueness: true, allow_nil: true
9+
validates :participants, presence: true
10+
validates :evented_at, presence: true
11+
end

config/application.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,8 @@ class Application < Rails::Application
1111
# Settings in config/environments/* take precedence over those specified here.
1212
# Application configuration should go into files in config/initializers
1313
# -- all .rb files in that directory are automatically loaded.
14+
15+
# Timezone
16+
config.time_zone = 'Asia/Tokyo'
1417
end
1518
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class CreateEventHistories < ActiveRecord::Migration[5.0]
2+
def change
3+
create_table :event_histories do |t|
4+
t.references :dojo, foreign_key: true, index: true, null: false
5+
t.string :dojo_name, null: false
6+
t.string :service_name, null: false
7+
t.integer :service_group_id, null: false
8+
t.integer :event_id, null: false
9+
t.string :event_url, unique: true, null: false
10+
t.integer :participants, null: false
11+
t.datetime :evented_at, null: false
12+
t.timestamps
13+
14+
t.index [:service_name, :event_id], unique: true
15+
t.index [:evented_at, :dojo_id]
16+
end
17+
end
18+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class CreateDojoEventServices < ActiveRecord::Migration[5.0]
2+
def change
3+
create_table :dojo_event_services do |t|
4+
t.references :dojo, foreign_key: true, index: true, null: false
5+
t.string :name
6+
t.string :url
7+
t.integer :group_id
8+
t.timestamps
9+
end
10+
end
11+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AddNotNullToDojoEventServices < ActiveRecord::Migration[5.0]
2+
def change
3+
change_column_null :dojo_event_services, :name, false
4+
change_column_null :dojo_event_services, :group_id, false
5+
end
6+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class ChangeColumnTypeDojoEventServices < ActiveRecord::Migration[5.0]
2+
def up
3+
change_column :dojo_event_services, :name, :integer, null: false
4+
end
5+
6+
def down
7+
change_column :dojo_event_services, :name, :string, null: false
8+
end
9+
end

db/schema.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,17 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema.define(version: 20161217063010) do
13+
ActiveRecord::Schema.define(version: 20170820090605) do
14+
15+
create_table "dojo_event_services", force: :cascade do |t|
16+
t.integer "dojo_id", null: false
17+
t.integer "name", null: false
18+
t.string "url"
19+
t.integer "group_id", null: false
20+
t.datetime "created_at", null: false
21+
t.datetime "updated_at", null: false
22+
t.index ["dojo_id"], name: "index_dojo_event_services_on_dojo_id"
23+
end
1424

1525
create_table "dojos", force: :cascade do |t|
1626
t.string "name"
@@ -24,4 +34,20 @@
2434
t.datetime "updated_at", null: false
2535
end
2636

37+
create_table "event_histories", force: :cascade do |t|
38+
t.integer "dojo_id", null: false
39+
t.string "dojo_name", null: false
40+
t.string "service_name", null: false
41+
t.integer "service_group_id", null: false
42+
t.integer "event_id", null: false
43+
t.string "event_url", null: false
44+
t.integer "participants", null: false
45+
t.datetime "evented_at", null: false
46+
t.datetime "created_at", null: false
47+
t.datetime "updated_at", null: false
48+
t.index ["dojo_id"], name: "index_event_histories_on_dojo_id"
49+
t.index ["evented_at", "dojo_id"], name: "index_event_histories_on_evented_at_and_dojo_id"
50+
t.index ["service_name", "event_id"], name: "index_event_histories_on_service_name_and_event_id", unique: true
51+
end
52+
2753
end

lib/statistics.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module Statistics; end
2+
3+
require_relative 'statistics/client'
4+
require_relative 'statistics/aggregation'

lib/statistics/aggregation.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
module Statistics
2+
class Aggregation
3+
class << self
4+
def run(date:)
5+
cnps_dojos = Dojo.joins(:dojo_event_service).where(dojo_event_services: { name: :connpass }).to_a
6+
drkp_dojos = Dojo.joins(:dojo_event_service).where(dojo_event_services: { name: :doorkeeper }).to_a
7+
8+
Connpass.run(cnps_dojos, date)
9+
Doorkeeper.run(drkp_dojos, date)
10+
end
11+
end
12+
13+
class Connpass
14+
class << self
15+
def run(dojos, date)
16+
cnps = Client::Connpass.new
17+
params = {
18+
yyyymm: "#{date.year}#{date.month}"
19+
}
20+
21+
dojos.each do |dojo|
22+
cnps.fetch_events(params.merge(series_id: dojo.dojo_event_service.group_id)).each do |e|
23+
next unless e.dig('series', 'id') == dojo.dojo_event_service.group_id
24+
25+
EventHistory.create!(dojo_id: dojo.id,
26+
dojo_name: dojo.name,
27+
service_name: dojo.dojo_event_service.name,
28+
service_group_id: dojo.dojo_event_service.group_id,
29+
event_id: e['event_id'],
30+
event_url: e['event_url'],
31+
participants: e['accepted'],
32+
evented_at: Time.zone.parse(e['started_at']))
33+
end
34+
end
35+
end
36+
end
37+
end
38+
39+
class Doorkeeper
40+
class << self
41+
def run(dojos, date)
42+
drkp = Client::Doorkeeper.new
43+
params = {
44+
since_at: date.beginning_of_month,
45+
until_at: date.end_of_month
46+
}
47+
48+
dojos.each do |dojo|
49+
drkp.fetch_events(params.merge(group_id: dojo.dojo_event_service.group_id)).each do |e|
50+
next unless e['group'] == dojo.dojo_event_service.group_id
51+
52+
EventHistory.create!(dojo_id: dojo.id,
53+
dojo_name: dojo.name,
54+
service_name: dojo.dojo_event_service.name,
55+
service_group_id: dojo.dojo_event_service.group_id,
56+
event_id: e['id'],
57+
event_url: e['public_url'],
58+
participants: e['participants'],
59+
evented_at: Time.zone.parse(e['starts_at']))
60+
end
61+
end
62+
end
63+
end
64+
end
65+
end
66+
end

lib/statistics/client.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
module Statistics
2+
class Client
3+
class_attribute :debug
4+
self.debug = false
5+
6+
def initialize(endpoint, &block)
7+
@conn = connection_for(endpoint, &block)
8+
end
9+
10+
def get(path, params)
11+
@conn.get(path, params).body
12+
end
13+
14+
private
15+
16+
def connection_for(endpoint)
17+
Faraday.new(endpoint) do |f|
18+
f.response :logger if self.class.debug
19+
f.response :json, :content_type => /\bjson$/
20+
f.response :raise_error
21+
22+
yield f if block_given?
23+
24+
f.adapter Faraday.default_adapter
25+
end
26+
end
27+
28+
class Connpass
29+
ENDPOINT = 'https://connpass.com/api/v1'.freeze
30+
31+
def initialize
32+
@client = Client.new(ENDPOINT)
33+
end
34+
35+
def search(keyword:)
36+
@client.get('event/', { keyword: keyword, count: 100 })
37+
end
38+
39+
def fetch_events(series_id:, yyyymm: nil)
40+
params = {
41+
series_id: series_id,
42+
start: 1,
43+
count: 100
44+
}
45+
params[:ym] = yyyymm if yyyymm
46+
events = []
47+
48+
loop do
49+
part = @client.get('event/', params)
50+
51+
break if part['results_returned'].zero?
52+
53+
events.push(*part.fetch('events'))
54+
55+
break if part.size < params[:count]
56+
57+
break if params[:start] + params[:count] > part['results_available']
58+
59+
params[:start] += params[:count]
60+
end
61+
62+
events
63+
end
64+
end
65+
66+
class Doorkeeper
67+
ENDPOINT = 'https://api.doorkeeper.jp'.freeze
68+
69+
def initialize
70+
@client = Client.new(ENDPOINT) do |c|
71+
c.authorization(:Bearer, ENV.fetch('DOORKEEPER_API_TOKEN'))
72+
end
73+
@default_since = Time.zone.parse('2010-07-01')
74+
@default_until = Time.zone.now.end_of_day
75+
end
76+
77+
def search(keyword:)
78+
@client.get('events', q: keyword, since: @default_since)
79+
end
80+
81+
def fetch_events(group_id:, since_at: @default_since, until_at: @default_until)
82+
params = {
83+
page: 1,
84+
since: since_at,
85+
until: until_at
86+
}
87+
events = []
88+
89+
loop do
90+
part = @client.get("groups/#{group_id}/events", params)
91+
92+
break if part.size.zero?
93+
94+
events.push(*part.map { |e| e['event'] })
95+
96+
break if part.size < 25 # 25 items / 1 request
97+
98+
params[:page] += 1
99+
end
100+
101+
events
102+
end
103+
end
104+
end
105+
end

lib/tasks/statistics.rake

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
require_relative '../statistics.rb'
2+
3+
namespace :statistics do
4+
desc '月次のイベント履歴を集計します'
5+
task :aggregation, [:yyyymm] => :environment do |tasks, args|
6+
date = Time.current.prev_month.beginning_of_month
7+
if args[:yyyymm].present?
8+
date = %w(%Y%m %Y/%m %Y-%m).map do |fmt|
9+
begin
10+
Time.zone.strptime(args[:yyyymm], fmt)
11+
rescue ArgumentError
12+
end
13+
end.compact.first
14+
end
15+
16+
raise ArgumentError, "Invalid format: #{args[:yyyymm]}" if date.nil?
17+
18+
19+
EventHistory.where(evented_at: date.beginning_of_month..date.end_of_month).delete_all
20+
21+
Statistics::Aggregation.run(date: date)
22+
end
23+
24+
desc 'キーワードからイベント情報を検索します'
25+
task :search, [:keyword] => :environment do |tasks, args|
26+
raise ArgumentError, 'Require the keyword' if args[:keyword].nil?
27+
28+
require 'pp'
29+
30+
puts 'Searching Connpass'
31+
pp Statistics::Client::Connpass.new.search(keyword: args[:keyword])
32+
33+
puts 'Searching Doorkeeper'
34+
pp Statistics::Client::Doorkeeper.new.search(keyword: args[:keyword])
35+
end
36+
end

0 commit comments

Comments
 (0)