kgautreaux/blog

An ocean of noise

Rails: 'undefined constant Foo' in_place_editing plugin

Warning to family and friends: I'm going to start publishing some stuff like this so Google can index it for me. Feel free to skip. Environment:
  • Rails 2.1 (Edge)
  • in_place_editing plugin Revision: 9250
First, since in_place_editing was moved out into a plugin in Rails 2.0 it hasn't been maintained. So if the error you're getting is that your in place page edits are stuck on "Saving..." forever you need to apply this patch to /vendor/plugins/in_place_editing/lib/in_place_macros_helper.rb This page can help you understand why and where to patch. Essentially the cross-site scripting forgery protection in Rails > 2.0 prevents the javascript callback from saving the in place edits into the database. Second, if you've made it past the stuck saving forever bug, but your controller is named differently than your model you might get an error such as undefined constant Foo when you try to access the edits. You will only see this error if you're stepping through the page scripts using something like Firebug. Install Firebug if you don't have it, trust me, you need it. Why would your controller be named differently than your model? Well, you could have a controller for directing the normal view of your page and then another controller for the administration of the pages but if you built the administrative interface first your model might be named Admin but your controller named Pages, as it was in my particular case. Anyway the crux of the issue is this method in /vendor/plugins/in_place_editing/lib/in_place_editing.rb [sourcecode language='rails'] def in_place_edit_for(object, attribute, options = {}) define_method("set_#{object}_#{attribute}") do @item = object.to_s.camelize.constantize.find(params[:id]) @item.update_attribute(attribute, params[:value]) render :text => @item.send(attribute).to_s end end [/sourcecode] Stepping through the code this method builds another Ruby method for your javascript callback to call. The method will be named set_(whateveryourmodelisnamed)_(whateverattributeyouaresetting). If your controller is called pages and your model is called something else this will fail. In your view you need some embedded Ruby code (Erb) to call in_place_editor_field and pass in the object and the attribute. It may look like this: [sourcecode language='rails'] < %= in_place_editor_field :page, 'body', {}, {:rows => 40, :cols => 65, :external_control => 'edit', :external_control_only => true} %> [/sourcecode] Uh, oh. So now def in_place_edit_for will be called with the argument object = :page so when it creates the method it WILL be named correctly (i.e. it will be named set_page_body instead of set_admin_body) but the find method will be trying to access a model named Page instead of the correct call which would be: [sourcecode language='rails'] Admin.find(params[:id]) [/sourcecode] Obviously this will fail but the error message makes you look for a constant, I'm not sure why. My fix was a horrible hack that replaced #{object} in the in_place_edit_for method with the string for the actual controller. Thus the method then became correctly named set_page_body but it actually accesses the Admin model for the information. [sourcecode language='rails'] def in_place_edit_for(object, attribute, options = {}) define_method("set_page_#{attribute}") do #uhhh, horrible hack, "set_page_#{attribute}" used to read "set_#{object}_#{attribute}" @item = object.to_s.camelize.constantize.find(params[:id]) @item.update_attribute(attribute, params[:value]) render :text => @item.send(attribute).to_s end end [/sourcecode] A more elegant method would probably be to pass an additional parameter to the in_place_edit_for method and then use that name to construct the set_page_body method name while leaving the object alone something like. [sourcecode language='rails'] def in_place_edit_for(object, attribute, name, options = {}) define_method("set_#{name}_#{attribute}") do #ooohhh, pretty @item = object.to_s.camelize.constantize.find(params[:id]) @item.update_attribute(attribute, params[:value]) render :text => @item.send(attribute).to_s end end [/sourcecode] Your method call in your pages controller would then have to read thus: [sourcecode language='rails'] in_place_edit_for :admin, :body, :page [/sourcecode] I'm not sure that would work. I haven't tested it, but the ugly looking one does work in practice and gives you whiz-bang in place editing for your administration interface even if you named your controller wrong. Sometimes convention over configuration can make things harder, but it makes almost everything else easier. I do think in_place_editing could be more flexible as a plugin. Update: Okay, I did just test the more elegant way and it does work, at least in my particularly simple instance. You do have to remember to restart your script/server because you're editing the plugin itself and plugins don't get reloaded automatically even in development mode.