almanack 1.0.5 → 1.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/almanack.gemspec +1 -0
  4. data/example.ru +4 -4
  5. data/lib/almanack/base.rb +22 -0
  6. data/lib/almanack/calendar.rb +9 -27
  7. data/lib/almanack/configuration.rb +15 -4
  8. data/lib/almanack/event.rb +56 -5
  9. data/lib/almanack/event_source/ical_feed.rb +6 -2
  10. data/lib/almanack/event_source/meetup_group.rb +26 -5
  11. data/lib/almanack/event_source/static.rb +5 -1
  12. data/lib/almanack/representation/ical_feed.rb +59 -0
  13. data/lib/almanack/representation/json_feed.rb +55 -0
  14. data/lib/almanack/serialized_transformation.rb +53 -0
  15. data/lib/almanack/server/environment.rb +38 -0
  16. data/lib/almanack/server/helpers.rb +32 -28
  17. data/lib/almanack/server.rb +23 -37
  18. data/lib/almanack/themes/legacy/views/events.erb +2 -2
  19. data/lib/almanack/themes/starter/views/events.erb +2 -2
  20. data/lib/almanack/version.rb +1 -1
  21. data/lib/almanack.rb +4 -18
  22. data/spec/calendar_spec.rb +10 -3
  23. data/spec/configuration_spec.rb +10 -0
  24. data/spec/event_source/ical_feed_spec.rb +15 -2
  25. data/spec/event_source/meetup_group_spec.rb +23 -2
  26. data/spec/event_source/static_spec.rb +25 -7
  27. data/spec/event_spec.rb +80 -11
  28. data/spec/features/api_feature_spec.rb +24 -0
  29. data/spec/features/calendar_feature_spec.rb +3 -3
  30. data/spec/features/subscription_feature_spec.rb +5 -5
  31. data/spec/representation/json_feed_spec.rb +44 -0
  32. data/spec/serialized_transformation_spec.rb +45 -0
  33. data/spec/spec_helper.rb +2 -0
  34. data/spec/support/event_matchers.rb +1 -1
  35. data/templates/new/config.ru.tt +1 -1
  36. metadata +29 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: edcbe493e1eeafbf85972b28c5427a87c48ef3e6
4
- data.tar.gz: 7b74a5466823eacc3ef1a50bc35966e575225f42
3
+ metadata.gz: 49d69b1b032ff7f536d34807c859021af3840f3e
4
+ data.tar.gz: b5ea878e85c09b1e967054e7038400fcc0125120
5
5
  SHA512:
6
- metadata.gz: d1082a9baf6ff0d079604b21f03e1eb7a21ea480e7d49ea9ff1db95e80246e914cccd30137292de9787416571df53916710f8ed6b83713789dadeaeb2e7440ca
7
- data.tar.gz: cb8546fed04c611ef7a93f60ad3a0bd3129558a9b3cfd33c5de86b68cc182115d28760d9521d5162c227ccf53293fa801c7f0eb2525277daf71f74276ab99cd2
6
+ metadata.gz: d40154226158a3bbf56e6de69e1cd495827eef1563cbf88e8823257cdf8653c2eec181070a6be6e2ae0f0b19d9fc9c56a5b02471945f6ae35bba68cecb6151b9
7
+ data.tar.gz: 0a8723f866ee7403cd8d66044c16c6e93c197c5a9b7485d7af61da75a00a8cc31f9c2a04fc3cdfc40d22b80c903b9e9ebd703f2e8704ab34e8246b8a302bd024
data/.travis.yml CHANGED
@@ -1,6 +1,8 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
+ - 2.1.4
5
+ - 2.1.3
4
6
  - 2.1.2
5
7
  - 2.1.0
6
8
  - 2.0.0
data/almanack.gemspec CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency "addressable"
25
25
  spec.add_dependency "thor"
26
26
  spec.add_dependency "faraday"
27
+ spec.add_dependency "rack-contrib"
27
28
 
28
29
  spec.add_development_dependency "bundler", "~> 1.5"
29
30
  spec.add_development_dependency "rake"
data/example.ru CHANGED
@@ -11,13 +11,13 @@ Almanack.config do |calendar|
11
11
  calendar.add_events [
12
12
  {
13
13
  title: "Hogswatch",
14
- start_date: now + Almanack::Calendar::ONE_DAY,
15
- end_date: now + Almanack::Calendar::ONE_DAY * 2,
14
+ start_time: now + Almanack::ONE_DAY,
15
+ end_time: now + Almanack::ONE_DAY * 2,
16
16
  description: 'The sausages have been strung, the wreaths of oakleaves hung and the stockings dangled. The pork pie, the sherry and the all-important turnip await their festive guests. The poker lent against the fireplace may or may not have been bent over the head of some nightmare creature.',
17
17
  location: 'Castle of Bones'
18
18
  },
19
- { title: "Soul Cake Tuesday", start_date: now + 10 * Almanack::Calendar::ONE_DAY },
20
- { title: "Eve of Small Gods", start_date: now + 30 * Almanack::Calendar::ONE_DAY },
19
+ { title: "Soul Cake Tuesday", start_time: now + 10 * Almanack::ONE_DAY },
20
+ { title: "Eve of Small Gods", start_time: now + 30 * Almanack::ONE_DAY },
21
21
  ]
22
22
  end
23
23
 
@@ -0,0 +1,22 @@
1
+ module Almanack
2
+ ONE_HOUR = 60 * 60
3
+ ONE_DAY = 24 * ONE_HOUR
4
+ ONE_MONTH = 30 * ONE_DAY
5
+ ONE_YEAR = 365 * ONE_DAY
6
+
7
+ class << self
8
+ def config(&block)
9
+ @config ||= Configuration.new
10
+ yield @config if block_given?
11
+ @config
12
+ end
13
+
14
+ def calendar
15
+ @calendar ||= Calendar.new(config)
16
+ end
17
+
18
+ def reset!
19
+ config.reset!
20
+ end
21
+ end
22
+ end
@@ -2,13 +2,11 @@ require 'forwardable'
2
2
 
3
3
  module Almanack
4
4
  class Calendar
5
- ONE_HOUR = 60 * 60
6
- ONE_DAY = 24 * ONE_HOUR
7
- ONE_MONTH = 30 * ONE_DAY
8
- ONE_YEAR = 365 * ONE_DAY
9
-
10
5
  extend Forwardable
11
- def_delegators :@config, :event_sources, :title, :days_lookahead
6
+ def_delegators :@config, :event_sources,
7
+ :title,
8
+ :days_lookahead,
9
+ :feed_lookahead
12
10
 
13
11
  def initialize(config)
14
12
  @config = config
@@ -26,32 +24,16 @@ module Almanack
26
24
  end.flatten
27
25
 
28
26
  event_list.sort_by do |event|
29
- event.start_date.to_time
27
+ event.start_time.to_time
30
28
  end
31
29
  end
32
30
 
33
31
  def ical_feed
34
- now = Time.now
35
- future = now + ONE_YEAR
36
-
37
- # Three hours is the duration for events missing end dates, a
38
- # recommendation suggested by Meetup.com.
39
- three_hours = 3 * ONE_HOUR
40
-
41
- ical = RiCal.Calendar
42
-
43
- events_between(now..future).each do |event|
44
- ical_event = RiCal.Event
45
- ical_event.summary = event.title
46
- ical_event.dtstart = event.start_date.utc
47
- ical_event.dtend = (event.end_date || event.start_date + three_hours).utc
48
- ical_event.description = event.description if event.description
49
- ical_event.location = event.location if event.location
50
-
51
- ical.add_subcomponent(ical_event)
52
- end
32
+ Representation::IcalFeed.from(self).to_s
33
+ end
53
34
 
54
- ical.to_s
35
+ def json_feed
36
+ Representation::JSONFeed.from(self).to_s
55
37
  end
56
38
  end
57
39
  end
@@ -4,9 +4,15 @@ module Almanack
4
4
 
5
5
  DEFAULT_THEME = "legacy"
6
6
  DEFAULT_DAYS_LOOKAHEAD = 30
7
+ DEFAULT_FEED_LOOKAHEAD = 365
7
8
 
8
9
  attr_reader :event_sources
9
- attr_accessor :title, :theme, :theme_paths, :theme_root, :days_lookahead
10
+ attr_accessor :title,
11
+ :theme,
12
+ :theme_paths,
13
+ :theme_root,
14
+ :days_lookahead,
15
+ :feed_lookahead
10
16
 
11
17
  def initialize
12
18
  reset!
@@ -19,6 +25,7 @@ module Almanack
19
25
  def reset!
20
26
  @theme = DEFAULT_THEME
21
27
  @days_lookahead = DEFAULT_DAYS_LOOKAHEAD
28
+ @feed_lookahead = DEFAULT_FEED_LOOKAHEAD
22
29
  @event_sources = []
23
30
 
24
31
  @theme_paths = [
@@ -33,16 +40,20 @@ module Almanack
33
40
  root || raise(ThemeNotFound, "Could not find theme #{theme} in #{paths}")
34
41
  end
35
42
 
43
+ def add_event_source(source)
44
+ @event_sources << source
45
+ end
46
+
36
47
  def add_ical_feed(url)
37
- @event_sources << EventSource::IcalFeed.new(url, connection: connection)
48
+ add_event_source EventSource::IcalFeed.new(url, connection: connection)
38
49
  end
39
50
 
40
51
  def add_events(events)
41
- @event_sources << EventSource::Static.new(events)
52
+ add_event_source EventSource::Static.new(events)
42
53
  end
43
54
 
44
55
  def add_meetup_group(options)
45
- @event_sources << EventSource::MeetupGroup.new(options.merge(connection: connection))
56
+ add_event_source EventSource::MeetupGroup.new(options.merge(connection: connection))
46
57
  end
47
58
  end
48
59
  end
@@ -3,21 +3,72 @@ require 'ostruct'
3
3
  module Almanack
4
4
  class Event < OpenStruct
5
5
  def formatted_date
6
- formatted = "#{formatted_day(start_date)} at #{formatted_time(start_date)}"
6
+ formatted = "#{formatted_day(start_time)} at #{formatted_time(start_time)}"
7
7
 
8
- if end_date
8
+ if end_time
9
9
  formatted << " to "
10
- formatted << "#{formatted_day(end_date)} at " unless ends_on_same_day?
11
- formatted << formatted_time(end_date)
10
+ formatted << "#{formatted_day(end_time)} at " unless ends_on_same_day?
11
+ formatted << formatted_time(end_time)
12
12
  end
13
13
 
14
14
  formatted
15
15
  end
16
16
 
17
+ # Deprecated in favour of start_time
18
+ def start_date
19
+ deprecated :start_date, newer_method: :start_time
20
+ end
21
+
22
+ def start_time
23
+ read_attribute :start_time, fallback: :start_date
24
+ end
25
+
26
+ # Deprecated in favour of end_time
27
+ def end_date
28
+ deprecated :end_date, newer_method: :end_time
29
+ end
30
+
31
+ def end_time
32
+ read_attribute :end_time, fallback: :end_date
33
+ end
34
+
35
+ def serialized
36
+ each_pair.with_object({}) do |(attr, _), hash|
37
+ hash[attr] = serialize_attribute(attr)
38
+ end
39
+ end
40
+
17
41
  private
18
42
 
43
+ def serialize_attribute(attribute)
44
+ value = send(attribute)
45
+ value.is_a?(Time) ? value.iso8601 : value
46
+ end
47
+
48
+ def deprecated(older_method, options = {})
49
+ newer_method = options.delete(:newer_method)
50
+ value = read_attribute(newer_method, fallback: older_method)
51
+ warn "Event method #{older_method} is deprecated; use #{newer_method} instead"
52
+ value
53
+ end
54
+
55
+ def read_attribute(newer_method, options = {})
56
+ older_method = options.delete(:fallback)
57
+ newer_value = self[newer_method]
58
+ fallback_value = self[older_method]
59
+
60
+ if fallback_value && newer_value
61
+ raise "Both #{older_method} and #{newer_method} properties are set, please use #{newer_method} only instead"
62
+ elsif newer_value
63
+ newer_value
64
+ elsif fallback_value
65
+ warn "Deprecated event property #{older_method} is set; set #{newer_method} property instead"
66
+ fallback_value
67
+ end
68
+ end
69
+
19
70
  def ends_on_same_day?
20
- [start_date.year, start_date.yday] == [end_date.year, end_date.yday]
71
+ [start_time.year, start_time.yday] == [end_time.year, end_time.yday]
21
72
  end
22
73
 
23
74
  def formatted_time(time)
@@ -12,6 +12,10 @@ module Almanack
12
12
  end
13
13
  end
14
14
 
15
+ def serialized_between(date_range)
16
+ { events: events_between(date_range).map(&:serialized) }
17
+ end
18
+
15
19
  private
16
20
 
17
21
  def each_ical_event(&block)
@@ -38,8 +42,8 @@ module Almanack
38
42
  def event_from(occurrence)
39
43
  Event.new(
40
44
  title: occurrence.summary,
41
- start_date: occurrence.dtstart,
42
- end_date: occurrence.dtend,
45
+ start_time: occurrence.dtstart,
46
+ end_time: occurrence.dtend,
43
47
  description: occurrence.description,
44
48
  location: occurrence.location
45
49
  )
@@ -3,19 +3,40 @@ module Almanack
3
3
  class MeetupGroup
4
4
  def initialize(options = {})
5
5
  @request_options = options
6
+ @group_properties = {}
6
7
  end
7
8
 
8
9
  def events_between(date_range)
9
10
  events.select do |event|
10
- event.start_date >= date_range.min && event.start_date <= date_range.max
11
+ event.start_time >= date_range.min && event.start_time <= date_range.max
11
12
  end
12
13
  end
13
14
 
15
+ def serialized_between(date_range)
16
+ # TODO `events` must be called before @group_properties is accessed
17
+ serialized_events = events_between(date_range).map(&:serialized)
18
+ @group_properties.merge(events: serialized_events)
19
+ end
20
+
14
21
  private
15
22
 
16
23
  def events
17
- request = MeetupAPIRequest.new(@request_options.clone)
18
- request.results.map { |result| event_from(result) }
24
+ results = MeetupAPIRequest.new(@request_options.clone).results
25
+ record_group_details_from results
26
+ results.map { |result| event_from(result) }
27
+ end
28
+
29
+ def record_group_details_from(results)
30
+ first_result = results.first
31
+ return if !first_result
32
+
33
+ group = first_result['group']
34
+ return if !group
35
+
36
+ @group_properties = {
37
+ name: group['name'],
38
+ url: "http://www.meetup.com/" + group['urlname']
39
+ }
19
40
  end
20
41
 
21
42
  def event_from(result)
@@ -29,8 +50,8 @@ module Almanack
29
50
 
30
51
  Event.new(
31
52
  title: event_name,
32
- start_date: start_time,
33
- end_date: end_time,
53
+ start_time: start_time,
54
+ end_time: end_time,
34
55
  description: result['description'],
35
56
  location: location_from_venue(result['venue']),
36
57
  url: result['event_url']
@@ -7,10 +7,14 @@ module Almanack
7
7
 
8
8
  def events_between(date_range)
9
9
  events.select do |event|
10
- event.start_date >= date_range.min && event.start_date <= date_range.max
10
+ event.start_time >= date_range.min && event.start_time <= date_range.max
11
11
  end
12
12
  end
13
13
 
14
+ def serialized_between(date_range)
15
+ { events: events_between(date_range).map(&:serialized) }
16
+ end
17
+
14
18
  private
15
19
 
16
20
  def events
@@ -0,0 +1,59 @@
1
+ module Almanack
2
+ module Representation
3
+ class IcalFeed
4
+ attr_reader :calendar
5
+
6
+ def initialize(calendar)
7
+ @calendar = calendar
8
+ end
9
+
10
+ def ical
11
+ @ical ||= ical_calendar
12
+ end
13
+
14
+ def to_s
15
+ ical.to_s
16
+ end
17
+
18
+ def self.from(calendar)
19
+ self.new(calendar)
20
+ end
21
+
22
+ private
23
+
24
+ def events
25
+ calendar.events_between(now..lookahead)
26
+ end
27
+
28
+ def ical_calendar
29
+ events.each_with_object(RiCal.Calendar) do |event, calendar_component|
30
+ calendar_component.add_subcomponent ical_event_for(event)
31
+ end
32
+ end
33
+
34
+ def ical_event_for(event)
35
+ ical_event = RiCal.Event
36
+ ical_event.summary = event.title
37
+ ical_event.dtstart = event.start_time.utc
38
+ ical_event.dtend = (event.end_time || event.start_time + default_event_duration ).utc
39
+ ical_event.description = event.description if event.description
40
+ ical_event.location = event.location if event.location
41
+ ical_event
42
+ end
43
+
44
+ def lookahead
45
+ now + calendar.feed_lookahead * ONE_DAY
46
+ end
47
+
48
+ def default_event_duration
49
+ # Three hours is the duration for events missing end dates, a
50
+ # recommendation suggested by Meetup.com.
51
+ 3 * ONE_HOUR
52
+ end
53
+
54
+ def now
55
+ @now ||= Time.now
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,55 @@
1
+ module Almanack
2
+ module Representation
3
+ class JSONFeed
4
+ attr_reader :calendar
5
+
6
+ def initialize(calendar)
7
+ @calendar = calendar
8
+ end
9
+
10
+ def to_s
11
+ json_friendly = SerializedTransformation.new(serialized)
12
+ json_friendly.key { |key| camelize(key.to_s) }
13
+ json_friendly.apply.to_json
14
+ end
15
+
16
+ def self.from(calendar)
17
+ self.new(calendar)
18
+ end
19
+
20
+ private
21
+
22
+ def camelize(string)
23
+ string.split('_').map.with_index do |part, index|
24
+ index.zero? ? part : part.capitalize
25
+ end.join
26
+ end
27
+
28
+ def date_range
29
+ now..lookahead
30
+ end
31
+
32
+ def serialized
33
+ { event_sources: serialized_event_sources }
34
+ end
35
+
36
+ def serialized_event_sources
37
+ event_sources.map do |source|
38
+ source.serialized_between(date_range)
39
+ end
40
+ end
41
+
42
+ def event_sources
43
+ calendar.event_sources
44
+ end
45
+
46
+ def lookahead
47
+ now + calendar.feed_lookahead * ONE_DAY
48
+ end
49
+
50
+ def now
51
+ @now ||= Time.now
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ module Almanack
2
+ class SerializedTransformation
3
+
4
+ def initialize(subject)
5
+ @subject = subject
6
+ @transformations = {}
7
+ end
8
+
9
+ def key(&block)
10
+ @transformations[:key] = block
11
+ end
12
+
13
+ def value(&block)
14
+ @transformations[:value] = block
15
+ end
16
+
17
+ def apply
18
+ recurse(cloned)
19
+ end
20
+
21
+ private
22
+
23
+ def cloned
24
+ @subject.dup
25
+ end
26
+
27
+ def recursable?(node)
28
+ node.is_a?(Array) || node.is_a?(Hash)
29
+ end
30
+
31
+ def no_change
32
+ -> (obj) { obj }
33
+ end
34
+
35
+ def transformation(type, entity)
36
+ (@transformations[type] || no_change).call(entity)
37
+ end
38
+
39
+ def recurse(entity)
40
+ cloned = case entity
41
+ when Array
42
+ entity.map { |child| recurse(child) }
43
+ when Hash
44
+ entity.each_with_object({}) do |(key, value), hash|
45
+ transformed_key = recursable?(key) ? recurse(key) : transformation(:key, key)
46
+ hash[transformed_key] = recurse(value)
47
+ end
48
+ else
49
+ transformation(:value, entity)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,38 @@
1
+ module Almanack
2
+ module ServerContext
3
+ module Environment
4
+ def basename(file)
5
+ Pathname(file).split.last.to_s.split(".", 2).first
6
+ end
7
+
8
+ def locate_asset(name, within: path)
9
+ name = basename(name)
10
+ path = settings.root.join(within)
11
+ available = Pathname.glob(path.join("*"))
12
+ asset = available.find { |path| basename(path) == name }
13
+ raise "Could not find stylesheet #{name} inside #{available}" if asset.nil?
14
+ asset
15
+ end
16
+
17
+ def auto_render_template(asset)
18
+ renderer = asset.extname.split(".").last
19
+ content = asset.read
20
+ respond_to?(renderer) ? send(renderer, content) : content
21
+ end
22
+
23
+ def auto_render_asset(*args)
24
+ auto_render_template locate_asset(*args)
25
+ end
26
+
27
+ def theme_stylesheet_path
28
+ settings.root.join('stylesheets')
29
+ end
30
+
31
+ def register_sass_loadpaths!
32
+ if !Sass.load_paths.include?(theme_stylesheet_path)
33
+ Sass.load_paths << theme_stylesheet_path
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,36 +1,40 @@
1
- Almanack::Server.helpers do
2
- # @return The URL to your consolidated iCal feed.
3
- def feed_url
4
- "webcal://#{request.host}:#{request.port}/#{settings.feed_path}"
5
- end
1
+ module Almanack
2
+ module ServerContext
3
+ module Helpers
4
+ # @return The URL to your consolidated iCal feed.
5
+ def feed_url
6
+ "webcal://#{request.host}:#{request.port}/#{settings.feed_path}"
7
+ end
6
8
 
7
- # @return The URL to Almanack's project homepage.
8
- def almanack_project_url
9
- Almanack::HOMEPAGE
10
- end
9
+ # @return The URL to Almanack's project homepage.
10
+ def almanack_project_url
11
+ Almanack::HOMEPAGE
12
+ end
11
13
 
12
- # @return The URL to Almanack's issues page.
13
- def almanack_issues_url
14
- Almanack::ISSUES
15
- end
14
+ # @return The URL to Almanack's issues page.
15
+ def almanack_issues_url
16
+ Almanack::ISSUES
17
+ end
16
18
 
17
- # @return The current time.
18
- def now
19
- Time.now
20
- end
19
+ # @return The current time.
20
+ def now
21
+ Time.now
22
+ end
21
23
 
22
- # @return The calendar.
23
- def calendar
24
- @calendar ||= Almanack.calendar
25
- end
24
+ # @return The calendar.
25
+ def calendar
26
+ @calendar ||= Almanack.calendar
27
+ end
26
28
 
27
- # @return The title of the page.
28
- def page_title(separator: " – ")
29
- [@title, calendar.title].compact.join(separator)
30
- end
29
+ # @return The title of the page.
30
+ def page_title(separator: " – ")
31
+ [@title, calendar.title].compact.join(separator)
32
+ end
31
33
 
32
- # Use to set the title of the page.
33
- def title(value)
34
- @title = value
34
+ # Use to set the title of the page.
35
+ def title(value)
36
+ @title = value
37
+ end
38
+ end
35
39
  end
36
40
  end