almanack 0.0.1.alpha3 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -13
  3. data/almanac.gemspec +2 -1
  4. data/bin/almanack +3 -0
  5. data/example.ru +18 -14
  6. data/lib/almanack.rb +16 -13
  7. data/lib/almanack/calendar.rb +35 -3
  8. data/lib/almanack/cli.rb +131 -0
  9. data/lib/almanack/configuration.rb +20 -4
  10. data/lib/almanack/event.rb +26 -1
  11. data/lib/almanack/event_source/ical_feed.rb +62 -0
  12. data/lib/almanack/event_source/meetup_group.rb +98 -0
  13. data/lib/almanack/event_source/static.rb +23 -0
  14. data/lib/almanack/server.rb +75 -4
  15. data/lib/almanack/themes/legacy/stylesheets/calendar.scss +168 -0
  16. data/lib/almanack/themes/legacy/views/error.erb +13 -0
  17. data/lib/almanack/themes/legacy/views/events.erb +4 -2
  18. data/lib/almanack/themes/legacy/views/layout.erb +9 -6
  19. data/lib/almanack/themes/starter/javascripts/calendar.js +5 -0
  20. data/lib/almanack/themes/starter/stylesheets/calendar.scss +27 -0
  21. data/lib/almanack/themes/starter/views/error.erb +13 -0
  22. data/lib/almanack/themes/starter/views/events.erb +31 -0
  23. data/lib/almanack/themes/starter/views/layout.erb +32 -0
  24. data/lib/almanack/version.rb +4 -1
  25. data/spec/almanack_spec.rb +8 -0
  26. data/spec/calendar_spec.rb +29 -25
  27. data/spec/configuration_spec.rb +19 -3
  28. data/spec/{ical_feed_spec.rb → event_source/ical_feed_spec.rb} +4 -4
  29. data/spec/{meetup_group_spec.rb → event_source/meetup_group_spec.rb} +4 -4
  30. data/spec/event_source/static_spec.rb +23 -0
  31. data/spec/event_spec.rb +24 -4
  32. data/spec/features/calendar_feature_spec.rb +7 -5
  33. data/spec/features/subscription_feature_spec.rb +60 -0
  34. data/spec/spec_helper.rb +2 -1
  35. data/spec/support/server_support.rb +2 -1
  36. data/spec/support/time_comparison_matchers.rb +14 -0
  37. data/templates/gitignore +2 -0
  38. data/templates/new/Gemfile +3 -0
  39. data/templates/new/config.ru.tt +24 -0
  40. metadata +45 -13
  41. data/lib/almanack/ical_feed.rb +0 -60
  42. data/lib/almanack/meetup_group.rb +0 -96
  43. data/lib/almanack/simple_event_collection.rb +0 -11
  44. data/lib/almanack/themes/legacy/views/stylesheets/legacy.css.sass +0 -119
  45. 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
@@ -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
- set :theme, 'legacy'
6
- set :root, Pathname(settings.root).join('themes', settings.theme)
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
- sass :"stylesheets/#{params[:name]}"
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
+ }