Thursday 16 September 2010

licenceable

I've been working quite a bit with Devise over the last week or so. I'm rewriting something that used to be a desktop application and turning it into a web app. The users currently have a seat-based licencing arrangement - 5 users, 10 users etc. Keeping this arrangement in a web application has proved a bit tricky. There are several posts in the devise group that suggest this is a fairly common requirement, but no obvious solutions. So here's my first hack at it - I've simply added one more method call to the resource inside the DatabaseAuthenticatable authenticate! method


require 'devise/strategies/authenticatable'

module Devise
module Strategies
# Default strategy for signing in a user, based on his email and password in the database.
class DatabaseAuthenticatable < Authenticatable
def authenticate!
resource = valid_password? && mapping.to.find_for_database_authentication(authentication_hash)

if resource && resource.licenced? && validate(resource){ resource.valid_password?(password) }
resource.after_database_authentication
success!(resource)
else
fail(:invalid)
end
end


end
end
end

Warden::Strategies.add(:database_authenticatable, Devise::Strategies::DatabaseAuthenticatable)



Then in the resource model - in this case the User model - I can have a licenced? method that checks whether the user has a licence to access the system. The method looks like this:



def licenced?
not_logged_in? and has_licence?
end



Next I added a 'last_signed_out_at' field to the resource record and override the destroy method in the Devise::SessionsController.


def destroy
current_user.update_attribute(:last_signed_out_at, Time.now) rescue nil
set_flash_message :notice, :signed_out if signed_in?(resource_name)
sign_out_and_redirect(resource_name)
end



On its own this is not enough to check whether the user is logged in, because they may just have closed the browser or their session may have timed out. We also need the fields added by the Trackable and Timeoutable modules. Together these changes allow us to write a not_logged_in? method in the User model


def not_logged_in?
ok = current_sign_in_at.nil? || last_request_at.nil?
ok = ok || last_request_at.to_i < Time.now.to_i - Devise.timeout_in.to_i rescue nil
ok= ok || last_signed_out_at > current_sign_in_at rescue nil
ok
end


Finally we need to decide whether the user is licenced. This will vary depending on the application of course. In our case, an event can have any number of users but only licence_count users can be logged in at the same time, so in the User model



def has_licence?
event.logged_in_users < event.licence_count
end

And finally, we need to know how many users are logged in for a given event. In our case an event has_many users, and in the Event model I just iterate through all the users and count the ones that are signed in


def logged_in_users
users.inject(0){|count, u|
count+= 1 unless u.not_logged_in?
}
end


If I can find the time and there is any interest I will try and bundle this up into a stand-alone extension but for now just putting it out there ...

Wednesday 25 August 2010

Testing with devise

Today I ran across a problem getting tests to pass with Devise. There were in fact two issues. First, in rails 3 the Gemfile does not stipulate the order in which gems are loaded and this can mess things up a bit. Especially with mocha, which I use for mocking and stubbing.

This post describes the issue.

To solve the problem I put

require 'mocha'
Bundler.require(:test)
include Devise::TestHelpers



at the end of test/helper.rb

But the sets still didn't pass ....

The other problem to which the solution was more obvious when I actually thought about it was because I am using confirmable. So the setup needed to be:

def setup
@user = users(:one)
sign_in @user
@user.confirm!
end

Friday 20 August 2010

i18n in rails 3

Well it's been a long time, but I'm going to make an effort to do more blogging now that rails 3 is almost here - partly to keep track of what I need to remember - but also because it might be useful to others.

I'm upgrading a big app to rails 3 before we go live with it - and of course there are a few glitches. Mostly these are to do with gems and plugins that don't work - more on them when I get to them. But the first big hiccup was internationalization. The app I'm upgrading is fully internationalized - but when I got the front page up i got things like

translation missing: 'en'::character varying, devise, sessions, user, signed_in

instead of the nice translations i was expecting.

The cause seems to lie in the I18n gem - where it gets the configuration object


class << self
# Gets I18n configuration object.
def config
Thread.current[:i18n_config] ||= I18n::Config.new
end


I patched this by dropping a file in the lib directory and requiring it from application.rb

module I18n
class << self
def config
# don't pick up the config object from the current thread -
# it returns 'en'::character varying
I18n::Config.new
end
end
end



My guess is that this might slow things down a touch - creating a new config object instead of using an existing one - but it works for me so far