almanack 0.0.1.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5ee646213641a7f73f5cd5d2164c92930875b58d
4
+ data.tar.gz: 474257e5d3e93edd1a5c4b5b4125129c7c645ba2
5
+ SHA512:
6
+ metadata.gz: caf49861afbd6bd62c5147da62425ed06b822a65ed4e5d543062b2a27e9ae67d9f53e4a88a8235ae590a4554c98475016b83ab9cc8237ac2194c03063d859bc5
7
+ data.tar.gz: 6d5458aa700b1c152caa5a31f2294a766e0a6ca6ab3eeb673bf4be9213aa806c9982663bc3c8088999130d08d637fccd8690abc7b445a7d8893ce6d1b68fb327
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.1.1
5
+ addons:
6
+ code_climate:
7
+ repo_token:
8
+ secure: "VmXcnoFkthPYiEdyMTgqopGNUirpoZ3/wRych2xm+k8g9yW3IVbnEsK0lLg7InlyUhM3r7IxC4NBTRa33MhkpuCtvQGFpUjt56lhDvLcb0OBLOnpReKpo8VWW/cy0okpiW8LNt488M67WH+Wnlx//Fpkop9/HNM1vTLjysISk/Q="
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in almanack.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Pete Nicholls
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Almanack
2
+
3
+ [![Build Status](https://travis-ci.org/Aupajo/sinatra-gcal.svg)](https://travis-ci.org/Aupajo/sinatra-gcal)
4
+ [![Code Climate](https://codeclimate.com/github/Aupajo/sinatra-gcal.png)](https://codeclimate.com/github/Aupajo/sinatra-gcal)
5
+ [![Code Climate](https://codeclimate.com/github/Aupajo/sinatra-gcal/coverage.png)](https://codeclimate.com/github/Aupajo/sinatra-gcal)
6
+
7
+ TODO: Write a gem description
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'almanack'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install almanack
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ### Meetup.com
28
+
29
+ You'll need your [Meetup.com API key](https://secure.meetup.com/meetup_api/key).
30
+
31
+ ## Contributing
32
+
33
+ 1. Fork it ( http://github.com/Aupajo/sinatra-gcal/fork )
34
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
35
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
36
+ 4. Push to the branch (`git push origin my-new-feature`)
37
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ desc 'Run a dummy server'
9
+ task :server do
10
+ require "almanack"
11
+
12
+ today = DateTime.now
13
+
14
+ Almanack.config.add_events [
15
+ { title: "Hogswatch", start_date: today },
16
+ { title: "Soul Cake Tuesday", start_date: today + 10 },
17
+ { title: "Eve of Small Gods", start_date: today + 30 },
18
+ ]
19
+
20
+ Rack::Handler::WEBrick.run(Almanack::Server)
21
+ end
data/almanac.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'almanack/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "almanack"
8
+ spec.version = Almanack::VERSION
9
+ spec.authors = ["Pete Nicholls"]
10
+ spec.email = ["pete@metanation.com"]
11
+ spec.summary = %q{Combined events calendar for Google Calendar, iCal, Meetup.com and friends.}
12
+ spec.homepage = "https://github.com/Aupajo/sinatra-gcal"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "sinatra"
21
+ spec.add_dependency "ri_cal"
22
+ spec.add_dependency "addressable"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.5"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "rack-test"
28
+ spec.add_development_dependency "webmock"
29
+ spec.add_development_dependency "vcr"
30
+ spec.add_development_dependency "timecop"
31
+ spec.add_development_dependency "nokogiri"
32
+ spec.add_development_dependency "codeclimate-test-reporter"
33
+ end
@@ -0,0 +1,21 @@
1
+ module Almanack
2
+ class Calendar
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def event_sources
8
+ @config.event_sources
9
+ end
10
+
11
+ def events
12
+ days_lookahead = 30
13
+ from_date = DateTime.now
14
+ to_date = DateTime.now + days_lookahead
15
+
16
+ event_sources.map do |event_source|
17
+ event_source.events_between(from_date..to_date)
18
+ end.flatten.sort_by(&:start_date)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'ri_cal'
4
+
5
+ module Almanack
6
+ class Configuration
7
+ attr_reader :event_sources
8
+
9
+ def initialize
10
+ reset!
11
+ end
12
+
13
+ def reset!
14
+ @event_sources = []
15
+ end
16
+
17
+ def add_ical_feed(url)
18
+ @event_sources << IcalFeed.new(url)
19
+ end
20
+
21
+ def add_events(events)
22
+ @event_sources << SimpleEventCollection.new(events)
23
+ end
24
+
25
+ def add_meetup_group(options)
26
+ @event_sources << MeetupGroup.new(options)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ require 'ostruct'
2
+
3
+ module Almanack
4
+ class Event < OpenStruct
5
+ end
6
+ end
@@ -0,0 +1,60 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'ri_cal'
4
+
5
+ module Almanack
6
+ class IcalFeed
7
+ def initialize(url)
8
+ @url = url
9
+ end
10
+
11
+ def events_between(date_range)
12
+ occurrences_between(date_range).map do |occurrence|
13
+ event_from(occurrence)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def each_ical_event(&block)
20
+ entities.each do |entity|
21
+ entity.events.each(&block) if entity.respond_to?(:events)
22
+ end
23
+ end
24
+
25
+ def occurrences_between(date_range)
26
+ to_date = date_range.max
27
+ from_date = date_range.min
28
+
29
+ occurrences = []
30
+
31
+ each_ical_event do |ical_event|
32
+ ical_event.occurrences(starting: from_date, before: to_date).each do |occurrence|
33
+ occurrences << occurrence
34
+ end
35
+ end
36
+
37
+ occurrences
38
+ end
39
+
40
+ def event_from(occurrence)
41
+ Event.new(
42
+ title: occurrence.summary,
43
+ start_date: occurrence.dtstart,
44
+ end_date: occurrence.dtend,
45
+ description: occurrence.description,
46
+ location: occurrence.location
47
+ )
48
+ end
49
+
50
+ def entities
51
+ RiCal.parse_string(body)
52
+ end
53
+
54
+ def body
55
+ uri = URI(@url)
56
+ Net::HTTP.get(uri)
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,96 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'addressable/uri'
4
+
5
+ module Almanack
6
+ class MeetupGroup
7
+ def initialize(options = {})
8
+ @request_options = options
9
+ end
10
+
11
+ def events_between(date_range)
12
+ events.select do |event|
13
+ event.start_date >= date_range.min && event.start_date <= date_range.max
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def events
20
+ request = MeetupAPIRequest.new(@request_options)
21
+ request.results.map { |result| event_from(result) }
22
+ end
23
+
24
+ def event_from(result)
25
+ # 3 hours, as recommended by Meetup.com if duration isn't present
26
+ default_duration_in_ms = 3 * 60 * 60 * 1000
27
+
28
+ event_name = [result['group']['name'], result['name']].compact.join(': ')
29
+ start_time = Time.at(result['time'] / 1000)
30
+ duration_in_secs = (result['duration'] || default_duration_in_ms) / 1000
31
+ end_time = start_time + duration_in_secs
32
+
33
+ Event.new(
34
+ title: event_name,
35
+ start_date: start_time.to_datetime,
36
+ end_date: end_time.to_datetime,
37
+ description: result['description'],
38
+ location: location_from_venue(result['venue']),
39
+ url: result['event_url']
40
+ )
41
+ end
42
+
43
+ def location_from_venue(venue)
44
+ %w{ name address_1 address_2 address_3 city state country }.map do |attr|
45
+ venue[attr]
46
+ end.compact.join(', ')
47
+ end
48
+ end
49
+
50
+ class MeetupAPIError < StandardError
51
+ end
52
+
53
+ class MeetupAPIException < Exception
54
+ end
55
+
56
+ class MeetupAPIRequest
57
+ REQUIRED_OPTIONS = [:group_domain, :group_urlname, :group_id]
58
+
59
+ attr_accessor :options
60
+
61
+ def initialize(options = {})
62
+ @options = options
63
+ end
64
+
65
+ def results
66
+ response['results']
67
+ end
68
+
69
+ def uri
70
+ if !options.has_key?(:key)
71
+ raise MeetupAPIException, 'Cannot form valid URI, missing :key option'
72
+ end
73
+
74
+ if (options.keys & REQUIRED_OPTIONS).empty?
75
+ raise MeetupAPIException, "Cannot form valid URI, missing one of: #{REQUIRED_OPTIONS}"
76
+ end
77
+
78
+ endpoint = "https://api.meetup.com/2/events"
79
+
80
+ Addressable::URI.parse(endpoint).tap do |uri|
81
+ uri.query_values = options
82
+ end
83
+ end
84
+
85
+ def response
86
+ json = Net::HTTP.get(uri)
87
+ data = JSON.parse(json)
88
+
89
+ if data['problem']
90
+ raise MeetupAPIError, data['problem']
91
+ end
92
+
93
+ data
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,13 @@
1
+ require "sinatra"
2
+
3
+ module Almanack
4
+ class Server < Sinatra::Base
5
+ set :theme, 'origami'
6
+ set :root, Pathname(settings.root).join('themes', settings.theme)
7
+
8
+ get "/" do
9
+ @calendar = Almanack.calendar
10
+ erb :events
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module Almanack
2
+ class SimpleEventCollection
3
+ def initialize(events)
4
+ @events = events
5
+ end
6
+
7
+ def events_between(date_range)
8
+ @events.map { |attrs| Event.new(attrs) }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ body {
2
+ font: 14pt/1.6em "Helvetica Neue", sans-serif;
3
+ font-weight: 300;
4
+ }
@@ -0,0 +1,8 @@
1
+ <div class="events">
2
+ <% @calendar.events.each do |event| %>
3
+ <div class="event">
4
+ <h2 class="title"><%= event.title %></h2>
5
+ (<%= event.start_date %>)
6
+ </div>
7
+ <% end %>
8
+ </div>
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Upcoming events</title>
6
+ <link rel="stylesheet" type="text/css" href="/stylesheets/origami.css">
7
+ </head>
8
+ <body>
9
+
10
+ <%= yield %>
11
+
12
+ </body>
13
+ </html>
@@ -0,0 +1,3 @@
1
+ module Almanack
2
+ VERSION = "0.0.1.alpha1"
3
+ end
data/lib/almanack.rb ADDED
@@ -0,0 +1,23 @@
1
+ require "pathname"
2
+ require "almanack/version"
3
+ require "almanack/server"
4
+ require "almanack/configuration"
5
+ require "almanack/calendar"
6
+ require "almanack/simple_event_collection"
7
+ require "almanack/meetup_group"
8
+ require "almanack/ical_feed"
9
+ require "almanack/event"
10
+
11
+ module Almanack
12
+ def self.config
13
+ @config ||= Configuration.new
14
+ end
15
+
16
+ def self.calendar
17
+ @calendar ||= Calendar.new(config)
18
+ end
19
+
20
+ def self.reset!
21
+ config.reset!
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ module Almanack
4
+ describe Calendar do
5
+
6
+ describe "#events" do
7
+ describe "with simple events" do
8
+ it "returns the events" do
9
+ config = Configuration.new
10
+ config.add_events [
11
+ { title: "Hogswatch" }
12
+ ]
13
+
14
+ calendar = Calendar.new(config)
15
+
16
+ expect(calendar.events.size).to eq(1)
17
+ expect(calendar.events.first.title).to eq('Hogswatch')
18
+ end
19
+ end
20
+
21
+ describe "with an iCal feed" do
22
+ it "returns the event occurrences" do
23
+ config = Configuration.new
24
+ config.add_ical_feed "https://www.google.com/calendar/ical/61s2re9bfk01abmla4d17tojuo%40group.calendar.google.com/public/basic.ics"
25
+
26
+ calendar = Calendar.new(config)
27
+
28
+ events = nil
29
+
30
+ Timecop.freeze(2014, 4, 3) do
31
+ VCR.use_cassette('google_calendar') do
32
+ events = calendar.events
33
+ end
34
+ end
35
+
36
+ expect(events.size).to eq(15)
37
+ expect(events).to all_have_properties(:title, :start_date)
38
+ expect(events).to be_in_order
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ module Almanack
4
+ describe Configuration do
5
+
6
+ describe "#add_events" do
7
+ it "adds a simple event collection event source" do
8
+ config = Configuration.new
9
+ expect(config.event_sources.size).to eq(0)
10
+
11
+ config.add_events [
12
+ { title: "Hogswatch" }
13
+ ]
14
+
15
+ expect(config.event_sources.size).to eq(1)
16
+ expect(config.event_sources.first).to be_an_instance_of(SimpleEventCollection)
17
+ end
18
+ end
19
+
20
+ describe "#ical_feed" do
21
+ it "adds an iCal feed event source" do
22
+ config = Configuration.new
23
+ expect(config.event_sources.size).to eq(0)
24
+
25
+ config.add_ical_feed "https://www.google.com/calendar/ical/61s2re9bfk01abmla4d17tojuo%40group.calendar.google.com/public/basic.ics"
26
+
27
+ expect(config.event_sources.size).to eq(1)
28
+ expect(config.event_sources.first).to be_an_instance_of(IcalFeed)
29
+ end
30
+ end
31
+
32
+ describe "#meetup_group" do
33
+ it "adds a Meetup group event source" do
34
+ config = Configuration.new
35
+ expect(config.event_sources.size).to eq(0)
36
+
37
+ config.add_meetup_group(group_urlname: "CHC-JS", key: "secrettoken")
38
+
39
+ expect(config.event_sources.size).to eq(1)
40
+ expect(config.event_sources.first).to be_an_instance_of(MeetupGroup)
41
+ end
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ module Almanack
4
+ describe Event do
5
+
6
+ it "has a title" do
7
+ event = Event.new(title: "Music with rocks in")
8
+ expect(event.title).to eq("Music with rocks in")
9
+ end
10
+
11
+ it "has a start date" do
12
+ event = Event.new(start_date: DateTime.new(2014, 01, 01))
13
+ expect(event.start_date).to eq(DateTime.new(2014, 01, 01))
14
+ end
15
+
16
+ it "has a end date" do
17
+ event = Event.new(end_date: DateTime.new(2014, 01, 02))
18
+ expect(event.end_date).to eq(DateTime.new(2014, 01, 02))
19
+ end
20
+
21
+ it "has a location" do
22
+ event = Event.new(location: "Street of Cunning Artificers")
23
+ expect(event.location).to eq("Street of Cunning Artificers")
24
+ end
25
+
26
+ it "has a description" do
27
+ event = Event.new(description: "Be there or be a rectangular thynge.")
28
+ expect(event.description).to eq("Be there or be a rectangular thynge.")
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Viewing a calendar", :feature do
4
+ before { Almanack.reset! }
5
+
6
+ it "displays all upcoming events" do
7
+ Almanack.config.add_events [
8
+ { title: "Hogswatch" },
9
+ { title: "Soul Cake Tuesday" },
10
+ { title: "Eve of Small Gods" },
11
+ ]
12
+
13
+ get "/"
14
+
15
+ expect(last_response).to have_event_on_page("Hogswatch")
16
+ expect(last_response).to have_event_on_page("Soul Cake Tuesday")
17
+ expect(last_response).to have_event_on_page("Eve of Small Gods")
18
+ end
19
+
20
+ it "displays events from an iCal feed" do
21
+ Almanack.config.add_ical_feed "https://www.google.com/calendar/ical/61s2re9bfk01abmla4d17tojuo%40group.calendar.google.com/public/basic.ics"
22
+
23
+ Timecop.freeze(2014, 4, 3) do
24
+ VCR.use_cassette('google_calendar') do
25
+ get "/"
26
+ end
27
+ end
28
+
29
+ expect(last_response).to have_event_on_page("Ruby Meetup @catalyst - Tanks! Guns!")
30
+ expect(last_response).to have_event_on_page("The Foundation")
31
+ expect(last_response).to have_event_on_page("WikiHouse/NZ weekly meet-up")
32
+ expect(last_response).to have_event_on_page("Christchurch Python Meetup")
33
+ expect(last_response).to have_event_on_page("Coffee & Jam")
34
+ end
35
+ end