railsware-gcal4ruby 0.5.5
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/CHANGELOG +80 -0
- data/README +97 -0
- data/lib/gcal4ruby/calendar.rb +366 -0
- data/lib/gcal4ruby/event.rb +446 -0
- data/lib/gcal4ruby/recurrence.rb +273 -0
- data/lib/gcal4ruby/service.rb +160 -0
- data/lib/gcal4ruby.rb +1 -0
- data/test/unit.rb +182 -0
- metadata +89 -0
@@ -0,0 +1,446 @@
|
|
1
|
+
# Author:: Mike Reich (mike@seabourneconsulting.com)
|
2
|
+
# Copyright:: Copyright (C) 2010 Mike Reich
|
3
|
+
# License:: GPL v2
|
4
|
+
#--
|
5
|
+
# Licensed under the General Public License (GPL), Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
#
|
15
|
+
# Feel free to use and update, but be sure to contribute your
|
16
|
+
# code back to the project and attribute as required by the license.
|
17
|
+
#++
|
18
|
+
|
19
|
+
require 'gcal4ruby/recurrence'
|
20
|
+
|
21
|
+
module GCal4Ruby
|
22
|
+
#The Event Class represents a remote event in calendar
|
23
|
+
#
|
24
|
+
#=Usage
|
25
|
+
#All usages assume a successfully authenticated Service and valid Calendar.
|
26
|
+
#1. Create a new Event
|
27
|
+
# event = Event.new(service, {:calendar => cal, :title => "Soccer Game", :start => Time.parse("12-06-2009 at 12:30 PM"), :end => Time.parse("12-06-2009 at 1:30 PM"), :where => "Merry Playfields"})
|
28
|
+
# event.save
|
29
|
+
#
|
30
|
+
#2. Find an existing Event by title
|
31
|
+
# event = Event.find(service, {:title => "Soccer Game"})
|
32
|
+
#
|
33
|
+
#3. Find an existing Event by ID
|
34
|
+
# event = Event.find(service, {:id => event.id})
|
35
|
+
#
|
36
|
+
#4. Find all events containing the search term
|
37
|
+
# event = Event.find(service, "Soccer Game")
|
38
|
+
#
|
39
|
+
#5. Find all events on a calendar containing the search term
|
40
|
+
# event = Event.find(service, "Soccer Game", {:calendar => cal.id})
|
41
|
+
#
|
42
|
+
#6. Find events within a date range
|
43
|
+
# event = Event.find(service, "Soccer Game", {'start-min' => Time.parse("01/01/2010").utc.xmlschema, 'start-max' => Time.parse("06/01/2010").utc.xmlschema})
|
44
|
+
#
|
45
|
+
#7. Create a recurring event for every saturday
|
46
|
+
# event = Event.new(service)
|
47
|
+
# event.title = "Baseball Game"
|
48
|
+
# event.calendar = cal
|
49
|
+
# event.where = "Municipal Stadium"
|
50
|
+
# event.recurrence = Recurrence.new
|
51
|
+
# event.recurrence.start_time = Time.parse("06/20/2009 at 4:30 PM")
|
52
|
+
# event.recurrence.end_time = Time.parse("06/20/2009 at 6:30 PM")
|
53
|
+
# event.recurrence.frequency = {"weekly" => ["SA"]}
|
54
|
+
# event.save
|
55
|
+
#
|
56
|
+
#8. Create an event with a 15 minute email reminder
|
57
|
+
# event = Event.new(service)
|
58
|
+
# event.calendar = cal
|
59
|
+
# event.title = "Dinner with Kate"
|
60
|
+
# event.start_time = Time.parse("06/20/2009 at 5 pm")
|
61
|
+
# event.end_time = Time.parse("06/20/2009 at 8 pm")
|
62
|
+
# event.where = "Luigi's"
|
63
|
+
# event.reminder = {:minutes => 15, :method => 'email'}
|
64
|
+
# event.save
|
65
|
+
#
|
66
|
+
#9. Create an event with attendees
|
67
|
+
# event = Event.new(service)
|
68
|
+
# event.calendar = cal
|
69
|
+
# event.title = "Dinner with Kate"
|
70
|
+
# event.start_time = Time.parse("06/20/2009 at 5 pm")
|
71
|
+
# event.end_time = Time.parse("06/20/2009 at 8 pm")
|
72
|
+
# event.attendees => {:name => "Kate", :email => "kate@gmail.com"}
|
73
|
+
# event.save
|
74
|
+
#
|
75
|
+
#After an event object has been created or loaded, you can change any of the
|
76
|
+
#attributes like you would any other object. Be sure to save the event to write changes
|
77
|
+
#to the Google Calendar service.
|
78
|
+
|
79
|
+
class Event < GData4Ruby::GDataObject
|
80
|
+
EVENT_QUERY_FEED = "http://www.google.com/calendar/feeds/default/private/full/"
|
81
|
+
EVENT_XML = "<entry xmlns='http://www.w3.org/2005/Atom'
|
82
|
+
xmlns:gd='http://schemas.google.com/g/2005'>
|
83
|
+
<category scheme='http://schemas.google.com/g/2005#kind'
|
84
|
+
term='http://schemas.google.com/g/2005#event'></category>
|
85
|
+
<title type='text'></title>
|
86
|
+
<content type='text'></content>
|
87
|
+
<gd:transparency
|
88
|
+
value='http://schemas.google.com/g/2005#event.opaque'>
|
89
|
+
</gd:transparency>
|
90
|
+
<gd:eventStatus
|
91
|
+
value='http://schemas.google.com/g/2005#event.confirmed'>
|
92
|
+
</gd:eventStatus>
|
93
|
+
<gd:where valueString=''></gd:where>
|
94
|
+
<gd:when startTime=''
|
95
|
+
endTime=''></gd:when>
|
96
|
+
</entry>"
|
97
|
+
STATUS = {:confirmed => "http://schemas.google.com/g/2005#event.confirmed",
|
98
|
+
:tentative => "http://schemas.google.com/g/2005#event.tentative",
|
99
|
+
:cancelled => "http://schemas.google.com/g/2005#event.canceled"}
|
100
|
+
|
101
|
+
TRANSPARENCY = {:free => "http://schemas.google.com/g/2005#event.transparent",
|
102
|
+
:busy => "http://schemas.google.com/g/2005#event.opaque"}
|
103
|
+
|
104
|
+
#The content for the event
|
105
|
+
attr_accessor :content
|
106
|
+
#The location of the event
|
107
|
+
attr_accessor :where
|
108
|
+
#A flag for whether the event show as :free or :busy
|
109
|
+
attr_accessor :transparency
|
110
|
+
#A flag indicating the status of the event. Values can be :confirmed, :tentative or :cancelled
|
111
|
+
attr_accessor :status
|
112
|
+
#Flag indicating whether it is an all day event
|
113
|
+
attr_reader :all_day
|
114
|
+
#The reminder settings for the event, returned as a hash
|
115
|
+
attr_reader :reminder
|
116
|
+
#The date the event was last edited
|
117
|
+
attr_reader :edited
|
118
|
+
#Id of the parent calendar
|
119
|
+
attr_reader :calendar_id
|
120
|
+
|
121
|
+
#Creates a new Event. Accepts a valid Service object and optional attributes hash.
|
122
|
+
def initialize(service, attributes = {})
|
123
|
+
super(service, attributes)
|
124
|
+
@xml = EVENT_XML
|
125
|
+
@transparency ||= :busy
|
126
|
+
@status ||= :confirmed
|
127
|
+
@attendees ||= []
|
128
|
+
@all_day ||= false
|
129
|
+
@reminder = []
|
130
|
+
attributes.each do |key, value|
|
131
|
+
self.send("#{key}=", value)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
#Sets the reminder options for the event. Parameter must be a hash a :minutes key with a value of 5 up to 40320 (4 weeks)
|
136
|
+
#and a :method key of with a value of one the following:
|
137
|
+
#alert:: causes an alert to appear when a user is viewing the calendar in a browser
|
138
|
+
#email:: sends the user an email message
|
139
|
+
def reminder=(r)
|
140
|
+
@reminder = r
|
141
|
+
end
|
142
|
+
|
143
|
+
#Returns the current event's Recurrence information
|
144
|
+
def recurrence
|
145
|
+
@recurrence
|
146
|
+
end
|
147
|
+
|
148
|
+
#Returns an array of the current attendees
|
149
|
+
def attendees
|
150
|
+
@attendees
|
151
|
+
end
|
152
|
+
|
153
|
+
def all_day=(value)
|
154
|
+
puts 'all_day value = '+value.to_s if service.debug
|
155
|
+
if value.is_a? String
|
156
|
+
@all_day = true if value.downcase == 'true'
|
157
|
+
@all_day = false if value.downcase == 'false'
|
158
|
+
else
|
159
|
+
@all_day = value
|
160
|
+
end
|
161
|
+
puts 'after all_day value = '+@all_day.to_s if service.debug
|
162
|
+
@all_day
|
163
|
+
end
|
164
|
+
|
165
|
+
#Accepts an array of email address/name pairs for attendees.
|
166
|
+
# [{:name => 'Mike Reich', :email => 'mike@seabourneconsulting.com'}]
|
167
|
+
#The email address is requried, but the name is optional
|
168
|
+
def attendees=(a)
|
169
|
+
raise ArgumentError, "Attendees must be an Array of email/name hash pairs" if not a.is_a?(Array)
|
170
|
+
@attendees = a
|
171
|
+
end
|
172
|
+
|
173
|
+
#Sets the event's recurrence information to a Recurrence object. Returns the recurrence if successful,
|
174
|
+
#false otherwise
|
175
|
+
def recurrence=(r)
|
176
|
+
raise ArgumentError, 'Recurrence must be a Recurrence object' if not r.is_a?(Recurrence)
|
177
|
+
@recurrence = r
|
178
|
+
end
|
179
|
+
|
180
|
+
#Returns a duplicate of the current event as a new Event object
|
181
|
+
def copy()
|
182
|
+
e = Event.new(service)
|
183
|
+
e.load(to_xml)
|
184
|
+
e.calendar = @calendar
|
185
|
+
return e
|
186
|
+
end
|
187
|
+
|
188
|
+
#Sets the start time of the Event. Must be a Time object or a parsable string representation
|
189
|
+
#of a time.
|
190
|
+
def start_time=(str)
|
191
|
+
raise ArgumentError, "Start Time must be either Time or String" if not str.is_a?String and not str.is_a?Time
|
192
|
+
@start_time = if str.is_a?String
|
193
|
+
Time.parse(str)
|
194
|
+
elsif str.is_a?Time
|
195
|
+
str
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
#Sets the end time of the Event. Must be a Time object or a parsable string representation
|
200
|
+
#of a time.
|
201
|
+
def end_time=(str)
|
202
|
+
raise ArgumentError, "End Time must be either Time or String" if not str.is_a?String and not str.is_a?Time
|
203
|
+
@end_time = if str.is_a?String
|
204
|
+
Time.parse(str)
|
205
|
+
elsif str.is_a?Time
|
206
|
+
str
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
#The event start time. If a recurring event, the recurrence start time.
|
211
|
+
def start_time
|
212
|
+
return @start_time ? @start_time : @recurrence ? @recurrence .start_time : nil
|
213
|
+
end
|
214
|
+
|
215
|
+
#The event end time. If a recurring event, the recurrence end time.
|
216
|
+
def end_time
|
217
|
+
return @end_time ? @end_time : @recurrence ? @recurrence.end_time : nil
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
#If the event does not exist on the Google Calendar service, save creates it. Otherwise
|
222
|
+
#updates the existing event data. Returns true on success, false otherwise.
|
223
|
+
def save
|
224
|
+
raise CalendarNotEditable if not calendar.editable
|
225
|
+
super
|
226
|
+
end
|
227
|
+
|
228
|
+
#Creates a new event
|
229
|
+
def create
|
230
|
+
service.send_request(GData4Ruby::Request.new(:post, @parent_calendar.content_uri, to_xml))
|
231
|
+
end
|
232
|
+
|
233
|
+
#Returns an XML representation of the event.
|
234
|
+
def to_xml()
|
235
|
+
xml = REXML::Document.new(super)
|
236
|
+
xml.root.elements.each(){}.map do |ele|
|
237
|
+
case ele.name
|
238
|
+
when "content"
|
239
|
+
ele.text = @content
|
240
|
+
when "when"
|
241
|
+
if not @recurrence
|
242
|
+
puts 'all_day = '+@all_day.to_s if service.debug
|
243
|
+
if @all_day
|
244
|
+
puts 'saving as all-day event' if service.debug
|
245
|
+
else
|
246
|
+
puts 'saving as timed event' if service.debug
|
247
|
+
end
|
248
|
+
ele.attributes["startTime"] = @all_day ? @start_time.strftime("%Y-%m-%d") : @start_time.utc.xmlschema
|
249
|
+
ele.attributes["endTime"] = @all_day ? @end_time.strftime("%Y-%m-%d") : @end_time.utc.xmlschema
|
250
|
+
set_reminder(ele)
|
251
|
+
else
|
252
|
+
xml.root.delete_element("/entry/gd:when")
|
253
|
+
ele = xml.root.add_element("gd:recurrence")
|
254
|
+
ele.text = @recurrence.to_recurrence_string
|
255
|
+
set_reminder(ele) if @reminder
|
256
|
+
end
|
257
|
+
when "eventStatus"
|
258
|
+
ele.attributes["value"] = STATUS[@status]
|
259
|
+
when "transparency"
|
260
|
+
ele.attributes["value"] = TRANSPARENCY[@transparency]
|
261
|
+
when "where"
|
262
|
+
ele.attributes["valueString"] = @where
|
263
|
+
when "recurrence"
|
264
|
+
puts 'recurrence element found' if service.debug
|
265
|
+
if @recurrence
|
266
|
+
puts 'setting recurrence' if service.debug
|
267
|
+
ele.text = @recurrence.to_recurrence_string
|
268
|
+
else
|
269
|
+
puts 'no recurrence, adding when' if service.debug
|
270
|
+
w = xml.root.add_element("gd:when")
|
271
|
+
xml.root.delete_element("/entry/gd:recurrence")
|
272
|
+
w.attributes["startTime"] = @all_day ? @start_time.strftime("%Y-%m-%d") : @start_time.xmlschema
|
273
|
+
w.attributes["endTime"] = @all_day ? @end_time.strftime("%Y-%m-%d") : @end_time.xmlschema
|
274
|
+
set_reminder(w)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
if not @attendees.empty?
|
279
|
+
xml.root.elements.delete_all "gd:who"
|
280
|
+
@attendees.each do |a|
|
281
|
+
xml.root.add_element("gd:who", {"email" => a[:email], "valueString" => a[:name], "rel" => "http://schemas.google.com/g/2005#event.attendee"})
|
282
|
+
end
|
283
|
+
end
|
284
|
+
xml.to_s
|
285
|
+
end
|
286
|
+
|
287
|
+
#The event's parent calendar
|
288
|
+
def calendar
|
289
|
+
@parent_calendar = Calendar.find(service, {:id => @calendar_id}) if not @parent_calendar and @calendar_id
|
290
|
+
return @parent_calendar
|
291
|
+
end
|
292
|
+
|
293
|
+
#Sets the event's calendar
|
294
|
+
def calendar=(p)
|
295
|
+
raise ArgumentError, 'Value must be a valid Calendar object' if not p.is_a? Calendar
|
296
|
+
@parent_calendar = p
|
297
|
+
end
|
298
|
+
|
299
|
+
#Loads the event info from an XML string.
|
300
|
+
def load(string)
|
301
|
+
super(string)
|
302
|
+
@xml = string
|
303
|
+
@exists = true
|
304
|
+
xml = REXML::Document.new(string)
|
305
|
+
@etag = xml.root.attributes['etag']
|
306
|
+
xml.root.elements.each(){}.map do |ele|
|
307
|
+
case ele.name
|
308
|
+
when 'id'
|
309
|
+
@calendar_id, @id = @feed_uri.gsub("http://www.google.com/calendar/feeds/", "").split("/events/")
|
310
|
+
@id = "#{@calendar_id}/private/full/#{@id}"
|
311
|
+
when 'edited'
|
312
|
+
@edited = Time.parse(ele.text)
|
313
|
+
when 'content'
|
314
|
+
@content = ele.text
|
315
|
+
when "when"
|
316
|
+
@start_time = Time.parse(ele.attributes['startTime'])
|
317
|
+
@end_time = Time.parse(ele.attributes['endTime'])
|
318
|
+
@all_day = !ele.attributes['startTime'].include?('T')
|
319
|
+
@reminder = []
|
320
|
+
ele.elements.each("gd:reminder") do |r|
|
321
|
+
rem = {}
|
322
|
+
rem[:minutes] = r.attributes['minutes'] if r.attributes['minutes']
|
323
|
+
rem[:method] = r.attributes['method'] if r.attributes['method']
|
324
|
+
@reminder << rem
|
325
|
+
end
|
326
|
+
when "where"
|
327
|
+
@where = ele.attributes['valueString']
|
328
|
+
when "link"
|
329
|
+
if ele.attributes['rel'] == 'edit'
|
330
|
+
@edit_feed = ele.attributes['href']
|
331
|
+
end
|
332
|
+
when "who"
|
333
|
+
@attendees << {:email => ele.attributes['email'], :name => ele.attributes['valueString'], :role => ele.attributes['rel'].gsub("http://schemas.google.com/g/2005#event.", ""), :status => ele.elements["gd:attendeeStatus"] ? ele.elements["gd:attendeeStatus"].attributes['value'].gsub("http://schemas.google.com/g/2005#event.", "") : ""}
|
334
|
+
when "eventStatus"
|
335
|
+
@status = ele.attributes["value"].gsub("http://schemas.google.com/g/2005#event.", "").to_sym
|
336
|
+
when 'recurrence'
|
337
|
+
@recurrence = Recurrence.new(ele.text)
|
338
|
+
when "transparency"
|
339
|
+
@transparency = case ele.attributes["value"]
|
340
|
+
when "http://schemas.google.com/g/2005#event.transparent" then :free
|
341
|
+
when "http://schemas.google.com/g/2005#event.opaque" then :busy
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
#Reloads the event data from the Google Calendar Service. Returns true if successful,
|
348
|
+
#false otherwise.
|
349
|
+
def reload
|
350
|
+
return false if not @exists
|
351
|
+
t = Event.find(service, {:id => @id})
|
352
|
+
if t and load(t.to_xml)
|
353
|
+
return true
|
354
|
+
end
|
355
|
+
return false
|
356
|
+
end
|
357
|
+
|
358
|
+
#Finds an Event based on a text query or by an id. Parameters are:
|
359
|
+
#*service*:: A valid Service object to search.
|
360
|
+
#*query*:: either a string containing a text query to search by, or a hash containing an +id+ key with an associated id to find, or a +query+ key containint a text query to search for, or a +title+ key containing a title to search. All searches are case insensitive.
|
361
|
+
#*args*:: a hash containing optional additional query paramters to use. Limit a search to a single calendar by passing the calendar id as {:calendar => calendar.id}. See here[http://code.google.com/apis/calendar/data/2.0/developers_guide_protocol.html#RetrievingEvents] and here[http://code.google.com/apis/gdata/docs/2.0/reference.html#Queries] for a full list of possible values. Example:
|
362
|
+
# {'max-results' => '100'}
|
363
|
+
#If an ID is specified, a single instance of the event is returned if found, otherwise false.
|
364
|
+
#If a query term or title text is specified, and array of matching results is returned, or an empty array if nothing
|
365
|
+
#was found.
|
366
|
+
def self.find(service, query, args = {})
|
367
|
+
raise ArgumentError, 'query must be a hash or string' if not query.is_a? Hash and not query.is_a? String
|
368
|
+
if query.is_a? Hash and query[:id]
|
369
|
+
id = query[:id]
|
370
|
+
puts "id passed, finding event by id" if service.debug
|
371
|
+
puts "id = "+id if service.debug
|
372
|
+
d = service.send_request(GData4Ruby::Request.new(:get, "http://www.google.com/calendar/feeds/"+id, {"If-Not-Match" => "*"}))
|
373
|
+
puts d.inspect if service.debug
|
374
|
+
if d
|
375
|
+
return get_instance(service, d)
|
376
|
+
end
|
377
|
+
else
|
378
|
+
results = []
|
379
|
+
if query.is_a?(Hash)
|
380
|
+
args["q"] = query[:query] if query[:query]
|
381
|
+
args['title'] = query[:title] if query[:title]
|
382
|
+
else
|
383
|
+
args["q"] = CGI::escape(query) if query != ''
|
384
|
+
end
|
385
|
+
if args[:calendar]
|
386
|
+
cal = Calendar.find(service, {:id => args[:calendar]})
|
387
|
+
args.delete(:calendar)
|
388
|
+
ret = service.send_request(GData4Ruby::Request.new(:get, cal.content_uri, nil, nil, args))
|
389
|
+
xml = REXML::Document.new(ret.body).root
|
390
|
+
xml.elements.each("entry") do |e|
|
391
|
+
results << get_instance(service, e)
|
392
|
+
end
|
393
|
+
else
|
394
|
+
service.calendars.each do |cal|
|
395
|
+
ret = service.send_request(GData4Ruby::Request.new(:get, cal.content_uri, nil, nil, args))
|
396
|
+
xml = REXML::Document.new(ret.body).root
|
397
|
+
xml.elements.each("entry") do |e|
|
398
|
+
results << get_instance(service, e)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
return results
|
403
|
+
end
|
404
|
+
return false
|
405
|
+
end
|
406
|
+
|
407
|
+
#Returns true if the event exists on the Google Calendar Service.
|
408
|
+
def exists?
|
409
|
+
return @exists
|
410
|
+
end
|
411
|
+
|
412
|
+
private
|
413
|
+
def set_reminder(ele)
|
414
|
+
num = ele.elements.delete_all "gd:reminder"
|
415
|
+
puts 'num = '+num.size.to_s if service.debug
|
416
|
+
if @reminder
|
417
|
+
@reminder.each do |reminder|
|
418
|
+
puts 'reminder added' if service.debug
|
419
|
+
e = ele.add_element("gd:reminder")
|
420
|
+
e.attributes['minutes'] = reminder[:minutes].to_s if reminder[:minutes]
|
421
|
+
if reminder[:method]
|
422
|
+
e.attributes['method'] = reminder[:method]
|
423
|
+
else
|
424
|
+
e.attributes['method'] = 'email'
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def self.get_instance(service, d)
|
431
|
+
if d.is_a? Net::HTTPOK
|
432
|
+
xml = REXML::Document.new(d.read_body).root
|
433
|
+
if xml.name == 'feed'
|
434
|
+
xml = xml.elements.each("entry"){}[0]
|
435
|
+
end
|
436
|
+
else
|
437
|
+
xml = d
|
438
|
+
end
|
439
|
+
ele = GData4Ruby::Utils::add_namespaces(xml)
|
440
|
+
e = Event.new(service)
|
441
|
+
e.load(ele.to_s)
|
442
|
+
e
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|