Monday, August 29, 2011

Associating has_many relationships in Rails 3 using checkboxes

(Not checkboxes, in my case, but a checkbox example is easy, and more common.)

Originally I thought I needed accepts_nested_attributes_for, but that seems to be mostly for when we're creating the related objects, which I'm not--I need to save relationships to existing objects.

My example (github) uses a simple product/category relationship. We need to get a product's categories, we need to get a category's products. In the olden days, we would have used habtm; this example uses has_many:through. We'll use checkboxes to associate categories on the product's form page.

A number of posts were helpful during this process, although none was an SSCCE (at least to my new-again-to-Rails eyes). I don't know if I ended up doing this in the (or a) Rails Way; feedback is welcome. The nutshell solution for me ended up boiling down to the following:

* I didn't need accepts_nested_attributes_for (I'm referencing, not creating).
* I needed attr_accessible to allow mass-assignments to the category IDs as per this post.
* This stackoverflow question provided form element hints.
These posts pointed me away from habtm, although I'm still fuzzy on why (not _why).
* The second post above also reinforced form element naming.

(Note: I became uncertain about needing attr_accessible; the example works without it. In my original project it didn't, but it's likely I screwed something up the first time around and need to revisit it.)

Here's the Product, Category, and ProductCategory models, in all their tiny little glory.

class Product < ActiveRecord::Base
  validates :name, :presence => true
  has_many :product_categories
  has_many :categories, :through => :product_categories
end

class Category < ActiveRecord::Base
  validates :name, :presence => true
  has_many :product_categories
  has_many :products, :through => :product_categories
end

class ProductCategory < ActiveRecord::Base
  belongs_to :product
  belongs_to :category
end

The migrations for Product and Category are what we'd expect. The mapping table migration is similarly trivial.

class CreateProductCategoriesTable < ActiveRecord::Migration
  def self.up
    create_table :product_categories, :id => false do |t|
      t.references :product
      t.references :category
    end
    add_index :product_categories, [:product_id, :category_id]
    add_index :product_categories, [:category_id, :product_id]
  end

  def self.down
    drop_table :product_categories
  end
end

The checkboxes are created with the snippet below. It's kind of "manual" this way, there's likely a cleaner way using stock Rails.

<% Category.all.each do |cat| %>
  <%= cat.name %>
  <%= check_box_tag :category_ids,
                    cat.id,
                    @product.categories.include?(cat),
                    :name => 'product[category_ids][]' %>
  

<% end %>

That's it: I suspect my original frenzied attempts, spinning through methodologies, got things out-of-sync and cost me a fair amount of time. Not needing attr_accessible is probably because I'm not using nested attributes. I'm not sure what's wrong with habtm. I'm not sure if there are performance penalties for using mapping classes. Lots to explore, which is both good and bad.

Monday, August 22, 2011

Preventing double-clicks in jQuery/Ajax-y handlers

Throwing this out there to see what people thing--I suspect there are better, cleaner ways to do this, and I'm wondering what they are.
$.fn.click1 = function(fn) {
  $(this).data('clicked', false);
  $(this).click(function(o) {
    if ($(this).data('clicked')) return false;
    $(this).data('clicked', true);
    fn(o);
    return false;
  });
};

(I return false because I know I don't want to follow the link; it'd be cleaner to return the results of the function call.)

I use the the same way I use the normal jQuery click() function. As a hack it served its purpose; I'm curious what issues there are with it, and how real jQuery developers handle this.

Updated: This already exists in the jQuery API.

$("#foo").one("click", function() {
  alert("This will be displayed only once.");
});

The documentation states that the handler is unbound after the first click. In my defense, originally the behavior was different after the first click--in other words, it wasn't as simple as one() allows, and I thought I was going to need the DOM element data. Which I didn't.

Oddly enough, my Google Fu failed me on this one, because this didn't show up on the first page of search results.

Tuesday, August 09, 2011

Rails 3 Custom Validator Quandary

I'm trying to figure out the "best" way to support discovery of custom validator errors on a Rails 3 model.

My model has both standard and custom validators. I need to check for validation errors from the custom validators, in both controller and template.

My slap-dash solution was to add an additional error message keyed to the field name suffixed with a validator-specific extension. For example, the standard message would be added to errors[:foo], and a custom message to errors[:foo__bar].

This works, but I really don't like it much--hence my plea for ideas. Here's the options I've considered so far, in no particular order.

(1) Add a model function that hides the logic. Easy, but vaguely discomforting.

(2) Check for the validator in the model's "initialize" method and if it's there, add an instance method. Similar to (1), but still far-removed from the validator.

(3) Expose something in the validator. Add it to the model class via an include, or dynamically in the model initializer.

(4) Have the validator add a method (foo_xxx? or something similar) to the instance at validation time. Only models that have been validated would have those methods, which gives me the heebie-jeebies.

(5) Pass in the class (instance?) in the EachValidator's options hash, and have the validator tweak the class (instance?) at init time. Having to pass in the class I'm already in seems janky, but...?

Updated:


(6) The "validates_with" also looks for a "setup" method in the validator (not an ActiveModel::EachValidator, but an ActiveModel::Validator) and passes "self" to it. "Sexy" validations use "validates_with" underneath, so this may be another approach.

The nature of my question, and my list of possible solutions, makes me wonder if I'm thinking about this the wrong way, and/or missing something *completely* obvious--am I?

Monday, August 08, 2011

Bash function of the day

I push the command line on people, because it's generally the most efficient way to do several different things. My devs resist me, although I don't understand why... part of it is that most places I've worked at are Windows houses. No problem, say I: Cygwin to the rescue. Seriously, gang, I'm not just making this up.

(I'll grant, however, that there are some Finder/Explorer replacements that wrap up some of this functionality pretty nicely in a GUI, including having a command line in whatever directory is open--but it's still a command line, and you still should know how to use it.

Two things I do a lot from the command line are find files, and find files containing a pattern. Here's two drop-dead simple Bash functions that do this. Seriously, doesn't get much easier.

function fn {
  find . -type f -name "$1"
}


function fng {
  find . -type f -name "$1" | xargs grep "$2"
}

Friday, August 05, 2011

Mysterious Facebook JS error, resolved!

Two FB apps, both using FB.ui to publish to a stream. One worked, one didn't, nobody knew why. I needed to know.

Nutshell: FB JS code was inadvertently included twice (not by me, which always makes things more difficult). This caused the app_id and app_key values in the FB.ui call to go away--everything else was there, but two pieces critical (obviously) weren't. Note that the app code didn't explicitly do any initializations twice, the act of including the code twice was enough.

We ended up doing the FB share a different way due to time pressures, but before we did, we had multiple people looking at the app trying to figure out what was wrong. In retrospect, I think I should have diagnosed the problem a day earlier than I did, and there's a couple of reasons why.

The most obvious dumb thing was that I didn't look at the wire traffic and pick apart the FB.ui call's URLs. That was ignorance; this was the first FB issue I've ever tried to diagnose--I had no clue where to look. That step, however, ended up pointing me in the right direction (eventually).

I also should have created a blank sample app earlier, as a sanity check. This was partially inertia, partially time constraints, partially wasn't sure how easy/difficult it was to do, partially a belief that the FB pros would be able to figure it out, so I did other stuff and waited--didn't need to.

Once I had the working app, compared the generated URLs, things progressed rapidly. First I made the blank app structurally similar to the broken app: put the JS code into a partial (it's a Rails app), cut-and-pasted the init code, etc. I ignored styling--I made an assumption it was a JS issue fairly early on, and pursued that path first.

The blank app continued to work, so I switched my attention to the broken app, and stripped out everything except the FB code. Workie workie... made some additional guesses, started poking through the partials, and found a partial with two issues--a repeated div id used by the init code, and an include of the FB JS file.

I don't actually know if it was the include or the div id: the FB JS code is inserted dynamically as a div's child element--a div with the id that was repeated :/ Either way, it was broken HTML, and broken JS.

Here's what bothers me about this exercise: multiple people looked at this and couldn't figure it out. Identical code worked in other apps (we knew (or at least "thought strongly") it was something app-specific), the docs made it seem like what I was trying should work. It should have been easier to diagnose.

Code that fails sans clueage is a pain. Disallowing multiple loads of the same JS is easy, and it would have saved a few people a frustrating amount of time diagnosing what ended up being ridiculously easy, and (somewhat) obvious.

Wrong code should either be impossible, or act very wrong, not just somewhat, and quietly, wrong. That notion reminded me of an old Spolsky article which is tangentially related, but speaks to making things that are broken break in such a way it's obvious: fail fast.