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.
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
+ }