Skip to content

Commit 6d13362

Browse files
authored
Merge pull request #459 from coderdojo-japan/add_upcoming_events_task
直近のイベント情報を集計するtaskの追加(仕切り直し)
2 parents 437c901 + b3bc088 commit 6d13362

File tree

14 files changed

+343
-2
lines changed

14 files changed

+343
-2
lines changed

app/models/dojo_event_service.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ class DojoEventService < ApplicationRecord
33
INTERNAL_SERVICES = %i( static_yaml )
44

55
belongs_to :dojo
6+
has_many :upcoming_events, dependent: :destroy
7+
68
enum name: EXTERNAL_SERVICES + INTERNAL_SERVICES
79

810
validates :name, presence: true

app/models/upcoming_event.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class UpcomingEvent < ApplicationRecord
2+
belongs_to :dojo_event_service
3+
4+
validates :service_name, presence: true, uniqueness: { scope: :event_id }
5+
validates :event_id, presence: true
6+
validates :event_url, presence: true
7+
validates :event_at, presence: true
8+
validates :participants, presence: true
9+
10+
scope :for, ->(service) { where(dojo_event_service: DojoEventService.for(service)) }
11+
scope :until, ->(date) { where('event_at < ?', date.beginning_of_day) }
12+
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class ModColumnsToUpcomingEvent < ActiveRecord::Migration[5.1]
2+
def up
3+
remove_index :upcoming_events, :event_at
4+
5+
add_column :upcoming_events, :service_name, :string, null: false
6+
add_column :upcoming_events, :participants, :integer, null: false
7+
8+
add_index :upcoming_events, [:service_name, :event_id], :unique => true
9+
end
10+
11+
def down
12+
remove_index :upcoming_events, [:service_name, :event_id]
13+
14+
remove_column :upcoming_events, :service_name, :string, null: false
15+
remove_column :upcoming_events, :participants, :integer, null: false
16+
17+
add_index :upcoming_events, :event_at, name: "index_upcoming_events_on_event_at"
18+
end
19+
end

db/schema.rb

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

13-
ActiveRecord::Schema.define(version: 20190423141200) do
13+
ActiveRecord::Schema.define(version: 20190526151359) do
1414

1515
# These are extensions that must be enabled in order to support this database
1616
enable_extension "plpgsql"
@@ -84,8 +84,10 @@
8484
t.string "event_id", null: false
8585
t.string "event_url", null: false
8686
t.datetime "event_at", null: false
87+
t.string "service_name", null: false
88+
t.integer "participants", null: false
8789
t.index ["dojo_event_service_id"], name: "index_upcoming_events_on_dojo_event_service_id"
88-
t.index ["event_at"], name: "index_upcoming_events_on_event_at"
90+
t.index ["service_name", "event_id"], name: "index_upcoming_events_on_service_name_and_event_id", unique: true
8991
end
9092

9193
add_foreign_key "dojo_event_services", "dojos"

docs/upcoming_events_aggregation.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# rake upcoming_events:aggregation[provider]
2+
3+
## 概要
4+
5+
近日開催(2ヶ月分)のイベント情報を収集する
6+
7+
## 引数
8+
9+
|引数名||必須|説明|
10+
|--|--|--|--|
11+
|provider|string|(省略可)|集計対象プロバイダ|
12+
13+
## 説明
14+
15+
過去(昨日分まで)のイベント情報を削除し、本日から 2 ヶ月後までのイベント情報を収集する。
16+
17+
provider が指定されたとき、指定プロバイダに対してのみ集計を行う。
18+
19+
+ provider には、connpass, doorkeeper, facebook が指定可能。ただし、現時点で facebook は収集対象外のため処理を skip する。

lib/tasks/upcoming_events.rake

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require_relative '../upcoming_events.rb'
2+
3+
namespace :upcoming_events do
4+
desc '指定期間/プロバイダのイベント履歴を集計します'
5+
task :aggregation, [:provider] => :environment do |tasks, args|
6+
UpcomingEvent.transaction do
7+
UpcomingEvents::Aggregation.new(args).run
8+
end
9+
end
10+
end

lib/upcoming_events.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module UpcomingEvents; end
2+
3+
require_relative 'upcoming_events/tasks'
4+
require_relative 'upcoming_events/aggregation'
5+
require_relative 'event_service'

lib/upcoming_events/aggregation.rb

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
module UpcomingEvents
2+
class Aggregation
3+
def initialize(args)
4+
@from = Time.zone.today
5+
@to = @from + 2.months
6+
@provider = args[:provider]
7+
# NOTE: 対象は一旦収集可能な connpass, doorkeeper のみにする
8+
@externals = fetch_dojos(@provider)
9+
end
10+
11+
def run
12+
puts "UpcomingEvents aggregate"
13+
with_notifying do
14+
delete_upcoming_events
15+
execute
16+
end
17+
end
18+
19+
private
20+
21+
def fetch_dojos(provider)
22+
base_providers = DojoEventService::EXTERNAL_SERVICES - [:facebook]
23+
services = if provider.blank?
24+
# 全プロバイダ対象
25+
base_providers
26+
elsif base_providers.include?(provider.to_sym)
27+
[provider.to_sym]
28+
end
29+
return [] unless services
30+
find_dojos_by(services)
31+
end
32+
33+
def find_dojos_by(services)
34+
services.each.with_object({}) do |name, hash|
35+
hash[name] = Dojo.eager_load(:dojo_event_services).where(dojo_event_services: { name: name }).to_a
36+
end
37+
end
38+
39+
def with_notifying
40+
yield
41+
Notifier.notify_success(@provider)
42+
rescue => e
43+
Notifier.notify_failure(@provider, e)
44+
end
45+
46+
def delete_upcoming_events
47+
UpcomingEvent.until(@from).delete_all
48+
end
49+
50+
def execute
51+
target_period = @from..@to
52+
@externals.each do |kind, list|
53+
puts "Aggregate of #{kind}"
54+
"UpcomingEvents::Tasks::#{kind.to_s.camelize}".constantize.new(list, target_period).run
55+
end
56+
end
57+
58+
class Notifier
59+
class << self
60+
def notify_success(provider)
61+
notify("近日開催イベント情報#{provider_info(provider)}を収集しました")
62+
end
63+
64+
def notify_failure(provider, exception)
65+
notify("近日開催イベント情報の収集#{provider_info(provider)}でエラーが発生しました\n#{exception.message}\n#{exception.backtrace.join("\n")}")
66+
end
67+
68+
private
69+
70+
def provider_info(provider)
71+
provider ? "(#{provider})" : nil
72+
end
73+
74+
def idobata_hook_url
75+
return @idobata_hook_url if defined?(@idobata_hook_url)
76+
@idobata_hook_url = ENV['IDOBATA_HOOK_URL']
77+
end
78+
79+
def notifierable?
80+
idobata_hook_url.present?
81+
end
82+
83+
def notify(msg)
84+
$stdout.puts msg
85+
puts `curl --data-urlencode "source=#{msg}" -s #{idobata_hook_url} -o /dev/null -w "idobata: %{http_code}"` if notifierable?
86+
end
87+
end
88+
end
89+
end
90+
end

lib/upcoming_events/tasks.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module UpcomingEvents
2+
module Tasks
3+
end
4+
end
5+
6+
require_relative 'tasks/connpass'
7+
require_relative 'tasks/doorkeeper'

lib/upcoming_events/tasks/connpass.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
module UpcomingEvents
2+
module Tasks
3+
class Connpass
4+
def initialize(dojos, period)
5+
@client = EventService::Providers::Connpass.new
6+
@dojos = dojos
7+
@params = build_params(period)
8+
end
9+
10+
def run
11+
@dojos.each do |dojo|
12+
dojo.dojo_event_services.for(:connpass).each do |dojo_event_service|
13+
@client.fetch_events(@params.merge(series_id: dojo_event_service.group_id)).each do |e|
14+
next unless e.dig('series', 'id').to_s == dojo_event_service.group_id
15+
16+
record = dojo_event_service.upcoming_events.find_or_initialize_by(event_id: e['event_id'])
17+
record.update!(service_name: dojo_event_service.name,
18+
event_url: e['event_url'],
19+
event_at: Time.zone.parse(e['started_at']),
20+
participants: e['accepted'])
21+
end
22+
end
23+
end
24+
end
25+
26+
private
27+
28+
def build_params(period)
29+
yyyymmdd = []
30+
yyyymm = []
31+
32+
st_date = period.first
33+
ed_date = period.last
34+
35+
date = period.first
36+
while date <= ed_date
37+
if date.day == 1 && date.end_of_month <= ed_date
38+
yyyymm << date.strftime('%Y%m')
39+
date += 1.month
40+
else
41+
yyyymmdd << date.strftime('%Y%m%d')
42+
date += 1.day
43+
end
44+
end
45+
46+
{
47+
yyyymmdd: yyyymmdd,
48+
yyyymm: yyyymm
49+
}
50+
end
51+
end
52+
end
53+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module UpcomingEvents
2+
module Tasks
3+
class Doorkeeper
4+
def initialize(dojos, period)
5+
@client = EventService::Providers::Doorkeeper.new
6+
@dojos = dojos
7+
@params = build_params(period)
8+
end
9+
10+
def run
11+
@dojos.each do |dojo|
12+
dojo.dojo_event_services.for(:doorkeeper).each do |dojo_event_service|
13+
@client.fetch_events(@params.merge(group_id: dojo_event_service.group_id)).each do |e|
14+
next unless e['group'].to_s == dojo_event_service.group_id
15+
16+
record = dojo_event_service.upcoming_events.find_or_initialize_by(event_id: e['id'])
17+
record.update!(service_name: dojo_event_service.name,
18+
event_url: e['public_url'],
19+
participants: e['participants'],
20+
event_at: Time.zone.parse(e['starts_at']))
21+
end
22+
end
23+
end
24+
end
25+
26+
private
27+
28+
def build_params(period)
29+
{
30+
since_at: period.first.beginning_of_day,
31+
until_at: period.last.end_of_day
32+
}
33+
end
34+
end
35+
end
36+
end

spec/factories/upcoming_events.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
require 'factory_bot'
2+
3+
FactoryBot.define do
4+
factory :upcoming_event do
5+
service_name { :connpass }
6+
event_id { '1234' }
7+
event_url { 'http:/www.aaa.com/events/1224' }
8+
event_at { '2019-05-01 10:00'.in_time_zone }
9+
participants { 1 }
10+
end
11+
end
12+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require 'rails_helper'
2+
require 'upcoming_events'
3+
4+
RSpec.describe UpcomingEvents::Aggregation do
5+
include_context 'Use stubs UpcomingEvents for Connpass'
6+
include_context 'Use stubs UpcomingEvents for Doorkeeper'
7+
8+
describe '.run' do
9+
before do
10+
@d1 = create(:dojo, name: 'Dojo1', email: 'info@dojo1.com', description: 'CoderDojo1', tags: %w(CoderDojo1), url: 'https://dojo1.com')
11+
@d2 = create(:dojo, name: 'Dojo2', email: 'info@dojo2.com', description: 'CoderDojo2', tags: %w(CoderDojo2), url: 'https://dojo2.com')
12+
@es1 = create(:dojo_event_service, dojo_id: @d1.id, name: :connpass, group_id: 9876)
13+
@es2 = create(:dojo_event_service, dojo_id: @d2.id, name: :doorkeeper, group_id: 5555)
14+
end
15+
16+
it 'プロバイダ指定なし' do
17+
expect{ UpcomingEvents::Aggregation.new({}).run }.to change{ UpcomingEvent.count }.from(0).to(3)
18+
end
19+
20+
it 'プロバイダ指定(connpass)' do
21+
expect{ UpcomingEvents::Aggregation.new(provider: 'connpass').run }.to change{ UpcomingEvent.count }.from(0).to(1)
22+
end
23+
24+
it 'プロバイダ指定(doorkeeper)' do
25+
expect{ UpcomingEvents::Aggregation.new(provider: 'doorkeeper').run }.to change{ UpcomingEvent.count }.from(0).to(2)
26+
end
27+
28+
it '昨日分までは削除' do
29+
create(:upcoming_event, dojo_event_service_id: @es1.id, service_name: 'connpass', event_id: '1111', event_at: "#{Time.zone.today - 3.days} 13:00:00".in_time_zone)
30+
create(:upcoming_event, dojo_event_service_id: @es1.id, service_name: 'connpass', event_id: '2222', event_at: "#{Time.zone.today - 2.days} 14:00:00".in_time_zone)
31+
create(:upcoming_event, dojo_event_service_id: @es1.id, service_name: 'connpass', event_id: '3333', event_at: "#{Time.zone.today - 1.days} 15:00:00".in_time_zone)
32+
create(:upcoming_event, dojo_event_service_id: @es2.id, service_name: 'doorkeeper', event_id: '4444', event_at: "#{Time.zone.today - 2.days} 10:00:00".in_time_zone)
33+
create(:upcoming_event, dojo_event_service_id: @es2.id, service_name: 'doorkeeper', event_id: '5555', event_at: "#{Time.zone.today - 1.days} 11:00:00".in_time_zone)
34+
35+
expect{ UpcomingEvents::Aggregation.new({}).run }.to change{ UpcomingEvent.count }.from(5).to(3)
36+
end
37+
end
38+
end

spec/support/shared_contexts/statistics.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,39 @@
4141
]
4242
end
4343
end
44+
45+
RSpec.shared_context 'Use stubs UpcomingEvents for Connpass' do
46+
include_context 'Use stub connection of Faraday'
47+
48+
let(:connpass_response) do
49+
[
50+
200,
51+
{ 'Content-Type' => 'application/json' },
52+
'{"results_returned": 1, "events": [{"event_url": "https://coderdojo-okutama.connpass.com/event/12345/", "event_type": "participation", "owner_nickname": "nalabjp", "series": {"url": "https://coderdojo-okutama.connpass.com/", "id": 9876, "title": "CoderDojo series"}, "updated_at": "' +
53+
"#{Time.zone.today}T14:59:30+09:00" + '", "lat": "35.801763000000", "started_at": "' +
54+
"#{Time.zone.today + 1.month}T13:00:00+09:00" + '", "hash_tag": "CoderDojo", "title": "CoderDojo title", "event_id": 12345, "lon": "139.087656000000", "waiting": 2, "limit": 10, "owner_id": 2525, "owner_display_name": "nalabjp", "description": "CoderDojo description", "address": "Okutama-cho Tokyo", "catch": "CoderDojo catch", "accepted": 10, "ended_at": "' +
55+
"#{Time.zone.today + 1.month}T15:00:00+09:00" + '", "place": "Tokyo"}], "results_start": 200, "results_available": 518}'
56+
]
57+
end
58+
end
59+
60+
RSpec.shared_context 'Use stubs UpcomingEvents for Doorkeeper' do
61+
include_context 'Use stub connection of Faraday'
62+
63+
let(:doorkeeper_response) do
64+
[
65+
200,
66+
{ 'Content-Type' => 'application/json' },
67+
'[{"event":{"title":"CoderDojo title","id":1234,"starts_at":"' +
68+
"#{Time.zone.today + 1.month}T01:00:00.000Z" + '","ends_at":"' +
69+
"#{Time.zone.today + 1.month}T04:00:00.000Z" + '","venue_name":"奥多摩町","address":"奥多摩町","lat":"35.801763000000","long":"139.087656000000","ticket_limit":30,"published_at":"' +
70+
"#{Time.zone.today - 4.days}T03:43:04.000Z" + '","updated_at":"' +
71+
"#{Time.zone.today}T11:31:21.810Z" + '","group":5555,"banner":null,"description":"CoderDojo description","public_url":"https://coderdojo-okutama.doorkeeper.jp/events/8888","participants":12,"waitlisted":0}},' +
72+
'{"event":{"title":"CoderDojo title","id":2345,"starts_at":"' +
73+
"#{Time.zone.today + 1.month + 1.day}T01:00:00.000Z" + '","ends_at":"' +
74+
"#{Time.zone.today + 1.month + 1.day}T04:00:00.000Z" + '","venue_name":"奥多摩町","address":"奥多摩町","lat":"35.801763000000","long":"139.087656000000","ticket_limit":30,"published_at":"' +
75+
"#{Time.zone.today - 4.days}T03:43:04.000Z" + '","updated_at":"' +
76+
"#{Time.zone.today}T11:31:21.810Z" + '","group":5555,"banner":null,"description":"CoderDojo description","public_url":"https://coderdojo-okutama.doorkeeper.jp/events/8888","participants":12,"waitlisted":0}}]'
77+
]
78+
end
79+
end

0 commit comments

Comments
 (0)