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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0d73adf865b8f26fc09b7964c10617ff54d6b381
4
- data.tar.gz: c9835f0434bf1821619eb22d86a0396c156860c3
3
+ metadata.gz: a36a4f4c0b5adffd13cfac0f50c223065fed1285
4
+ data.tar.gz: 3c5788c29f66ce658e7def8103707a4897660523
5
5
  SHA512:
6
- metadata.gz: 64d23b956709c3127395f09e774f7acc6ec4761222c085d6b86bc3cfd4e49c77bc578c7ee93abc007dc6dcba762f4eabff9b24d43ec5705b086cc620df858b31
7
- data.tar.gz: 164eb43910f561b1fd46e154e460dff36e70cd392cd1311e08f3ad78ae63b5afe179f3f89beb1181cc064260a393dcefe2b64c48aae5e84efa2267a413a27cc8
6
+ metadata.gz: f8673f497e3c53d7adf7f6b5a668ddfe38a29e2ec949e235489b9f1302cd93573c1ec7c12bf19993528fbb0c5d93a4991f5d431408f8372b6258caa706e70e2e
7
+ data.tar.gz: 0a3d77922eef10c8fe8f36591e13a3d6f982d353ed7bdc5b4ea72e073e56190ff249920b687fbbb6dcc125cbd0109d6e359d01ebcfa4832292073dee196ce51a
data/README.md CHANGED
@@ -4,34 +4,76 @@
4
4
  [![Code Climate](https://codeclimate.com/github/Aupajo/sinatra-gcal.png)](https://codeclimate.com/github/Aupajo/sinatra-gcal)
5
5
  [![Code Climate](https://codeclimate.com/github/Aupajo/sinatra-gcal/coverage.png)](https://codeclimate.com/github/Aupajo/sinatra-gcal)
6
6
 
7
- TODO: Write a gem description
7
+ A calendar that combines events from different sources (such as Google Calendar, Meetup.com, and iCal feeds), and can be hosted for free on [Heroku](http://heroku.com).
8
+
9
+ ## Features
10
+
11
+ * Aggregate multiple calendars together into one stream
12
+ * Supports iCal feeds (incuding Google Calendars)
13
+ * Supports Meetup.com groups
14
+ * Just supply a hash to create any arbitrary event
15
+ * Supports being freely hosted on Heroku
16
+ * 100% customisable themes with Sass and CoffeeScript support
17
+ * Server optional (you can use the underlying calendar library by itself)
18
+ * Rack-compatible (mount inside any Rails application)
8
19
 
9
20
  ## Installation
10
21
 
11
- Add this line to your application's Gemfile:
22
+ Run the following command:
23
+
24
+ gem install almanack --pre
25
+
26
+ ## Creating a calendar
27
+
28
+ Generate a new calendar with:
29
+
30
+ almanack new my-calendar
31
+
32
+ This will create a directory called `my-calendar` and set up your new project.
33
+
34
+ Once set up, run:
35
+
36
+ cd my-calendar
37
+ almanack start
38
+
39
+ By default, your calendar will run on http://localhost:9292.
12
40
 
13
- gem 'almanack'
41
+ ## Configuration
14
42
 
15
- And then execute:
43
+ See examples inside `config.ru` for iCal feeds, Meetup.com, or static events.
16
44
 
17
- $ bundle
45
+ ```ruby
46
+ Almanack.config do |c|
47
+ c.title = 'My Calendar'
48
+ c.theme = 'my-custom-theme'
49
+ c.add_ical_feed 'http://example.org/events.ics'
50
+ c.add_ical_feed 'http://example.org/more-events.ics'
51
+ c.add_meetup_group group_urlname: 'Christchurch-Ruby-Group', key: 'mysecretkey'
52
+ end
53
+ ```
18
54
 
19
- Or install it yourself as:
55
+ **Note:** You'll need your [Meetup.com API key](https://secure.meetup.com/meetup_api/key) to use Meetup.
20
56
 
21
- $ gem install almanack
57
+ ## Custom themes
22
58
 
23
- ## Usage
59
+ Inside your project, you can generate a new theme with:
24
60
 
25
- TODO: Write usage instructions here
61
+ almanack theme my-theme-name
26
62
 
27
- ### Meetup.com
63
+ Remember to update your `config.ru` to switch themes:
28
64
 
29
- You'll need your [Meetup.com API key](https://secure.meetup.com/meetup_api/key).
65
+ ```ruby
66
+ Almanack.config do |c|
67
+ ...
68
+ c.theme = 'my-theme-name'
69
+ ...
70
+ end
71
+ ```
30
72
 
31
73
  ## Contributing
32
74
 
33
- 1. Fork it ( http://github.com/Aupajo/sinatra-gcal/fork )
75
+ 1. Fork it ( http://github.com/Aupajo/almanack/fork )
34
76
  2. Create your feature branch (`git checkout -b my-new-feature`)
35
77
  3. Commit your changes (`git commit -am 'Add some feature'`)
36
78
  4. Push to the branch (`git push origin my-new-feature`)
37
- 5. Create new Pull Request
79
+ 5. Create new Pull Request
data/almanac.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Pete Nicholls"]
10
10
  spec.email = ["pete@metanation.com"]
11
11
  spec.summary = %q{Combined events calendar for Google Calendar, iCal, Meetup.com and friends.}
12
- spec.homepage = "https://github.com/Aupajo/sinatra-gcal"
12
+ spec.homepage = Almanack::HOMEPAGE
13
13
  spec.license = "MIT"
14
14
 
15
15
  spec.files = `git ls-files -z`.split("\x0")
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_dependency "sass"
23
23
  spec.add_dependency "ri_cal"
24
24
  spec.add_dependency "addressable"
25
+ spec.add_dependency "thor"
25
26
 
26
27
  spec.add_development_dependency "bundler", "~> 1.5"
27
28
  spec.add_development_dependency "rake"
data/bin/almanack ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'almanack/cli'
3
+ Almanack::CLI.start
data/example.ru CHANGED
@@ -1,20 +1,24 @@
1
1
  require "bundler/setup"
2
- require "rack/reloader"
3
- require "almanack"
2
+ require "almanack/server"
4
3
 
5
- today = DateTime.now
4
+ now = Time.now
6
5
 
7
- Almanack.config.title = 'Discworld Holidays'
6
+ Almanack.config do |calendar|
7
+ calendar.title = 'Discworld Holidays'
8
8
 
9
- Almanack.config.add_events [
10
- {
11
- title: "Hogswatch",
12
- start_date: today,
13
- 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.',
14
- location: 'Castle of Bones'
15
- },
16
- { title: "Soul Cake Tuesday", start_date: today + 10 },
17
- { title: "Eve of Small Gods", start_date: today + 30 },
18
- ]
9
+ calendar.theme = 'starter'
10
+
11
+ calendar.add_events [
12
+ {
13
+ title: "Hogswatch",
14
+ start_date: now + Almanack::Calendar::ONE_DAY,
15
+ end_date: now + Almanack::Calendar::ONE_DAY * 2,
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
+ location: 'Castle of Bones'
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 },
21
+ ]
22
+ end
19
23
 
20
24
  run Almanack::Server
data/lib/almanack.rb CHANGED
@@ -1,23 +1,26 @@
1
1
  require "pathname"
2
2
  require "almanack/version"
3
- require "almanack/server"
4
3
  require "almanack/configuration"
5
4
  require "almanack/calendar"
6
- require "almanack/simple_event_collection"
7
- require "almanack/meetup_group"
8
- require "almanack/ical_feed"
9
5
  require "almanack/event"
6
+ require "almanack/event_source/static"
7
+ require "almanack/event_source/meetup_group"
8
+ require "almanack/event_source/ical_feed"
10
9
 
11
10
  module Almanack
12
- def self.config
13
- @config ||= Configuration.new
14
- end
11
+ class << self
12
+ def config(&block)
13
+ @config ||= Configuration.new
14
+ yield @config if block_given?
15
+ @config
16
+ end
15
17
 
16
- def self.calendar
17
- @calendar ||= Calendar.new(config)
18
- end
18
+ def calendar
19
+ @calendar ||= Calendar.new(config)
20
+ end
19
21
 
20
- def self.reset!
21
- config.reset!
22
+ def reset!
23
+ config.reset!
24
+ end
22
25
  end
23
- end
26
+ end
@@ -2,6 +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
+
5
10
  extend Forwardable
6
11
  def_delegators :@config, :event_sources, :title
7
12
 
@@ -10,14 +15,41 @@ module Almanack
10
15
  end
11
16
 
12
17
  def events
13
- from_date = DateTime.now
14
- to_date = DateTime.now + days_lookahead
18
+ now = Time.now
19
+ future = now + days_lookahead * ONE_DAY
20
+ events_between(now..future)
21
+ end
15
22
 
23
+ def events_between(date_range)
16
24
  event_sources.map do |event_source|
17
- event_source.events_between(from_date..to_date)
25
+ event_source.events_between(date_range)
18
26
  end.flatten.sort_by(&:start_date)
19
27
  end
20
28
 
29
+ def ical_feed
30
+ now = Time.now
31
+ future = now + ONE_YEAR
32
+
33
+ # Three hours is the duration for events missing end dates, a
34
+ # recommendation suggested by Meetup.com.
35
+ three_hours = 3 * ONE_HOUR
36
+
37
+ ical = RiCal.Calendar
38
+
39
+ events_between(now..future).each do |event|
40
+ ical_event = RiCal.Event
41
+ ical_event.summary = event.title
42
+ ical_event.dtstart = event.start_date.utc
43
+ ical_event.dtend = (event.end_date || event.start_date + three_hours).utc
44
+ ical_event.description = event.description if event.description
45
+ ical_event.location = event.location if event.location
46
+
47
+ ical.add_subcomponent(ical_event)
48
+ end
49
+
50
+ ical.to_s
51
+ end
52
+
21
53
  def days_lookahead
22
54
  30
23
55
  end
@@ -0,0 +1,131 @@
1
+ require "thor"
2
+ require "pathname"
3
+
4
+ module Almanack
5
+ class CLI < Thor
6
+ include Thor::Actions
7
+
8
+ SKIP_THEMES = %w( starter )
9
+
10
+ map "--version" => :version
11
+
12
+ def self.available_themes
13
+ dirs = Pathname(__dir__).join("themes").children.select(&:directory?)
14
+
15
+ dirs.map do |path|
16
+ path.basename.to_s unless SKIP_THEMES.include?(path.basename.to_s)
17
+ end.compact
18
+ end
19
+
20
+ desc "version", "Show Almanack version"
21
+ def version
22
+ say "Almanack version #{VERSION} (#{CODENAME})"
23
+ end
24
+
25
+ desc "start", "Start an Almanack server"
26
+ option :config, aliases: "-c", desc: "Path to config file"
27
+ def start
28
+ exec "bundle exec rackup #{options[:config]}"
29
+ end
30
+
31
+ desc "new PATH", "Create a new Almanack project"
32
+ option :theme, default: 'legacy', desc: "Which theme to use (available: #{available_themes.join(', ')})"
33
+ option :git, type: :boolean, default: true, desc: "Whether to initialize an empty git repo"
34
+ def new(path)
35
+ path = Pathname(path).cleanpath
36
+
37
+ directory "templates/new", path
38
+
39
+ if options[:git]
40
+ template('templates/gitignore', path.join(".gitignore"))
41
+ end
42
+
43
+ inside path do
44
+ say_status :installing, "bundler dependencies"
45
+ system "bundle install --quiet"
46
+
47
+ if options[:git]
48
+ say_status :git, "initializing repository"
49
+ git "init"
50
+ end
51
+
52
+ say
53
+ say "==> Run your new calendar!"
54
+ say
55
+ say " cd #{path}"
56
+ say " almnack start"
57
+ say
58
+ end
59
+ end
60
+
61
+ desc "theme NAME", "Create a new theme"
62
+ def theme(name)
63
+ directory "lib/almanack/themes/starter", "themes/#{name}"
64
+
65
+ config_file = Pathname("config.ru")
66
+
67
+ if config_file.exist?
68
+ say_status :update, "config.ru"
69
+ set_theme_pattern = /\.theme\s*=\s*['"].*?['"]/
70
+ replacement = config_file.read.gsub(set_theme_pattern, ".theme = '#{name}'")
71
+ config_file.open('w') { |file| file.puts replacement }
72
+ end
73
+ end
74
+
75
+ desc "deploy [NAME]", "Deploy your site to Heroku (requires Heroku toolbelt)"
76
+ def deploy(name = nil)
77
+ remotes = `git remote -v`
78
+
79
+ heroku_remote = remotes.lines.find do |remote|
80
+ remote.split(' ').first == "heroku"
81
+ end
82
+
83
+ if !heroku_remote
84
+ say "No Heroku remote detected."
85
+ create_heroku_app(name)
86
+ end
87
+
88
+ current_branch = git("rev-parse --abbrev-ref HEAD")
89
+
90
+ say "Deploying #{current_branch}..."
91
+ run "git push heroku #{current_branch}:master --force"
92
+ end
93
+
94
+ no_tasks do
95
+ def create_heroku_app(name)
96
+ heroku_command = `which heroku`.strip
97
+
98
+ if heroku_command.empty?
99
+ say "Heroku Toolbelt not detected. Please install from:"
100
+ say " https://toolbelt.heroku.com"
101
+ abort
102
+ end
103
+
104
+ say_status :heroku, "creating app..."
105
+ run "#{heroku_command} create #{name}"
106
+ end
107
+
108
+ def theme_name
109
+ options[:theme]
110
+ end
111
+
112
+ def git(command)
113
+ output = `git #{command}`
114
+ abort "Git failed: #{output}" if $?.exitstatus != 0
115
+ output.strip
116
+ end
117
+
118
+ def available_themes
119
+ self.class.available_themes
120
+ end
121
+ end
122
+
123
+ def self.exit_on_failure?
124
+ true
125
+ end
126
+
127
+ def self.source_root
128
+ Pathname(__dir__).parent.parent
129
+ end
130
+ end
131
+ end
@@ -4,27 +4,43 @@ require 'ri_cal'
4
4
 
5
5
  module Almanack
6
6
  class Configuration
7
+ class ThemeNotFound < StandardError; end
8
+
9
+ DEFAULT_THEME = "legacy"
10
+
7
11
  attr_reader :event_sources
8
- attr_accessor :title
12
+ attr_accessor :title, :theme, :theme_paths, :theme_root
9
13
 
10
14
  def initialize
11
15
  reset!
12
16
  end
13
17
 
14
18
  def reset!
19
+ @theme = DEFAULT_THEME
15
20
  @event_sources = []
21
+
22
+ @theme_paths = [
23
+ Pathname.pwd.join('themes'),
24
+ Pathname(__dir__).join('themes')
25
+ ]
26
+ end
27
+
28
+ def theme_root
29
+ paths = theme_paths.map { |path| path.join(theme) }
30
+ root = paths.find { |path| path.exist? }
31
+ root || raise(ThemeNotFound, "Could not find theme #{theme} in #{paths}")
16
32
  end
17
33
 
18
34
  def add_ical_feed(url)
19
- @event_sources << IcalFeed.new(url)
35
+ @event_sources << EventSource::IcalFeed.new(url)
20
36
  end
21
37
 
22
38
  def add_events(events)
23
- @event_sources << SimpleEventCollection.new(events)
39
+ @event_sources << EventSource::Static.new(events)
24
40
  end
25
41
 
26
42
  def add_meetup_group(options)
27
- @event_sources << MeetupGroup.new(options)
43
+ @event_sources << EventSource::MeetupGroup.new(options)
28
44
  end
29
45
  end
30
46
  end
@@ -2,5 +2,30 @@ require 'ostruct'
2
2
 
3
3
  module Almanack
4
4
  class Event < OpenStruct
5
+ def formatted_date
6
+ formatted = "#{formatted_day(start_date)} at #{formatted_time(start_date)}"
7
+
8
+ if end_date
9
+ formatted << " to "
10
+ formatted << "#{formatted_day(end_date)} at " unless ends_on_same_day?
11
+ formatted << formatted_time(end_date)
12
+ end
13
+
14
+ formatted
15
+ end
16
+
17
+ private
18
+
19
+ def ends_on_same_day?
20
+ [start_date.year, start_date.yday] == [end_date.year, end_date.yday]
21
+ end
22
+
23
+ def formatted_time(time)
24
+ time.strftime('%-l:%M%P')
25
+ end
26
+
27
+ def formatted_day(time)
28
+ time.strftime('%B %-d %Y')
29
+ end
5
30
  end
6
- end
31
+ end