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