Musings of a Flatiron Ruby Student

A collection of musings

Allowing Temporary Anonymous Users

Mixing in Temporary Anonymous Users with Omniauth

On a site I made with some fellow students, world-view.today, we allowed users who logged in with Twitter to customize the site by adding in their own cities. The log in was created using omniauth 2 and basically followed the steps outlined in fellow classmate Randall’s blog, substituting twitter for github.

One of the improvements we really wanted to make was to allow non logged in users to add in cities, a task that was not as daunting as we anticipated.

Our site before anonymous users

Our cities controller would associate a new city with a user whenever a logged in user added a city. Our user table simply stored the active recorded created user_id, created_at, updated_at, as well as name, provider, uid, and image. The provider column represented the provider used by omniauth, which in our case was always Twitter. The uid column was the unique id returned by the oath provider, twitter in our case, and the image was the Twitter profile image of the user.

A city_users join table kept the user city relationships.

Our site relies heavily on javascript and ajax calls so that it never has to load another page after the initial page load. Thus, our controllers would pass JSON objects, containing the cities objects, info on a users logged in status, uid’s of saved items, ect.., back to our front end javascript.

Adding temporary anonymous users

Searching Google for creating users just based on a temporary basis using omniauth didn’t yield any results so we set off to figure it out on our own.

We settled on using the session_id that rails automatically creates and stores in a cookie for any visitor on your site. This id string is unique for any given visitor and thus we used this session_id as the unique identifier to store in the user’s uid column.

The first step in implementing this logically fell with our cities controller which handled the task of adding new cities and associating them with the current user. (Technically probably best to move this functionality into the model) When a new city was passed to the cities controller Create method via an ajax post request, the first thing the method did was set an @user variable:

1
@user = User.find(session[:user_id]) if session[:user_id]

The session[:user_id] was a variable the sessions controller would add to the session cookie after a successful twitter oauth negotiation, aka only logged in users would have it. This variable also corresponds to the user_id assigned to a user when it is created with active record.

The first step was to create the temp user when the controller received a new city and the current user was not logged in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if !@user #create a temp user tied to session ID
  @user = User.create(name: 'Guest', provider: 'anon', uid: session[:session_id], image: 'https://origin.ih.constantcontact.com/fs197/1110193228531/img/301.jpg?a=1115291249439')
  @user.cities << [City.find(1), City.find(2), City.find(3), City.find(4), City.find(5)]
  session[:user_id] = @user.id
end

unless @user.cities.find_by(lat: city_params[:lat], lon: city_params[:lon])
 #above checks if the user already has this city and rejects the addition if so
  @user.cities << @city if @user

 #the below handles deleting a city, which is mostly beyond the scope of this post. 
  del_city_id = city_params[:lastClock].to_i

  CityUser.find_by(user_id: @user.id, city_id: del_city_id).destroy if @user
  render json: @city
else
  render json: "this city already exists".to_json
end

The user would immediately get shoveled in the 5 default cities, allowing them to be treated as a normal user that should always have exactly 5 cities. Whenever a new city is added, the last city is deleted and the new city is added as the first.

Also note we set provider to anon. This allowed us to know this was a temporary user account. We also added to this temp user’s session cookie the user_id variable that now corresponded to their user_id in our users’ table.

Overall this allowed us to treat temporary users that could be treated like a real user without having to change much code.

When a temp user isn’t a real user

There were some distinctions that we decided would be practical to keep. We didn’t want our temp user’s to be able to save content from within each of their cities and these temp users still needed the option to actually log in with twitter.

These distinctions actually fit within our code base pretty well. Basically we wanted to treat a temp user as not logged in except for purposes of tracking which cities they had.

Rails was keeping track of whether or not a user was logged in via a helper method in the application controller called logged_in?

1
2
3
4
5
6
7
helper_method def logged_in?
    !!current_user
end

helper_method def current_user
    @current_user ||= User.find(session[:user_id]) if session[:user_id]
end

We added a simple conditional check that made it so logged_in? would only return true if the provider did not equal anon in addition to their being a user_id var in the session.

1
2
3
helper_method def logged_in?
    !!current_user && @current_user.provider != "anon"
  end

On the front end side we created a similar global variable called loggedIn that would get set and update as needed via ajax requests. The rails users controller sets the value of this variable based on checking the users logged in status.

This meant we had a temp user whose cities would persist, but for all other purposes they would be treated as logged out.

Transitioning a temp user to a twitter authenticated user

To make the user experience as seamless as possible we wanted to make it so that a temp user who customized their cities and then logged in to our site with their twitter account for the first time would be able to keep their cities.

To do this we modified the Sessions Controller’s Create method which we had originally made when implementing OmniAuth with Twitter. This route is called as the twitter oauth callback and thus is invoked right after someone authenticates with twitter.

We added the third nested if that checks to see if the session contains a user_id. This was a perfect test for finding temp users who then authenticate with twitter because only a temp user or a fully logged in user would have a session user_id; however, a user who is already logged in would never be routed to this create method, an oauth callback route.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def create
  if auth_hash.present?
    if !@user = User.find_by_provider_and_uid(auth_hash[:provider], auth_hash[:uid]) #ie a new twitter auth user. 
      if session[:user_id] #ie if was a temp user & this is first time authing with twitter then replace their temp account uid, provider, name and image with this new auth hash info. 
        @user = User.find(session[:user_id])
        @user.uid = auth_hash[:uid]
        @user.provider = auth_hash[:provider]
        @user.name = auth_hash[:info][:name]
        @user.image = auth_hash[:info][:image]
        @user.save
      else
        @user = User.create_from_omniauth(auth_hash)
        @user.cities << [City.find(1), City.find(2), City.find(3), City.find(4), City.find(5)]
      end
    end

    if @user
      session[:user_id] = @user.id
      redirect_to root_url
    else
      redirect_to root_url
    end
  else
    redirect_to root_url, :notice => "Please login via twitter"
  end
end

This third nested if sets the @user variable to the temp user if it finds a session user_id. It then replaces the uid of that user with the twitter provided oauth uid and also replaces the provider, name and image provided by the twitter authentication. Effectively this converts the temp user to a twitter authenticated, persistent world-view user who retains the cities they selected as a temp user.

If a user has authenticated with twitter on our site before but they come back as an anonymous temp user, when they re-authenticate with twitter, their information from their temp account is not passed on to their previous user.

A database full of temp users

One slight draw back to our temp user approach is that our database fills up with temp users who will never be accessed after a temp users session cookie is lost or expires.

On solution to this is to create a cron job that periodically checks for users with provider anon who were created over 30 days ago and then purges them.

Getting Geolocation in Ruby / Sinatra

Getting Location in Ruby

It is now very common to determine a user’s location by using HTML5 Geolocation. Below, I’ll discuss how to pass this geolocation information to a Ruby Sinatra environment by using the post method and some JavaScript.

While the HTML5 geolocation is great, its not available if you are only operating in a command line interface (CLI). Thus, I’d like to first point out a way to get location when your ruby program is command line only.

Location in CLI Only Environment

This is a very simple way to get surprisingly accurate (at least in my few tests in NYC) location information using the IP of the computer running the Ruby app, Nokogiri web scraping, and one’s IP address.

You will need gem ‘nokogiri’ and to require ‘open-uri’

1
2
3
4
5
6
7
8
def location
  page = "http://freegeoip.net/json/"
  doc = Nokogiri::HTML(open(page, 'User-Agent' => 'ruby'))
  loc = /(latitude)(\":)(\d+.\d+)(,\"longitude\":)(-\d+.\d+)/.match(doc.text)
  lat = loc[3]
  lon = loc[5]
  return [lat,lon]
end

This will query the site http://freegeoip.net/json which in turn determines your public IP address and matches it against a database. Nokogiri then scrapes this result and then I use a Ruby regex match object to parse out the latitude and a longitude. Finally I return an array containing latidue and longitude.

I found that http://freegeoip.net/json was fairly accurate. There is a gem called geocoder that seems to use the same database but it seemed to do a lot more and required active record. When I created this, I already was using nokogiri in the project so this was an easy add on and just seemed simpler.

Location using HTML5 in a Sinatra environment

This describes how to use HTML5 Geolocation and javascript to send your longitude / latitude to the Sintra server using a http post request.

Overview

It starts by loading a page that says getting location and requests the user’s location from their browser. Once it has a location, it loads a google map on the page showing it. While loading the map, javascript also fills in 2 hidden form values, one for latitude and the other longitude.

The user then clicks the Submit Location button which will do a post request to the server at the address specified in the form’s action attribute. Sinatra can then grab lat and long from the parmas variable and render a new page using erb or preform a redirect.

I’ll note that there may be a better way to do the following but this method seemed good enough and was within the scope of my current knowledge.

Step 1

This methods assume you are running a Ruby Sintra web based frame work.

First, you would set a get controller for your page that will request the location from the user’s browser using html. I used by root index page for this.

1
2
3
get '/' do
    erb :location
end

Step 2

Next, you set up an ERB template that includes a hidden form. Once the user’s browser responds to the geolocation request, this hidden form will be filled by JavaScript with the user’s longitude and latitude.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!DOCTYPE html>
<html>
<body>

<p id="demo">Getting your location... </p>
<div id="mapholder"></div>

<script src="http://maps.google.com/maps/api/js?sensor=false"></script>

<script>
getLocation();
var x = document.getElementById("demo");

function getLocation() {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(showPosition,showError);
    } else {
        x.innerHTML = "Geolocation is not supported by this browser.";}
}

function showPosition(position) {

    lat = position.coords.latitude;
    lon = position.coords.longitude;
    document.getElementById('lat').value = lat;
    document.getElementById('lon').value = lon;

    latlon = new google.maps.LatLng(lat, lon)
    mapholder = document.getElementById('mapholder')
    mapholder.style.height='250px';
    mapholder.style.width='500px';

    var myOptions={
    center:latlon,zoom:14,
    mapTypeId:google.maps.MapTypeId.ROADMAP,
    mapTypeControl:false,
    navigationControlOptions:{style:google.maps.NavigationControlStyle.SMALL}
    }

    var map = new google.maps.Map(document.getElementById("mapholder"),myOptions);
    var marker = new google.maps.Marker({position:latlon,map:map,title:"You are here!"});


}

function showError(error) {
    switch(error.code) {
        case error.PERMISSION_DENIED:
            x.innerHTML = "User denied the request for Geolocation."
            break;
        case error.POSITION_UNAVAILABLE:
            x.innerHTML = "Location information is unavailable."
            break;
        case error.TIMEOUT:
            x.innerHTML = "The request to get user location timed out."
            break;
        case error.UNKNOWN_ERROR:
            x.innerHTML = "An unknown error occurred."
            break;
    }

}
</script>
<form id="myForm" action='/go' method="post">
  <input type='hidden' id="lat" name='lat' value='' />
  <input type='hidden' id="lon" name='lon' value='' />
  <input type='submit' name='submit' value='Submit Location' />
</form>

</body>
</html>

In the showPosition(position) function above the following JavaScript code causes the hidden form at the bottom to be filled with the user’s longitude and latitude values.

1
2
document.getElementById('lat').value = lat;
document.getElementById('lon').value = lon;

Step 3

Setup up a post controller that receives the post request from the form submission.

1
2
3
4
5
6
post '/go' do
    @lat = params[:lat]
    @lon = params[:lon]
    erb :index

end

The parmas var is automatically passed to the post method in Sinatra and you can then extract the lat and lon by calling the symbol version of their names as they were labled in the form. You can then pass the location info to another method or make them directly available to the erb template.

Using both HTML5 Geolocation with an IP based fallback

You can use both the methods described above, reserving the IP based geolocation for situations in which HTML5 Geolocation fails. While you could probably do this with Javascript and error handling, I achieved this by passing the location to a Ruby class method. By checking if the lat or lon paramaters were nil, I was able to conditionally fall back to the IP based lookup.

1
2
3
4
5
6
7
8
9
10
11
def self.set_location (lat,lon)
    if lat.nil?|| lon.nil? #if html5 location isn't passed, revert to this method
      page = "http://freegeoip.net/json/"
      doc = Nokogiri::HTML(open(page, 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36'))
      loc = /(latitude)(\":)(\d+.\d+)(,\"longitude\":)(-\d+.\d+)/.match(doc.text)
      @lat = loc[3]
      @lon = loc[5]
    else
      @lat = lat
      @lon = lon
    end

HTML5 Geolocation code based on http://www.w3schools.com/ templates

The Shoveled Hash

‘Where’s My Shovel’ asks the Hash

As a typical Ruby student, I became acquainted with arrays before hashes. The array shovel, aka << really made sense to me and once I moved on to Hashes I found it very disconcerting that hashes were left shovelless.

Now that my studies have moved into classes and duck punching I decided I would try and give my hashes some shovels and see what became of it.

A hash without a shovel

If you ever attempted to use a shovel with a hash, the ruby interpreter greets you with:

1
2
{:a => "one", :b => "two", :c => "three"} << {:d => "three"}
=>NoMethodError: undefined method `<<' for {:a=>"one", :b=>"two", :c=>"three"}:Hash

In the above scenario, the main ways to add the {:d => "three"} to the existing hash would be to use merge! or store

merge!:

1
2
{:a => "one", :b => "two", :c => "three"}.merge!({:d => "three"})
 => {:a=>"one", :b=>"two", :c=>"three", :d=>"three"}

store:

1
2
3
4
5
6
7
8
test = {:a => "one", :b => "two", :c => "three"}
 => {:a=>"one", :b=>"two", :c=>"three"}

test.store(:d, "three")
 => "three"

test
 => {:a=>"one", :b=>"two", :c=>"three", :d=>"three"}

Despite the verbosity of the above methods, they also have the affect of replacing a key’s value if you merge! or store using a key that already exists in the hash.

A hash with a shovel is a beautiful thing

1
2
{:a => "one", :b => "two"} << {:c => "three"}
=> {:a=>"one", :b=>"two", :c=>"three"}

The above seems so simple and elegant. Taking it further, what if the key you shovel already exists? Wouldn’t it be nice if the << method took the existing value in the hash and put it into an array and then added the passed values? I think so.

1
2
{:a => "one", :b => "two"} << {:b => "an additional b value!!"}
=> {:a=>"one", :b=>["two", "an additional b value!!"]}

What if the the key exists and it has a value that is already an array? Wouldn’t it be nice if the shoveled value was justed added to the array?

1
2
{:a => "one", :b => ["two", 2]} << {:b => "an additional b value!!"}
=> {:a=>"one", :b=>["two", 2, "an additional b value!!"]}

Making the above a reality – duck punching the Hash class

Achieving the above can be done by adding to / duck punching the Hash class. Defining a class Hash at the top of your ruby script is the simplest way to achieve this. The ruby interpretor will read from this version of the Hash class before looking up the main Hash class included with ruby. Any new methods added will exist in addition to the original Hash class methods.

A very simple implementation

1
2
3
4
5
class Hash
  def <<(a)
    self.merge!(a)
  end
end

With this simple class method added to Hash class you can now shovel an existing hash with another hash passed in the {} format. The ruby interpreter allows you to do hash1 << hash2 whereas most other methods would require the ., ie hash1 << hash2 is the same as hash1.<< hash2. In the above the (a) is the passed parameter given after the <<

1
2
{:a => "alpha"} << {:b => "bravo"}
 => {:a=>"alpha", :b=>"bravo"}

Done with the baby steps

While the above is great, the << method needs to grow a bit to give me the full functionality I described above (plus more!) Below is my full Hash class duck punch, followed by some explanations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Hash
  def <<(a, b=nil)
    if a.class == Hash
      passed_key = a.first[0]
      passed_value = a.first[1]
    end
    if a.class == Array
      passed_key = a[0]
      passed_value = a[1..-1] if a.length > 2 #more than 2 values in passed array means pass_value becomes an array of all the values after [0]
      passed_value = a[1] if a.length == 2 # only 2 values in array so first must be key and 2nd must be value
    end
    if !b.nil? #if a second argument is passed, treat it as first being the key and second being the value
      passed_key=a
      passed_value = b
    end

    #IMPLEMENTATION 
    if self[passed_key].nil?
      self.merge!({passed_key => passed_value})
    else #if the key passed exists, then take current value of key and put it in array w/ passed valued
      temp = self[passed_key] #grab current value of existing key
        if temp.class == Array #if the existing value is already an array then add to that array
          self[passed_key] << passed_value
        else
          self[passed_key] = [temp,passed_value] #create an array with original value and passed value
      end
    end

    return self
  end

  def self.array
    Hash.new {|hash, key| hash[key] = []}
  end

end
The optional b parameter

I added an optional parameter b which allows you to pass 2 parameters, the first being the key and the second being the value. Unfortunately, ruby does not let you use the space << shortcut explained above when passing two parameters. Instead, the only way to pass this second parameter (as far as I know) is using the hash1.<< key,value.

1
2
{:a => "one", :b => "two"}.<< :c,"three"
=> {:a=>"one", :b=>"two", :c=>"three"}

The rest of this new << method expects only one parameter, thus the space << space can be used.

The conditional class checks at the top

The initial if statements determine the class type of the parameter passed and break it into a key and a value assigned to passed_key and passed_value respectively.

The array method conditional allows you to pass a single parameter as an array containing a key with one or more values. The first element of the array will be treated as the key and the subsequent ones will be values. If there are multiple values passed they are kept in an array.

1
2
{:a => "one", :b => "two"} << [:c, "three"]
=> {:a=>"one", :b=>"two", :c=>"three"}
The implementation

The first step under #IMPLEMENTATION is to see if the hash that is being added to already has a key with the same name as the key bing passed. When accessing a non existent hash key, the ruby interpreter will return nil. Thus if self[passed_key].nil? equals true, then the key does not exist in the original hash and a simple merge! can be used.

If the key does exist, then the existing value is put into an array and the passed value is added to that array. If the existing value is already an array then the passed value is added to the array.

1
2
 {:a => "one", :b => "two", :c => ["three", "additional value"]} << {:c => "a third value"}
=> {:a=>"one", :b=>"two", :c=>["three", "additional value", "a third value"]}

The final bit

1
2
3
def self.array
    Hash.new {|hash, key| hash[key] = []}
end

This method is a little different from the main topic of the post. However, while I was ducking punching the hash class I figured I’d add in a simple but very useful functionality that also relates to the <<. This functionality is based on fellow classmate Peter’s blog post regarding initializing a hash with empty array values.

Instead of having to type out a = Hash.new {|hash, key| hash[key] = []} I can just type a = Hash.array. Both of these initialize an empty hash with a default value of an empty array. This allows you to add to the hash by specifying a new or existing key and using a shovel to add to the array.

1
2
3
4
5
6
7
8
a = Hash.array
=> {}

a[:new_key] << "Value to be added to value array"
=> ["Value to be added to value array"]

a
=> {:new_key=>["Value to be added to value array"]}

Practicality

It is admittedly not convenient to add this lengthy duck punch at the top of your scripts. You could put this code in a separate file and include it, but not only does every person running your ruby code also need the class modification, doing this may create confusion since it is not part of the normal ruby Hash class.

With that said, this new method does add functionality that comports with the style already used for << and arrays, thus giving a code reader contextual clues that strongly hint at the methods functionality. Additionally, it is my hope that this experiment provides a basis for the consideration and discussion of possibly seeing how a shovel could be introduced as an official method to the Hash class.

Tested using Ruby Version 2.1.2