Class: Tilia::VObject::Component::VCalendar

Inherits:
Document show all
Defined in:
lib/tilia/v_object/component/v_calendar.rb

Overview

The VCalendar component.

This component adds functionality to a component, specific for a VCALENDAR.

Constant Summary

Constants inherited from Document

Document::ICALENDAR20, Document::UNKNOWN, Document::VCALENDAR10, Document::VCARD21, Document::VCARD30, Document::VCARD40

Constants inherited from Node

Node::PROFILE_CALDAV, Node::PROFILE_CARDDAV, Node::REPAIR

Instance Attribute Summary

Attributes inherited from Tilia::VObject::Component

#name

Attributes inherited from Node

#iterator, #parent

Instance Method Summary collapse

Methods inherited from Document

#class_name_for_property_name, #class_name_for_property_value, #create, #create_component, #create_property, #initialize

Methods inherited from Tilia::VObject::Component

#[], #[]=, #add, #children, #components, #delete, #destroy, #initialize, #initialize_copy, #json_serialize, #key?, #remove, #select, #serialize, #to_s, #xml_serialize

Methods inherited from Node

#==, #[], #[]=, #delete, #destroy, #each, #initialize, #json_serialize, #key?, #serialize, #size, #xml_serialize

Constructor Details

This class inherits a constructor from Tilia::VObject::Document

Instance Method Details

#base_component(component_name = nil) ⇒ Component?

Returns the first component that is not a VTIMEZONE, and does not have an RECURRENCE-ID.

If there is no such component, null will be returned.

Parameters:

  • component_name (String) (defaults to: nil)

    filter by component name

Returns:



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/tilia/v_object/component/v_calendar.rb', line 185

def base_component(component_name = nil)
  is_base_component = lambda do |component|
    return false unless component.is_a?(Component)
    return false if component.name == 'VTIMEZONE'
    return false if component.key?('RECURRENCE-ID')
    true
  end

  if component_name
    select(component_name).each do |child|
      return child if is_base_component.call(child)
    end
    return nil
  end

  children.each do |child_group|
    child_group.each do |child|
      return child if is_base_component.call(child)
    end
  end

  nil
end

#base_components(component_name = nil) ⇒ Array<Component>

Returns a list of all ‘base components’. For instance, if an Event has a recurrence rule, and one instance is overridden, the overridden event will have the same UID, but will be excluded from this list.

VTIMEZONE components will always be excluded.

Parameters:

  • component_name (String) (defaults to: nil)

    filter by component name

Returns:



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/tilia/v_object/component/v_calendar.rb', line 146

def base_components(component_name = nil)
  is_base_component = lambda do |component|
    return false unless component.is_a?(Component)
    return false if component.name == 'VTIMEZONE'
    return false if component.key?('RECURRENCE-ID')
    true
  end

  if component_name
    # Early exit
    return select(component_name).select is_base_component
  end

  components = []
  children.each do |child_group|
    do_skip = false
    child_group.each do |child|
      unless child.is_a?(Component)
        # If one child is not a component, they all are so we skip
        # the entire group.
        do_skip = true
        break
      end
      components << child if is_base_component.call(child)
    end
    next if do_skip
  end

  components
end

#by_uid(uid) ⇒ array

Returns all components with a specific UID value.

Returns:

  • (array)


422
423
424
425
426
427
428
429
430
431
432
# File 'lib/tilia/v_object/component/v_calendar.rb', line 422

def by_uid(uid)
  components.select do |item|
    item_uid = item.select('UID')
    if item_uid.empty?
      false
    else
      item_uid = item_uid.first.value
      uid == item_uid
    end
  end
end

#document_typeFixnum

Returns the current document type.

Returns:

  • (Fixnum)


133
134
135
# File 'lib/tilia/v_object/component/v_calendar.rb', line 133

def document_type
  ICALENDAR20
end

#expand(start, ending, time_zone = nil) ⇒ VCalendar

Expand all events in this VCalendar object and return a new VCalendar with the expanded events.

If this calendar object, has events with recurrence rules, this method can be used to expand the event into multiple sub-events.

Each event will be stripped from it’s recurrence information, and only the instances of the event in the specified timerange will be left alone.

In addition, this method will cause timezone information to be stripped, and normalized to UTC.

Parameters:

  • start (Time)
  • end (Time)
  • time_zone (ActiveSupport::TimeZone, nil) (defaults to: nil)

    reference timezone for floating dates and times.

Returns:



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/tilia/v_object/component/v_calendar.rb', line 228

def expand(start, ending, time_zone = nil)
  new_children = []
  recurring_events = {}

  time_zone = ActiveSupport::TimeZone.new('UTC') unless time_zone

  strip_timezones = lambda do |component|
    component.children.each do |component_child|
      if component_child.is_a?(Property::ICalendar::DateTime) && component_child.time?
        dt = component_child.date_times(time_zone)

        # We only need to update the first timezone, because
        # setDateTimes will match all other timezones to the
        # first.
        dt[0] = dt[0].in_time_zone(ActiveSupport::TimeZone.new('UTC'))
        component_child.date_times = dt
      elsif component_child.is_a?(Component)
        strip_timezones.call(component_child)
      end
    end

    component
  end

  children.each do |child|
    if child.is_a?(Property) && child.name != 'PRODID'
      # We explictly want to ignore PRODID, because we want to
      # overwrite it with our own.
      new_children << child.clone
    elsif child.is_a?(Component) && child.name != 'VTIMEZONE'
      # We're also stripping all VTIMEZONE objects because we're
      # converting everything to UTC.

      if child.name == 'VEVENT' && (child.key?('RECURRENCE-ID') || child.key?('RRULE') || child.key?('RDATE'))
        # Handle these a bit later.
        uid = child['UID'].to_s

        fail InvalidDataException, 'Every VEVENT object must have a UID property' if uid.blank?

        if recurring_events.key?(uid)
          recurring_events[uid] << child.clone
        else
          recurring_events[uid] = [child.clone]
        end
      elsif child.name == 'VEVENT' && child.in_time_range?(start, ending)
        new_children << strip_timezones.call(child.clone)
      end
    end
  end

  recurring_events.each do |_uid, events|
    begin
      it = Recur::EventIterator.new(events, time_zone)
    rescue Recur::NoInstancesException
      # This event is recurring, but it doesn't have a single
      # instance. We are skipping this event from the output
      # entirely.
      next
    end

    it.fast_forward(start)

    while it.valid && it.dt_start < ending
      new_children << strip_timezones.call(it.event_object) if it.dt_end > start
      it.next
    end
  end

  self.class.new(new_children)
end

#validate(options = 0) ⇒ array

Validates the node for correctness.

The following options are supported:

Node::REPAIR - May attempt to automatically repair the problem.
Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.

This method returns an array with detected problems. Every element has the following properties:

* level - problem level.
* message - A human-readable string describing the issue.
* node - A reference to the problematic node.

The level means:

1 - The issue was repaired (only happens if REPAIR was turned on).
2 - A warning.
3 - An error.

Parameters:

  • options (Fixnum) (defaults to: 0)

Returns:

  • (array)


326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/tilia/v_object/component/v_calendar.rb', line 326

def validate(options = 0)
  warnings = super(options)

  ver = self['VERSION']
  if ver
    unless ver.to_s == '2.0'
      warnings << {
        'level'   => 3,
        'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
        'node'    => self
      }
    end
  end

  uid_list = {}
  components_found = 0
  component_types = []

  children.each do |child|
    next unless child.is_a?(Component)
    components_found += 1

    next unless ['VEVENT', 'VTODO', 'VJOURNAL'].include?(child.name)

    component_types << child.name

    uid = child['UID'].to_s
    is_master = child.key?('RECURRENCE-ID') ? 0 : 1

    if uid_list.key?(uid)
      uid_list[uid]['count'] += 1
      if is_master == 1 && uid_list[uid]['hasMaster'] > 0
        warnings << {
          'level'   => 3,
          'message' => "More than one master object was found for the object with UID #{uid}",
          'node'    => self
        }
      end
      uid_list[uid]['hasMaster'] += is_master
    else
      uid_list[uid] = {
        'count'     => 1,
        'hasMaster' => is_master
      }
    end
  end

  if components_found == 0
    warnings << {
      'level'   => 3,
      'message' => 'An iCalendar object must have at least 1 component.',
      'node'    => self
    }
  end

  if options & PROFILE_CALDAV > 0
    if uid_list.size > 1
      warnings << {
        'level'   => 3,
        'message' => 'A calendar object on a CalDAV server may only have components with the same UID.',
        'node'    => self
      }
    end

    if component_types.size == 0
      warnings << {
        'level'   => 3,
        'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).',
        'node'    => self
      }
    end

    if component_types.uniq.size > 1
      warnings << {
        'level'   => 3,
        'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).',
        'node'    => self
      }
    end

    if key?('METHOD')
      warnings <<
        {
          'level'   => 3,
          'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.',
          'node'    => self
        }
    end
  end

  warnings
end

#validation_rulesarray

A simple list of validation rules.

This is simply a list of properties, and how many times they either must or must not appear.

Possible values per property:

* 0 - Must not appear.
* 1 - Must appear exactly once.
* + - Must appear at least once.
* * - Can appear any number of times.
* ? - May appear, but not more than once.

It is also possible to specify defaults and severity levels for violating the rule.

See the VEVENT implementation for getValidationRules for a more complex example.

Returns:

  • (array)


315
316
317
318
319
320
321
322
323
# File 'lib/tilia/v_object/component/v_calendar.rb', line 315

def validation_rules
  {
    'PRODID'  => 1,
    'VERSION' => 1,

    'CALSCALE' => '?',
    'METHOD'   => '?'
  }
end