вторник, мая 30, 2006

Acts As Authenticated and Ajax

I've been using AAA-based authentication system and got problems with session expiry. When session is expired action from protected page should redirect to login page. But this doesn't work right if the action is called by Ajax.

The solution is to do the redirect with RJS if it is Ajax request.

Though I don't plan to handle redirects in Ajax request other then to do them, I've changed ActionController::Base.redirect_to to produce RJS redirect for Ajax requests:


def redirect_to(options = {}, *params)
if request.xhr?
render :update do |page|
page.redirect_to url_for(options, *params)
end
else
super
end
end

RJS update and redirects

Recently I've ran into problem with processing redirects with Ajax.
First solution that I've found was using custom status code handler in link_to_remote (and some other functions), like this:


<% link_to_remote "Foo",
:url => { :action => "foo" },
302 => "document.location = request.getResponseHeader('location')" %>

But I've got problems with it:
1. It didn't work for me
2. I find it rather harassing to add 302 => .... to every link_to_remote function (which sometimes are hidden deep inside helper methods)

Second attempt to solve this problem was using RJS redirect_to:


render :update do |page|
page.redirect_to '/some/url'
end

This worked fine but had some unwanted side-effects: when redirect is rendered it appeared on a page inside updated element while browser was preparing to do the redirect.
After some investigation I've found out that the javascript was sent with content-type of 'text/javascript' and plain text (without any <script> tag surrounding). That's why after Prototype's Ajax.Updater inserted it into update element it showed up. Then I dug some Prototype's sources and found out that it can strip <script> tags and evaluates everything inside it (if needed). So, my solution was to alter ActionController::Base.render_javascript
to surround javascript code with <script> tags if it was Ajax request.

Here is the code:


module ActionController
class Base
def render_javascript(javascript, status = nil) #:nodoc:
if @request.xhr?
render_text("<script type="'text/javascript'">#{javascript}</script>", status)
else
@response.headers['Content-Type'] = 'text/javascript; charset=UTF-8'
render_text(javascript, status)
end
end
end
end




I found out that content-type recongnition is implemented in Prototype v1.5something... So there is no need to patch render_javascript anymore...

Just be sure to run "rake rails:update:javascripts"

среда, апреля 05, 2006

ActiveRecord observers and STI

Last couple of days I spent my time trying to figure out, why my AR model observers are not working.

I've got several models (actually, different user types) that shared the same table through Single Table Inheritance. So, I've got User and Member models (where Member inherit from User).

I've also got an observer for User model (a UserObserver) that should send a notification email after User has registered. Then I've made a form for Member registration and was confused by the fact that my observer callbacks didn't get triggered every time I register new user.

After digging Rails internals, I've found out that my understanding of how observers work was not correct: when observer was registered for particular class, it traverses that class and all it's subclasses and applies callback hooks to each of them. The problem was that when observer is applied, my Member model is not loaded yet (and thus it had no callback hooks). So, I put "require 'member'" at the end of my "user.rb" and it worked.

So, it worked first but soon broke. After another digging Rails internals I figured out, that the problem was in class caching. In development mode, reloadable classes (that are controller, model and some other classes) are reloaded every request. BUT, observers are not applied after the model has been reloaded.

Then I went digging Rails on how class reload is accomplished and found out that there were pretty nice schema: ActiveSupport package provided simple dependency loading by implementing custom "const_missing" method. Every time user reference some class that wasn't loaded yet, the "const_missing" is called and that class got loaded.

ActionController also had some dependency tricks like "model ...", "observer ..." etc class methods. So, I set up "observer :user_observer" in the ApplicationController to force observer to be applied on every reload:

class ApplicationController < ActionController::Base
observer :user_observer
end


This would be sufficient if there were no STI thing: UserObserver observer get loaded and reference User model, which is loaded then (but it appears that Member model doesn't registered as User subclass at that time)..

So, after all that the solution was to include "model :user" before "observer :user_observer" in the ApplicationController:

class ApplicationController < ActionController::Base
model :user
observer :user_observer
end