diff --git a/.env.sample b/.env.sample index 3c2d25e7..2e9223bc 100644 --- a/.env.sample +++ b/.env.sample @@ -5,3 +5,5 @@ TWITTER_CONSUMER_KEY="your_key" TWITTER_CONSUMER_SECRET="your_secret" TWITTER_ACCESS_TOKEN="your_token" TWITTER_ACCESS_SECRET="your_token_secret" +MEETUP_API_KEY="your_token" +MEETUP_URLNAME="montrealrb" diff --git a/Gemfile b/Gemfile index 6daa472e..2d9aca38 100644 --- a/Gemfile +++ b/Gemfile @@ -61,6 +61,9 @@ gem "colorize" # Authorization system gem "pundit" +# Meetup API +gem 'meetup_client' + group :development do gem "annotate" gem "bullet" diff --git a/Gemfile.lock b/Gemfile.lock index ab940931..43a67be5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,7 +97,7 @@ GEM execjs coffee-script-source (1.12.2) colorize (0.8.1) - concurrent-ruby (1.1.3) + concurrent-ruby (1.1.4) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.4) @@ -212,6 +212,7 @@ GEM lumberjack (1.0.12) mail (2.7.0) mini_mime (>= 0.1.1) + meetup_client (1.0.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) method_source (0.9.2) @@ -463,6 +464,7 @@ DEPENDENCIES guard-rubocop jquery-rails jquery-turbolinks + meetup_client omniauth-github omniauth-twitter pg diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 00000000..d394c3d1 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/meetup/base_job.rb b/app/jobs/meetup/base_job.rb new file mode 100644 index 00000000..8dd5483b --- /dev/null +++ b/app/jobs/meetup/base_job.rb @@ -0,0 +1,15 @@ +module Meetup + class BaseJob < ApplicationJob + queue_as :default + + def perform + raise Exception.new('Your class needs to implement #perform') + end + + protected + def meetup_client + @meetup_client ||= MeetupApi.new + end + + end +end diff --git a/app/jobs/meetup/fetch_events_job.rb b/app/jobs/meetup/fetch_events_job.rb new file mode 100644 index 00000000..e020a667 --- /dev/null +++ b/app/jobs/meetup/fetch_events_job.rb @@ -0,0 +1,26 @@ +module Meetup + class FetchEventsJob < BaseJob + + def perform(from_time=Time.now.beginning_of_day, to_time=nil) + @from_time = from_time + @to_time = to_time + + fetch_events.each do |meetup_event| + time = DateTime.strptime(meetup_event['time'].to_s, '%Q').in_time_zone + if event = Event.where(starts_at: (time - 1.day)..(time + 1.day)).sort_by{|e| (e.starts_at - time).abs}.first + event.update rsvp_count: meetup_event['yes_rsvp_count'] + end + end + end + + private + def fetch_events + @events ||= begin + timestamp_range = [(@from_time.to_i * 1000), (@to_time ? @to_time.to_i * 1000 : nil)].compact.join(',') + data = meetup_client.events({ group_urlname: ENV['MEETUP_URLNAME'], status: 'upcoming,past', time: timestamp_range }) + data['results'] + end + end + + end +end diff --git a/app/models/event/meetup.rb b/app/models/event/meetup.rb new file mode 100644 index 00000000..e9be6374 --- /dev/null +++ b/app/models/event/meetup.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: events +# +# id :integer not null, primary key +# type :string(255) not null +# starts_at :datetime not null +# created_at :datetime +# updated_at :datetime +# + +class Event + class Meetup < ::Event + class NotScheduledYet + def starts_at + date = third_tuesday_of(Time.current) + return third_tuesday_of(date.next_month) if date.end_of_day <= Time.current + date + end + + private + + def third_tuesday_of(time) + date = time.beginning_of_month.to_date + date = date.succ until date.tuesday? + (date + 2.weeks).in_time_zone + 20.hours + end + end + + translates :title, :body + # Someday, we should be able to remove those lines + attribute :title + attribute :body + + globalize_accessors locales: I18n.available_locales, attributes: %i(title introduction conclusion) + # validates_translated :title, :introduction, :conclusion, presence: true + + def self.next + order(starts_at: :asc).find_by("starts_at > ?", Time.now) || NotScheduledYet.new + end + end +end diff --git a/app/models/meetup.rb b/app/models/meetup.rb deleted file mode 100644 index 40d74162..00000000 --- a/app/models/meetup.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -# == Schema Information -# -# Table name: events -# -# id :integer not null, primary key -# type :string(255) not null -# starts_at :datetime not null -# created_at :datetime -# updated_at :datetime -# - -class Meetup < Event - class NotScheduledYet - def starts_at - date = third_tuesday_of(Time.current) - return third_tuesday_of(date.next_month) if date.end_of_day <= Time.current - date - end - - private - - def third_tuesday_of(time) - date = time.beginning_of_month.to_date - date = date.succ until date.tuesday? - (date + 2.weeks).in_time_zone + 20.hours - end - end - - translates :title, :body - # Someday, we should be able to remove those lines - attribute :title - attribute :body - - globalize_accessors locales: I18n.available_locales, attributes: %i(title introduction conclusion) - # validates_translated :title, :introduction, :conclusion, presence: true - - def self.next - order(starts_at: :asc).find_by("starts_at > ?", Time.now) || NotScheduledYet.new - end -end diff --git a/config/initializers/meetup_client.rb b/config/initializers/meetup_client.rb new file mode 100644 index 00000000..5fbae70f --- /dev/null +++ b/config/initializers/meetup_client.rb @@ -0,0 +1,3 @@ +MeetupClient.configure do |config| + config.api_key = ENV['MEETUP_API_KEY'] +end diff --git a/db/migrate/20181219144356_add_rsvp_count_to_events.rb b/db/migrate/20181219144356_add_rsvp_count_to_events.rb new file mode 100644 index 00000000..60ebf168 --- /dev/null +++ b/db/migrate/20181219144356_add_rsvp_count_to_events.rb @@ -0,0 +1,5 @@ +class AddRsvpCountToEvents < ActiveRecord::Migration[5.1] + def change + add_column :events, :rsvp_count, :integer, default: 0, after: :sponsorships_count + end +end diff --git a/db/schema.rb b/db/schema.rb index bf2b3b18..ee6c3282 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20181117014059) do +ActiveRecord::Schema.define(version: 20181219144356) do create_table "contacts", force: :cascade do |t| t.string "name" @@ -46,6 +46,7 @@ t.integer "talks_count", default: 0 t.integer "sponsorships_count", default: 0 t.integer "organizer_id" + t.integer "rsvp_count", default: 0 t.index ["location_id"], name: "index_events_on_location_id" t.index ["starts_at"], name: "index_events_on_starts_at" t.index ["user_id"], name: "index_events_on_user_id" diff --git a/spec/jobs/meetup/base_job_spec.rb b/spec/jobs/meetup/base_job_spec.rb new file mode 100644 index 00000000..39338872 --- /dev/null +++ b/spec/jobs/meetup/base_job_spec.rb @@ -0,0 +1,25 @@ +require 'rails_helper' + +RSpec.describe Meetup::BaseJob, type: :job do + include ActiveJob::TestHelper + + let(:fake_job) { Class.new(described_class) } + + describe '#perform' do + it 'raises an error' do + expect { fake_job.perform_now }.to raise_error(Exception) + end + end + + describe 'queueing' do + it 'queues the job' do + expect { + fake_job.perform_later + }.to change(ActiveJob::Base.queue_adapter.enqueued_jobs, :size).by(1) + end + + it 'queues as :default' do + expect(fake_job.new.queue_name).to eq('default') + end + end +end diff --git a/spec/jobs/meetup/fetch_events_job_spec.rb b/spec/jobs/meetup/fetch_events_job_spec.rb new file mode 100644 index 00000000..2753178d --- /dev/null +++ b/spec/jobs/meetup/fetch_events_job_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe Meetup::FetchEventsJob, type: :job do + let(:job) { described_class.new } + let(:mock_meetup_client) { + Struct.new(:data) do + def events(attrs = {}) + data[:events] + end + end + } + let!(:event) { create(:event, starts_at: 2.weeks.from_now.noon) } + + it 'attemps to reach the Meetup API' do + expect(job).to receive(:meetup_client) { + mock_meetup_client.new(events: { + 'results' => [ + { 'id' => 'foo', 'yes_rsvp_count' => 7, 'time' => (Time.zone.tomorrow.noon.to_i * 1000) }, + { 'id' => 'bar', 'yes_rsvp_count' => 13, 'time' => (1.month.from_now.noon.to_i * 1000) }, + { 'id' => 'baz', 'yes_rsvp_count' => 42, 'time' => (2.weeks.from_now.noon.to_i * 1000) } + ], + 'meta' => [] + }) + } + + expect { + job.perform + event.reload + }.to change(event, :rsvp_count).from(0).to(42) + end +end diff --git a/spec/models/meetup_spec.rb b/spec/models/meetup_spec.rb index 728b34c7..d30044d3 100644 --- a/spec/models/meetup_spec.rb +++ b/spec/models/meetup_spec.rb @@ -2,14 +2,14 @@ require "rails_helper" -RSpec.describe Meetup, type: :model do +RSpec.describe Event::Meetup, type: :model do describe ".next" do it "returns a Meetup::NotScheduledYet if there is upcomming meetups" do - expect(Meetup.next).to be_a Meetup::NotScheduledYet + expect(Event::Meetup.next).to be_a Event::Meetup::NotScheduledYet end end - describe Meetup::NotScheduledYet do + describe Event::Meetup::NotScheduledYet do describe "#starts_at" do let(:next_meetup) { described_class.new }