vcita-gcal4ruby 0.7.2

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.
@@ -0,0 +1,449 @@
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
+ puts "Event.find() searching calendar: " if service.debug
396
+ puts cal.content_uri if service.debug
397
+ puts args.inspect if service.debug
398
+ ret = service.send_request(GData4Ruby::Request.new(:get, cal.content_uri, nil, nil, args))
399
+ xml = REXML::Document.new(ret.body).root
400
+ xml.elements.each("entry") do |e|
401
+ results << get_instance(service, e)
402
+ end
403
+ end
404
+ end
405
+ return results
406
+ end
407
+ return false
408
+ end
409
+
410
+ #Returns true if the event exists on the Google Calendar Service.
411
+ def exists?
412
+ return @exists
413
+ end
414
+
415
+ private
416
+ def set_reminder(ele)
417
+ num = ele.elements.delete_all "gd:reminder"
418
+ puts 'num = '+num.size.to_s if service.debug
419
+ if @reminder
420
+ @reminder.each do |reminder|
421
+ puts 'reminder added' if service.debug
422
+ e = ele.add_element("gd:reminder")
423
+ e.attributes['minutes'] = reminder[:minutes].to_s if reminder[:minutes]
424
+ if reminder[:method]
425
+ e.attributes['method'] = reminder[:method]
426
+ else
427
+ e.attributes['method'] = 'email'
428
+ end
429
+ end
430
+ end
431
+ end
432
+
433
+ def self.get_instance(service, d)
434
+ if d.is_a? Net::HTTPOK
435
+ xml = REXML::Document.new(d.read_body).root
436
+ if xml.name == 'feed'
437
+ xml = xml.elements.each("entry"){}[0]
438
+ end
439
+ else
440
+ xml = d
441
+ end
442
+ ele = GData4Ruby::Utils::add_namespaces(xml)
443
+ e = Event.new(service)
444
+ e.load(ele.to_s)
445
+ e
446
+ end
447
+ end
448
+ end
449
+
@@ -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