edave-gcal4ruby 0.6.0
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 +85 -0
- data/README +103 -0
- data/lib/gcal4ruby.rb +1 -0
- data/lib/gcal4ruby/calendar.rb +378 -0
- data/lib/gcal4ruby/event.rb +446 -0
- data/lib/gcal4ruby/recurrence.rb +302 -0
- data/lib/gcal4ruby/service.rb +196 -0
- data/test/unit.rb +228 -0
- metadata +94 -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
|
+
#An array of reminders. Each item in the array is a hash representing the event reminder.
|
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 an array of hashes containing 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 a calendar object as {:calender => calendar} or 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 = args[:calendar].is_a?(Calendar) ? args[:calendar] : 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
|
+
|
@@ -0,0 +1,302 @@
|
|
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
|
+
class Time
|
20
|
+
|
21
|
+
#Returns a ISO 8601 complete formatted string of the time
|
22
|
+
def complete
|
23
|
+
self.utc.strftime("%Y%m%dT%H%M%S")
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.parse_complete(value)
|
27
|
+
unless value.nil? || value.empty?
|
28
|
+
if value.include?("T")
|
29
|
+
d, h = value.split("T")
|
30
|
+
return Time.parse(d+" "+h.gsub("Z", ""))
|
31
|
+
else
|
32
|
+
value = value.to_s
|
33
|
+
return Time.parse("#{value[0..3]}-#{value[4..5]}-#{value[6..7]}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
module GCal4Ruby
|
41
|
+
#The Recurrence class stores information on an Event's recurrence. The class implements
|
42
|
+
#the RFC 2445 iCalendar recurrence description.
|
43
|
+
class Recurrence
|
44
|
+
#The event start date/time
|
45
|
+
attr_reader :start_time
|
46
|
+
#The event end date/time
|
47
|
+
attr_reader :end_time
|
48
|
+
#the event reference
|
49
|
+
attr_reader :event
|
50
|
+
#The date until which the event will be repeated
|
51
|
+
attr_reader :repeat_until
|
52
|
+
#The event frequency
|
53
|
+
attr_reader :frequency
|
54
|
+
#True if the event is all day (i.e. no start/end time)
|
55
|
+
attr_accessor :all_day
|
56
|
+
|
57
|
+
#Accepts an optional attributes hash or a string containing a properly formatted ISO 8601 recurrence rule. Returns a new Recurrence object
|
58
|
+
def initialize(vars = {})
|
59
|
+
if vars.is_a? Hash
|
60
|
+
vars.each do |key, value|
|
61
|
+
self.send("#{key}=", value)
|
62
|
+
end
|
63
|
+
elsif vars.is_a? String
|
64
|
+
self.load(vars)
|
65
|
+
end
|
66
|
+
@all_day ||= false
|
67
|
+
end
|
68
|
+
|
69
|
+
#Accepts a string containing a properly formatted ISO 8601 recurrence rule and loads it into the recurrence object.
|
70
|
+
#Contributed by John Paul Narowski.
|
71
|
+
def load(rec)
|
72
|
+
@frequency = {}
|
73
|
+
attrs = rec.split("\n")
|
74
|
+
attrs.each do |val|
|
75
|
+
break if val == "BEGIN:VTIMEZONE" # Ignoring the time zone for now
|
76
|
+
key, value = val.split(":")
|
77
|
+
if key == 'RRULE'
|
78
|
+
args = {}
|
79
|
+
value.split(";").each do |rr|
|
80
|
+
rr_key, rr_value = rr.split("=")
|
81
|
+
rr_key = rr_key.downcase.to_sym
|
82
|
+
unless @frequency.has_key?(rr_key)
|
83
|
+
if rr_key == :until
|
84
|
+
@repeat_until = Time.parse_complete(rr_value)
|
85
|
+
else
|
86
|
+
args[rr_key] = rr_value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
case args[:freq]
|
91
|
+
when 'DAILY'
|
92
|
+
@frequency['daily'] = true
|
93
|
+
when 'WEEKLY'
|
94
|
+
@frequency['weekly'] = args[:byday].split(',')
|
95
|
+
when 'MONTHLY'
|
96
|
+
if args[:byday]
|
97
|
+
@frequency['monthly'] = args[:byday]
|
98
|
+
@frequency[:day_of_week] = true
|
99
|
+
else
|
100
|
+
@frequency['monthly'] = args[:bymonthday].to_i
|
101
|
+
end
|
102
|
+
when 'YEARLY'
|
103
|
+
@frequency['yearly'] = args[:byyearday].to_i
|
104
|
+
end
|
105
|
+
elsif key == 'INTERVAL'
|
106
|
+
@frequency[:interval] = value.to_i unless value.nil? || value.empty?
|
107
|
+
elsif key.include?('DTSTART;VALUE=DATE')
|
108
|
+
@start_time ||= Time.parse(value)
|
109
|
+
@all_day = true
|
110
|
+
elsif key.include?("DTSTART;TZID") or key.include?("DTSTART") or key.include?('DTSTART;VALUE=DATE-TIME')
|
111
|
+
@start_time ||= Time.parse_complete(value)
|
112
|
+
elsif key.include?('DTEND;VALUE=DATE')
|
113
|
+
@end_time ||= Time.parse(value)
|
114
|
+
elsif key.include?("DTEND;TZID") or key.include?("DTEND") or key.include?('DTEND;VALUE=DATE-TIME')
|
115
|
+
@end_time ||= Time.parse_complete(value)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
@frequency[:interval] = 1 unless @frequency[:interval] && @frequency[:interval].to_i > 0
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_s
|
122
|
+
output = ''
|
123
|
+
if @frequency
|
124
|
+
f = ''
|
125
|
+
i = ''
|
126
|
+
by = ''
|
127
|
+
@frequency.each do |key, v|
|
128
|
+
key = key.to_s.downcase
|
129
|
+
|
130
|
+
if v.is_a?(Array)
|
131
|
+
if v.size > 0
|
132
|
+
value = v.join(",")
|
133
|
+
else
|
134
|
+
value = nil
|
135
|
+
end
|
136
|
+
else
|
137
|
+
value = v
|
138
|
+
end
|
139
|
+
f += "#{key}" if key != 'interval'
|
140
|
+
case key
|
141
|
+
when "secondly"
|
142
|
+
by += " every #{value} second"
|
143
|
+
when "minutely"
|
144
|
+
by += " every #{value} minute"
|
145
|
+
when "hourly"
|
146
|
+
by += " every #{value} hour"
|
147
|
+
when "weekly"
|
148
|
+
by += " on #{value}" if value
|
149
|
+
when "monthly"
|
150
|
+
by += " on #{value}"
|
151
|
+
when "yearly"
|
152
|
+
by += " on the #{value} day of the year"
|
153
|
+
when 'interval'
|
154
|
+
i += " for #{value} times"
|
155
|
+
end
|
156
|
+
end
|
157
|
+
output += f+i+by
|
158
|
+
end
|
159
|
+
if @repeat_until
|
160
|
+
output += " and repeats until #{@repeat_until.strftime("%m/%d/%Y")}"
|
161
|
+
end
|
162
|
+
output
|
163
|
+
end
|
164
|
+
|
165
|
+
#Returns a string with the correctly formatted ISO 8601 recurrence rule
|
166
|
+
def to_recurrence_string
|
167
|
+
output = ''
|
168
|
+
if @all_day
|
169
|
+
output += "DTSTART;VALUE=DATE:#{@start_time.utc.strftime("%Y%m%d")}\n"
|
170
|
+
else
|
171
|
+
output += "DTSTART;VALUE=DATE-TIME:#{@start_time.utc.complete}\n"
|
172
|
+
end
|
173
|
+
if @all_day
|
174
|
+
output += "DTEND;VALUE=DATE:#{@end_time.utc.strftime("%Y%m%d")}\n"
|
175
|
+
else
|
176
|
+
output += "DTEND;VALUE=DATE-TIME:#{@end_time.utc.complete}\n"
|
177
|
+
end
|
178
|
+
output += "RRULE:"
|
179
|
+
if @frequency
|
180
|
+
f = 'FREQ='
|
181
|
+
i = ''
|
182
|
+
by = ''
|
183
|
+
day_of_week = @frequency.delete(:day_of_week)
|
184
|
+
@frequency.each do |key, v|
|
185
|
+
if v.is_a?(Array)
|
186
|
+
if v.size > 0
|
187
|
+
value = v.join(",")
|
188
|
+
else
|
189
|
+
value = nil
|
190
|
+
end
|
191
|
+
else
|
192
|
+
value = v
|
193
|
+
end
|
194
|
+
f += "#{key.to_s.upcase};" if key.to_s.downcase != 'interval'
|
195
|
+
case key.to_s.downcase
|
196
|
+
when "secondly"
|
197
|
+
by += "BYSECOND=#{value};"
|
198
|
+
when "minutely"
|
199
|
+
by += "BYMINUTE=#{value};"
|
200
|
+
when "hourly"
|
201
|
+
by += "BYHOUR=#{value};"
|
202
|
+
when "weekly"
|
203
|
+
by += "BYDAY=#{value};" if value
|
204
|
+
when "monthly"
|
205
|
+
if day_of_week
|
206
|
+
by += "BYDAY=#{value};"
|
207
|
+
else
|
208
|
+
by += "BYMONTHDAY=#{value};"
|
209
|
+
end
|
210
|
+
when "yearly"
|
211
|
+
by += "BYYEARDAY=#{value};"
|
212
|
+
when 'interval'
|
213
|
+
i += "INTERVAL=#{value};"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
output += f+by+i
|
217
|
+
end
|
218
|
+
if @repeat_until
|
219
|
+
output += "UNTIL=#{@repeat_until.strftime("%Y%m%d")}"
|
220
|
+
end
|
221
|
+
output += "\n"
|
222
|
+
end
|
223
|
+
|
224
|
+
#Sets the start date/time. Must be a Time object.
|
225
|
+
def start_time=(s)
|
226
|
+
if not s.is_a?(Time)
|
227
|
+
raise RecurrenceValueError, "Start must be a date or a time"
|
228
|
+
else
|
229
|
+
@start_time = s
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
#Sets the end Date/Time. Must be a Time object.
|
234
|
+
def end_time=(e)
|
235
|
+
if not e.is_a?(Time)
|
236
|
+
raise RecurrenceValueError, "End must be a date or a time"
|
237
|
+
else
|
238
|
+
@end_time = e
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
#Sets the parent event reference
|
243
|
+
def event=(e)
|
244
|
+
if not e.is_a?(Event)
|
245
|
+
raise RecurrenceValueError, "Event must be an event"
|
246
|
+
else
|
247
|
+
@event = e
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
#Sets the end date for the recurrence
|
252
|
+
def repeat_until=(r)
|
253
|
+
if not r.is_a?(Date)
|
254
|
+
raise RecurrenceValueError, "Repeat_until must be a date"
|
255
|
+
else
|
256
|
+
@repeat_until = r
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
#Sets the frequency of the recurrence. Should be a hash with one of
|
261
|
+
#"SECONDLY", "MINUTELY", "HOURLY", "DAILY", "WEEKLY", "MONTHLY", "YEARLY" as the key,
|
262
|
+
#and as the value, an array containing zero to n of the following:
|
263
|
+
#- *Secondly*: A value between 0 and 59. Causes the event to repeat on that second of each minut.
|
264
|
+
#- *Minutely*: A value between 0 and 59. Causes the event to repeat on that minute of every hour.
|
265
|
+
#- *Hourly*: A value between 0 and 23. Causes the even to repeat on that hour of every day.
|
266
|
+
#- *Daily*: A true value - will cause the event to repeat every day until the repeat_until date.
|
267
|
+
#- *Weekly*: A value of the first two letters of a day of the week. Causes the event to repeat on that day.
|
268
|
+
#- *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.
|
269
|
+
#- *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.
|
270
|
+
#
|
271
|
+
#Optionally, you may specific a second hash pair to set the interval the event repeats:
|
272
|
+
# "interval" => '2'
|
273
|
+
#If the interval is missing, it is assumed to be 1.
|
274
|
+
#
|
275
|
+
#===Examples
|
276
|
+
#Repeat event daily
|
277
|
+
# frequency = {"daily" => true}
|
278
|
+
#
|
279
|
+
#Repeat event every Tuesday:
|
280
|
+
# frequency = {"weekly" => ["TU"]}
|
281
|
+
#
|
282
|
+
#Repeat every first monday of the month
|
283
|
+
# frequency = {"monthly" => "+1MO", :day_of_week => true}
|
284
|
+
#
|
285
|
+
#Repeat on the 9th of each month regardless of the day
|
286
|
+
# frequency = {"monthly" => 9}
|
287
|
+
#
|
288
|
+
#Repeat on the last day of every year
|
289
|
+
# frequency = {"Yearly" => 366}
|
290
|
+
#
|
291
|
+
#Repeat every other week on Friday
|
292
|
+
# frequency = {"Weekly" => ["FR"], "interval" => "2"}
|
293
|
+
|
294
|
+
def frequency=(f)
|
295
|
+
if f.is_a?(Hash)
|
296
|
+
@frequency = f
|
297
|
+
else
|
298
|
+
raise RecurrenceValueError, "Frequency must be a hash (see documentation)"
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|