lib/google/event.rb
require 'time'
require 'json'
require 'timezone_parser'
module Google
#
# Represents a Google Event.
#
# === Attributes
#
# * +id+ - The google assigned id of the event (nil until saved). Read Write.
# * +status+ - The status of the event (confirmed, tentative or cancelled). Read only.
# * +title+ - The title of the event. Read Write.
# * +description+ - The content of the event. Read Write.
# * +location+ - The location of the event. Read Write.
# * +start_time+ - The start time of the event (Time object, defaults to now). Read Write.
# * +end_time+ - The end time of the event (Time object, defaults to one hour from now). Read Write.
# * +recurrence+ - A hash containing recurrence info for repeating events. Read write.
# * +calendar+ - What calendar the event belongs to. Read Write.
# * +all_day+ - Does the event run all day. Read Write.
# * +quickadd+ - A string that Google parses when setting up a new event. If set and then saved it will take priority over any attributes you have set. Read Write.
# * +reminders+ - A hash containing reminders. Read Write.
# * +attendees+ - An array of hashes containing information about attendees. Read Write
# * +transparency+ - Does the event 'block out space' on the calendar. Valid values are true, false or 'transparent', 'opaque'. Read Write.
# * +duration+ - The duration of the event in seconds. Read only.
# * +html_link+ - An absolute link to this event in the Google Calendar Web UI. Read only.
# * +raw+ - The full google json representation of the event. Read only.
# * +visibility+ - The visibility of the event (*'default'*, 'public', 'private', 'confidential'). Read Write.
# * +extended_properties+ - Custom properties which may be shared or private. Read Write
# * +guests_can_invite_others+ - Whether attendees other than the organizer can invite others to the event (*true*, false). Read Write.
# * +guests_can_see_other_guests+ - Whether attendees other than the organizer can see who the event's attendees are (*true*, false). Read Write.
# * +send_notifications+ - Whether to send notifications about the event update (true, *false*). Write only.
#
class Event
attr_reader :id, :raw, :html_link, :status, :transparency, :visibility
attr_writer :reminders, :recurrence, :extended_properties
attr_accessor :title, :location, :calendar, :quickadd, :attendees, :description, :creator_name, :color_id, :guests_can_invite_others, :guests_can_see_other_guests, :send_notifications, :new_event_with_id_specified
#
# Create a new event, and optionally set it's attributes.
#
# ==== Example
#
# event = Google::Event.new
# event.calendar = AnInstanceOfGoogleCalendaer
# event.id = "0123456789abcdefghijklmopqrstuv"
# event.start_time = Time.now
# event.end_time = Time.now + (60 * 60)
# event.recurrence = {'freq' => 'monthly'}
# event.title = "Go Swimming"
# event.description = "The polar bear plunge"
# event.location = "In the arctic ocean"
# event.transparency = "opaque"
# event.visibility = "public"
# event.reminders = {'useDefault' => false, 'overrides' => ['minutes' => 10, 'method' => "popup"]}
# event.attendees = [
# {'email' => 'some.a.one@gmail.com', 'displayName' => 'Some A One', 'responseStatus' => 'tentative'},
# {'email' => 'some.b.one@gmail.com', 'displayName' => 'Some B One', 'responseStatus' => 'tentative'}
# ]
# event.extendedProperties = {'shared' => {'custom_str' => 'some custom string'}}
# event.guests_can_invite_others = false
# event.guests_can_see_other_guests = false
# event.send_notifications = true
#
def initialize(params = {})
[:id, :status, :raw, :html_link, :title, :location, :calendar, :quickadd, :attendees, :description, :reminders, :recurrence, :start_time, :end_time, :color_id, :extended_properties, :guests_can_invite_others, :guests_can_see_other_guests, :send_notifications].each do |attribute|
instance_variable_set("@#{attribute}", params[attribute])
end
self.visibility = params[:visibility]
self.transparency = params[:transparency]
self.all_day = params[:all_day] if params[:all_day]
self.creator_name = params[:creator]['displayName'] if params[:creator]
self.new_event_with_id_specified = !!params[:new_event_with_id_specified]
end
#
# Sets the id of the Event.
#
def id=(id)
@id = Event.parse_id(id) unless id.nil?
end
#
# Sets the start time of the Event. Must be a Time object or a parse-able string representation of a time.
#
def start_time=(time)
@start_time = Event.parse_time(time)
end
#
# Get the start_time of the event.
#
# If no time is set (i.e. new event) it defaults to the current time.
#
def start_time
@start_time ||= Time.now.utc
(@start_time.is_a? String) ? @start_time : @start_time.xmlschema
end
#
# Get the end_time of the event.
#
# If no time is set (i.e. new event) it defaults to one hour in the future.
#
def end_time
@end_time ||= Time.now.utc + (60 * 60) # seconds * min
(@end_time.is_a? String) ? @end_time : @end_time.xmlschema
end
#
# Sets the end time of the Event. Must be a Time object or a parse-able string representation of a time.
#
def end_time=(time)
@end_time = Event.parse_time(time)
end
#
# Returns whether the Event is an all-day event, based on whether the event starts at the beginning and ends at the end of the day.
#
def all_day?
time = (@start_time.is_a? String) ? Time.parse(@start_time) : @start_time.dup.utc
duration % (24 * 60 * 60) == 0 && time == Time.local(time.year,time.month,time.day)
end
#
# Makes an event all day, by setting it's start time to the passed in time and it's end time 24 hours later.
# Note: this will clobber both the start and end times currently set.
#
def all_day=(time)
if time.class == String
time = Time.parse(time)
end
@start_time = time.strftime("%Y-%m-%d")
@end_time = (time + 24*60*60).strftime("%Y-%m-%d")
end
#
# Duration of the event in seconds
#
def duration
Time.parse(end_time) - Time.parse(start_time)
end
#
# Stores reminders for this event. Multiple reminders are allowed.
#
# Examples
#
# event = cal.create_event do |e|
# e.title = 'Some Event'
# e.start_time = Time.now + (60 * 10)
# e.end_time = Time.now + (60 * 60) # seconds * min
# e.reminders = { 'useDefault' => false, 'overrides' => [{method: 'email', minutes: 4}, {method: 'popup', minutes: 60}, {method: 'sms', minutes: 30}]}
# end
#
# event = Event.new :start_time => "2012-03-31", :end_time => "2012-04-03", :reminders => { 'useDefault' => false, 'overrides' => [{'minutes' => 10, 'method' => "popup"}]}
#
def reminders
@reminders ||= {}
end
#
# Stores recurrence rules for repeating events.
#
# Allowed contents:
# :freq => frequence information ("daily", "weekly", "monthly", "yearly") REQUIRED
# :count => how many times the repeating event should occur OPTIONAL
# :until => Time class, until when the event should occur OPTIONAL
# :interval => how often should the event occur (every "2" weeks, ...) OPTIONAL
# :byday => if frequence is "weekly", contains ordered (starting with OPTIONAL
# Sunday)comma separated abbreviations of days the event
# should occur on ("su,mo,th")
# if frequence is "monthly", can specify which day of month
# the event should occur on ("2mo" - second Monday, "-1th" - last Thursday,
# allowed indices are 1,2,3,4,-1)
#
# Note: The hash should not contain :count and :until keys simultaneously.
#
# ===== Example
# event = cal.create_event do |e|
# e.title = 'Work-day Event'
# e.start_time = Time.now
# e.end_time = Time.now + (60 * 60) # seconds * min
# e.recurrence = {freq: "weekly", byday: "mo,tu,we,th,fr"}
# end
#
def recurrence
@recurrence ||= {}
end
#
# Stores custom data within extended properties which can be shared or private.
#
# Allowed contents:
# :private => a hash containing custom key/values (strings) private to the event OPTIONAL
# :shared => a hash containing custom key/values (strings) shared with others OPTIONAL
#
# Note: Both private and shared can be specified at once
#
# ===== Example
# event = cal.create_event do |e|
# e.title = 'Work-day Event'
# e.start_time = Time.now
# e.end_time = Time.now + (60 * 60) # seconds * min
# e.extended_properties = {'shared' => {'prop1' => 'value 1'}}
# end
#
def extended_properties
@extended_properties ||= {}
end
#
# Utility method that simplifies setting the transparency of an event.
# You can pass true or false. Defaults to transparent.
#
def transparency=(val)
if val == true || val.to_s.downcase == 'transparent'
@transparency = 'transparent'
else
@transparency = 'opaque'
end
end
#
# Returns true if the event is transparent otherwise returns false.
# Transparent events do not block time on a calendar.
#
def transparent?
@transparency == "transparent"
end
#
# Returns true if the event is opaque otherwise returns false.
# Opaque events block time on a calendar.
#
def opaque?
@transparency == "opaque"
end
#
# Sets the visibility of the Event.
#
def visibility=(val)
if val
@visibility = Event.parse_visibility(val)
else
@visibility = "default"
end
end
#
# Convenience method used to build an array of events from a Google feed.
#
def self.build_from_google_feed(response, calendar)
events = response['items'] ? response['items'] : [response]
events.collect {|e| new_from_feed(e, calendar)}.flatten
end
#
# Google JSON representation of an event object.
#
def to_json
attributes = {
"summary" => title,
"visibility" => visibility,
"transparency" => transparency,
"description" => description,
"location" => location,
"start" => time_or_all_day(start_time),
"end" => time_or_all_day(end_time),
"reminders" => reminders_attributes,
"guestsCanInviteOthers" => guests_can_invite_others,
"guestsCanSeeOtherGuests" => guests_can_see_other_guests
}
if id
attributes["id"] = id
end
if timezone_needed?
attributes['start'].merge!(local_timezone_attributes)
attributes['end'].merge!(local_timezone_attributes)
end
attributes.merge!(recurrence_attributes)
attributes.merge!(color_attributes)
attributes.merge!(attendees_attributes)
attributes.merge!(extended_properties_attributes)
JSON.generate attributes
end
#
# Hash representation of colors
#
def color_attributes
return {} unless color_id
{ "colorId" => "#{color_id}" }
end
#
# JSON representation of colors
#
def color_json
color_attributes.to_json
end
#
# Hash representation of attendees
#
def attendees_attributes
return {} unless @attendees
attendees = @attendees.map do |attendee|
attendee.select { |k,_v| ['displayName', 'email', 'responseStatus'].include?(k) }
end
{ "attendees" => attendees }
end
#
# JSON representation of attendees
#
def attendees_json
attendees_attributes.to_json
end
#
# Hash representation of a reminder
#
def reminders_attributes
if reminders && reminders.is_a?(Hash) && reminders['overrides']
{ "useDefault" => false, "overrides" => reminders['overrides'] }
else
{ "useDefault" => true}
end
end
#
# JSON representation of a reminder
#
def reminders_json
reminders_attributes.to_json
end
#
# Timezone info is needed only at recurring events
#
def timezone_needed?
is_recurring_event?
end
#
# Hash representation of local timezone
#
def local_timezone_attributes
tz = Time.now.getlocal.zone
tz_name = TimezoneParser::getTimezones(tz).last
{ "timeZone" => tz_name }
end
#
# JSON representation of local timezone
#
def local_timezone_json
local_timezone_attributes.to_json
end
#
# Hash representation of recurrence rules for repeating events
#
def recurrence_attributes
return {} unless is_recurring_event?
@recurrence[:until] = @recurrence[:until].strftime('%Y%m%dT%H%M%SZ') if @recurrence[:until]
rrule = "RRULE:" + @recurrence.collect { |k,v| "#{k}=#{v}" }.join(';').upcase
@recurrence[:until] = Time.parse(@recurrence[:until]) if @recurrence[:until]
{ "recurrence" => [rrule] }
end
#
# JSON representation of recurrence rules for repeating events
#
def recurrence_json
recurrence_attributes.to_json
end
#
# Hash representation of extended properties
# shared : whether this should handle shared or public properties
#
def extended_properties_attributes
return {} unless @extended_properties && (@extended_properties['shared'] || @extended_properties['private'])
{ "extendedProperties" => @extended_properties.select {|k,_v| ['shared', 'private'].include?(k) } }
end
#
# JSON representation of extended properties
# shared : whether this should handle shared or public properties
#
def extended_properties_json
extended_properties_attributes.to_json
end
#
# String representation of an event object.
#
def to_s
"Event Id '#{self.id}'\n\tStatus: #{status}\n\tTitle: #{title}\n\tStarts: #{start_time}\n\tEnds: #{end_time}\n\tLocation: #{location}\n\tDescription: #{description}\n\tColor: #{color_id}\n\n"
end
#
# Saves an event.
# Note: make sure to set the calendar before calling this method.
#
def save
update_after_save(@calendar.save_event(self))
end
#
# Deletes an event.
# Note: If using this on an event you created without using a calendar object,
# make sure to set the calendar before calling this method.
#
def delete
@calendar.delete_event(self)
@id = nil
end
#
# Returns true if the event will use quickadd when it is saved.
#
def use_quickadd?
quickadd && id == nil
end
#
# Returns true if this a new event.
#
def new_event?
new_event_with_id_specified? || id == nil || id == ''
end
#
# Returns true if notifications were requested to be sent
#
def send_notifications?
!!send_notifications
end
private
def new_event_with_id_specified?
!!new_event_with_id_specified
end
def time_or_all_day(time)
time = Time.parse(time) if time.is_a? String
if all_day?
{ "date" => time.strftime("%Y-%m-%d") }
else
{ "dateTime" => time.xmlschema }
end
end
protected
#
# Create a new event from a google 'entry'
#
def self.new_from_feed(e, calendar) #:nodoc:
params = {}
%w(id status description location creator transparency updated reminders attendees visibility).each do |p|
params[p.to_sym] = e[p]
end
params[:raw] = e
params[:calendar] = calendar
params[:title] = e['summary']
params[:color_id] = e['colorId']
params[:extended_properties] = e['extendedProperties']
params[:guests_can_invite_others] = e['guestsCanInviteOthers']
params[:guests_can_see_other_guests] = e['guestsCanSeeOtherGuests']
params[:html_link] = e['htmlLink']
params[:start_time] = Event.parse_json_time(e['start'])
params[:end_time] = Event.parse_json_time(e['end'])
params[:recurrence] = Event.parse_recurrence_rule(e['recurrence'])
Event.new(params)
end
#
# Parse recurrence rule
# Returns hash with recurrence info
#
def self.parse_recurrence_rule(recurrence_entry)
return {} unless recurrence_entry && recurrence_entry != []
rrule = /(?<=RRULE:)(.*)(?="\])/.match(recurrence_entry.to_s).to_s
rhash = Hash[*rrule.downcase.split(/[=;]/)]
rhash[:until] = Time.parse(rhash[:until]) if rhash[:until]
rhash
end
#
# Set the ID after google assigns it (only necessary when we are creating a new event)
#
def update_after_save(response) #:nodoc:
return if @id && @id != ''
@raw = JSON.parse(response.body)
@id = @raw['id']
@html_link = @raw['htmlLink']
end
#
# A utility method used to centralize parsing of time in json format
#
def self.parse_json_time(time_hash) #:nodoc
return nil unless time_hash
if time_hash['date']
Time.parse(time_hash['date']).utc
elsif time_hash['dateTime']
Time.parse(time_hash['dateTime']).utc
else
Time.now.utc
end
end
#
# A utility method used to centralize checking for recurring events
#
def is_recurring_event? #:nodoc
@recurrence && (@recurrence[:freq] || @recurrence['FREQ'] || @recurrence['freq'])
end
#
# A utility method used centralize time parsing.
#
def self.parse_time(time) #:nodoc
raise ArgumentError, "Start Time must be either Time or String" unless (time.is_a?(String) || time.is_a?(Time))
(time.is_a? String) ? Time.parse(time) : time.dup.utc
end
#
# Validates id format
#
def self.parse_id(id)
if id.to_s =~ /\A[a-v0-9]{5,1024}\Z/
id
else
raise ArgumentError, "Event ID is invalid. Please check Google documentation: https://developers.google.com/google-apps/calendar/v3/reference/events/insert"
end
end
#
# Validates visibility value
#
def self.parse_visibility(visibility)
raise ArgumentError, "Event visibility must be 'default', 'public', 'private' or 'confidential'." unless ['default', 'public', 'private', 'confidential'].include?(visibility)
return visibility
end
end
end