clio-gcal4ruby 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +55 -0
- data/README.markdown +136 -0
- data/lib/gcal4ruby.rb +1 -0
- data/lib/gcal4ruby/base.rb +339 -0
- data/lib/gcal4ruby/calendar.rb +320 -0
- data/lib/gcal4ruby/event.rb +501 -0
- data/lib/gcal4ruby/recurrence.rb +213 -0
- data/lib/gcal4ruby/service.rb +100 -0
- data/test/unit.rb +180 -0
- metadata +78 -0
@@ -0,0 +1,213 @@
|
|
1
|
+
class Time
|
2
|
+
#Returns a ISO 8601 complete formatted string of the time
|
3
|
+
def complete
|
4
|
+
self.utc.strftime("%Y%m%dT%H%M%S")
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.parse_complete(value)
|
8
|
+
d, h = value.split("T")
|
9
|
+
return Time.parse(d+" "+h.gsub("Z", ""))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module GCal4Ruby
|
14
|
+
#The Recurrence class stores information on an Event's recurrence. The class implements
|
15
|
+
#the RFC 2445 iCalendar recurrence description.
|
16
|
+
class Recurrence
|
17
|
+
#The event start date/time
|
18
|
+
attr_reader :start
|
19
|
+
#The event end date/time
|
20
|
+
attr_reader :end
|
21
|
+
#the event reference
|
22
|
+
attr_reader :event
|
23
|
+
#The date until which the event will be repeated
|
24
|
+
attr_reader :repeat_until
|
25
|
+
#The event frequency
|
26
|
+
attr_reader :frequency
|
27
|
+
#True if the event is all day (i.e. no start/end time)
|
28
|
+
attr_accessor :all_day
|
29
|
+
|
30
|
+
#Accepts an optional attributes hash or a string containing a properly formatted ISO 8601 recurrence rule. Returns a new Recurrence object
|
31
|
+
def initialize(vars = {})
|
32
|
+
if vars.is_a? Hash
|
33
|
+
vars.each do |key, value|
|
34
|
+
self.send("#{key}=", value)
|
35
|
+
end
|
36
|
+
elsif vars.is_a? String
|
37
|
+
self.load(vars)
|
38
|
+
end
|
39
|
+
@all_day ||= false
|
40
|
+
end
|
41
|
+
|
42
|
+
#Accepts a string containing a properly formatted ISO 8601 recurrence rule and loads it into the recurrence object
|
43
|
+
def load(rec)
|
44
|
+
attrs = rec.split("\n")
|
45
|
+
attrs.each do |val|
|
46
|
+
key, value = val.split(":")
|
47
|
+
case key
|
48
|
+
when 'DTSTART'
|
49
|
+
@start = Time.parse_complete(value)
|
50
|
+
when 'DTSTART;VALUE=DATE'
|
51
|
+
@start = Time.parse(value)
|
52
|
+
@all_day = true
|
53
|
+
when 'DTSTART;VALUE=DATE-TIME'
|
54
|
+
@start = Time.parse_complete(value)
|
55
|
+
when 'DTEND'
|
56
|
+
@end = Time.parse_complete(value)
|
57
|
+
when 'DTEND;VALUE=DATE'
|
58
|
+
@end = Time.parse(value)
|
59
|
+
when 'DTEND;VALUE=DATE-TIME'
|
60
|
+
@end = Time.parse_complete(value)
|
61
|
+
when 'RRULE'
|
62
|
+
vals = value.split(";")
|
63
|
+
key = ''
|
64
|
+
by = ''
|
65
|
+
int = nil
|
66
|
+
vals.each do |rr|
|
67
|
+
a, h = rr.split("=")
|
68
|
+
case a
|
69
|
+
when 'FREQ'
|
70
|
+
key = h.downcase.capitalize
|
71
|
+
when 'INTERVAL'
|
72
|
+
int = h
|
73
|
+
when 'UNTIL'
|
74
|
+
@repeat_until = Time.parse(value)
|
75
|
+
else
|
76
|
+
by = h.split(",")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@frequency = {key => by}
|
80
|
+
@frequency.merge({'interval' => int}) if int
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
#Returns a string with the correctly formatted ISO 8601 recurrence rule
|
86
|
+
def to_s
|
87
|
+
|
88
|
+
output = ''
|
89
|
+
if @all_day
|
90
|
+
output += "DTSTART;VALUE=DATE:#{@start.utc.strftime("%Y%m%d")}\n"
|
91
|
+
else
|
92
|
+
output += "DTSTART;VALUE=DATE-TIME:#{@start.complete}\n"
|
93
|
+
end
|
94
|
+
if @all_day
|
95
|
+
output += "DTEND;VALUE=DATE:#{@end.utc.strftime("%Y%m%d")}\n"
|
96
|
+
else
|
97
|
+
output += "DTEND;VALUE=DATE-TIME:#{@end.complete}\n"
|
98
|
+
end
|
99
|
+
output += "RRULE:"
|
100
|
+
if @frequency
|
101
|
+
f = 'FREQ='
|
102
|
+
i = ''
|
103
|
+
by = ''
|
104
|
+
@frequency.each do |key, v|
|
105
|
+
if v.is_a?(Array)
|
106
|
+
if v.size > 0
|
107
|
+
value = v.join(",")
|
108
|
+
else
|
109
|
+
value = nil
|
110
|
+
end
|
111
|
+
else
|
112
|
+
value = v
|
113
|
+
end
|
114
|
+
f += "#{key.upcase};" if key != 'interval'
|
115
|
+
case key.downcase
|
116
|
+
when "secondly"
|
117
|
+
by += "BYSECOND=#{value};"
|
118
|
+
when "minutely"
|
119
|
+
by += "BYMINUTE=#{value};"
|
120
|
+
when "hourly"
|
121
|
+
by += "BYHOUR=#{value};"
|
122
|
+
when "weekly"
|
123
|
+
by += "BYDAY=#{value};" if value
|
124
|
+
when "monthly"
|
125
|
+
by += "BYDAY=#{value};"
|
126
|
+
when "yearly"
|
127
|
+
by += "BYYEARDAY=#{value};"
|
128
|
+
when 'interval'
|
129
|
+
i += "INTERVAL=#{value};"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
output += f+i+by
|
133
|
+
end
|
134
|
+
if @repeat_until
|
135
|
+
output += "UNTIL=#{@repeat_until.strftime("%Y%m%d")}"
|
136
|
+
end
|
137
|
+
|
138
|
+
output += "\n"
|
139
|
+
end
|
140
|
+
|
141
|
+
#Sets the start date/time. Must be a Time object.
|
142
|
+
def start=(s)
|
143
|
+
if not s.is_a?(Time)
|
144
|
+
raise RecurrenceValueError, "Start must be a date or a time"
|
145
|
+
else
|
146
|
+
@start = s
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
#Sets the end Date/Time. Must be a Time object.
|
151
|
+
def end=(e)
|
152
|
+
if not e.is_a?(Time)
|
153
|
+
raise RecurrenceValueError, "End must be a date or a time"
|
154
|
+
else
|
155
|
+
@end = e
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
#Sets the parent event reference
|
160
|
+
def event=(e)
|
161
|
+
if not e.is_a?(Event)
|
162
|
+
raise RecurrenceValueError, "Event must be an event"
|
163
|
+
else
|
164
|
+
@event = e
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
#Sets the end date for the recurrence
|
169
|
+
def repeat_until=(r)
|
170
|
+
if not r.is_a?(Date)
|
171
|
+
raise RecurrenceValueError, "Repeat_until must be a date"
|
172
|
+
else
|
173
|
+
@repeat_until = r
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
#Sets the frequency of the recurrence. Should be a hash with one of
|
178
|
+
#"SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY" as the key,
|
179
|
+
#and as the value, an array containing zero to n of the following:
|
180
|
+
#- *Secondly*: A value between 0 and 59. Causes the event to repeat on that second of each minut.
|
181
|
+
#- *Minutely*: A value between 0 and 59. Causes the event to repeat on that minute of every hour.
|
182
|
+
#- *Hourly*: A value between 0 and 23. Causes the even to repeat on that hour of every day.
|
183
|
+
#- *Daily*: No value needed - will cause the event to repeat every day until the repeat_until date.
|
184
|
+
#- *Weekly*: A value of the first two letters of a day of the week. Causes the event to repeat on that day.
|
185
|
+
#- *Monthly*: A value of a positive or negative integer (i.e. +1) prepended to a day-of-week string ('TU') to indicate the position of the day within the month. E.g. +1TU would be the first tuesday of the month.
|
186
|
+
#- *Yearly*: A value of 1 to 366 indicating the day of the year. May be negative to indicate counting down from the last day of the year.
|
187
|
+
#
|
188
|
+
#Optionally, you may specific a second hash pair to set the interval the event repeats:
|
189
|
+
# "interval" => '2'
|
190
|
+
#If the interval is missing, it is assumed to be 1.
|
191
|
+
#
|
192
|
+
#===Examples
|
193
|
+
#Repeat event every Tuesday:
|
194
|
+
# frequency = {"Weekly" => ["TU"]}
|
195
|
+
#
|
196
|
+
#Repeat every first and third Monday of the month
|
197
|
+
# frequency = {"Monthly" => ["+1MO", "+3MO"]}
|
198
|
+
#
|
199
|
+
#Repeat on the last day of every year
|
200
|
+
# frequency = {"Yearly" => [366]}
|
201
|
+
#
|
202
|
+
#Repeat every other week on Friday
|
203
|
+
# frequency = {"Weekly" => ["FR"], "interval" => "2"}
|
204
|
+
|
205
|
+
def frequency=(f)
|
206
|
+
if f.is_a?(Hash)
|
207
|
+
@frequency = f
|
208
|
+
else
|
209
|
+
raise RecurrenceValueError, "Frequency must be a hash (see documentation)"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'gcal4ruby/base'
|
2
|
+
require 'gcal4ruby/calendar'
|
3
|
+
|
4
|
+
module GCal4Ruby
|
5
|
+
|
6
|
+
#The service class is the main handler for all direct interactions with the
|
7
|
+
#Google Calendar API. A service represents a single user account. Each user
|
8
|
+
#account can have multiple calendars, so you'll need to find the calendar you
|
9
|
+
#want from the service, using the Calendar#find class method.
|
10
|
+
#=Usage
|
11
|
+
#
|
12
|
+
#1. Authenticate
|
13
|
+
# service = Service.new
|
14
|
+
# service.authenticate("user@gmail.com", "password")
|
15
|
+
#
|
16
|
+
#2. Get Calendar List
|
17
|
+
# calendars = service.calendars
|
18
|
+
#
|
19
|
+
|
20
|
+
class Service < Base
|
21
|
+
AUTH_TYPE_OAUTH = 'OAuth'
|
22
|
+
|
23
|
+
attr_accessor :account, :auth_token, :check_public, :auth_type
|
24
|
+
|
25
|
+
def initialize(attributes = {})
|
26
|
+
super()
|
27
|
+
attributes.each do |key, value|
|
28
|
+
self.send("#{key}=", value)
|
29
|
+
end
|
30
|
+
@check_public ||= true
|
31
|
+
end
|
32
|
+
|
33
|
+
def authenticate(username, password)
|
34
|
+
ret = nil
|
35
|
+
ret = send_post(AUTH_URL, "Email=#{username}&Passwd=#{password}&source=GCal4Ruby&service=cl&accountType=HOSTED_OR_GOOGLE")
|
36
|
+
if ret.class == Net::HTTPOK
|
37
|
+
@auth_token = ret.read_body.to_a[2].gsub("Auth=", "").strip
|
38
|
+
@account = username
|
39
|
+
@auth_type = 'ClientLogin'
|
40
|
+
return true
|
41
|
+
else
|
42
|
+
raise AuthenticationFailed
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# added authsub authentication. pass in the upgraded authsub token and the username/email address
|
47
|
+
def authsub_authenticate(authsub_token, account)
|
48
|
+
@auth_token = authsub_token
|
49
|
+
@account = account
|
50
|
+
@auth_type = 'AuthSub'
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
|
54
|
+
def oauth_authenticate(access_token)
|
55
|
+
@auth_token = access_token
|
56
|
+
@auth_type = AUTH_TYPE_OAUTH
|
57
|
+
end
|
58
|
+
|
59
|
+
def oauth?
|
60
|
+
@auth_type == AUTH_TYPE_OAUTH
|
61
|
+
end
|
62
|
+
|
63
|
+
#Returns an array of Calendar objects for each calendar associated with
|
64
|
+
#the authenticated account.
|
65
|
+
def calendars
|
66
|
+
unless @auth_token
|
67
|
+
raise NotAuthenticated
|
68
|
+
end
|
69
|
+
ret = send_get(CALENDAR_LIST_FEED+"?max-results=100")
|
70
|
+
cals = []
|
71
|
+
REXML::Document.new(ret.body).root.elements.each("entry"){}.map do |entry|
|
72
|
+
entry.attributes["xmlns:gCal"] = "http://schemas.google.com/gCal/2005"
|
73
|
+
entry.attributes["xmlns:gd"] = "http://schemas.google.com/g/2005"
|
74
|
+
entry.attributes["xmlns:app"] = "http://www.w3.org/2007/app"
|
75
|
+
entry.attributes["xmlns"] = "http://www.w3.org/2005/Atom"
|
76
|
+
cal = Calendar.new(self)
|
77
|
+
cal.load("<?xml version='1.0' encoding='UTF-8'?>#{entry.to_s}")
|
78
|
+
cals << cal
|
79
|
+
end
|
80
|
+
return cals
|
81
|
+
end
|
82
|
+
|
83
|
+
# This is for building a composite calendar
|
84
|
+
# I'm sure it doesn't work, needs review!
|
85
|
+
def to_iframe(cals, params = {})
|
86
|
+
calendar_set = case cals
|
87
|
+
when :all then calendars
|
88
|
+
when :first then calendars[0]
|
89
|
+
else cals
|
90
|
+
end
|
91
|
+
|
92
|
+
units = calendar_set.collect do |cal|
|
93
|
+
"src=#{cal.id}" + cal.build_options_set(params).join("&")
|
94
|
+
end
|
95
|
+
|
96
|
+
"<iframe src='http://www.google.com/calendar/embed?#{units.join("&")}' width='#{params[:width]}' height='#{params[:height]}' frameborder='#{params[:border]}' scrolling='no'></iframe>"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
data/test/unit.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'gcal4ruby'
|
5
|
+
include GCal4Ruby
|
6
|
+
|
7
|
+
@service = Service.new
|
8
|
+
@username = nil
|
9
|
+
@password = nil
|
10
|
+
|
11
|
+
def tester
|
12
|
+
if ARGV.include?("-d")
|
13
|
+
@service.debug = true
|
14
|
+
end
|
15
|
+
ARGV.each do |ar|
|
16
|
+
if ar.match("username=")
|
17
|
+
@username = ar.gsub("username=", "")
|
18
|
+
end
|
19
|
+
if ar.match("password=")
|
20
|
+
@password = ar.gsub("password=", "")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
service_test
|
24
|
+
calendar_test
|
25
|
+
event_test
|
26
|
+
event_recurrence_test
|
27
|
+
end
|
28
|
+
|
29
|
+
def service_test
|
30
|
+
puts "---Starting Service Test---"
|
31
|
+
puts "1. Authenticate"
|
32
|
+
if @service.authenticate(@username, @password)
|
33
|
+
successful
|
34
|
+
else
|
35
|
+
failed
|
36
|
+
end
|
37
|
+
|
38
|
+
puts "2. Calendar List"
|
39
|
+
cals = @service.calendars
|
40
|
+
if cals
|
41
|
+
successful "Calendars for this Account:"
|
42
|
+
cals.each do |cal|
|
43
|
+
puts cal.title
|
44
|
+
end
|
45
|
+
else
|
46
|
+
failed
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def calendar_test
|
51
|
+
puts "---Starting Calendar Test---"
|
52
|
+
|
53
|
+
puts "1. Create Calendar"
|
54
|
+
cal = Calendar.new(@service)
|
55
|
+
cal.title = "test calendar"+Time.now.to_s
|
56
|
+
puts "Calender exists = "+cal.exists?.to_s
|
57
|
+
if cal.save
|
58
|
+
successful cal.to_xml
|
59
|
+
else
|
60
|
+
failed
|
61
|
+
end
|
62
|
+
|
63
|
+
puts "2. Edit Calendar"
|
64
|
+
cal.title = "renamed title"
|
65
|
+
if cal.save
|
66
|
+
successful cal.to_xml
|
67
|
+
else
|
68
|
+
puts "Test 2 Failed"
|
69
|
+
end
|
70
|
+
|
71
|
+
puts "3. Find Calendar by ID"
|
72
|
+
c = Calendar.find(@service, cal.id)
|
73
|
+
if c.title == cal.title
|
74
|
+
successful
|
75
|
+
else
|
76
|
+
failed "#{c.title} not equal to #{cal.title}"
|
77
|
+
end
|
78
|
+
|
79
|
+
puts "4. Delete Calendar"
|
80
|
+
if cal.delete and not cal.title
|
81
|
+
successful
|
82
|
+
else
|
83
|
+
failed
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def event_test
|
88
|
+
puts "---Starting Event Test---"
|
89
|
+
|
90
|
+
puts "1. Create Event"
|
91
|
+
event = Event.new(@service.calendars[0])
|
92
|
+
event.title = "Test Event"
|
93
|
+
event.content = "Test event content"
|
94
|
+
event.start = Time.now+1800
|
95
|
+
event.end = Time.now+5400
|
96
|
+
if event.save
|
97
|
+
successful event.to_xml
|
98
|
+
else
|
99
|
+
failed
|
100
|
+
end
|
101
|
+
|
102
|
+
puts "2. Edit Event"
|
103
|
+
event.title = "Edited title"
|
104
|
+
if event.save
|
105
|
+
successful event.to_xml
|
106
|
+
else
|
107
|
+
failed
|
108
|
+
end
|
109
|
+
|
110
|
+
puts "3. Reload Event"
|
111
|
+
if event.reload
|
112
|
+
successful
|
113
|
+
end
|
114
|
+
|
115
|
+
puts "4. Find Event by id"
|
116
|
+
e = Event.find(@service.calendars[0], event.id)
|
117
|
+
if e.title == event.title
|
118
|
+
successful
|
119
|
+
else
|
120
|
+
failed "Found event doesn't match existing event"
|
121
|
+
end
|
122
|
+
|
123
|
+
puts "5. Delete Event"
|
124
|
+
if event.delete
|
125
|
+
successful
|
126
|
+
else
|
127
|
+
failed
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def event_recurrence_test
|
132
|
+
puts "---Starting Event Recurrence Test---"
|
133
|
+
|
134
|
+
@first_start = Time.now
|
135
|
+
@first_end = Time.now+3600
|
136
|
+
@first_freq = {'weekly' => ['TU']}
|
137
|
+
@second_start = Time.now+86000
|
138
|
+
@second_end = Time.now+89600
|
139
|
+
@second_freq = {'weekly' => ['SA']}
|
140
|
+
|
141
|
+
puts "1. Create Recurring Event"
|
142
|
+
event = Event.new(@service.calendars[0])
|
143
|
+
event.title = "Test Recurring Event"
|
144
|
+
event.content = "Test event content"
|
145
|
+
event.recurrence = Recurrence.new({:start => @first_start, :end => @first_end, :frequency => @first_freq})
|
146
|
+
if event.save
|
147
|
+
successful event.to_xml
|
148
|
+
else
|
149
|
+
failed("recurrence = "+event.recurrence.to_s)
|
150
|
+
end
|
151
|
+
|
152
|
+
puts "2. Edit Recurrence"
|
153
|
+
event.title = "Edited recurring title"
|
154
|
+
event.recurrence = Recurrence.new({:start => @second_start, :end => @second_end, :frequency => @second_freq})
|
155
|
+
if event.save
|
156
|
+
successful event.to_xml
|
157
|
+
else
|
158
|
+
failed
|
159
|
+
end
|
160
|
+
|
161
|
+
puts "3. Delete Event"
|
162
|
+
if event.delete
|
163
|
+
successful
|
164
|
+
else
|
165
|
+
failed
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def failed(m = nil)
|
170
|
+
puts "Test Failed"
|
171
|
+
puts m if m
|
172
|
+
exit()
|
173
|
+
end
|
174
|
+
|
175
|
+
def successful(m = nil)
|
176
|
+
puts "Test Successful"
|
177
|
+
puts m if m
|
178
|
+
end
|
179
|
+
|
180
|
+
tester
|