суббота, января 10, 2009

Specific or default templates in Rails

I want to develop admin controller to do simple CRUD operations on not-so-complex models. I want it to be DRY and to allow adding features to all of them easily. This is my second attempt, the first one was way too ugly: it used inheritance from admin's base controller and descendants passed customization parameters to base class; also, have used custom "render" methods to find template for specific controller or, if the specific template is missing, fall back to general template.

And then it broke. Yeah, this could be fixed, but the whole idea seemed bad.

I just finished the second attempt and it looks promising.

First, it includes only one controller and uses routes to determine the actual model to operate on. Every route includes additional parameter that is passed to controller that contains name of model:
map.resources :users, :controller => 'items',
    :requirements => { :item_type => 'User' }
map.resources :regions, :controller => 'items',
    :requirements => { :item_type => 'Region' }

Second, the most troublesome was to make it work like this: I have default views in app/views/items... I wanted to have subdirectories with name of particular model that would contain overrides to default templates:
app/views/items
    index.html.erb
    new.html.erb
    edit.html.erb
   _item.html.erb
    _form.html.erb
app/views/items/users
    _item.html.erb
    _form.html.erb
app/views/items/regions
    _form.html.erb

Regions doesn't need custom 'item' partial because default one (which outputs only item.to_s) fits well.

Obviously, controller's view_paths should be manipulated to search first in concrete subfolders and then in default place. But the problem was that it always tried to prepend controller name and so find it in corresponding subfolder. It's not very nice being forced to create additional directories like this:
app/views/items
app/views/items/users/items
    _item.html.erb

Well, after some digging the way to do it elegantly I found this solution:

1. Redefine controller's controller_path to return nil. After this all templates are searched in app/views/ instead of app/views/items, so it needs to be fixed, so
2. Redefine controller's view_paths to base in app/views/items.
3. Based on current item type prepend corresponding view path.

The result is as follows:
class ItemsController < ApplicationController
  def self.controller_path
    nil
  end
  self.view_paths = [ RAILS_ROOT + '/app/views/items' ]

  before_filter :setup_model

  # ... action code

  private

  def setup_model
    @model  = params[:item_type].constantize
    prepend_view_path(RAILS_ROOT + '/app/views/items/' + params[:item_type].underscore)
 end
end

Happy coding!

пятница, января 09, 2009

Back to Rails

Lately I wasn't doing any Rails development because I was busy re-learning C/C++ "the right way". Turns out it's not that painful as they say.

Then I had to do some Rails coding using latest and greatest Rails 2.2 and I find that that the simple things are broken (or somewhat obscure).

Case 1.

I was developing some general resource controller and wanted to spec out a redirect after #create action. My controller should not be bound to any specific resource url (as it is planned to bind many different resources with that controller), so I decided to make use of Rails' url rewriting. Spec was like this:

it "should redirect to #index after item creation" do
  @model.stubs(:create!)
  post :create
  response.should redirect_to(:action => :index)
end

And this spec failed because of MethodNotAllowed: Only get and post requests are allowed.

WTF? It's the very basic functionality. Why does it fail to work ?

After half a day of investigation I found out that this is a bug in rspec-rails package and redirect_to matcher fails to handle hash-like url specifier.

Case 2.

I wanted to handle AR::RecordInvalid exceptions with one handler to DRY up controller code. I wrote corresponding "rescue_from" call in controller and wrote specs (in fact, the reverse order: specs then code). And those specs failed too.

The other several hours of investigation revealed hidden option: RSpec-rails overrides default Rails' error handling (which is based on ActiveSupport's #rescue_from feature) with pass-through exception handler, and you have to write following code to make YOUR exception handlers work:
before do
  controller.use_rails_error_handling!
end

Yeah, Rails is nice, but upgrading to a new version is a pain.

среда, мая 21, 2008

Sphinx FAIL

On my recent Rails project I decided to try Sphinx search engine. Before that I used Ferret and then - SOLR. I abandoned Ferret because it's instability and lack of tools to track problems (like Luke - index browser for Lucene).

So, Sphinx. I have found two plugins for Rails - acts_as_sphinx and Unltrasphinx. Ok, I've heard in mailing lists that Ultrasphinx is better (and Sphinx recipe in Advanced Rails Recipes book also involves it) so I decided to use it.

First I have to install Sphinx from sources because version in MacPorts (although, latest released version) is too old and Ultrasphinx requiresa newer one (at that point - release candidate 2 of next revision, which is 0.9.8rc2 vs 0.9.7). Then I had to do various dances around Sphinx to compile it on my Mac OS X (which are described in my previous post.).

And then it began:

1. It was needed that new data (e.g. new articles) could be searched with Sphinx right after it was added. And then I find out, that it is not encouraged to do such updates often, you'd better run full updates once a day... Wtf? I have heard that there are something called "deltas", but as I found out from plugin, it doesn't install any hooks on models, so I assume that deltas need to be built periodically (which is also unacceptable).
After consulting with other people who have used Sphinx I found out that:
1) they don't use plugins and do all communication with some low level library (Riddle, as far as I remember) manually.
2) they install their own hooks on models and call indexer manually to reindex their models. I have tried to to install after_save hook but it is run BEFORE transaction is commited and indexer can't see inserted/updated data, so I don't know how this can be accomplished easily.

2. Sphinx configuration is one big scary thing. Although Ultrasphinx managed to build it for me, I needed to do some tweaks to it and then next time I needed to build configuration for another model my tweaks were lost.

3. Model on which I have used Ultrasphinx (called ::is_indexed) failed to load by automatic dependency loader. After several hours of tracking this problem I stuck "require 'my_model'" into environment.rb (which helped) and cursed that Sphinx and all it's plugins.

So, for me Sphinx definitely FAIL.

PS
I have tried SOLR and it worked like charm:
1) almost no configuration.
SOLR configuration consists of type definitions (which describe how value of that type should be analyzed), field definitions and dynamic field definitions (acts_as_solr rails plugin uses only dynamic fields). Acts_as_solr comes with default SOLR configuration which contains commonly used types and dynamic field definitions for them.
2) no compilation needed. Acts_as_solr comes with SOLR JAR files, so you just need proper version of Java runtime installed.
3) works like charm. Everything you would expect from full-text search engine.

четверг, мая 15, 2008

Installing Sphinx on Mac OS Leopard

One of my ongoing projects required using Sphinx search engine. While I'm more comfortable with Lucene based solutions (I'm evaluating SOLR currently), I decided to give Sphinx a try.

I have installed Sphinx via MacPorts system and was going to use UltraSphinx Rails plugin. But when I tried to run some test query, it complained, that server version is older than the client version. So, I needed to update Sphinx as UltraSphinx depends on some development release of Sphinx. But when I downloaded sources, configured and issued "make" command, the build failed because there were link problems with iconv library:
Undefined symbols:
  "_iconv_close", referenced from:
      xmlUnknownEncoding(void*, char const*, XML_Encoding*)in libsphinx.a(sphinx.o)
  "_iconv", referenced from:
      xmlUnknownEncoding(void*, char const*, XML_Encoding*)in libsphinx.a(sphinx.o)
  "_iconv_open", referenced from:
      xmlUnknownEncoding(void*, char const*, XML_Encoding*)in libsphinx.a(sphinx.o)
ld: symbol(s) not found


I saw some posts on how to overcome and they suggest downloading and installing iconv library. But I was strongly against doing such things (and also iconv library installed on my system was the latest one), so after some trial-and-error I have figured how to build it: just give standard system's library path precedence:
export LDFLAGS="-L/usr/lib"
./configure
make
sudo make install

Hope this will help somebody.

среда, апреля 30, 2008

On Prototype vs jQuery


I see more and more posts like this or this recently and I always see that when people are telling about how poor the Prototype framework is they really don't know even half of it's possibilities.

As it always happens, the code comparison in those posts is written by person who is not experienced in one framework or another.

I agree, that jQuery has some nice features: 1) actions on groups of objects (that you have mentioned), 2) more powerful selectors: prototype has support for css3 selectors only, but jQuery can handle more sophisticated queries like selecting hidden elements or inputs of particular type.

I haven't dug into what it takes to write custom effect or plugin (plan to do it soon), but I don't see how you can base your effects on existing ones in a way other than aggregating/compositing (the thing with Prototype effects is that you can inherit your effect from some base effect and have some nice features like configurable 'easing' function, which can control effect speed over time, or some other stuff for free).

But seeing syntax comparisons like that just makes me mad.
First, adding a class name to element in Prototype is just as easy as in jQuery:

$('element').addClassName('className')
$('element').removeClassName('classsName')

Second, the Ajax syntax. Prototype's variant is designed to do POST requests by default. You don't need to specify 'method' parameter if you do POST request. As for GET requests, in Prototype you can do just as in jQuery:

new Ajax.Updater('element', url + '&' + parameters)

(I believe, the '&' thing is needed in jQuery also, but author of code comparison just 'forgot' to mention that)

Moreover, if you don't like the wordy syntax of Ajax.Updater, you can extend Element to support whatever syntax you like:

Element.addMethods({
load: function(element, url) {
new Ajax.Updater(element, url)
}
})

and then you can do

$('element').load(url + params)

just as in jQuery.

And, yes, Prototype doesn't handle working with sets of elements by default. To add a particular class to a set of elements in Prototype you need to do some extra stuff, but not as it is mentioned in code comparison:

$$('.element').invoke('addClassName', 'className')

And yes, I agree, that Prototype and Scriptaculous have bad documentation and I personally have bought a book on them (see Pragmatic Programmers bookshelf) and my problems with Prototype and Scripaculous have gone since then.

I don't see any big reason to switch to jQuery.


PS There are lots of other javascript frameworks like mooTools (if I spell it correctly), Dojo, Rhino and such and I always haven't paid much attention to those, because usually my web application require custom solutions, not those that are present in such frameworks. But recently I came across some book ("Mastering Dojo" published by Pragmatic Programmers) and a podcast on it and it got my attention. They say that lot's of fortune 500 companies are using their framework because it is very solid and stable. And I'm curious about what particular features of Dojo make them think so. I have visited Dojo's site and tried to see at some of the examples (e.g. fish eye component) and they were running way too slow on my latest Firefox 2.x (which I consider as rather modern web browser). Probably, I need to give it one more chance and, probably, read that book.