ical_importer 0.0.1

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