ical_importer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1,5 @@
1
+ rvm --create ruby-1.9.3-p125@ical_importer
2
+ # Ensure that Bundler is installed, install it if it is not.
3
+ if ! command -v bundle > /dev/null; then
4
+ gem install bundler
5
+ fi
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ # uncomment this line if your project needs to run something other than `rake`:
6
+ # script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in automation.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ical_importer (0.0.1)
5
+ activesupport (~> 3.0.15)
6
+ i18n
7
+ ri_cal
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activesupport (3.0.16)
13
+ awesome_print (1.0.2)
14
+ diff-lcs (1.1.3)
15
+ i18n (0.6.0)
16
+ rake (0.9.2.2)
17
+ ri_cal (0.8.8)
18
+ rspec (2.11.0)
19
+ rspec-core (~> 2.11.0)
20
+ rspec-expectations (~> 2.11.0)
21
+ rspec-mocks (~> 2.11.0)
22
+ rspec-core (2.11.1)
23
+ rspec-expectations (2.11.2)
24
+ diff-lcs (~> 1.1.3)
25
+ rspec-mocks (2.11.1)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ awesome_print
32
+ ical_importer!
33
+ rake
34
+ rspec
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # Ical Importer
2
+
3
+ Easily import your iCal feeds.
4
+
5
+ [![TravisCI](https://secure.travis-ci.org/tstmedia/ical_importer.png "TravisCI")](http://travis-ci.org/tstmedia/ical_importer "Travis-CI IcalImporter")
6
+
7
+ <!---
8
+ [RubyGems](NOT YET "NOT YET")
9
+ --->
10
+
11
+ # Notes
12
+
13
+ * Recurrence events are not the same as recurring events
14
+
15
+ # Usage
16
+
17
+ Add
18
+
19
+ ```ruby
20
+ gem 'ical_importer', :git => 'http://github.com/tstmedia/ical_importer.git'
21
+ ```
22
+
23
+ to your Gemfile
24
+
25
+ Then you can do:
26
+
27
+ ```ruby
28
+ IcalImporter::Parser.new(a_url).parse # To get just an array of events
29
+
30
+ IcalImporter::Parser.new(a_url).parse do |event|
31
+ event.uid
32
+ event.title
33
+ event.description
34
+ event.location
35
+ event.start_date_time
36
+ event.end_date_time
37
+ event.utc
38
+ event.date_exclusions
39
+ event.recur_end_date
40
+ event.recur_month_repeat_by
41
+ event.recur_interval
42
+ event.recur_interval_value
43
+ event.recurrence_id
44
+ event.all_day_event
45
+ event.recurrence
46
+ event.utc?
47
+ event.all_day_event?
48
+ event.recurrence?
49
+ event.recur_week_sunday
50
+ event.recur_week_monday
51
+ event.recur_week_tuesday
52
+ event.recur_week_wednesday
53
+ event.recur_week_thursday
54
+ event.recur_week_friday
55
+ event.recur_week_saturday
56
+ end
57
+
58
+ parser = IcalImporter::Parser.new(a_url)
59
+ parser.parse do |e|
60
+ # block stuffs
61
+ end
62
+
63
+ # Each of these also accepts blocks and returns a list
64
+ parser.all_events
65
+ parser.recurrence_events
66
+ parser.single_events
67
+ ```
68
+
69
+ # TODO
70
+
71
+ * Current implementation based on an extraction from another app
72
+ - some of the recurring/recurrence/single logic is fragmented
73
+ - Re-implement to be more similarly classifiable across these different scenarios
74
+ * Document Methods
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ namespace :spec do
8
+ RSpec::Core::RakeTask.new(:docs) do |t|
9
+ t.rspec_opts = ["--format doc"]
10
+ end
11
+ end
12
+
13
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ical_importer/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jon Phenow"]
6
+ gem.email = ["jon.phenow@tstmedia.com"]
7
+ gem.description = %q{Easily import iCal Events from a URL and handle their output}
8
+ gem.summary = %q{}
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "ical_importer"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = IcalImporter::VERSION
17
+
18
+ gem.add_dependency 'activesupport', "~> 3.0.15"
19
+ gem.add_dependency 'ri_cal'
20
+ gem.add_dependency 'i18n'
21
+
22
+ gem.add_development_dependency 'rake'
23
+ gem.add_development_dependency 'rspec'
24
+ gem.add_development_dependency 'awesome_print'
25
+ end
@@ -0,0 +1,22 @@
1
+ module IcalImporter
2
+ class Builder
3
+ attr_reader :event, :recurrence_builder
4
+ def initialize(event, recurrence_builder)
5
+ @event = event
6
+ @recurrence_builder = recurrence_builder
7
+ end
8
+
9
+ def handle_as_recurrence?
10
+ event.recurrence_id.present?
11
+ end
12
+
13
+ def build
14
+ if handle_as_recurrence?
15
+ recurrence_builder << event
16
+ nil # Don't want this messing up our collect in Collector
17
+ else
18
+ SingleEventBuilder.new(event).build
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module IcalImporter
2
+ class Collector
3
+ attr_reader :single_events, :events, :recurrence_events
4
+
5
+ def initialize(events)
6
+ @events = events
7
+ @single_events = []
8
+ @recurrence_events = []
9
+ end
10
+
11
+ def collect
12
+ self.tap do
13
+ recurrence_builder = RecurrenceEventBuilder.new
14
+ single_events.tap do |c|
15
+ events.each do |remote_event|
16
+ c << Builder.new(remote_event, recurrence_builder).build
17
+ end
18
+ @recurrence_events = recurrence_builder.build.built_events.flatten.compact
19
+ c.flatten!
20
+ c.compact!
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module IcalImporter
2
+ class DateExclusion
3
+ attr_accessor :date_exclusion
4
+
5
+ def initialize(attributes)
6
+ attributes.each do |name, value|
7
+ instance_variable_set "@#{name}", value if [:date_exclusion].include? name.to_sym
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ module IcalImporter
2
+ class LocalEvent
3
+ class << self
4
+ attr_accessor :class_attributes
5
+ end
6
+
7
+ @class_attributes = [
8
+ :uid,
9
+ :title,
10
+ :description,
11
+ :location,
12
+ :start_date_time,
13
+ :end_date_time,
14
+ :utc,
15
+ :date_exclusions,
16
+ :recur_end_date,
17
+ :recur_month_repeat_by,
18
+ :recur_interval,
19
+ :recur_interval_value,
20
+ :recur_end_date,
21
+ :recurrence_id,
22
+ :all_day_event,
23
+ :recurrence
24
+ ]
25
+
26
+ attr_accessor *class_attributes
27
+ alias utc? utc
28
+ alias all_day_event? all_day_event
29
+ alias recurrence? recurrence
30
+
31
+ DAYS = %w[sunday monday tuesday wednesday thursday friday saturday]
32
+ DAYS.each do |day|
33
+ class_attributes << "recur_week_#{day}".to_sym
34
+ attr_accessor "recur_week_#{day}"
35
+ end
36
+
37
+ def initialize(attributes)
38
+ self.attributes = attributes
39
+ @date_exclusions ||= []
40
+ end
41
+
42
+ def get_attributes(list)
43
+ raise ArgumentError, "Must be an Array" unless list.is_a? Array
44
+ list.collect! { |e| e.to_s }
45
+ attributes.select { |k,_| list.include? k.to_s }
46
+ end
47
+
48
+ def to_hash
49
+ Hash[*self.class.class_attributes.collect { |attribute| [attribute.to_sym, send(attribute)] }.flatten(1)]
50
+ end
51
+ alias :attributes :to_hash
52
+
53
+ def attributes=(attributes)
54
+ attributes.each do |name, value|
55
+ instance_variable_set "@#{name}", value if self.class.class_attributes.include? name.to_sym
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,75 @@
1
+ module IcalImporter
2
+ class Parser
3
+ attr_reader :feed, :bare_feed, :url
4
+
5
+ def initialize(url)
6
+ @url = url
7
+ @bare_feed = open_ical
8
+ if should_parse?
9
+ @bare_feed.pos = 0
10
+ @feed = RiCal.parse @bare_feed
11
+ end
12
+ end
13
+
14
+ def should_parse?
15
+ bare_feed.present?
16
+ end
17
+
18
+ def worth_parsing?
19
+ should_parse? && feed.present? && feed.first
20
+ end
21
+
22
+ def all_events(&block)
23
+ tap_and_each (@imported_single_events || []) + (@imported_recurrence_events || []), &block
24
+ end
25
+
26
+ def single_events(&block)
27
+ tap_and_each (@imported_single_events || []), &block
28
+ end
29
+
30
+ def recurrence_events(&block)
31
+ tap_and_each (@imported_recurrence_events || []), &block
32
+ end
33
+
34
+ def parse(&block)
35
+ if worth_parsing?
36
+ collected = Collector.new(feed.first.events).collect
37
+ @imported_single_events = collected.single_events
38
+ @imported_recurrence_events = collected.recurrence_events
39
+ tap_and_each (@imported_single_events + @imported_recurrence_events), &block
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def tap_and_each(list)
46
+ list.tap do |r|
47
+ r.each do |event|
48
+ yield event if block_given?
49
+ end
50
+ end
51
+ end
52
+
53
+ def open_ical(protocol = 'http')
54
+ raise ArgumentError, "Must be http or https" unless %w[http https].include? protocol
55
+ begin
56
+ Timeout::timeout(5) do
57
+ open prepped_uri(protocol)
58
+ end
59
+ rescue
60
+ return open_ical 'https' if protocol == 'http'
61
+ nil
62
+ end
63
+ end
64
+
65
+ def prepped_uri(protocol)
66
+ uri = url.strip.gsub(/^[Ww]ebcal:/, "#{protocol}:")
67
+ uri = begin
68
+ URI.unescape(uri)
69
+ rescue URI::InvalidURIError
70
+ end
71
+ URI.escape(uri)
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,37 @@
1
+ module IcalImporter
2
+ class RecurrenceEventBuilder
3
+ attr_reader :events_to_build, :built_events
4
+ def initialize
5
+ @events_to_build = []
6
+ @built_events = []
7
+ end
8
+
9
+ def <<(event)
10
+ raise ArgumentError, "Must be a RiCal Event" unless event.is_a? RiCal::Component::Event
11
+ @events_to_build << event
12
+ end
13
+
14
+ def build
15
+ self.tap do
16
+ events_to_build.each do |remote_event|
17
+ @built_events << build_new_local_event(remote_event)
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def build_new_local_event(remote_event)
25
+ LocalEvent.new({
26
+ :uid => remote_event.uid,
27
+ :title => remote_event.summary,
28
+ :description => remote_event.description,
29
+ :location => remote_event.location || '',
30
+ :start_date_time => remote_event.start_date_time,
31
+ :end_date_time => remote_event.end_date_time,
32
+ :date_exclusions => [DateExclusion.new(:exclude_date => remote_event.recurrence_id)],
33
+ :recurrence => true
34
+ })
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ module IcalImporter
2
+ class RemoteEvent
3
+ attr_accessor :event, :utc
4
+ alias :utc? :utc
5
+ delegate :description, :recurs?, :rrule_property, :exdate, :to => :event
6
+
7
+ def initialize(event)
8
+ @event = event
9
+ begin
10
+ @utc = @event.dtstart.try(:tzid) != :floating
11
+ rescue
12
+ @utc = true
13
+ end
14
+ end
15
+
16
+ def start_date_time
17
+ get_date_time_for :dtstart
18
+ end
19
+
20
+ def end_date_time
21
+ get_date_time_for :dtend
22
+ end
23
+
24
+ def all_day_event?
25
+ (Time.parse(end_date_time.to_s) - Time.parse(start_date_time.to_s)) >= 1.day
26
+ end
27
+
28
+ def event_attributes
29
+ {
30
+ :uid => event.uid,
31
+ :title => event.summary,
32
+ :utc => utc?,
33
+ :description => event.description,
34
+ :location => event.location || '',
35
+ :start_date_time => start_date_time,
36
+ :end_date_time => end_date_time,
37
+ :all_day_event => all_day_event?
38
+ }
39
+ end
40
+
41
+ private
42
+
43
+ def get_date_time_for(event_method)
44
+ event_method = event_method.to_sym
45
+ raise ArgumentError, "Should be dtend or dtstart" unless [:dtstart, :dtend].include? event_method
46
+ event_time = event.send event_method
47
+ if event_time.is_a? DateTime
48
+ (event_time.tzid == :floating) ? event_time : event_time.utc
49
+ else
50
+ event_time.to_datetime
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,128 @@
1
+ module IcalImporter
2
+ class SingleEventBuilder
3
+ attr_reader :event, :local_event
4
+
5
+ def initialize(event)
6
+ @event = RemoteEvent.new event
7
+ @local_event = LocalEvent.new @event.event_attributes
8
+ end
9
+
10
+ # Get single-occurrence events built and get a lits of recurrence
11
+ # events, these must be build last
12
+ def build
13
+ # handle recuring events
14
+ @local_event.tap do |le|
15
+ if @event.recurs?
16
+ rrule = @event.rrule_property.first # only support recurrence on one schedule
17
+ # set out new event's basic rucurring properties
18
+ le.attributes = recurrence_attributes rrule
19
+
20
+ set_date_exclusion
21
+ frequency_set rrule
22
+ else # make sure we remove this if it changed
23
+ le.attributes = non_recurrence_attributes
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def non_recurrence_attributes
31
+ attributes = {
32
+ :recur_interval => "none",
33
+ :recur_interval_value => nil,
34
+ :recur_end_date => nil
35
+ }
36
+ if !@local_event.all_day_event && @event.start_date_time.day != @event.end_date_time.day # single event that spans multiple days
37
+ attributes.merge({
38
+ :recur_interval => "day",
39
+ :recur_interval_value => 1,
40
+ :recur_end_date => @event.end_date_time.end_of_day - 1.day,
41
+ :all_day_event => true
42
+ })
43
+ end
44
+ attributes
45
+ end
46
+
47
+ def recurrence_attributes(rrule)
48
+ {
49
+ :recur_interval => recur_map[rrule.freq],
50
+ :recur_interval_value => rrule.interval,
51
+ :recur_end_date => rrule.until.try(:to_datetime)
52
+ }
53
+ end
54
+
55
+ def set_date_exclusion
56
+ # set any date exclusions
57
+ @local_event.date_exclusions = @event.exdate.flatten.map{|d| DateExclusion.new(:exclude_date => d)}
58
+ end
59
+
60
+ def frequency_set(rrule)
61
+ # if .bounded? is an integer that's googles "recur X times"
62
+ # if that's the case we try to figure out the date it should be by
63
+ # multiplying thise "X" times by the frequency that the event recurrs
64
+ if rrule.bounded?.is_a? Fixnum # convert X times to a date
65
+ case rrule.freq
66
+ when "DAILY"
67
+ @local_event.recur_end_date = frequency_template.days
68
+ when "WEEKLY"
69
+ if rrule.to_ical.include?("BYDAY=")
70
+ remote_days = rrule.to_ical.split("BYDAY=").last.split(";WKST=").first.split(',')
71
+ day_map.each do |abbr, day|
72
+ @local_event.send "recur_week_#{day}=", remote_days.include?(abbr)
73
+ end
74
+ else
75
+ remote_days = [@local_event.start_date_time.wday]
76
+ wday_map.each do |abbr, day|
77
+ @local_event.send "recur_week_#{day}=", remote_days.include?(abbr)
78
+ end
79
+ end
80
+ # recurrence X times is probably broken - we can select multiple times in a week
81
+ @local_event.recur_end_date = (frequency_template / remote_days.length).weeks
82
+ when "MONTHLY"
83
+ @local_event.recur_month_repeat_by = (rrule.to_ical =~ /BYDAY/) ? "day_of_week" : "day_of_month"
84
+ @local_event.recur_end_date = frequency_template.months
85
+ when "YEARLY"
86
+ @local_event.recur_end_date = frequency_template.years
87
+ end
88
+ end
89
+ end
90
+
91
+ def frequency_template
92
+ @local_event.start_date_time + (rrule.bounded? * rrule.interval - 1)
93
+ end
94
+
95
+ def recur_map
96
+ {
97
+ "DAILY" => "day",
98
+ "WEEKLY" => "week",
99
+ "MONTHLY" => "month",
100
+ "YEARLY" => "year"
101
+ }
102
+ end
103
+
104
+ def day_map
105
+ {
106
+ "SU" => "sunday",
107
+ "MO" => "monday",
108
+ "TU" => "tuesday",
109
+ "WE" => "wednesday",
110
+ "TH" => "thursday",
111
+ "FR" => "friday",
112
+ "SA" => "saturday"
113
+ }
114
+ end
115
+
116
+ def wday_map
117
+ {
118
+ 0 => "sunday",
119
+ 1 => "monday",
120
+ 2 => "tuesday",
121
+ 3 => "wednesday",
122
+ 4 => "thursday",
123
+ 5 => "friday",
124
+ 6 => "saturday"
125
+ }
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,3 @@
1
+ module IcalImporter
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_support/all'
2
+ require 'ri_cal'
3
+
4
+ require 'ical_importer/date_exclusion'
5
+ require 'ical_importer/builder'
6
+ require 'ical_importer/collector'
7
+ require 'ical_importer/remote_event'
8
+ require 'ical_importer/local_event'
9
+ require 'ical_importer/parser'
10
+ require 'ical_importer/recurrence_event_builder'
11
+ require 'ical_importer/single_event_builder'
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+ module IcalImporter
3
+ describe Builder do
4
+ subject { Builder.new(event, recurrence_builder) }
5
+ let(:recurrence_builder) { RecurrenceEventBuilder.new }
6
+
7
+ describe "#handle_as_recurrence?" do
8
+ describe "recurrence_id'd event" do
9
+ let(:event) { stub :recurrence_id => 1 }
10
+ it "handles as recurrence" do
11
+ subject.handle_as_recurrence?.should == true
12
+ end
13
+ end
14
+
15
+ describe "non-recurrence_id'd event" do
16
+ let(:event) { stub :recurrence_id => nil }
17
+ it "handles as recurrence" do
18
+ subject.handle_as_recurrence?.should == false
19
+ end
20
+ end
21
+ end
22
+
23
+ describe "#build" do
24
+ let(:event) { stub }
25
+ describe "recurrence" do
26
+ let(:event) { stub :recurrence_id => 1 }
27
+ it "adds events to be computed with recurrence_builder" do
28
+ recurrence_builder.should_receive(:<<).with(event)
29
+ subject.build.should == nil
30
+ end
31
+ end
32
+
33
+ describe "non-recurrence" do
34
+ let(:event) { stub :recurrence_id => nil }
35
+ it "builds single event" do
36
+ single_event_builder = stub
37
+ returned = stub
38
+ SingleEventBuilder.should_receive(:new).with(event).and_return single_event_builder
39
+ single_event_builder.should_receive(:build).and_return(returned)
40
+ subject.build.should == returned
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ module IcalImporter
3
+ describe Collector do
4
+ let(:events) { [] }
5
+ subject { Collector.new events }
6
+ it { should respond_to :collect }
7
+
8
+ describe "#collect" do
9
+ let(:events) { [stub, stub] }
10
+ let(:r_builder) { stub(:build => stub(:built_events => [])) }
11
+ it "tries to build, then cleanup the returns" do
12
+ built = stub
13
+ RecurrenceEventBuilder.should_receive(:new).and_return r_builder
14
+ Builder.should_receive(:new).with(events[0], r_builder).ordered.and_return(built)
15
+ Builder.should_receive(:new).with(events[1], r_builder).ordered.and_return(built)
16
+ built.should_receive(:build).twice.and_return "boom"
17
+ subject.collect
18
+ end
19
+ end
20
+ end
21
+ end