almanack 1.0.5 → 1.1.0.beta1

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 (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