This article walks you through the process of creating your very own Where Are My Friends web application with Ruby on Rails.
Introduction
I've been working on a project involving Facebook authentication and embedding "likes" and "posts", as well as mapping locations of crowdfunding projects onto Google Maps. A few days ago, I thought, gee, what if I could map my Facebook friends locations on Google Maps. Turns out this has already been done, but that doesn't stop me from wanting to learn how it's done! This article walks you through the process of creating your very own "Where Are My Friends" web application with Ruby on Rails.
Prerequisites for Ruby on Rails Development in Windows
Since we're mostly Windows developers on Code Project, let's stay in the Windows platform environment. To do this, you'll need to:
- Download and install RailsInstaller
- Download and install a decent IDE, namely RubyMine (there's a 30 day trial version, but it's quite affordable and quite excellent)
Prerequisites for Facebook Development
You'll need to create an app from your Facebook account. To do this (note that the UI might change over time), log in to Facebook and, from the "gear" pulldown, select "Create App":
Create an application, providing:
- a unique app namespace
- set the Site Domain to "localhost"
- set the Site URL to http://localhost:3000/
- Sandbox mode should be enabled
Once you've completed the process, you will be provided with an App ID and App Secret. You'll need these for obtaining a user access token later on.
Prequisites for Querying Facebook For Testing Purposes
A user access token is needed to obtain the friend location and hometown, which we use to map where our friends are. For testing purposes, we can acquire this token manually, but beware that it expires every two hours, so you will have to repeat this step if you get a "token expired" error.
Go to https://developers.facebook.com/tools/explorer.
Then click on Get Access Token. A popup will appear, from which you should select "Friends Data Permissions" and then check "friends_hometown" and "friends_location":
Click on Get Access Token, which closes the dialog and now your access token will be displayed in the Graph
API Explorer. You can copy this access token into your code for temporary access when testing the application.
Test the Query
While we're here, we might as well test the query we'll be using. Click on "FQL Query" and enter:
SELECT uid, name, pic_square, current_address, current_location,
hometown_location FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 =
me())
Then click the Submit button. You should see an array returned, and if your friends entered information as to where they are living and/or their hometown and make this information public, then you should see something along the lines of:
We'll be parsing these records later in Ruby.
Prerequisites for Connecting to Facebook
Windows is lacking SSL information which will result in an SSL authentication failure. To correct this problem, follow exactly these instructions:
- Download http://curl.haxx.se/ca/cacert.pem into c:\railsinstaller\cacert.pem
- Go to your Computer -> Advanced Settings -> Environment Variables
- Create a new System Variable:
- Variable:
SSL_CERT_FILE
- Value: C:\RailsInstaller\cacert.pem
For some reason, it originally took me three tries to get this right.
Prerequisites for GitHub
This part is optional. The source code for this project is available on GitHub at WhereAreMyFriends. If you want to set up your own GitHub project, this is what I did:
- If you don't already have a GitHub account, create one
- Create a new repository with a readme so you can immediately clone the repository
- From the command line, clone the repository into the desired folder. I cloned mine using "git clone https://github.com/cliftonm/WhereAreMyFriends"
- You can also simply clone my repository and use the code that I wrote, but you won't be able to commit any changes back to me. For that, you'd need to fork my repository and then issue "pull requests" if you want me to incorporate some changes in my project that you've made.
Prerequisites for Using Git
This is optional for two reasons: you can use the Git command line or you can use RubyMine's Git integration for working with the repository. Personally, I prefer to use a separate visual tool such as SmartGit/Hg, which I've found to be the best of the various visual tools for Git.
Getting Started
We've got a few housekeeping things to take care of before we get started with actual coding.
Creating the Initial Rails App
If you've cloned my repository, ignore this step, as you can simply open the directory in RubyMine.
If you're starting from a blank slate because you want to walk through how I wrote this app, then you'll need to create a Rails app. Again, from the command line, go to the parent directory into which you create the directory and contents for the project. If you've cloned a blank repository from GitHub, don't worry, just do this as well.
From the command line, type in "rails new WhereAreMyFriends
" (or, if you gave your project a different name, use that name.) This will create all the pieces for a Ruby on Rails application.
In the RubyMine
IDE, you should now see something like this when you open the directory:
If you've cloned a Git repository, RubyMine should already be configured to use Git as the VCS. Personally, I much prefer using SmartGit/Hg, but you should know that RubyMine has built-in Git support.
We Want Git to Ignore RubyMine Files
We don't want all the RubyMine IDE files to be part of the repository, so open the ".gitignore" file (in the application's root folder) and add:
/.idea
which excludes the entire .idea folder that RubyMine creates.
Components (Gems) We'll be Using
We need to pull in a few components, so edit the Gemfile (in root of your application folder), adding:
gem 'gmaps4rails'
gem 'fql'
gem 'slim'
gem 'thin'
Once the Gemfile is updated, click on the Tools menu is RubyMine, then select "Bundler..." then "Install", then click on the Install button (leaving the optional arguments blank.) This installs the gems and any dependencies that they have.
What are all these gems?
gmaps4rails
This is the gem for interfacing to Google Maps (as well as others, such as OpenLayers, Bing, and Mapquest).
fql
This gem supports using the Facebook Query Language in Ruby, which I use to query the locations of my friends. There are other options as well and other techniques for querying Facebook, but this is the approach I've chosen.
The Facebook FQL reference documentation can be found here.
slim
This gem, from the website: "is a template language whose goal is [to] reduce the syntax of essential parts without becoming cryptic." I find it makes HTML a lot more readable, de-cluttering the angle brackets, closing tags, etc. There's a great online utility for converting HTML to slim for here.
thin
This gem is a much faster web server than the default, which is WEBrick.
Add All the Pieces Needed for Gmaps
The gmaps4rails gem includes an installer that adds all the JavaScript and CSS that you need for actually displaying a map. To do this, open a command line prompt and cd to your application folder. Then type:
rails generate gmaps4rails:install
Create the Page Controller
While we're on the command line, let's create the controller and view. Type:
rails generate controller map_my_friends index
This creates:
- the file "map_my_friends_controller.rb" in the app\controllers folder.
- the folder "map_my_friends" in the app\views folder.
- the file "index.html.erb" in the app\views\map_my_friends folder.
Delete the "index.html.erb" file and create a new file called "index.html.slim", so that we're using the slim HTML syntax rather than straight HTML.
You should see something like this now in your project tree:
Setting the Root Route
Finally, before we do anything else, let's set the root route to this page, so we can get to it simply from "localhost:3000". Edit the routes.rb file (in the config folder), adding:
root to: "map_my_friends#index"
Note that when we created the page controller, the route:
get "map_my_friends/index"
was automatically added for us.
Start Coding!
Now we're ready to do some coding. First, we're going to create a basic model, "Friend
", to hold the information about our friends. We could do this with a generator similar to how we created the controller, but because it's not a vanilla solution, I prefer to simply create the file manually.
Create the 'Friend' Model
In RubyMine, under the app\models folder, create the file "friend.rb":
class Friend < ActiveRecord::Base
acts_as_gmappable
# Fields we get from FB
attr_accessible :uid, :name, :pic, :address
# Fields required by gmaps4rails (lat and long also come from FB)
attr_accessible :gmaps, :latitude, :longitude
# gmaps4rails methods
def gmaps4rails_address
address
end
def gmaps4rails_infowindow
"#{name}"
end
end
The line "acts_as_gmappable
" is a hook that generates latitude and longitude data when an address is persisted. While it wasn't my intention to even have a persisting Friend model, the gmaps4rails gem is somewhat coupled with the Rails ActiveRecord and the five minutes I spent Googling and playing around trying to decouple it, without success, was five minutes more than I wanted to spend on the issue, so as a result, we have a persistable Friend model.
Create the 'Friend' Table
No model is usually complete without its associated table, so we will use a database migration to create the table. Since we're using sqlite3 as the database, there's no need to futz around with database authentication issues, database servers, etc.
In RubyMine, in the "db" folder, create a sub-folder called "migrate", and in that folder, create a file called "001_create_friends_table.rb":
class CreateFriendsTable < ActiveRecord::Migration
def change
create_table :friends do |t|
t.string :uid
t.string :name
t.string :pic
t.string :address
t.float :latitude
t.float :longitude
t.boolean :gmaps
t.timestamps
end
end
end
Your project tree should now reflect these two new files:
Now, run the migration by pressing Ctrl+F9, or right-clicking on the migration and from the popup menu selecting "run db:migrate".
Create Our Facebook Library
A "standard" practice in Ruby on Rails code is to put directly into the controller all the code that's needed to render a page. So, typically, you would see the code that queries Facebook and populates the model either in the controller or in the model. Personally, I prefer to put this kind of code into the lib folder and provide helper methods to interface to whatever model supports the necessary fields. I've read some articles that disagree with my on this point, saying that all business logic should go in the model. The problem as I see it is that there is application-model-independent business logic (as in, agnostic business logic), such as how we interface with Facebook, that shouldn't go into the application's model because it is agnostic.
However, because Rails does not auto-load the code in the lib folder, we have to coerce it. Also note that files in the lib folder are don't automatically cause the server to reload the Ruby script, so you'll have to restart the server if you make changes in the lib folder's files.
First, edit the application.rb file found in the app\config folder, adding the line:
config.autoload_paths += %W(#{config.root}/lib/facebook_wrapper)
which tells Rails we specifically want to include files found in this folder.
Second, in the lib folder, create a sub-folder called "facebook_wrapper".
Third, create a file in that sub-folder called "facebook_wrapper.rb".
Your project structure should now look like this:
Now we're going to wrap our class, FacebookWrapper
, in a module called FacebookWrapperModule
:
module FacebookWrapperModule
class FacebookWrapper
def ... my functions ...
end
end
and implement the following functions.
get_fb_friends
This function returns an array of friends in Facebook structure:
def self.get_fb_friends
options = {access_token: "[your access token goes here]"}
friends = Fql.execute("SELECT uid, name, pic_square, current_address,
current_location, hometown_location FROM user WHERE uid IN (
SELECT uid2 FROM friend WHERE uid1 = me())", options)
friends
end
We will fix the hardcoded access token later - for the moment, we just want to get something up and running.
from_fb_friends
This function converts the Facebook friends array into an array of our model instances, which we callback to the application to create each model instance. Because Ruby is a duck-typing language, all the application needs to do is implement attributes (properties) or methods for the attributes we expect to initialize - we don't need to know the "type" or implement this as an interface, as we would in C#. Furthermore, by utilizing the callback capability of Ruby, we can request that the application instantiates its model instance itself:
def self.from_fb_friends(fb_friends)
friends = []
fb_friends.each do |fb_friend|
location = get_location_or_hometown_address(fb_friend)
if !location.nil? # or: unless location.nil?
friend = yield(fb_friend, location)
friends << friend
end
end
friends
end
Another Ruby-ism is to use the "unless
" keyword rather than "if !
" (if not), which I personally find reduces the readability of the code. I have no problem with negative logic, and saying "unless location.nil?
" requires me to do mental gyrations back to "if locations does not equal nil
."
get_location_or_hometown_address
Lastly, we have a private helper method for getting the address information from either the friend's location (preferable) or their hometown (a fallback):
private
def self.get_location_or_hometown_address(fb_friend)
location = fb_friend["current_location"]
if location.nil?
location = fb_friend["hometown_location"]
end
location
end
Note that we never explicitly use the "return
" keyword. There's a reason for that, which I'll illustrate next.
Update the Controller to Pass Along the Location Information
Next, we'll update the map_my_friends_controller.rb file, the controller for our index. The first thing we need to do is reference our facebook_wrapper
library helper. This reveals some of the intricacies of Ruby's module and file handling.
First, we need to tell Ruby that we "require" the facebook_wrapper.rb file (which it knows how to get because we added lib\facebook_wrapper to the auto_load
config paths):
require 'facebook_wrapper'
Then, we need to tell Ruby that we want to use the objects defined in the FacebookWrapperModule
:
include FacebookWrapperModule
If we don't do this, we have to qualify the objects with "FacebookWrapperModule::
". The include
keyword is similar to the using
keyword in C#, and the module
keyword is similar to the namespace
keyword. The only thing new here is the dynamic loading of a dependent file facebook_wrapper.rb.
The implementation for the index
method gathers the arrays and provides the callback method for populating a model (Friend
) instance for each instance of a Facebook structure, and finally that array is formatted as JSON and passed back to the client:
class MapMyFriendsController < ApplicationController
def index
fb_friends = FacebookWrapper.get_fb_friends
@friends = FacebookWrapper.from_fb_friends(fb_friends) { |fb_friend, location|
friend = Friend.new
friend.uid = fb_friend["uid"]
friend.name = fb_friend["name"]
friend.pic = fb_friend["pic_square"]
friend.address = location["name"]
friend.latitude = location["latitude"]
friend.longitude = location["longitude"]
friend.gmaps = true
friend
}
@json = @friends.to_gmaps4rails
respond_to do |format|
format.html # index.html.erb
format.json { render json: @friends }
end
end
end
Note that we do not call return friend
in the callback code - if we do this, it's treated as a return from the calling code and the @friends
property is never initialized!
Getting the View to Render the Map
Edit the index.html.slim file (in the app\views\map_my_friends folder), adding this one line as the entire contents of the file:
= gmaps4rails(@json)
Test This
If you run the application (with a current user access token), you should see your friends mapped onto a Google map:
Making the Map Bigger
However, what we'd to do is make the map bigger, so that it takes up most of a full-screen browser window (I do everything in full screen which is why I like this). To do this, replace the line that we created above with:
= gmaps( :map_options => { :container_class => "my_map_container" },
"markers" => {"data" => @json,
"options" => {"auto_zoom" => false} })
and edit the map_my_friends.css.scss (found in the app\assets\stylesheets folder), adding:
div.my_map_container {
margin-top: 30px;
padding: 6px;
border-width: 1px;
border-style: solid;
border-color: #ccc #ccc #999 #ccc;
-webkit-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
-moz-box-shadow: rgba(64, 64, 64, 0.5) 0 2px 5px;
box-shadow: rgba(64, 64, 64, 0.1) 0 2px 5px;
width: 80%;
height: 80%;
margin-left:auto;
margin-right:auto;
}
div.my_map_container #map {
width: 100%;
height: 100%;
}
Refresh the browser and you will get bigger map which sizes based on the browser window.
Adding Some Pizzazz to the Thumbnail Popup
First, we'll add profile_url
to the FQL query that we're using, and adjust our model and controller accordingly. We also need to add a migration to add this field to the Friend
table:
class AddProfileUrlField < ActiveRecord::Migration
def change
add_column :friends, :profile_url, :string
end
end
Next, we provide some HTML to render in the Google Maps info window:
def gmaps4rails_infowindow
"<p><a href = '#{profile_url}' target='_blank'>#{name}</a>
<br>#{address}<br><img src = '#{pic}'/></p>"
end
and the result is:
showing us
- the person's name as a clickable link to their Facebook profile that opens in a new window,
- their location/hometown, and
- their Facebook picture.
Omniauth-Facebook
Now that we have a basic application running, let's deal with a different set of complexity which will also resolve the pesky user access token expiration problem. The issue is this - rather than gathering our friends, we need authorization to gather the friends of anyone that visits our site, which means that we'll need the ability for users to log in using their Facebook login and authorize us to query their data.
Add omniauth-facebook to the Gemfile
First, add:
gem 'omniauth-facebook'
to the Gemfile found in your root folder. The gems we've now added to the Gemfile for this project are:
gem 'gmaps4rails'
gem 'fql'
gem 'slim'
gem 'thin'
gem 'omniauth-facebook'
Create a User Model
This time, let's create the user model with the model generator. From RubyMine's Tool menu, select "Run Rails Generator" then double-click on "model". Enter the options for the rails generator:
User provider:string uid:string name:string email:string oauth_token:string
This creates a new migration file, so find it under db\migrate, right click on it and select "Run 'db:migrate'
"
In the newly create User model, add the following code:
def self.create_with_omniauth(auth)
create! do |user|
user.provider = auth.provider
user.uid = auth.uid
user.oauth_token = auth.credentials.token
if auth.info
user.name = auth.info.name || ""
user.email = auth.info.email || ""
end
end
end
This code creates a user in the database with the provided authentication information.
Notice that we have access here to the user access token, which is saved in the field oauth_token
.
Setup Authentication
In the config\initializers folder, create the file omniauth.rb with the contents:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'],
:scope => 'friends_location, friends_hometown, user_friends, email',
:display => 'popup'
end
This informs omniauth that we're authenticating with Facebook. Notice the "scope
" key, whose values are friends_location
and friends_hometown
, which specifies that we're interested in the location and hometown of our friends, and we need user_friends
so that we can get the friends of the Facebook user.
Setup Environment Variables
Personally, I don't like environment variables - I would rather use a file that isn't stored in the Git repository. Previously, I've used a local_env.yml file to programmatically add items to the ENV collection:
Edit the application.rb file (located in the config folder) and add:
config.before_configuration do
env_file = File.join(Rails.root, 'config', 'local_env.yml')
YAML.load(File.open(env_file)).each do |key, value|
ENV[key.to_s] = value
end if File.exists?(env_file)
end
This code adds additional items to the ENV
collection. Now we need to create the file. In the config folder, create the file local_env.yml, whose contents are:
FACEBOOK_KEY: '[your key]'
FACEBOOK_SECRET: '[your secret id]'
Make sure that when you put in your key and secret ID, that you preserve the single quotes.
Also, add config/local_env.yml to your .gitignore file -- this prevents the file from being added to your repository.
Create the Sessions Controller
In the app\controllers folder, create the file sessions_controller.rb, whose contents are:
class SessionsController < ApplicationController
def new
redirect_to '/auth/facebook'
end
def create
auth = request.env["omniauth.auth"]
user = User.where(:provider => auth['provider'],
:uid => auth['uid']).first || User.create_with_omniauth(auth)
session[:user_id] = user.id
redirect_to root_url, :notice => "Signed in!"
end
def destroy
session[:user_id] = nil
redirect_to root_url, notice: 'Signed out!'
end
end
This handles three routes:
- new - simply redirects to the Facebook authentication page
- create - performs the sign in, registering the user in the database if the uid is unique for this provider (being Facebook)
- destroy - signs out from Facebook and removes the user's UID from the database, forcing the user to sign in again (useful for testing and getting a new authentication token)
Create the Necessary Routes
Add the following routes to the routes.rb file (in the config folder):
match '/auth/:provider/callback' => 'sessions#create'
match '/signout' => 'sessions#destroy'
match '/signin' => 'sessions#new'
Change the User Access Token to Use the oauth_token
In our map_my_friends_controller
, we're going to pass in the access token, which we acquire from the database with the user's id stored when the session was created:
def index
user_id = session[:user_id]
@friends = []
if !user_id.nil?
oauth_token = User.find(user_id).oauth_token
@friends = get_friends(oauth_token)
end
@json = @friends.to_gmaps4rails
respond_to do |format|
format.html # index.html.erb
format.json { render json: @friends }
end
end
Notice that I separated out the get_friends
code into a separate function. Another thing you'll often see in a lot of Ruby on Rails code is very long functions with code blocks that really should be broken out. It's easy to write the code the "wrong" way because you're dealing with specific, isolated route handler functions, but it makes things a lot less readable and maintainable.
And in facebook_wrapper.rb:
def self.get_fb_friends(oauth_token)
options = {access_token: oauth_token}
friends = Fql.execute("SELECT uid, name, pic_square, current_address,
current_location, hometown_location,
profile_url FROM user WHERE uid IN (SELECT uid2
FROM friend WHERE uid1 = me())", options)
friends
end
Update the Application View with Sign In / Sign Out
In our application view (common to all pages), we want to provide:
- sign in / sign out notification messages
- a sign in / sign out button
- the name of the user signed in
We might as well convert this file to a "slim" file as well, so delete application.html.erb (in the app\views\layouts folder), and replace it with the slim file "application.html.slim":
Edit the application.html.erb file (in the app\views\map_my_friends folder), inserting at the top of the file:
doctype
html
head
title Where Are My Friends
= stylesheet_link_tag "application", media: "all"
= javascript_include_tag "application"
= csrf_meta_tag
body
#container
#user_nav
- if current_user
| Signed in as
|
strong= current_user.name
|
= link_to "Sign out", signout_path
- else
= link_to "Sign in with Facebook", signin_path
- flash.each do |name, msg|
= content_tag :div, msg, id: "flash_#{name}"
= yield
= yield :scripts
Access the User Record from our View
To access the current_user
that we used above, we'll add a helper function to application_controller.rb (in the app\controllers folder):
class ApplicationController < ActionController::Base
protect_from_forgery
private
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
end
There's also a bunch of CSS that I'm not showing. The result is now a usable website:
Try It Out Live!
The application is hosted here: http://wherearemyfriends.herokuapp.com/ give it a try!
Caveats
If your friends haven't set a current location or hometown or if this information is blocked, they won't show up on the map. I also don't distinguish between current location and hometown - that would be something nice to do with a different marker. So there's a couple things I'll get around to at some point and update the article.
Also, it's interesting working with a Facebook app. For example, if I want my housemate to try out the site after I've signed in, while I can sign out from my site, I also need to sign out from Facebook (by going to Facebook!) and only then will I get the Facebook sign in so my housemate can sign in with her Facebook username and password.
Acknowledgements
I'm indebted specifically to the following people who have no idea that they helped me put all this together! This, of course, omits all the people that have put in countless hours writing Ruby, Rails, and all these amazing gems.
History
- 7th October, 2013: Initial version