Rails — A faster way for next and previous links on a post, article, or any model

Stamped: 2012-01-12 00:00:00 -0500 | Show comments

This is a simple, but common question I come across when dealing with people learning rails, as they inevitably do a bloggish type app first.

I assume you have a model like the one below

class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.string :title
      t.text :body

      t.timestamps
    end
  end
end

Adding the next/prev links

Personally, I prefer to add instance methods as well as a scope or a class method. Check it out below:

class Article < ActiveRecord::Base
    scope :next, lambda {|id| where("id > ?",id).order("id ASC") } # this is the default ordering for AR
    scope :previous, lambda {|id| where("id < ?",id).order("id DESC") }

    def next
      Article.next(self.id).first
    end

    def previous
      Article.previous(self.id).first
    end

    #### used below to benchmark
    # scope :next_by_date, lambda {|created_at| where("created_at > ?",created_at).order("created_at ASC") } # this is the default ordering for AR
    # scope :previous_by_date, lambda {|created_at| where("created_at < ?",created_at).order("created_at DESC") }

    # def next_by_date
    #  Article.next_by_date(self.created_at).first
    # end

    # def previous_by_date
    #  Article.previous_by_date(self.created_at).first
    # end

end

How it works

If you're confused, don't be! Let's assume we have an array of article ids: [1,2,4,6]. If we're looking for the next article for 2, it would obviously be 4. Programmatically, you can't just say Article.find(id+1), because it won't exist in some situations where articles are deleted.

We have to find the next id above 2, hence where("id > ?", id). However, if you order the ids descending you'll get 6, because 6 is the highest in the list while still being larger than 2. Knowing that, we then sort the list ascending, because 4 will be first on the list when ordering ascending.

This, of course, assumes you're using the instance methods as I did, if you used .last instead, the ordering for each would be reversed.

It's slightly faster than sorting by a date, or any other field not indexed

Since you're sorting by the primary key, it should be much faster than sorting by any other field, as the primary key is usually indexed in most databases. Plus, it's computationally easier to sort an integer, however slightly.

For example:

sql = "INSERT INTO articles (title, body, user_id, created_at) VALUES "
array = []
n = 100000
n.times do |i| 
  array << "('#{i}','#{i}', 1, '#{eval("#{i}.days.ago")}')"
end
sql += array.join(",")
ActiveRecord::Base.connection.execute(sql)
Benchmark.bm do |x|
  x.report { Article.first.next_by_date }
  x.report("by date: ") { Article.first.next_by_date }
  x.report { Article.first.next }
  x.report("by id: ") { Article.first.next }
end

I ran each twice just incase some caching occurred, and the results were:

user     system      total        real
by date:   0.000000   0.000000   0.000000 (  0.017141)
by id:     0.000000   0.000000   0.000000 (  0.000682)

Changing the repetition will demonstrate a linear pattern, say O(n), for finding the next by, whereas by date will demonstrate a more exponential pattern, perhaps nearing O(n^2).

tags: rails

Node.js — Getting oAuth Up and Running Using Express.js and Mongoose

Stamped: 2011-10-10 00:00:00 -0400 | Show comments

Getting Started

If you're interested in how to incorporate this into railwayjs, see this other article instead. I normally use coffeescript, but for the purposes of this article, I converted it to javascript. For the impatient, this project can be found in my github repos.

Let's assume our app.js/server.js looks like this:

var express = require('express');
var app = express.createServer();

app.get('/', function(req, res){
  res.send('hello world');
});

app.configure(function() {
  app.set('views', __dirname + '/app/views');
  app.set('view engine', 'jade');
});

app.listen(3001);

Now, getting oAuth up and running can be easy, but you have to make sure you have mongodb up and running first, since we're using mongoose-auth. You could very well use everyauth, which mongoose-auth is based upon, though it would require a little more effort if you were to change to another database. This is pretty easy: in Ubuntu, you can just apt-get install mongodb, or in osx, brew install mongodb;. If you need to install homebrew for OSX, /usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/gist/323731)".

Working with MongoDB

Let's connect to mongo:

var mongoose = require('mongoose')
  , Schema = mongoose.Schema
  , ObjectId = mongoose.SchemaTypes.ObjectId;
  
mongoose.connect('mongodb://localhost/example');

Incorporate mongoose-auth

To get this working, we have to add the following to our app.js:

var conf = require('./config/oauth_providers');
var UserSchema = new Schema({})
    , User;
var mongooseAuth = require('mongoose-auth');

UserSchema.plugin(mongooseAuth, {
  everymodule: {
    everyauth: {
      User: function() {
        return User;
      }
    }
  },
  facebook: {
    everyauth: {
      myHostname: 'http://local.host:3001',
      appId: conf.fb.appId,
      appSecret: conf.fb.appSecret,
      redirectPath: '/'
    }
  },
  twitter: {
    everyauth: {
      myHostname: 'http://local.host:3001',
      consumerKey: conf.twit.consumerKey,
      consumerSecret: conf.twit.consumerSecret,
      redirectPath: '/'
    }
  },
  github: {
    everyauth: {
      myHostname: 'http://local.host:3001',
      appId: conf.github.appId,
      appSecret: conf.github.appSecret,
      redirectPath: '/'
    }
  }
});

mongoose.model('User', UserSchema);

mongoose.connect('mongodb://localhost/example');

User = mongoose.model('User');

The configuration for this file resides in a different folder, I made a config folder and put the oauth_providers.js file in there, it looks like this:

module.exports = {
  fb: {
    appId: '111565172259433',
    appSecret: '85f7e0a0cc804886180b887c1f04a3c1'
  },
  twit: {
    consumerKey: 'JLCGyLzuOK1BjnKPKGyQ',
    consumerSecret: 'GNqKfPqtzOcsCtFbGTMqinoATHvBcy1nzCTimeA9M0'
  },
  github: {
    appId: '11932f2b6d05d2a5fa18',
    appSecret: '2603d1bc663b74d6732500c1e9ad05b0f4013593'
  },
  instagram: {
    clientId: 'be147b077ddf49368d6fb5cf3112b9e0',
    clientSecret: 'b65ad83daed242c0aa059ffae42feddd'
  },
};

NOTE: these keypairs really shouldn't be made public, but they exist in the mongoose-auth repo, so you can test using them. This also assumes you have something setup in /etc/hosts to map local.host to 127.0.0.1, example: 127.0.0.1 local.host

In addition, we have to configure express a little differently:

app.configure(function() {
  app.set('views', __dirname + '/app/views');
  app.set('view engine', 'jade');
  app.use(express.bodyParser());
  app.use(express.cookieParser());
  app.use(express.session({secret: 'secret'}));
  app.use(mongooseAuth.middleware());
});

mongooseAuth.helpExpress(app);

The above sets up cookies, adds a session key, hooks into mongooseAuth to the app, and exposes everyauth's routes to express' views.

Logging in

Now, just by visiting http://local.host:3000/auth/twitter, for example, and logging in, you'll be redirected back to the homepage. To check to make sure it's working, we have to add some views, make a folder called views and place the following in it named index.jade

- var items = ["facebook", "github", "twitter", "instagram"]
- if (!everyauth.loggedIn)
  h2 Not Authenticated
  each item in items
   a(href='/auth/' + item)
     span Connect with <span style="text-transform: capitalize">!{item}</span><br />

- else
  h2 Authenticated
  #user-id Logged in with `user.id` #{user.id} - aka `everyauth.user.id` #{everyauth.user.id}
  - if (everyauth.facebook)
    h3 Facebook User Data
    p= JSON.stringify(everyauth.facebook.user)
  - if (everyauth.twitter)
    h3 Twitter User Data
    p= JSON.stringify(everyauth.twitter.user)
  - if (everyauth.github)
    h3 GitHub User Data
    p= JSON.stringify(everyauth.github.user)
  - if (everyauth.instagram)
    h3 Instagram User Data
    p= JSON.stringify(everyauth.instagram.user)
  h3
    a(href='/logout') Logout

And don't forget to change the route, to

app.get('/', function(req, res){
    res.render('index');
});

And if you've been following, you'll see that all the twitter metadata is displayed. You're done! There's a lot of other things you can do, like being able to link each different account, whether or not to remember the user, and if anyone needs help with that, I can show you how to do it.

This project can be found in my github repo, expressjs & oauth.

tags: nodejs, oauth, railwayjs, expressjs

Node.js — Getting oAuth Up and Running Using Express.js, Railway.js and Mongoose

Stamped: 2011-10-10 00:00:00 -0400 | Show comments

Let's get started

If you're interested in how to incorporate this without using railwayjs, see this other article instead.

This is assuming you already have a working railwayjs project working. If not:

sudo npm install railway -g
rw i blog && cd blog
npm install -l
rw g crud post title content
# make sure mongodb is running
rw s 3000
open http://localhost:3000/posts

If you need to install mongodb, it's simple. In Ubuntu, you can just apt-get install mongodb, or in osx, brew install mongodb;. If you need to install homebrew for OSX, /usr/bin/ruby -e "$(curl -fsSL https://raw.github.com/gist/323731)".

Setting up the environment

First things first, change your package.json to include mongoose-auth.

{ "name": "railwayjs project"
, "version": "0.0.1"
, "engines": ["node >= 0.4.0"]
, "main": "server.js"
, "dependencies":
  { "jade-ext":  ">= 0"
  , "jade":      ">= 0"
  , "express":          ">= 2.2.2"
  , "connect":          ">= 1.4.2"
  , "railway":          ">= 0.1.6"
  , "yaml":             ">= 0.1.2"
  , "coffee-script":    ">= 1.1.1"
  , "mime":             ">= 1.2.2"
  , "qs":               ">= 0.1.0"
  , "mongoose":         ">= 1.3.6"
  , "mongodb":          ">= 0.9.4-5"
  , "connect-mongodb":  ">= 0.3.0"
  , "mongoose-auth":    ">= 0.0.2"
  }
, "scripts":
  { "test": "nodeunit test/*/*"
  }
}

Next, we're going to change our config/environment.js to look like the following:

var express = require('express');
var mongooseAuth = require('mongoose-auth');

require('./mongoose_oauth');

app.configure(function() {
  var cwd;
  cwd = process.cwd();
  app.set('views', cwd + '/app/views');
  app.set('view engine', 'jade');
  app.use(express.static(cwd + '/public', {
    maxAge: 86400000
  }));
  app.use(express.bodyParser());
  app.use(express.cookieParser());
  app.use(express.session({
    secret: 'secret'
  }));
  // MAKE SURE THIS IS COMMENTED OUT, otherwise it will produce errors that are mostly nonsensical
  //app.use(app.router)
  app.use(express.methodOverride());
  app.use(mongooseAuth.middleware());
});
mongooseAuth.helpExpress(app);

Notice the use of the require statement, require('./mongoose_oauth');, this files contents are here and look like:

var mongoose = require('mongoose')
  , Schema = mongoose.Schema;

var conf = require('./oauth_providers');

var UserSchema = new Schema({})
  , User;

mongooseAuth = require('mongoose-auth');

UserSchema.plugin(mongooseAuth, {
  everymodule: {
    everyauth: {
      User: function() {
        return User;
      }
    }
  },
  facebook: {
    everyauth: {
      myHostname: 'http://local.host:3001',
      appId: conf.fb.appId,
      appSecret: conf.fb.appSecret,
      redirectPath: '/'
    }
  },
  twitter: {
    everyauth: {
      myHostname: 'http://local.host:3001',
      consumerKey: conf.twit.consumerKey,
      consumerSecret: conf.twit.consumerSecret,
      redirectPath: '/'
    }
  },
  github: {
    everyauth: {
      myHostname: 'http://local.host:3001',
      appId: conf.github.appId,
      appSecret: conf.github.appSecret,
      redirectPath: '/'
    }
  }
});
User = mongoose.model('User', UserSchema);
module.exports["User"] = mongoose.model("User");
module.exports["User"].modelName = "User";

Also notice, the configuration file, config/oauth_providers.js, this looks like:

module.exports = {
  fb: {
    appId: '111565172259433',
    appSecret: '85f7e0a0cc804886180b887c1f04a3c1'
  },
  twit: {
    consumerKey: 'JLCGyLzuOK1BjnKPKGyQ',
    consumerSecret: 'GNqKfPqtzOcsCtFbGTMqinoATHvBcy1nzCTimeA9M0'
  },
  github: {
    appId: '11932f2b6d05d2a5fa18',
    appSecret: '2603d1bc663b74d6732500c1e9ad05b0f4013593'
  },
  instagram: {
    clientId: 'be147b077ddf49368d6fb5cf3112b9e0',
    clientSecret: 'b65ad83daed242c0aa059ffae42feddd'
  },
};

NOTE: these keypairs really shouldn't be made public, but they exist in the mongoose-auth repo, so you can test using them. This also assumes you have something setup in /etc/hosts to map local.host to 127.0.0.1, example: 127.0.0.1 local.host

The views

Now, if you were to run your app, rw s 3000, and go to http://local.host:3000/auth/twitter, you would find, after logging in to twitter, it redirects you back to the homepage, as per the mongoose_oauth.js config. But how do you know if you're actually logged in, and the user is being stored in the session?

Well, we have to update a view to contain the following information:

- var items = ["facebook", "github", "twitter", "instagram"]
- if (!everyauth.loggedIn)
  h2 Not Authenticated
  each item in items
   a(href='/auth/' + item)
     span Connect with <span style="text-transform: capitalize">!{item}</span><br />

- else
  h2 Authenticated
  #user-id Logged in with `user.id` #{user.id} - aka `everyauth.user.id` #{everyauth.user.id}
  - if (everyauth.facebook)
    h3 Facebook User Data
    p= JSON.stringify(everyauth.facebook.user)
  - if (everyauth.twitter)
    h3 Twitter User Data
    p= JSON.stringify(everyauth.twitter.user)
  - if (everyauth.github)
    h3 GitHub User Data
    p= JSON.stringify(everyauth.github.user)
  - if (everyauth.instagram)
    h3 Instagram User Data
    p= JSON.stringify(everyauth.instagram.user)
  h3
    a(href='/logout') Logout

And Done!

Now you should see various metadata depending on how you're logged in. All the files above can be found in this gist.

tags: nodejs, oauth, railwayjs, expressjs
recent entries
Migrating from Rails 3.1 RC4 to RC5 using Heroku's Cedar Stack (also compass, unicorn, and sendgrid)
Random Freeze Fix for GTX 460 in 10.6 (osx86)
Wasted on Steam - an analytic tool for the Steam platform
Rails 3.1 — SQL logging to STDOUT during testing (with rspec, test::unit, or cucumber)
Rails 3.1 — Using ERB/HAML/etc within a Coffeescript JS file
Rails 3.1 — 'load_missing_contant': Expected ... to define ... (LoadError)
Rails 3.1 — Testing with remote XML/HTML using Nokogiri, stubs, and rspec
Rails 3.1 — use the jQuery Google Library instead of the default
Rails 3.1 — Fixing the 'ajax:loading' event
Integrating and maintaining a calendar of events within the Symphony CMS
View the entire archive of articles