Thursday, September 15, 2011

Rails 3 Custom Validator Quandary -- Solution Step One

Remember when I had a Rails 3 custom validator quandary? My bottom-line question was "how should I access a specific error condition, cleanly, in both an action, and a template?" I sketched a few solutions, ranging from checking for a specific error message to providing a function that indicates if the error has occurred.

For now, I want simple boolean methods on the model to encapsulate the error condition. I don't want to write them by hand; I want the validator itself to dictate the interface. For example, if I'm validating an "email" property that can only repeat n times on a given day, I'd like to have an email_repeats_error? method available in the model, returning what we'd expect.

This will be developed in two steps: first, as a simple library (makes tweaking easier, at least for me). Second, as a standalone gem (the way it should be). Naturally, this led to a brief WTF spike as I couldn't figure out how to get the library to expose the validator properly.

Loading the Library

Rails 3.0 changed how libraries are loaded, at least a little bit. I did two things to get things loading from an arbitrarily-named file in the /lib directory. First in my application.rb file, I added the /lib directory to the autoload path.

module RepeatsValidatorWork
  class Application < Rails::Application
    config.autoload_paths += %W(#{config.root}/lib)
    # auto-require; see next code fragment.
    # ... etc ...

Second, I require the library manually since... well, I'm not 100% sure I understand why, although this blog entry pointed me in the right direction. For testing purposes, this is fine--it might not be a great idea in general. Once it's in a gem, it won't matter anyway.

    config.autoload_paths.each do |path|
      Dir["#{path}/*.rb"].each do |file|
        puts "Auto-requiring #{file}..."
        require file
      end
    end

The Validator Proper

We'll work a bit backwards to get to the final implementation--starting with the familiar and common, ending with tasty snacks.

I'd like to configure the validator as below. For now, we'll just use a lambda to determine how to count the repeats--in the future we might want shortcuts for common use cases. All mine have been different so far, so a lambda it is. We'll pass it the same things a custom validator normally gets (see next code block).

class Item < ActiveRecord::Base
  validates :name,
            :presence => true,
            :repeats => {
              :upto => 2,
              :how_count => lambda { |rec, attr, val|
                Item.count(:conditions => ['name=?', val])
              }
            }
end

The validator is an ActiveModel::EachValidator subclass. We'll save the "upto" and "how_count" options in the class's initialize method. Since we'll care which model attributes are using the validator, we'll save that before calling super: Rails initializes that option, and plucks it out again in the base class. If we want 'em, we gotta act fast.

      def initialize(options)
        @attributes = options[:attributes]
        super
        @how_count = options[:how_count]
        @upto = options[:upto]
      end

The validate_each method uses a saved copy of the "upto" option, runs the "how_count" lambda, and compares the values.

      def validate_each(record, attr, value)
        err = false
        if @how_count.call(record, attr, value) >= @upto
          record.errors[attr] << (options[:message] \
                                  || "repeats too much")
          err = true
        end
        record.instance_variable_set \
            "@#{attr}_repeats_error", err
      end

The last line of the method is important: it sets an instance property to true or false depending on whether or not we had a repeats violation. Now we just need to access that in an attribute-specific method for every attribute that uses this validator.

The final bit of pseudo-magic occurs in our class's setup method. Turns out that Rails validation internals call a setup method in each of the registered validators that defines one. (Discovered while looking through Rails source--I can't emphasize the usefulness of reading through source code.) The argument is the class of the model--if that sounds handy, you're on to something.

      def setup(clazz)
        clazz.class_eval @attributes.inject("") {|s, attr| s += <<END
          def #{attr}_repeats_error?
            @#{attr}_repeats_error
          end
END
      end

This code is less-dense than it seems (it seems dense to me, I guess). In a nutshell, we're looping over our saved attributes (in @attributes) and using inject to build up a string containing everything between the two upper-case ENDs (except for the closing bracket). (Didn't know that was legal syntax.) If it makes it easier to visualize, think of it like this:

      def plain_ol_setup(clazz)
        s = ""
        @attributes.each do |attr|
          s += """
          def #{attr}_repeats_error?
            @#{attr}_repeats_error
          end
          """
        end
        clazz.class_eval s
      end

If it didn't make sense before, does it now? We're adding a method

The only thing left to do is to tell Rails that our validator is alive and we wants me some; this is just a single line at the bottom of our library file. (The validator code above lives in some nested modules.)

ActiveModel::Validations.__send__(:include, Newton::Validator)

Here's the complete code in a gist (also appended at the bottom of the post).

So, does it work?

pry(main)> i = Item.new(:name => 'wat!')
=> #
pry(main)> i.name_repeats_error?
=> nil

Huh? Oh, right--the model hasn't been validated yet. Remember, as it stands right now, the method is added during validation.

(This may be a Bad Idea--it might be better to add the method to the model in some other way, but unless we add it manually, or add it to all our models, create a method our models can use to get the method from the same module our validator is in, or...? this (for now!) seems the way most-tied-to-the-validator, which was one of the things I wanted.)

We'll use create instead for illustration and keep creating items with the same name. Once we have two with the same name, additional items with the same name should have a validation error.

pry(main)> i = Item.create(:name => 'wat!')
=> #<Item id: 7, name: "wat!", ...>
pry(main)> i.name_repeats_error?
=> false
pry(main)> i = Item.create(:name => 'wat!')
=> #<Item id: 8, name: "wat!", ...>
pry(main)> i.name_repeats_error?
=> false
pry(main)> i = Item.create(:name => 'wat!')
=> #<Item id: nil, name: "wat!", ...>
pry(main)> i.name_repeats_error?
=> true

That, as they say, is that.

For now.

Tuesday, September 06, 2011

Simple Ajax property toggle in Rails 3.0

Entry-level overview of one way to add trivial Ajax functionality to a Rails 3.0 app, originally written for a specific audience. The repository is on github.

Let's say we have an Article model with an "approved" flag. We need to be able to toggle this flag. Normal scaffolding would have us view the article, click a checkbox, and submit. We'll keep that functionality, but add a simple Ajax-based toggle on the articles index page.

Model Generation
rails g scaffold Article name:string approved:boolean

Original Model Index View

The default scaffolding includes the following chunk, repeated for each article.
 <td><%= article.name %></td>
 <td><%= article.approved %></td>
  ...

An Approving Helper
"True" and "false" are a yucky user experience, what we really want is "approved" and "un-approved". We'll think ahead, and turn it instead into text suitable for a command link. We could also use images, but we'll stick with text for now.
def approve_link_text(approvable)
  approvable.approved? ? 'Un-approve' : 'Approve'
end

Two quick things about how this was written.

1) It's not tied to articles; it'll quack at anything with an "approved?" method.
2) The parameter is named something that provides a hint to future devs, more useful in an IDE that provides popup help.

Add a Method to the ArticlesController to Toggle Approval Status
Two-step process: first, add a method to the articles resources routing.
resources :articles do
  get 'toggle_approve', :on => :member
end

Then the method itself (this is not the final version).
def toggle_approve
  @a = Article.find(params[:id])
  @a.toggle!(:approved)
  render :nothing => true
end

Update the Template
Our first step will just toggle the attribute without any feedback. It's actually a one-liner, just make the true/false "approved" text from before be a link to our new action.
<%= link_to approve_link_text(article), 
            toggle_approve_article_path(article), 
            :remote => true %>

The :remote => true turns it into an Ajax request. Not too shabby.

If we refresh the page after clicking the link we'll see that the text has changed. Approved articles will have an "Un-approve" link, un-approved articles an "Approve" link. Clicky-clicky, it does what we expect.

But refreshing is deeply unsatisfying.

Dynamic Feedback, Part 1

Remember when fadey-yellow things were cool? Yeah, we're all about that.

In order to change stuff on our page, we have to be able to access the appropriate DOM elements. Let's say we'd like to highlight the entire row when we toggle an article's approval status, and change the link text.

We'll use "article_n" for the article rows, and "approval_link_n" for the links.

<% @articles.each do |article| %>
  <%= article.name %>
    <%= link_to approve_link_text(@article), toggle_approve_article_path(@article), :remote => true, :id => "approve_link_#{@article.id}" %>

Dynamic Feedback, Part 2

Now we can access the DOM elements we care about on a per-article basis. How do we make them change? By creating a JavaScript template for our action, just like we usually create HTML templates. Rails will execute the returned JavaScript. The JavaScript template can contain ERb constructs, also just like HTML templates.

Note that some people don't like creating JavaScript this way. An alternative is to create DOM elements that our JavaScript can pull from. That's fine, but honestly, for simple things like this, I don't have a big problem with doing it the "old-fashioned way".

Our action is called "toggle_approve". We name JavaScript templates the same way we do ERb templates, so we'll create a toggle_approve.js.erb.
$("#approve_link_<%= @article.id %>").text("<%= approve_link_text(@article) %>");
$("#article_<%= @article.id %>").effect("highlight");

Line 1 sets the link text to Approve/Un-approve using the same helper we used in the HTML template.
Line 2 highlights the row we just updated.

That's it--we no longer have to refresh our page--the text of our link changes after the property is toggled, the row is highlighted, and we're all set!

Monday, September 05, 2011

Example app test failures from authlogic user sessions

This question on stackoverflow led me to believe that if I actually ran rake test that my example app's tests would fail--and they did, with the same error. (Why I wasn't running tests from the beginning? Meh!) The tests throw up wads of stack trace, headed with this:

SQLException: no such table: user_sessions: DELETE FROM "user_sessions" WHERE 1=1

What's causing this error? We created our user session object like this:

rails g authlogic:session UserSession

Unfortunately, this created a fixture file as well. During testing, fixture files normally map to tables, from which all data is deleted before subsequent reloading of the fixture.

But authlogic user sessions aren't ActiveRecord models, they're AR-like, and are actually authlogic-derived classes. No table: they serve as a link between users (which are in a table) and their authlogic sessions. My takeaway? Authlogic sessions should be created without a fixture file.

rails g authlogic:session UserSession --skip-fixture # Or --fixture=false

The file has been removed from the example app's github repository.

Two other minor changes were made to get the tests to pass (w/o any care to what they're testing). First, the user fixture file was tweaked (replace the old "login" property with the new "nickname" property, make the persistence tokens unique). Second, the HomeControllerTest stub was tweaked to expect a 302 when hitting the index page, which is the redirect encountered when the user isn't logged in.

TextMate, rvm, ActiveSupport outside of Rails, "require"s from current directory, all together now!

I'm prototyping some calendar/date stuff for a Rails app in standalone scripts, and want access to both normal Rails things (in this case, ActiveSupport's date math, like Date.today - 3.days) and my own classes within the prototyping directory. I'd like to continue using TextMate's "Run" command to run the current buffer as a Ruby script, since it's convenient. I'm using rvm; my prototyping directory has the same .rvmrc file as my Rails app.

My previous TextMate/rvm post (written for a different MacBook Pro) detailed setting TM's PATH environment variable; on this one I didn't have to do that--I just set TM's TM_RUBY to rvm's auto-ruby, ~/.rvm/bin/rvm-auto-ruby, and with a simple require 'active_support/all', date math.

Setting TM_RUBY made my own requires of code in the prototype directory fail, though. My quick fix was to set TM's RUBYLIB env var to .:${RUBYLIB} since everything I needed for this was in the current directory.

Potentially a little gross, but for now, it's letting me continue. I suspect I may rely more and more on Pry for this type of hacking in the future.

Using email or nickname to log in using authlogic and Rails 3

This is a continuation of my first authlogic/Rails 3 post.

Authlogic users have both "login" and "email" properties by default. I wanted to allow users to log in via their email address or nickname. I tackled this in two steps. First, switch to logging in using the "email" property. Second, create a "nickname" property, and allow users to log in with either one. (Yes, I actually wanted to call it "nickname"; I probably could have just aliased it or something.)

In addition to the column-dropping migration, I needed to change the authlogic config to use the "email" property for user lookup, and to not validate the "login" property, since it no longer existed. This is handled in the acts_as_authentic configuration as discussed previously.
  acts_as_authentic do |c|
    c.login_field = :email
    c.validate_login_field = false
    c.require_password_confirmation = false
  end

Lines two and three do what we expect. These configuration fields are documented in the login field configuration section of the authlogic docs. Part the first, done.

If we want to allow users to log in by either email or nickname we need to change both the user and user session classes (in addition to adding the "nickname" property to our user model, of course).

First we add a user class method to look up users by nickname or email.
class User < ActiveRecord::Base
  # acts_as_authentic config removed for clarity.
  class << self
    def find_by_nickname_or_email(s)
      find_by_nickname(s) || find_by_email(s)
    end
  end
end

In the user session class we'll define the user lookup method. While we're here, we'll genericize the error message to something not tied to property names.
class UserSession < Authlogic::Session::Base
  # Use authlogic's default message
  # generalize_credentials_error_messages true
  # Use our own message
  generalize_credentials_error_messages "Login info is invalid!"
  find_by_login_method :find_by_nickname_or_email
end

This is documented in the session configuration section of the authlogic docs.

Users can now log in using either their email, or their nickname. As it stands, the sample app doesn't do much to enforce uniqueness (other than the authlogic defaults) of emails or nicknames.

(It also seems like we should disallow using someone else's email as a nickname, perhaps disallow emails-as-nicknames altogether.)

Sunday, September 04, 2011

Rails 3 + authlogic explorations

I've started a simple Rails 3 + authlogic example project on github, mostly for myself to experiment with. Right now it's basically a copy of this post's implementation (and unfinished at that), but I'll be expanding it over the next few days in various ways.

As it stands, the "application" (I use the term loosely) consists of a home page (root path) requiring login, and login/logout actions. You may follow the directions in the README file (and cut-and-paste some code), or fork it and do a DB create/migrate/seed, and have a user with a login ID of "plugh", password "xyzzy".

The example app will change its shape a bit as I poke authlogic with sticks, tailor the example to my specific needs, and see what all I can do with it. Ultimately, I'm not sure if I'll end up using devise, or if what I use depends on the app itself. (My own apps have different requirements than my work apps--I may use both!)

Part two; logging in using either a user's nickname or email.