Visit More Parks React and Rails App

Posted by Aaron Parkening on February 29, 2020

Wouldn’t it be great if you could see National Parks that are close to places you’re already visiting? My React and Rails student project, Visit More Parks, scans your upcoming Google Calendar events, geocodes locations for those events, and suggests parks within100 miles of each destination. Taking advantage of the suggestions, the app also makes it easy to add a park visit to your calendar.

What It Does

  • The user logs in with their Google Calendar account.
  • US National Parks are pre-loaded with geocoded coordinates and addresses (using the NPS.gov Parks API and the Geocoder Ruby gem).
  • Upcoming calendar events are geocoded and displayed with their nearby parks.
  • The user can add visits to any of the parks as calendar events.
  • Those new events include park information, such as description, address, and URL.
  • All 497 National Parks are browsable.

Why

As a final student project, I wanted to try integrating and manipulating multiple data sources within a single cohesive app. I also wanted to allow users to interact with their own Google information outside of a Google-specific app.

The National Park Service has an HTTP-based API that returns robust, straightforward JSON data. Google Calendar has a complex API, involving persistent authorization tokens, multiple interaction methods, and variability within returned data. That’s a lot to tackle!

Finding the Right Calendar API

The latest, fastest, and best-supported method of using Google’s Calendar API is with Google’s robust JavaScript browser client, also known as gapi. The requirements for my project specified a Rails backend to persist data and a React frontend that minimally manipulated data, so using JavaScript/React as middleware to manipulate Google Calendar data and send it to a Rails server didn’t make much sense. What I really wanted was a way for Rails to directly grab and process all the data and simply supply React with some easy JSON feeds.

Luckily, Google also created a Ruby API client. It’s not as robustly documented as the Javascript browser client, but it is still officially maintained for big fixes and does have some documentation. Woohoo!

Persistent Authentication

My excitement quickly wore off when I realized the Ruby API and documentation applied to all Google services. I found it difficult to glean what was relevant to my situation, especially regarding authentication credentials between requests.

Combing through a ton of online resources, I found a couple articles that outlined helpful approaches to refreshing authorization: Google OAuth for Ruby On Rails and Using The Google API Ruby Client with Google Calendar API. It turns out the key to successful authorization was to initialize a new Google Calendar each time my controller created, read, updated, or destroyed a calendar event. So I created some helper methods patterned after the calendar_list approach from Google OAuth for Ruby On Rails.

  # Start and authorize new calendar service
  def start_google_service
    # Initialize Google Calendar API
    service = Google::Apis::CalendarV3::CalendarService.new
    
    # Use google keys to authorize
    service.authorization = google_secret.to_authorization
    
    # Request new access token in case it expired
    service.authorization.refresh!

    return service
  end

  # Tokens and client env variables
  def google_secret
    Google::APIClient::ClientSecrets.new(
      { "web" =>
        { "access_token" => current_user.google_token,
          "refresh_token" => current_user.google_refresh_token,
          "client_id" => ENV['GOOGLE_CLIENT_ID'],
          "client_secret" => ENV['GOOGLE_CLIENT_SECRET']
        }
      }
    )
  end

Interacting with Google Calendar

Once I understood how to authenticate each Google Calendar request, I needed to actually create, read, update, and destroy events. The official examples only covered a couple interaction scenarios and were also confusingly different between the GitHub versions and the website API versions.

After unsuccessful attempts to expand the canned interaction examples, I dove directly into the API client code to understand the available methods and their expected parameters. The service file provided the gold I needed: all the interaction methods and code comments listing their required parameters!

Using my newfound knowledge, I was able to create additional helper methods to grab existing event lists and format Google-friendly events.

# Return hash of Google events
def get_google_events
  calendar = start_google_service
    
  # Set calendar ('primary' is main Google account)
  calendar_id = "primary"

  # Get up to 1000 events in calendar
  events = calendar.list_events(
    calendar_id,
    max_results: 1000,
    single_events: true,
    order_by: "startTime",
    time_min: DateTime.now.rfc3339
  )
    
  # Convert event list into hash
  return JSON.parse(events.to_json)
end
  
# Return event hash in Google-friendly format  
def format_google_event(event)
  Google::Apis::CalendarV3::Event.new(
    summary: event.title,
    location: event.location,
    description: event.description,
    start: Google::Apis::CalendarV3::EventDateTime.new(
      date_time: event.start_time,
      time_zone: event.timezone
     ),
     end: Google::Apis::CalendarV3::EventDateTime.new(
       date_time: event.end_time,
       time_zone: event.timezone
    )
  )
end

Using the authentication and events helpers, my index, create, update, and delete methods became nicely straightforward:

# events_controller.rb

require 'google/api_client/client_secrets.rb'
require 'google/apis/calendar_v3'	
class Api::V1::EventsController < ApplicationController
  before_action :authenticate

  # All events
  def index
    # Only keep locations that have comma (indicates city, state)
    location_hash = get_google_events["items"].select{|event| event["location"] && event["location"].include?(",")}

    ### Sample output
    # location_names = location_hash.each{|e| puts e["summary"] +" - "+ e["location"]}
    # location_name: Boston Trip - Boston, MA

    # Add array of parks within 100 miles to each location
    events_and_parks = location_hash.each{|event| event["nearParks"]= Park.near(event["location"], 100).as_json}

    # Return events with parks
    render json: { events: events_and_parks }
  end

  # Create record
  def create
    event = current_user.events.build(event_params)

    # If event can save to database, also send to Google Calendar
    if event.save

      # Start Google calendar
      calendar = start_google_service

      # Format event for Google
			g_event = format_google_event(event)
			
			# Add event to Google Calendar
      result = calendar.insert_event('primary', g_event)

			# Add Google Calendar id to database
      event.g_cal_id = result.id
      event.save

      # Render json
      render json: {event: event}
    else
      resource_error
    end
  end

  # Update record
  def update
    event = Event.find(params[:id])
    event.update(event_params)
    
    # If event can save, also send to Google Calendar
    if event.save

      # Start Google calendar
      calendar = start_google_service

      # Format event for Google
      g_event = format_google_event(event)

			# Update Google Calendar event
      result = calendar.update_event('primary', event.g_cal_id, g_event)

      # Return event id
      render json: {event: event}
    else
      resource_error
    end
  end

  # Delete record
  def destroy
    event = Event.find(params[:id])
    
    # Only event owner can delete
    authorize_resource(event)
    
    # Start Google calendar
    calendar = start_google_service

    # Delete Google event
    result = calendar.delete_event('primary', event.g_cal_id)

    # Delete database event
    event.destroy

    # Render json
    render json: { event: event.id}
  end
  
  private
  
  ### helper methods

end

Finding Answers in Articles and Code

Reading about other people’s experiences and directly diving into the Ruby code was much more valuable than slogging through the confusing official Google Calendar API documentation. Where the documentation lacked, external resources illustrated robust approaches and gave me hope that my own app could work. And reading the API methods provided the needed interaction requirements while showing me the product wasn’t nearly as complicated as it seemed. Now users can seamlessly interact with Google Calendar through my app!