Just A Summary

Piers Cawley Practices Punditry

My first 'acts_as' plugin 7

Posted by Piers Cawley Tue, 23 Jan 2007 23:39:00 GMT

So, you’ve upgraded to Rails 1.2.1 and you’re working on a tool to maintain a database of all the tunes you have in your various songbooks and (eventually) your record collection. You start with:

$ ./script/generate rspec_resource MusicBook title:string author_id:integer \ abstract:text $ ./script/generate rspec_resource Tune title:string composer_id:integer \ abc:text book_id:integer

You decide to come back to composers and authors later, so you set up your models1:

MusicBook.has_many :tunes Tune.belongs_to :music_book

And your routes:

map.resource :music_books do |book| book.resource :tunes end

Problems start here

Being a cautious sort, before you start adding behaviour, you fire up a development server and go and check things with the browser. The /music_books/ stuff works fine, but once you start looking at /music_books/1/tunes things start to get weird; all of a sudden your links aren’t making sense.

The problem is, that your scaffolding is calling named routes with something like: edit_tune_url @tune, when, because of the nesting of your resources, they should really be calling edit_tune_url @tune.book, @tune. But how to fix things?

Well, you could go through all your controllers and views, replacing all the calls to named_routes with the right version. But, if you’re anything like me, you’ll get bored stiff of the repetition after you’ve fixed up the first file. And that’s before you start fixing up your controllers to do

@tune = Book.find(params[:book_id]).tunes.find(params[:id])

So, I’m going to suggest that a better bet is to install the acts_as_resource plugin I’m in the process of writing.

Making the model pull its weight

Once you’ve got acts_as_resource installed, you can just do:

Book.acts_as_resource Tune.acts_as_resource :parent => :book

And your models will magically acquire a resource_chain which returns exactly the list of objects that your named routes need. I’m currently investigating the innards of named routes, but the plan is that you’ll never have to call resource_chain yourself, the named_route helper will do it for you and use the resulting list to build the url.

Your model class, meanwhile, gets a handy

Tune.find_resource(params)

which will find your resource, verifying that it can be reached through the chain of resources specified in your params hash, which means we can fix up the scaffolding generator to use find_resource (or, more likely, a helper method that will set appropriately named instance variables in the controller).

If I understand what simply_helpful is up to, the named_routes hack should play well with that too, which means that form_for @tune will get its url right without you having to remember to call form_for @tune.book, @tune.

Release date?

I’m not quite ready to release yet, I’m busy wrapping my head around how named routes work so I can fix ’em up to use resource_chain. find_resource is written though.

It’s amazing how much leverage you can get, simply by adding one class attribute and a support method to your model…

Expect a release some time next week. However, if you’re desperate for a look, drop me a line and I’ll send you my local snapshot.

1 Look, vertical space is precious. Pretend this is in the usual blocky type stuff you usually find in the files you’ll find in app/models/whatever.rb

Comments

Leave a response

  1. Avatar
    mathie about 10 hours later:

    Ah, I’m taking a slightly different tack — modifying the existing generator to spit out the right code in the first place. Instead of putting in:

    @tune = Book.find(params[:book_id]).tunes.find(params[:id])

    everywhere, the generator spits out a before_filter:

    before_filter :load_book

    which does something along the lines of:

    private def before_filter load_book @book = Book.find(params[:book_id], :include => :tunes) end

    That way the controllers now look like:

    @tune = @book.tunes.find(params[:id])

    which is a bit more familiar. Hmm, just thinking out loud as I type, but I wonder if I could use an around_filter with a with_scope to make the controllers work unchanged?

    resource_chain looks like a really good idea. I’ve been spending far too much time digging through the routing recently, so I might be able to make some suggestions, if you haven’t figured it out already.

  2. Avatar
    Piers Cawley about 12 hours later:

    My affection for the resource_chain approach is that the various named controllers get used in magic fashion all over the place, so if you can fix ‘em up to take a single object as an argument, there’s a huge win.

    Also, making generators do the ‘right thing’ becomes enormously tricky once you start considering routes like:

    map.resources :books do |book|
      book.resources :annotations
      book.resources :tunes |tune|
        tune.resources :annotations
      end
    end

    Right now, that route declaration is going to fall over in a heap trying to make annotation_* routes, but if your models have a resource_chain mechanism, routing can just check to see if the named route already exists and carry on regardless. The same controller and views will work whether your annotation is on a book or a tune.

    Once that’s working, there’s the possibility of taking skinny controller, fat model to extremes and have your resource controllers looking like:

    class BookController < ResourceController
    end

    Which abstracts the boilerplate away, which would be no bad thing.

    But, this comment is a little narrow for this stuff. Expect another post looking at that.

  3. Avatar
    Anton 25 days later:

    How far can ttis parent-child relationship be extended?

    Grandparent.has_many :parents
    Parent.has_many :children
    Child.has_many :grandchildren
    Grandchild.has_many :greatgrandchildren

    And what about multiple decendants as in lists?
    The ‘child’ has an ‘uncle’ which is another parent of the same grandparent.
    Then there’s ‘cousins’, the children of an ‘uncle’.
    The ‘child’ could also have ‘schoolfriend’ .

    Let me guess. You’d do this as a polymorphic “Person”. OK, I can accept that, but there are going to be situations where the takes do have these kinds of relationships but aren’t the single table.

    Libraries,
    recordings
    books
    articles that refer to articles in other books
    or to recodings

    How general is this model?

  4. Avatar
    Piers Cawley 25 days later:

    I think you may be missing the point.

  5. Avatar
    mathie 26 days later:

    I have finally incorporated the acts_as_resource plugin into one of the apps I’ve been working on and it’s cleaned things up nicely. Thanks! On first reading of the source, I missed that it does clean up the URL helpers because I assumed there wouldn’t be any ‘real’ code in init.rb and couldn’t figure out how it could possibly work, but once I got past that…

    One issue that cropped up with the URL helpers. We use STI for Authors < Users, both of which are exposed as resources. I’m not convinced this is the right way to do it, because I’m generally wary of STI — I’d rather an Author has_one :login — but I digress. At one point we’re calling toggle_activation_user_path(author) and of course the resource_chain is set up for that of an author (who’s parent is a content_provider) rather than a user (who has no parent).

    Now that I’m writing this out, it strikes me that the patch I’m about to point to is not a general solution — it’ll only work if the user resource has no parent or is a sibling of the author’s ancestors. Umm, I think.

    Anyway, all it does is to reverse the resource chain and segment keys before zipping them, so that the last one on the chain gets :id, and ignores nil keys. You can find it here: http://woss.name/dist/fix_for_sti_resources.diff

    Yeah, reading over my comment again in preview, that’s definitely a bad smell in the design, really, isn’t it?

  6. Avatar
    mathie 26 days later:

    Is there any reason you’re pushing the collection on to resource_chain in fetch_resources? It appears to break things later on…

  7. Avatar
    Piers Cawley about 1 month later:

    Um… d’you know, I’m not entirely sure why.

    I’m just writing acts_as_resource up for a presentation and it’s got me worrying about how things work. Mostly to do with the routing, but that’s more that I’m not entirely keen on how routing works anyway…

Comments



Just A Summary