almanack 0.0.1.alpha3 → 1.0.0.pre
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 +4 -4
- data/README.md +55 -13
- data/almanac.gemspec +2 -1
- data/bin/almanack +3 -0
- data/example.ru +18 -14
- data/lib/almanack.rb +16 -13
- data/lib/almanack/calendar.rb +35 -3
- data/lib/almanack/cli.rb +131 -0
- data/lib/almanack/configuration.rb +20 -4
- data/lib/almanack/event.rb +26 -1
- data/lib/almanack/event_source/ical_feed.rb +62 -0
- data/lib/almanack/event_source/meetup_group.rb +98 -0
- data/lib/almanack/event_source/static.rb +23 -0
- data/lib/almanack/server.rb +75 -4
- data/lib/almanack/themes/legacy/stylesheets/calendar.scss +168 -0
- data/lib/almanack/themes/legacy/views/error.erb +13 -0
- data/lib/almanack/themes/legacy/views/events.erb +4 -2
- data/lib/almanack/themes/legacy/views/layout.erb +9 -6
- data/lib/almanack/themes/starter/javascripts/calendar.js +5 -0
- data/lib/almanack/themes/starter/stylesheets/calendar.scss +27 -0
- data/lib/almanack/themes/starter/views/error.erb +13 -0
- data/lib/almanack/themes/starter/views/events.erb +31 -0
- data/lib/almanack/themes/starter/views/layout.erb +32 -0
- data/lib/almanack/version.rb +4 -1
- data/spec/almanack_spec.rb +8 -0
- data/spec/calendar_spec.rb +29 -25
- data/spec/configuration_spec.rb +19 -3
- data/spec/{ical_feed_spec.rb → event_source/ical_feed_spec.rb} +4 -4
- data/spec/{meetup_group_spec.rb → event_source/meetup_group_spec.rb} +4 -4
- data/spec/event_source/static_spec.rb +23 -0
- data/spec/event_spec.rb +24 -4
- data/spec/features/calendar_feature_spec.rb +7 -5
- data/spec/features/subscription_feature_spec.rb +60 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/support/server_support.rb +2 -1
- data/spec/support/time_comparison_matchers.rb +14 -0
- data/templates/gitignore +2 -0
- data/templates/new/Gemfile +3 -0
- data/templates/new/config.ru.tt +24 -0
- metadata +45 -13
- data/lib/almanack/ical_feed.rb +0 -60
- data/lib/almanack/meetup_group.rb +0 -96
- data/lib/almanack/simple_event_collection.rb +0 -11
- data/lib/almanack/themes/legacy/views/stylesheets/legacy.css.sass +0 -119
- data/spec/simple_event_collection_spec.rb +0 -23
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
require 'ri_cal'
|
4
|
+
|
5
|
+
module Almanack
|
6
|
+
module EventSource
|
7
|
+
class IcalFeed
|
8
|
+
def initialize(url)
|
9
|
+
@url = url
|
10
|
+
end
|
11
|
+
|
12
|
+
def events_between(date_range)
|
13
|
+
occurrences_between(date_range).map do |occurrence|
|
14
|
+
event_from(occurrence)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def each_ical_event(&block)
|
21
|
+
entities.each do |entity|
|
22
|
+
entity.events.each(&block) if entity.respond_to?(:events)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def occurrences_between(date_range)
|
27
|
+
to_date = date_range.max
|
28
|
+
from_date = date_range.min
|
29
|
+
|
30
|
+
occurrences = []
|
31
|
+
|
32
|
+
each_ical_event do |ical_event|
|
33
|
+
ical_event.occurrences(starting: from_date, before: to_date).each do |occurrence|
|
34
|
+
occurrences << occurrence
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
occurrences
|
39
|
+
end
|
40
|
+
|
41
|
+
def event_from(occurrence)
|
42
|
+
Event.new(
|
43
|
+
title: occurrence.summary,
|
44
|
+
start_date: occurrence.dtstart,
|
45
|
+
end_date: occurrence.dtend,
|
46
|
+
description: occurrence.description,
|
47
|
+
location: occurrence.location
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
def entities
|
52
|
+
RiCal.parse_string(body)
|
53
|
+
end
|
54
|
+
|
55
|
+
def body
|
56
|
+
uri = URI(@url)
|
57
|
+
Net::HTTP.get(uri)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'addressable/uri'
|
4
|
+
|
5
|
+
module Almanack
|
6
|
+
module EventSource
|
7
|
+
class MeetupGroup
|
8
|
+
def initialize(options = {})
|
9
|
+
@request_options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def events_between(date_range)
|
13
|
+
events.select do |event|
|
14
|
+
event.start_date >= date_range.min && event.start_date <= date_range.max
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def events
|
21
|
+
request = MeetupAPIRequest.new(@request_options)
|
22
|
+
request.results.map { |result| event_from(result) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def event_from(result)
|
26
|
+
# 3 hours, as recommended by Meetup.com if duration isn't present
|
27
|
+
default_duration_in_ms = 3 * 60 * 60 * 1000
|
28
|
+
|
29
|
+
event_name = [result['group']['name'], result['name']].compact.join(': ')
|
30
|
+
start_time = Time.at(result['time'] / 1000)
|
31
|
+
duration_in_secs = (result['duration'] || default_duration_in_ms) / 1000
|
32
|
+
end_time = start_time + duration_in_secs
|
33
|
+
|
34
|
+
Event.new(
|
35
|
+
title: event_name,
|
36
|
+
start_date: start_time,
|
37
|
+
end_date: end_time,
|
38
|
+
description: result['description'],
|
39
|
+
location: location_from_venue(result['venue']),
|
40
|
+
url: result['event_url']
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def location_from_venue(venue)
|
45
|
+
%w{ name address_1 address_2 address_3 city state country }.map do |attr|
|
46
|
+
venue[attr]
|
47
|
+
end.compact.join(', ')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class MeetupAPIError < StandardError
|
52
|
+
end
|
53
|
+
|
54
|
+
class MeetupAPIException < Exception
|
55
|
+
end
|
56
|
+
|
57
|
+
class MeetupAPIRequest
|
58
|
+
REQUIRED_OPTIONS = [:group_domain, :group_urlname, :group_id]
|
59
|
+
|
60
|
+
attr_accessor :options
|
61
|
+
|
62
|
+
def initialize(options = {})
|
63
|
+
@options = options
|
64
|
+
end
|
65
|
+
|
66
|
+
def results
|
67
|
+
response['results']
|
68
|
+
end
|
69
|
+
|
70
|
+
def uri
|
71
|
+
if !options.has_key?(:key)
|
72
|
+
raise MeetupAPIException, 'Cannot form valid URI, missing :key option'
|
73
|
+
end
|
74
|
+
|
75
|
+
if (options.keys & REQUIRED_OPTIONS).empty?
|
76
|
+
raise MeetupAPIException, "Cannot form valid URI, missing one of: #{REQUIRED_OPTIONS}"
|
77
|
+
end
|
78
|
+
|
79
|
+
endpoint = "https://api.meetup.com/2/events"
|
80
|
+
|
81
|
+
Addressable::URI.parse(endpoint).tap do |uri|
|
82
|
+
uri.query_values = options
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def response
|
87
|
+
json = Net::HTTP.get(uri)
|
88
|
+
data = JSON.parse(json)
|
89
|
+
|
90
|
+
if data['problem']
|
91
|
+
raise MeetupAPIError, data['problem']
|
92
|
+
end
|
93
|
+
|
94
|
+
data
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Almanack
|
2
|
+
module EventSource
|
3
|
+
class Static
|
4
|
+
def initialize(events)
|
5
|
+
@event_attrs = events
|
6
|
+
end
|
7
|
+
|
8
|
+
def events_between(date_range)
|
9
|
+
events.select do |event|
|
10
|
+
event.start_date >= date_range.min && event.start_date <= date_range.max
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def events
|
17
|
+
@events ||= @event_attrs.map do |attrs|
|
18
|
+
Event.new(attrs)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/almanack/server.rb
CHANGED
@@ -1,18 +1,89 @@
|
|
1
1
|
require "sinatra"
|
2
|
+
require "sinatra/reloader"
|
3
|
+
require "sass"
|
4
|
+
require "almanack"
|
2
5
|
|
3
6
|
module Almanack
|
4
7
|
class Server < Sinatra::Base
|
5
|
-
|
6
|
-
|
8
|
+
configure :development do
|
9
|
+
register Sinatra::Reloader
|
10
|
+
end
|
11
|
+
|
12
|
+
set :root, -> { Almanack.config.theme_root }
|
7
13
|
set :protection, except: :frame_options
|
14
|
+
set :feed_path, "feed.ics"
|
15
|
+
|
16
|
+
helpers do
|
17
|
+
def feed_url
|
18
|
+
"webcal://#{request.host}:#{request.port}/#{settings.feed_path}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def almanack_project_url
|
22
|
+
Almanack::HOMEPAGE
|
23
|
+
end
|
24
|
+
|
25
|
+
def almanack_issues_url
|
26
|
+
Almanack::ISSUES
|
27
|
+
end
|
28
|
+
|
29
|
+
def calendar
|
30
|
+
@calendar ||= Almanack.calendar
|
31
|
+
end
|
32
|
+
|
33
|
+
def page_title(separator: " – ")
|
34
|
+
[@title, calendar.title].compact.join(separator)
|
35
|
+
end
|
36
|
+
|
37
|
+
def title(value)
|
38
|
+
@title = value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
not_found do
|
43
|
+
status 404
|
44
|
+
erb :error
|
45
|
+
end
|
46
|
+
|
47
|
+
def basename(file)
|
48
|
+
Pathname(file).split.last.to_s.split(".", 2).first
|
49
|
+
end
|
50
|
+
|
51
|
+
def locate_asset(name, within: path)
|
52
|
+
name = basename(name)
|
53
|
+
path = options.root.join(within)
|
54
|
+
available = Pathname.glob(path.join("*"))
|
55
|
+
asset = available.find { |path| basename(path) == name }
|
56
|
+
raise "Could not find stylesheet #{name} inside #{available}" if asset.nil?
|
57
|
+
asset
|
58
|
+
end
|
59
|
+
|
60
|
+
def auto_render_template(asset)
|
61
|
+
renderer = asset.extname.split(".").last
|
62
|
+
content = asset.read
|
63
|
+
respond_to?(renderer) ? send(renderer, content) : content
|
64
|
+
end
|
65
|
+
|
66
|
+
def auto_render_asset(*args)
|
67
|
+
auto_render_template locate_asset(*args)
|
68
|
+
end
|
8
69
|
|
9
70
|
get "/" do
|
10
|
-
@calendar = Almanack.calendar
|
11
71
|
erb :events
|
12
72
|
end
|
13
73
|
|
74
|
+
get "/#{settings.feed_path}" do
|
75
|
+
content_type "text/calendar"
|
76
|
+
Almanack.calendar.ical_feed
|
77
|
+
end
|
78
|
+
|
14
79
|
get "/stylesheets/:name" do
|
15
|
-
|
80
|
+
content_type :css
|
81
|
+
auto_render_asset params[:name], within: "stylesheets"
|
82
|
+
end
|
83
|
+
|
84
|
+
get "/javascripts/:name" do
|
85
|
+
content_type :js
|
86
|
+
auto_render_asset params[:name], within: "javascripts"
|
16
87
|
end
|
17
88
|
end
|
18
89
|
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
@mixin border-radius($size) {
|
2
|
+
border-radius: $size;
|
3
|
+
-webkit-border-radius: $size;
|
4
|
+
-moz-border-radius: $size;
|
5
|
+
}
|
6
|
+
|
7
|
+
@mixin box-shadow($x, $y, $size, $colour) {
|
8
|
+
-webkit-box-shadow: $x $y $size $colour;
|
9
|
+
-moz-box-shadow: $x $y $size $colour;
|
10
|
+
}
|
11
|
+
|
12
|
+
body {
|
13
|
+
background-color: #e3e3e3;
|
14
|
+
color: #595959;
|
15
|
+
font: 12pt / 1.6em "Helvetica Neue", Helvetica, sans-serif;
|
16
|
+
margin: 2em auto;
|
17
|
+
width: 600px;
|
18
|
+
}
|
19
|
+
|
20
|
+
a {
|
21
|
+
text-decoration: none;
|
22
|
+
&:link, &:visited {
|
23
|
+
color: #2960aa;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
em {
|
28
|
+
font-style: italic;
|
29
|
+
}
|
30
|
+
|
31
|
+
strong {
|
32
|
+
font-weight: bold;
|
33
|
+
}
|
34
|
+
|
35
|
+
.event {
|
36
|
+
background-color: #efefef;
|
37
|
+
background: -webkit-gradient(linear, left top, left bottom, from(#f9f9f9), to(#efefef));
|
38
|
+
background: -moz-linear-gradient(top, #f9f9f9, #efefef);
|
39
|
+
border-top: 1px solid white;
|
40
|
+
@include border-radius(4px);
|
41
|
+
overflow: hidden;
|
42
|
+
padding: 20px;
|
43
|
+
margin-bottom: 1em;
|
44
|
+
@include box-shadow(0, 1px, 3px, rgba(0, 0, 0, 0.3));
|
45
|
+
&.today {
|
46
|
+
background-color: #ebe4de;
|
47
|
+
background: -webkit-gradient(linear, left top, left bottom, from(#f3ebe3), to(#e9d6c1));
|
48
|
+
}
|
49
|
+
.details {
|
50
|
+
float: right;
|
51
|
+
width: 480px;
|
52
|
+
}
|
53
|
+
dl {
|
54
|
+
color: #777777;
|
55
|
+
font-size: 0.9em;
|
56
|
+
}
|
57
|
+
dt {
|
58
|
+
font-weight: bold;
|
59
|
+
float: left;
|
60
|
+
}
|
61
|
+
dd {
|
62
|
+
float: right;
|
63
|
+
margin: 0;
|
64
|
+
width: 420px;
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
header, footer {
|
69
|
+
// display: block for browsers with poor HTML5 support
|
70
|
+
display: block;
|
71
|
+
}
|
72
|
+
|
73
|
+
header {
|
74
|
+
border-bottom: 1px solid #cccccc;
|
75
|
+
overflow: hidden;
|
76
|
+
padding-bottom: 0.5em;
|
77
|
+
margin-bottom: 1em;
|
78
|
+
@include box-shadow(0, 1px, 0, #f6f6f6);
|
79
|
+
}
|
80
|
+
|
81
|
+
footer {
|
82
|
+
border-top: 1px solid #cccccc;
|
83
|
+
color: #999999;
|
84
|
+
font-size: 0.9em;
|
85
|
+
padding: 1em;
|
86
|
+
.subscribe {
|
87
|
+
float: left;
|
88
|
+
a {
|
89
|
+
margin-right: 1em;
|
90
|
+
}
|
91
|
+
}
|
92
|
+
.add {
|
93
|
+
float: right;
|
94
|
+
}
|
95
|
+
.fork {
|
96
|
+
clear: both;
|
97
|
+
font-size: 0.8em;
|
98
|
+
}
|
99
|
+
a:link, a:visited {
|
100
|
+
color: #8aa0b6;
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
h1, h2, h3, h4 {
|
105
|
+
margin: 0;
|
106
|
+
text-shadow: 0 1px 0 white;
|
107
|
+
}
|
108
|
+
|
109
|
+
h1 {
|
110
|
+
color: #666666;
|
111
|
+
font-size: 1.8em;
|
112
|
+
float: left;
|
113
|
+
|
114
|
+
a:link, a:visited {
|
115
|
+
color: #666666;
|
116
|
+
}
|
117
|
+
|
118
|
+
a:hover {
|
119
|
+
color: black;
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
h2 {
|
124
|
+
color: #999999;
|
125
|
+
font-size: 1em;
|
126
|
+
float: right;
|
127
|
+
text-transform: uppercase;
|
128
|
+
}
|
129
|
+
|
130
|
+
h3 {
|
131
|
+
color: #333333;
|
132
|
+
}
|
133
|
+
|
134
|
+
h4 {
|
135
|
+
display: inline;
|
136
|
+
font-size: 1em;
|
137
|
+
margin-right: 1em;
|
138
|
+
}
|
139
|
+
|
140
|
+
.date {
|
141
|
+
background-color: #cc0000;
|
142
|
+
@include border-radius(4px);
|
143
|
+
color: white;
|
144
|
+
float: left;
|
145
|
+
text-align: center;
|
146
|
+
padding: 2px;
|
147
|
+
width: 60px;
|
148
|
+
|
149
|
+
.day {
|
150
|
+
background-color: white;
|
151
|
+
color: #333333;
|
152
|
+
font-size: 1.6em;
|
153
|
+
padding: 0.2em;
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
#error {
|
158
|
+
background: white;
|
159
|
+
@include border-radius(4px);
|
160
|
+
padding: 2em;
|
161
|
+
margin: 1em 0;
|
162
|
+
text-align: center;
|
163
|
+
|
164
|
+
h1 {
|
165
|
+
float: none;
|
166
|
+
margin: 0.5em 0;
|
167
|
+
}
|
168
|
+
}
|