REST nested resources

Hi

I'm porting an existing project to fit the REST philosofie. I have 3 resources that are nested: users, companies and roles. A user has and belongs to multiple companies, and depening on the company, a role will be assigned.

So I have an association table: users_companies with the following fields: user_id, company_id, role_id. But how do I assign a user to a company with a role the REST way?

Normally you would create a new controller, but I would like to do the assignments in the user form.

Anyone that has some experience on this?

Thank you in advance.

Michael Rigart wrote:

Hi

I'm porting an existing project to fit the REST philosofie. I have 3 resources that are nested: users, companies and roles. A user has and belongs to multiple companies, and depening on the company, a role will be assigned.

So I have an association table: users_companies with the following fields: user_id, company_id, role_id. But how do I assign a user to a company with a role the REST way?

Normally you would create a new controller, but I would like to do the assignments in the user form.

Anyone that has some experience on this?

Thank you in advance.

Sorry for adding this, but the association should also made possible on the company form.

I thought I had an answer until I saw this addendum :slight_smile:

Can you explain a little more what the company form is trying to do?

Jeff

Jeff Cohen wrote:

Michael Rigart wrote:

Sorry for adding this, but the association should also made possible on the company form.

I thought I had an answer until I saw this addendum :slight_smile:

Can you explain a little more what the company form is trying to do?

Jeff

Sorry Jeff if I wasn't clear enough. The company and user form are basically used to create and / or edit a company or user.

Under the user details, there will be another "section" where the administrator can assign the companies to the user he is creating /editing with a sertain role.

On the other hand, when he is creating a new company, under the company details, there will be another "section" where the admin can assign users to the company he is creating / editing with a sertain role.

So its a litle complex and I can't see how to do this as restful as possible.

Thank you for your intereset.

Anyone who has an idea this one?

It sounds like you need to update multiple kinds of resources from one form, which unfortunately Rails doesn't handle very well yet (there are some proposed changes in edge that might help this, though).

I'd recommend posting to the controller that seems to represent the "main" resource (the user or company, as needed). I'm imagining you've got some checkboxes for users (or companies) that will come along for the ride.

That being the case, I would suggest that your create action (say, for users) pull out the company list from the params hash first:

# remove company IDs from the params hash # and keep the array of company IDs for later use # You might not need this step depending on # how your checkbox names are setup company_ids = params[:user].delete :company

Now the params hash has just the user info remaining in it:

user = User.build(params[:user])

Now you can associate the user to the companies represented by the ids in one fell swoop:

user.company_ids = company_ids

Then save everything:

user.save # should save the relationships as well, I believe

Although it might seem "non-restful" to be assigning the company relationships from within the create action, I can't think of a better way to do it, and to me it actually seems reasonable. If the UI is specifying that creating a user includes assigning some associations, then I think it's legit to do this all within the create action.

What do you think?

Jeff

Hi Jeff

sorry for the late reply, but I have come to some solution that fits my needs. But their are still some minor code details that need some upgrading becouse they can lead to errors due to possible wrong user input.

As you said, I send the data to the controller that represents the main resource (in the example I'm giving now that will be the company resource).

One thing that you need to know is that the related users table is build through Javascript. This is becouse you can add/delete more/less users dynamically. For that, I just create a simple table, and add the rules dynamically in the body. So on the end of the page, I do the following:

<% @company.company_roles.each do |role| %>   Company.addAssociatedUser(<%= role.to_json(:only => [ :id, :user_id, :role_id ]) %>); <% end %>

The addAssociateUser mehthod adds the associations dynamicaly. On my form, I can add new rules by calling the method again, but without parameters.

So far, so good, that works. Now I submit my form, and all the data get send to the create/update action. To make it simple, I will show the create action:

  def create     @company = Company.new(params[:company])

    if @company.save && save_user_role_associations       flash[:message] = l("company_create_response")       redirect_to(companies_url())     else       render :action => "new"     end   end

So, I create my company object and save it. But, it also calls for a private action "save_user_role_associations". This action looks like this:

  def save_user_role_associations     saved_ids =

    params[:company_user_association].each do |key, item_vals|       if item_vals[:id].to_i > 0         association = CompanyRole.find(item_vals[:id])         association.update_attributes(item_vals)       else         association = CompanyRole.new(:company_id => @company.id, :role_id => item_vals[:role_id], :user_id => item_vals[:user_id])         association.save       end

      item_vals.delete :id       saved_ids << association.id     end

    CompanyRole.find(:all, :conditions => ["company_id = ? AND id NOT IN (?)",@company.id, saved_ids]).each do |association|       association.destroy     end   end

This all works, as long as the user input is correct. But lets state, that there is a wrong user input in the association data. This will reside in an application crash, becouse the error is not caught. The CompanyRole data doesn't get validated and displayed if an error occurs.

The second problem lies in the create method.

@company.save && save_user_role_associations

Here the @company gets save, even if their is an error in the save_user_role_associations action. How can I handle this?

Then another question (well, 2 question that relate to the same problem). Lets say I have related models that I need to use. Like a company can have a role. The roles are shown in a select, but ofcourse we need to load the resources before we can fill them. What I do now is create a private action in my controller that pulls all related resources that are needed.

  def relational_models     @roles = Role.find(:all)   end

So, for those resources to be available in the view, I call the relational_models action in my new / edit action. That works like a charm. But, the problem occurs when there is an error on the form submit. As you can see in my above example, I redirect to the new action. But then the related resources don't get loaded again, and that resides in an application crash. How can I make sure, the related resources are loaded again properly? The 2nd question relates to this problem. I always load custom css/javascripts dynamically. So only when they are needed. I group my javascript per controller. So in overwrite the initialize action in my controller:

  def initialize     super

    add_styles_and_scripts   end

The add_styles_and_scripts action is again, a private action in my controller and can look like this:

  def add_styles_and_scripts     @scripts = ["company.js"]   end

This works, but again, when their is an error when the user submits wrong data, the javascripts/css files don't get loaded again. Its like the initialize action doesnt get executed again.

I hope you, or someone else can help me with those problems, becouse once they are out, I have a more solid application.

Thank you in advance

Hi Jeff

[snip]

Wow, I don't think I can respond to everything, but here are a couple of thoughts....

The second problem lies in the create method.

@company.save && save_user_role_associations

Here the @company gets save, even if their is an error in the save_user_role_associations action. How can I handle this?

Wrap everything in a transaction:

transaction do   @company.save!   save_user_role_assocations    ...   end end

If you raise an exception inside the block, AR will roll everything back for you.

charm. But, the problem occurs when there is an error on the form submit. As you can see in my above example, I redirect to the new action. But then the related resources don't get loaded again, and that resides in an application crash. How can I make sure, the related resources are loaded again properly?

Don't redirect back to :new, you need to just *render* your form again. I know, sounds a bit weird, but otherwise you'll lose your instance variables, including all your validation errors. Just do:

render :action => :new

to redisplay your form.

Jeff

REST with Rails, Oct 4, 2008, Austin, TX: http://www.purpleworkshops.com/workshops/rest-and-web-services

Hi Jeff,

If you raise an exception inside the block, AR will roll everything back for you.

The rollback did come to mind, but will that display the errors that I have set in my model?

Don't redirect back to :new, you need to just *render* your form again. I know, sounds a bit weird, but otherwise you'll lose your instance variables, including all your validation errors. Just do:

render :action => :new

to redisplay your form.

This was my bad, I do rerender the form. But still I lose the instance variables that I have are related + it looks like the initialize action isn't invoked

Thank you for all your patience

I can't believe that no one ever came across a similar problem.

It seems that nested resources still can cause some problems in Rails.

If anyone knows why my instance variables get lost or why the initialize action doesn't get invoked, please do tell me.

I'm eager to learn :slight_smile:

Michael Rigart wrote:

I can't believe that no one ever came across a similar problem.

It seems that nested resources still can cause some problems in Rails.

If anyone knows why my instance variables get lost or why the initialize action doesn't get invoked, please do tell me.

I'm eager to learn :slight_smile:

Ok, so what I have found out. When you invoke render :action => .... it doens't run the action. So all the related models don't get loaded again, I need to load them before the render :action statement :slight_smile:

Invoking those actions can do look a bit nasty in the long run. Would it be a good idea to run them through a filter (like for example the before_filter :only => )?

Now I havent got the time to see if the nested resources give their error messages. Going tot test this together with the rollbacks :slight_smile:

Also, I find using validates_associated on the models concerned and then doing a valid? call on the top level object is quite a good way to get at the error messages and avoid trying to save objects only to find they have errors, although I still do use a transaction to ensure integrity.

Tonypm

tonypm wrote:

Also, I find using validates_associated on the models concerned and then doing a valid? call on the top level object is quite a good way to get at the error messages and avoid trying to save objects only to find they have errors, although I still do use a transaction to ensure integrity.

Tonypm

Hi tonypm,

could you collaborate your statement a bit further? I don't understand it fully.

Thank you in advance

Michael Rigart wrote:

tonypm wrote:

Also, I find using validates_associated on the models concerned and then doing a valid? call on the top level object is quite a good way to get at the error messages and avoid trying to save objects only to find they have errors, although I still do use a transaction to ensure integrity.

Tonypm

Hi tonypm,

could you collaborate your statement a bit further? I don't understand it fully.

Thank you in advance

Ok, so lets say I'm saving data like this:

  def create     @company = Company.new(params[:company])

    if @company.save && save_user_role_associations       flash[:message] = l("company_create_response")       redirect_to(companies_url)     else       add_styles_and_scripts       relational_objects       render :action => "new"     end   end

private

  def save_user_role_associations     saved_ids =

    params[:company_user_association].each do |key, item_vals|       if item_vals[:id].to_i > 0         association = CompanyRole.find(item_vals[:id])         association.update_attributes(item_vals)       else         association = CompanyRole.new(:company_id => @company.id, :role_id => item_vals[:role_id], :user_id => item_vals[:user_id])         association.save       end

      item_vals.delete :id       saved_ids << association.id     end

    CompanyRole.find(:all, :conditions => ["company_id = ? AND id NOT IN (?)",@company.id, saved_ids]).each do |association|       association.destroy     end   end

How do I implement the transaction in a clean way.

And how do I merge the CompanyRole errors when they occure? At this moment I display the company errors like this:

<%= error_messages_for 'company', :header_message => l("try_again"), :message => l("message_company") %>

But I also want to display the CompanyRole errors in the same list as the company errors.

Ok, what I am suggesting is to build the object and its associated objects before saving them. Then doing valid? is a good way to get at the error messages, and have all the objects constructed so you can render them. Create is a different case because you are trying to handle associations for an object that has not yet been created (ie. has no id). I will make a suggestion for Update

def update     @company = Company.find(params[:id])     if @company.save_company_and_roles(params[:company_user_association]) ........etc

In the Company model

validates_associated :company_role

def save_company_and_company_roles(roles)    # first build the objects    roles.each do |key, item_vals|       if item_vals[:id].to_i > 0         CompanyRole.find(item_vals[:id]) = item_vals       else         company_roles.build(item_vals)       end     end     return false if !self.valid? # this should mostly avoid the transaction ever failing     transaction do        self.save        company_roles.each { |r| r.save}        company_roles.find(:all, :conditions => [ id NOT IN(?)", saved_ids]).each do |association|          association.destroy        end     end end

By having this method in the model, using the company scoped finders tidies the code a bit. I have tweaked your code to reduce it a bit so I may have borked it, but you should be able to see what I am driving at. (Some of the self's are not actually needed)

In your view you can now use error_messages_for ['company', 'company_role']. To improve the error messages, I actually go through the child objects and do add_to_base for the parent errors. This allows me to include the child id so that the error is more explicit.

For the create situation, I would start with a similar method to figure out what you want to do. Once you have that you will probably find you can do both together. Incidentally, if you do c=Company.new and then use c.company_roles.build, then you can still do c.valid? to generate the errors.

Using the transaction, if there is any failure to save, then an exception will get raised which you may want to handle in the controller.

I dont see the Railscasts on complex forms mentioned in this thread, but I take it you have looked at those. Some great ideas in there.

hope this makes some sort of sense Tonypm

tonypm wrote:

Ok, what I am suggesting is to build the object and its associated objects before saving them. Then doing valid? is a good way to get at the error messages, and have all the objects constructed so you can render them. Create is a different case because you are trying to handle associations for an object that has not yet been created (ie. has no id). I will make a suggestion for Update

def update     @company = Company.find(params[:id])     if @company.save_company_and_roles(params[:company_user_association]) ........etc

In the Company model

validates_associated :company_role

def save_company_and_company_roles(roles)    # first build the objects    roles.each do |key, item_vals|       if item_vals[:id].to_i > 0         CompanyRole.find(item_vals[:id]) = item_vals       else         company_roles.build(item_vals)       end     end     return false if !self.valid? # this should mostly avoid the transaction ever failing     transaction do        self.save        company_roles.each { |r| r.save}        company_roles.find(:all, :conditions => [ id NOT IN(?)", saved_ids]).each do |association|          association.destroy        end     end end

By having this method in the model, using the company scoped finders tidies the code a bit. I have tweaked your code to reduce it a bit so I may have borked it, but you should be able to see what I am driving at. (Some of the self's are not actually needed)

In your view you can now use error_messages_for ['company', 'company_role']. To improve the error messages, I actually go through the child objects and do add_to_base for the parent errors. This allows me to include the child id so that the error is more explicit.

For the create situation, I would start with a similar method to figure out what you want to do. Once you have that you will probably find you can do both together. Incidentally, if you do c=Company.new and then use c.company_roles.build, then you can still do c.valid? to generate the errors.

Using the transaction, if there is any failure to save, then an exception will get raised which you may want to handle in the controller.

I dont see the Railscasts on complex forms mentioned in this thread, but I take it you have looked at those. Some great ideas in there. #73 Complex Forms Part 1 - RailsCasts

hope this makes some sort of sense Tonypm

Thi Tonypm,

thanks for you explenation. I will look in to it this weekend, since I'm working on another part of the project today and I need to finish the rewrite by the end of next week.

You example does make sense, and I also found the railcast threads on complex forms this morning, so I'm definataly going to take a look at it.

When I come up with a working solution, I'll post my actions here, so it might help other people in the future if they run into a similar problem.

Hi Tonypm

I couldn't resist and start working on it now. And I think I'm almost there. For good testing perposes I've implemented the algoritm in a my invoices, witch has many invoice_items. The filosophie is the same with companies and roles.

So in my invoice model, I've added:

..... has_many :invoice_items validates_associated :invoice_items

  def save_outgoing_invoice_and_items(items)     saved_ids =

    return false unless self.valid?

    transaction do       self.save

      items.each do |key, item_vals|         if item_vals[:id].to_i > 0           item = OutgoingInvoiceItem.find(item_vals[:id])           item.attributes = item_vals         else           item = OutgoingInvoiceItem.new(item_vals)         end         item.save

        saved_ids << item.id       end

      outgoing_invoice_items.find(:all, :conditions => ["id NOT IN (?)", saved_ids]).each do |association|         association.destroy       end     end   end .....

Then in the invoice controller, I call the model action to save the invoice and the items:

   if @outgoing_invoice.save_outgoing_invoice_and_items(params[:outgoing_invoice_item])

Then in my view I have <%= error_messages_for ['invoice', 'invoice_items'], :header_message => l("try_again"), :message => l("message_invoice") %>

To display the error messages.

The thing now is, that when I make a mistake in the invoice_items, the invoice gets saved without any invoice_items, and I don't get any error messages saying that something went wrong. So the transaction doesn't rollback the parent entry, only the child entries + the fact that I miss some error reporting.

I can feel it will be something stupid that I overlooked.

PS: The way the model action is now written, it can be used for both create and update

I think the problem is that you are not building the object before you do the valid? test. Which means validation will pass, but then in the transaction, validation will fail so that the invoice_item will not get saved, but the transaction will not fail because a validation error is soft. It doesn't raise an exception.

If you want to break the transaction you would need to do a raise if the save is false. But then you would need to handle the exception in the controller - and of course subsequent items would not get saved or loaded into the objects so you would not get their values re- displayed.

That is why in my example, I build the objects first, then do the valid? check and then open the transaction and do the saves.

So the valid? is there to trap normal user errors, and the transaction is there to trap unexpected system errors. Which means that if for some reason a save fails unexpectedly, the database is not left in an possible inconsistent state.

hth Tonypm