Scott Rutherford

Life on and off the Rails

 

AjaxScaffold has been deprecated in favour of ActiveScaffold

The aim of this post is to show how to extend or replace the default functionality of the Ajax Scaffold plugin (ASp). I will go over two areas, firstly how you filter the dataset the table displays, and secondly the methods you can override to really gain control.

Simple Filtering of the Dataset

In order to filter the data that the table displays you need to simply override a single method (for each model) in your controller.

conditions_for_{table_name}_collection

This method is expected to return the same array that the Rails find() method accepts as its :conditions parameter.

Lets take a simple user model as an example. The db table has the following columns: id, name, password, employer_id where the employer_id represents a has-one relationship to the user’s employer (represented by another table). Now lets say we only want to see users from the current employer in our table, then all we need to do is define the following method in our controller:

def conditions_for_users_collection
  [ 'employer_id = ?', params[:current_employer] ]
end

This of course assumes that the current request defines :current_employer as the employer’s id.

You can use any conditional statements you like, if they would work in the Rails find() method, they will work here.

More Complex Filtering

At a lower level the collection used by the table is provided by a combination of three methods:

  1. count_{table_name}_collection(model, options)
  2. page_and_sort_{table_name}_collection(model, options, paginator)
  3. page_and_sort_{table_name}_collection_with_method(model, options, paginator)

These methods represent a slightly extended version of the methods found in the Rails paginator. The first two are called for any normal page or sort (i.e not using a ScaffoldColumn defined using the :sort parameter) and the last is called only for ScaffoldColumns defined with, wait for it, the :sort parameter.

For example lets say we have a set of Report objects that are displayed using the following column definitions:

@@scaffold_columns = [ 
    AjaxScaffold::ScaffoldColumn.new(Report, { 
        :name => "title",
        :eval => "row.current_definition.title", 
        :sort => 'current_definition.title'}),
    AjaxScaffold::ScaffoldColumn.new(Report, { 
        :name => "viewed", 
        :eval => "row.views", 
        :sort => 'views' }),
]

We also have defined a method that returns the reports for a particular project, or the reports for a project in a particular category (all of which would be difficult to do with a simple :conditions statement):

def get_reports
  project = get_project
  reports = project.reports

  if request.post?
    selected_category = params[:category]
    reports = project.reports_for_category(selected_category) if selected_category
  end
  reports    
end

We could then define the three methods above as follows (in ReportsController.rb):

def count_reports_collection(model, options)
  get_reports.size
end

def page_and_sort_reports_collection_with_method(model, options, paginator)
  collection = get_reports
  order_parts = options[:order].split(' ')
  sort_collection_by_method(collection, order_parts[0], order_parts[1]).slice(paginator.current.offset,options[:per_page])   
end 

def page_and_sort_reports_collection(model, options, paginator)
  page_and_sort_reports_collection_with_method(model, options, paginator)
end 

This would cause every sort / page to use the with_method methods, as we need due to all columns using the :sort parameter. Our count would come from our get_reports method and everything should work as expected.

Other Extension Points

Here are some of the other methods that you can override to change the behaviour of the scaffold, its probably a good idea to actually look at the plugin code to see how each method is used, although some are pretty self explanatory.

In all of these {prefix} is defined as {table_name}_.That is the table name with an underscore appended. {suffix} is defined as _{model_name_in_lower_case}. That is the model name in lower case with an underscore prepended. These are only required in multi table scenarios (I have written it like this here to match the actual code). The reason for doing this is to give CRUD like methods when only using a single table.

Default Sort Column

By default the rows are sorted using the primary key, id. To change this you can override: default_sort / default_{prefix}sort

def default_{prefix}sort
   "users.name" 
end

Default Sort Direction

To change this from ‘asc’ you can override: default_sort_direction / default_{prefix}sort_direction. The only other possible return value is “desc”.

def default_{prefix}sort_direction
   "desc" 
end

Create

If you want to change the default behaviour when a model is created you can override the following method which is called internally:

def do_create#{suffix}
    @user = User.new(params[:user])
    @successful = @user.save
end

Update

If you want to change the default behaviour when a model is updated you can override the following method which is called internally:

def do_update#{suffix}
    @user = User.find(params[:id])
    @successful = @user.update_attributes(params[:user])
end

Table

To add custom behvaiour to the table rendering you can use this method. This is really to add functionality, you still need to call the methods as shown or things may go a little wrong:

def #{prefix}table
    self.#{prefix}table_setup

    OTHER CODE GOES IN HERE

    render#{suffix}_template(:action => 'table')
end

Added in 3.2.2

New

Called before the new form appears

def do_new#{suffix}
  @user = User.new
  @successful = true
end

Edit

Called to find the object to edit

def do_edit#{suffix}
  @user = User.find(params[:id])
  @successful = !@user.nil?
end

Destroy

Called to find and destroy an object

def do_destroy#{suffix}            
  @successful = User.find(params[:id]).destroy
end

55 Responses to “Extending the Ajax Scaffold Plugin”

  1. gabson@gfc.no Says:
    think theres a bug in your conditions example. def conditions_for_collection [ 'employer_id' => params[:current_employer] ] end makes nothing happen. def conditions_for_users_collection [ 'employer_id' => params[:current_employer] ] end produces an not method error. but def conditions_for_users_collection [ 'employer_id=?',params[:current_employer] ] end works. Makes sense when you look at :conditions in the rails api gabs
  2. Scott Rutherford Says:
    Hi Gabson, Well spotted. I have updated the post. Thanks Scott.
  3. gabson@gfc.no Says:
    Happy to help :) By the way, I was in such a rush before, I forgot to mention how much I appreciate your work. Thank you very very much for all your time and efford! You guys have done a great job. Now, back to finding out how I can maintain parent - child relationships between scaffolds, when editing. Cheers, Gabs.
  4. Haim Says:
    Hi, Great work. I needed to allow users to edit only the records belonging to them, so I changed the ajax_scaffold.rb to implement a do_edit method. Then I overrode it in my controller.I am not sure this is the best way to do this but it worked. Here is my modified code: def edit#{suffix} begin do_edit#{suffix} rescue flash[:error], @successful = $!.to_s, false end return render#{suffix}_template(:action => 'edit.rjs') if request.xhr? if @successful @options = { :scaffold_id => params[:scaffold_id], :action => "update", :id => params[:id] } render#{suffix}_template(:action => "_new_edit.rhtml", :layout => true) else #{prefix}return_to_main end end def do_edit#{suffix} @#{singular_name} = #{class_name}.find(params[:id]) @successful = !@#{singular_name}.nil? end
  5. Scott Rutherford Says:
    @gabson: No problem. The relationship thing is a bit of a tricky one isn't it. We haven't really looked at how to implement it in the plugin yet. There is some info on then "wiki":http://ajaxscaffold.stikipad.com about doing it with the generator. Let me know how you get on. @Haim: Your approach seems reasonable (EDIT: although see my later comment on why changing the plugin isn't a good idea), we probably should of included an extension point for the edit, and maybe for the new. I think I probably would call your do_edit method something different as it isn't actually doing the edit. Maybe find#{suffix} or something that actually implies the action (see below). I take it you actually meant you updated the code in the ajax_scaffold_plugin.rb file.
    def edit#{suffix}
      begin
        find#{suffix}
      rescue
        flash[:error], @successful  = $!.to_s, false
      end
    
      [REST OF METHOD STAYS THE SAME]
    end
    
    def find#{suffix}
      @#{singular_name} = #{class_name}.find(params[:id])
      @successful = !@#{singular_name}.nil?
    end
    
    Cheers guys, Scott.
  6. Haim Schlesinger Says:
    Yes, I did change it in the ajax_scaffold_plugin.rb, and yes your naming is probably better. I also added it to the destroy method, so I placed the find method in the private section, and created the following two methods, each right after the relevant main method. Here is edit:
    def edit#{suffix}
      begin
        prepare_edit#{suffix}
      rescue
        flash[:error], @successful  = $!.to_s, false
      end
    
      [REST OF METHOD STAYS THE SAME]
    end
    
    def prepare_edit#{suffix}
        find#{suffix}
    end
    
    Thanks. Haim Btw, how can you format the code here like you did?
  7. Haim Schlesinger Says:
    Is there a way to override this code so it will redirect to list instead of table?:
    verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ],
           :redirect_to => { :action => :#{prefix}table }
    
  8. Scott Rutherford Says:
    Hi Haim, For the formatting you just wrap the code in 'pre' tags (I added them to your comment). As for overriding the verify method, I think if you just redefine it in your controller then that will be picked up. All of those post actions are from ajax calls though so you need to be sure thats what you want / need to do. Scott.
  9. Scott Rutherford Says:
    @Haim: Just a point, but if you can (and I see no reason why you can't) you should really implement these changes in your controller and not change the plugin code. That way you can upgrade without fear of breaking anything. With this particular change I may well include something like it, but it would be better to override the plugin functionality on a controller by controller basis.
  10. Chad Pytel Says:
    It doesn't look like your conditions_for_users_collection example has been updated to change the '=>' to ',' thanks, -Chad
  11. Haim Schlesinger Says:
    I tried putting the following in my controller, but it did not work (it stil redirects to the table action:
      verify :method => :post, :only => [ :destroy, :create, :update ], :redirect_to => { :action => :list }
    
    I want to change this because if someone is trying to access the destroy method by typing the url directly they will get the table with no layout or css formatting (ugly).
  12. Scott Rutherford Says:
    @Chad: Thanks, updated. The other syntax (but using a hash not an array):
    :conditions => { :employer_id => params[:current_employer] }
    
    is available in edge Rails, see "this Ryan Daigle post":http://www.ryandaigle.com/articles/2006/06/06/whats-new-in-edge-rails-convenient-finder-parameter-hashes. So if you are using edge the conditions method can return a hash like this too.
    @Haim: Fair enough. I guess thats actually more likely than someone submitting an ajax request using a GET anyway. Just place your declaration before the ajax_scaffold call and that should work.
  13. Haim Schlesinger Says:
    It worked, thanks. I guess I can also add
      verify :method => :post, :xhr => true, :only => [ :destroy, :create, :update ], :redirect_to => { :action => :table}
    
    and solve the xhr get problem. Thanks again :-) Haim
  14. Scott Rutherford Says:
    Yep, sounds good. This seems like a good thing to have in the plugin as default really, as do the other extension points. Thanks for the feedback Haim. Scott.
  15. B.Scherer Says:
    Hi Scott, first of all: great work, nice plugin, just what many of us generator-users wanted... Elsewhere you mentioned either to use the generator OR the plugin, not both at the same time. So (wanting to migrate) I got another question concerning the wiki-article about integrating associations. With the generator it works well, but I'm just too stupid to do the same with the plugin. Is this possible at all? If yes, a short description would be more than perfect. thx
  16. Scott Rutherford Says:
    Hi B, Thanks. You should be able to use the latest version of the generator and the plugin together fine now, Richard fixed the outstanding issues a couple of weeks ago. As for the associations, unfortunately I haven't had time yet to sit down and figure it out. If you have any success please let me know. Thanks Scott.
  17. Terry Tompkins Says:
    Update on my problem with the top and bottom table borders: I had been using IE (which works fine with the generator code, but not the plugin-based code on my PC). When I load http://localhost:3000/users in Firefox, it renders perfectly. Can someone try a plugin-based scaffold in IE6 and let me know if the problem is mine only?
  18. Scott Rutherford Says:
    Hi Terry (there is also some more on this on the other post..), You can see the issue in IE in my demo at http://fckeditor.caronsoftware.com. So it has happening for me too. Its now on the todo list. Cheers Scott.
  19. Michael Edlund Says:
    I implemented a live search using a combo of a text_field_tag and observe_field. I pass the value of the text field to my custom conditions_for_{pluralname}_collection where I filter through [ 'somefield LIKE ?', @phrase ]. This works very well as long as I keep to fields in the same table. But I can't get joining with other tables to work as I would want to search on results in the scaffold from a joined table. I see that the page_and_sort_#{plural_name}_collection_with_method does accept a join parameter, but I can't seem to pass the magically correct parameters. Could someone help me out with an example? Thanks for the good work! /Michael
  20. Scott Rutherford Says:
    Hi Michael, If you look at the example above in the "More Complex Filtering" section you can see the basis for solving your problem. All you need to do is define a method like the get_reports which returns the filtered collection. Within that method you can then use any of the scoped ActiveRecord methods or find_by_sql to do your joins. Cheers Scott.
  21. Walter McGinnis Says:
    I'm a RoR newbie, so take this with usual caveats, etc. I'm setting up dynamic forms for topics based on a topic_type and the topic_type_fields associated with them. I've enjoyed the plugin thus far and have used the overriding of supporting view files via app/views/my_model_plural/ successfully. Namely I've added two additional forms to the topic_type edit. Still some kinks with the ajax, but basically working. However, for new topics, before a dynamic form can be created, a user has to pick a topic_type and then proceed to the generated form. I've tried to place an intermediate "pick_topic_type_id" by adding it to my controller, copying and modifying to suit various bits and pieces of _form.rhtml, _new_edit.rhtml, and new.rjs, etc., but I keep getting caught up on loss of the ajaxscaffold instance variables in the pick_topic_type_id stage. Whew. So I guess my questions are two fold: * If you were me, what would you do to achieve the same outcome (it needs to be able to fall back to non-js working version)? * Am I missing a simple way of preserving these instance variables while going through the intermediate form? Cheers, Walter
  22. Walter McGinnis Says:
    I ended up switching to the generator which worked for what I needed to do. Cheers, Walter
  23. Scott Rutherford Says:
    Hi Walter, Glad to hear you got one of them to do your bidding... Cheers Scott.
  24. Raphael Gillett Says:
    Scott, Thanks for the plugin! Trying it with JRuby, I noticed that pagination, and footer page number links, are fine, but footer Previous and Next are unresponsive. (These are fine with Ruby of course.) Any thoughts on what the cause might be? Regards, Raphael
  25. Bryan Childs Says:
    Is it possible to easily override the format that the plugin uses to display dates? I'm a bit of a RoR newb, and I found in the plugin code where it's set - but I'm not sure if it's overridable, or whether I'd have to change the plugin itself (obviously not good for upgrade purposes). Thanks, Bryan
  26. Scott Rutherford Says:
    @Raphael - I would guess that it may be to do with the javascript calls the previous and next links make to the dhtml_history code. They are in the :before section of the helper. You can take them out without affecting the main functionality of the code. Just the back button on the browser will stop working. @ Bryan - Probably the easiest thing to do is copy the formating code from the plugin to the helper file for your controller and call it something different. Then simply copy the _row.rhtml template over to the app/views/controller dir and change the call in it to format_column to match your renamed method. Cheers Scott.
  27. Jonathan DOnaldson Says:
    How can we filter the columns output into the table? Say based on authentication roles... ie. I want an admin to see columns a,b,c,d,e but I want a regular user to see only b,d,e help!
  28. Scott Rutherford Says:
    Hi Jonathon, I think I will put up a post describing how to do this tomorrow (bit long winded for a comment) as its come up before and is I think a pretty standard thing to do. Scott.
  29. Tom Says:
    Ok, again I'm a newb... thanks for the tutorial and tips... I'm trying to create an admin portal and when an admin creates a user the ruby on rails code needs to make a couple of xmlrpc calls to existing systems that have xmlrpc APIs for configuring users and such... I tried creating a do_create#{suffix} method in my controller but that doesn't seem to do anything, I tried putting it in the model also didn't do anything... and I also don't know where to "require xmlrpc/client" should I put that in my controller or in my environment.rb? my controller is called Admin and my user model is User. I have the plugin installed and working (I can create new users in the database). I've tried calling the function do_create_user, do_create_users, do_create#{suffix} in both the model and the controller... it never seems to do anything, with those functions in there it still creates users just fine, its not making any errors happen, but I've tried to making the do_create render some text to see if it is calling my code at all and it doesn't seem to be.
  30. Tom Says:
    Ok, I figured it out.. I don't know why do_create_user wasn't working... do_create is what does work for me anyway....
  31. Scott Rutherford Says:
    Hi Tom, you only need to add the class name if you have passed the :suffix option to the ajax_scaffold call. This was to allow multiple tables on a page. However, that behaviour is deprecated in the next version so this potential complication will vanish. Cheers Scott
  32. Colin Says:
    Hi Scott, I'm trying to display a custom column that's, for example, a concatenation of data from two different columns; ScaffoldColumn.new(Obj, {:name => 'custom', :eval => "row.data1 + ':' + row.data2" }) DOESN'T work. What should the syntax of the eval be, please? Thanks.
  33. Colin Says:
    Nevermind. I needed to use
    :eval => "row.data1.to_s + ':' + row.data2.to_s"
    
  34. Shimon Amit Says:
    Is there a way to add custom actions to each record other than create, edit, delete? For example, in our application we have a 'manage' action for each account which lists all the associated account records (users, bills, orders, etc.) Thanks for all your work and this wonderful plugin!
  35. Scott Rutherford Says:
    Hi Shimon, simply copy the _row.rhtml template to your controllers template directory and then add the custom actions there. Scott.
  36. Sjors Says:
    Hi Scott, Thanks for all the great work you have put in the plugin and the documentation. One small thing. I think you made a typo in the 'More Complex Filtering' paragraph. You showed the three methods to override: * def count_reports_collection * def page_and_sort_reports_collection_with_method * def page_and_sorts_reports_collection The last method should be 'page_and_sort_reports_collection' ('_sorts_' -> '_sort_'). One other thing. On the wiki (http://ajaxscaffold.stikipad.com/doc/) in the 'Differences between the generator and the plugin' section it is written that the plugin doesn't have any unit/functional tests. Does that mean that the testing is not automated? If so, do you have plans to create unit/functional tests in the future? Thanks again!
  37. Bart Duchesne Says:
    hi Scott, Is it possible to use :include in the queries to have lazy population; I have the impression that it is foreseen but it doesn't show up in the upper API's. Thanks for the plugin.
  38. Bart Duchesne Says:
    hi Scott, I have made the rowactions a partial; makes it easy to override them in a view. made a include_for_#{plural}_collection and pass that to options[;include]; easy
  39. Scott Rutherford Says:
    @Sjors, thanks for pointing that out, I'll update the post. As for the tests right now, well there aren't any. But they are in the pipeline...... @Bart, cool. Thanks for posting that back. Glad you got it working. We have done something similar with the actions in the next version. Cheers Scott.
  40. Valin Says:
    Scott, hi Any news on how to make it work [the plugin] in a parent-child relation ? Saw gabson's post, but nothing more. I really, really don't want to go back to the generator, but I might have no choice ? Any advice appreciated. Thanks BTW, amaising great stuff you did here !!!
  41. Scott Rutherford Says:
    Hi Valin, thanks. Unfortunately as this stuff is much better supported in v4.0 I haven't really spent any time trying to get the associations to work in v3.*. There is no fundamental reason why its shouldn't though. Cheers Scott.
  42. Ariel Diaz Says:
    Hi, I'm trying to use the plugin and the single table inheritance, but I don't know how. Are there any guide like the ajax scaffold generator to build scaffolds with single table inheritance?? There's an example with person and pets. Thanks. Ariel
  43. Mark Miller Says:
    I'm just starting with R/R and this plugin is saving me a lot of hassle. Only one problem so far. I'm working on a little sandbox app - a quotes app - and I have authors has_many quotes. The author_id field and column won't show up in the table or forms. I don't care if it's a number, I just want to be able to enter a value. I was able to get the table for show the author_id column through the instructions on the other page on this site. But how can I get the author_id field to show up in the edit form?
  44. Scott Rutherford Says:
    @Ariel, I'm not sure why you would have difficulties with STI, just pass the appropriate model name to the ajax_scaffold call and all should work fine. @Mark, to change the edit form you need to override the _form.rhtml template by copying it your controllers view dir and altering it to suit your needs. Cheers Scott.
  45. NAYAK Says:
    Great work Scott, I use ajax scaffold too a great extent and hence I could spot a bug (not sure) in the conditions_for_methodName_collection. As you had said, you can put any query which works in conditions of find in Rails, but I could not use nested SQL which could be used in the find but not the ajax scaffold method. waiting for response
  46. Horst P Says:
    the ajax_scaffold plugin needs to
    require 'csv.rb'
    Otherwise I get an "uninitialized constant MyController::CSV" error when using "totals". That said, the plugin seems to rock. Thanks! Still need to check, whether extensions (group by aggregations etc.) can be implemented in a clean and easy way, though.
  47. juantar Says:
    I modified the _form field to fit my needs. I have a helper method that adds a text_field_with_auto_complete field to the form. My form is failing to show up and I see in the console that the action is returning an http error 500 23217 but that is all it shows. Is there any way I can make it show the standard rails error page that shows me the exact error (not just a number)?
  48. Horst P Says:
    Why isn't sorting and pagination done by the database system, using something like
    
    model.find 
    	:order => options[:order],
    	:limit => options[:per_page],
    	:offset => paginator.current.offset,
    	...
    
    I actually implemented a page_and_sort_my_model_collection_with_method that overrides the default, because I need a special column and I implemented sorting/pagination similar to the code shown. This should be faster and more efficient than doing it in Ruby, shouldn't it? Are there any restrictions to the SQL sorting/pagination or what is the reason to do that in Ruby?
  49. Scott Rutherford Says:
    @Horst, the sorting is only done in ruby for methods that don't exist in the database. i.e for virtual columns. If it can be done in sql it is. As for the csv.rb, that should already be required if needed. I'll have to take a look. @Nayak, I haven't tried nested SQL, perhaps you could email me an example. @juantar, try using firefox and firebug that should give you all the info you need to debug ajax issues.
  50. Arthur Holstvoogd Says:
    Hi! I've extended ajax_scaffold a bit with an extra action: details it's pretty much like edit except you don't get to edit anything :P It allows you to create a custom detailed view of an entry containing data you don't want or can't show in the table. It's still pretty crude and probably has some load from copying it from edit, but if anybody wants it, let me know. If there is some real intrest i'll clean it up and test it. An example of why i made it: I have a People in my model with about 10 attributes and any amount of custom attributes which i wouldn't want to show in the table ofcourse, but i don't want to leave the table to take a quick peek at them. So i click 'details' and just like edit a view pops up but the fields are uneditable and there only is a close action. It even works with :except :)
  51. Jo Says:
    Thank you for your helpful tutorial. I'm new to RoR and have found your scaffold plugin very helpful. Is there a way to only filter the dataset for a table under certain conditions? I would like to display the table with all data on one page and a subset of the data on another page. I'm using render_component :action => 'table'. With the filter example, I can get the correct subset but I don't know how to have the full set sometimes and the subset sometimes. Thanks.
  52. Scott Rutherford Says:
    Hi Jo, you can just pass parameters in the component call to use in the decision making process: render_component :action => 'table', :params => params.merge(:filter => 'all') or something. Cheers Scott
  53. zxpduan@126.com Says:
    H,I override the method do_update#{suffix},but there is a exception when I do "edit" option,why? thanks.
  54. Scott Says:
    Hi, I suspect because you are now using "ActiveScaffold":http://www.activescaffold.com. You will need to refer to those docs for customization. The original AjaxScaffold plugin is now deprecated. Cheers Scott
  55. David Says:
    Does anyone knows how to get FCKEDITOR to be saved to the database while editing in the activescaffold ? I've used a column overide to load the Fckeditor but it doesn't allow me to save it to the database.
Leave a Reply

This blog used the Shay theme as a base and is powered by Mephisto