northworld/google_calendar

View on GitHub
lib/google/event.rb

Summary

Maintainability
C
1 day
Test Coverage
A
93%
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