In this third and final episode on complex forms I will show you how to edit a project and multiple tasks all in one form. This includes removing and adding tasks dynamically as well. See the show notes for updated code.
I believe that you should delete :id from parameters before model going to save or you will get error like this: "Can't mass-assign these protected attributes: id".
Here is the code of method in the Project model that will fix that issue:
def task_attributes=(task_attributes)
task_attributes.each do |attributes|
unless id = attributes.delete(:id)
tasks.build(attributes)
else
task = tasks.detect { |t| t.id == id.to_i }
task.attributes = attributes
end
end
end
Have you tried out this technique in Rails 2.0 preview? The task_attributes= method doesn't seem to be getting called on @project.update_attributes.
However, the Task#update method is still getting called, so I wonder if this was another change in Rails 2.0, eliminating the need for the after_update filter on Project.
One thing came to mind, though: instead of sending destroy flags from the client side, wouldn't it be easier to set the flag in Project#task_attributes= for any record for which there's no attributes passed in?
@Ilya & Andrew, I have yet to test this extensively in edge rails and 2.0 preview. I'll do that and try to post the fixes in the show notes.
@George, that's an interesting idea. I hadn't thought of that! I'm wondering if it will be anymore difficult to carry the state across when validation fails. I'll have to play around with it. Thanks!
I hope somebody convinces you do to a part 4 covering the error messages :-)
Just a matter of style preference, shouldn't save_tasks in Project be marked as private? Perhaps it's just my old-school tendencies but I try to make everything private by default and only expose what needs to be exposed. I understand if you just didn't bother in order to keep the episode on topic.
Hey Ryan,
thanks for another curious ingenious screencast. Before I was doing that Javascript stuff with rendering a partial into a string and then inserting this with js. But your method is more rubyish.
But one thing is getting me a headache. You are using :index => nil in the form helpers for text_field. I tried to use that with select but it does not work. The index is rendered anyway. Do you have any clue?
As I was watching this episode, I notice in your application layout you have a helper called show_title?, related to your "Pretty Page Titles" from episode 30. I was wondering how that particular little snippet works.
I love these screencasts! I've been working on Rails for almost a year now, and you seem to come out with the one that I need just as I need it.
Nice episode. I agree that the ":index => nil" snippets are a bit ugly, so I submitted a patch that adds support to pass the index option to the fields_for helper:
http://dev.rubyonrails.org/ticket/9883
Please feel free to "+1" the patch if this patch looks of interest to you.
Thanks ryan, this has to be one of the longest railscasts.
i would like an episode on error messages. i have two models, one is post, the other is photo. photo is a model using image_column, users can upload 'photo' while they create a post, but it's also important to display the error message of 'photo' while saving, as image_column does validations like format check and size check. i guess quite a lot people are doing it this way, so i really hope there will be an episode on error messages.
I love this technique and use it on a few projects I'm working on. A few bits of contention from an accessibility stand point:
1. You should use label tags instead of plain text.
2. One thing that I'd like to see more of is unobtrusive javascript. It's pretty easy to pick up and makes pages more accessible. I think it'd be great for you to push this since you're in a position to reach a lot of people.
Nice tutorial Ryan, I took at stab at pluginizing this technique and have uploaded the plugin and a sample project using it at: http://code.google.com/p/multimodel-forms/
If anyone has any suggestion feel free to email me (my email is on the project home page), or just add it under "Issues". Im currently working on view helpers so try and simplify the views alot.
This is the most awesome yet! I agree with the many others! "Complex Forms Part 4" Single Form Multiple Model Validations! That would be so much help to so many people. Thanks for your amazing work.
I have a question. Let's say my project has many tasks, but it also has many tags and I want the project form to be able to accommodate building two models in reference to a project on the same form. How would one refactor the following *_attributes=(*_attributes) methods to avoid duplication?
def task_attributes=(task_attributes)
task_attributes.each do |attributes|
if attributes[:id].blank?
tasks.build(attributes)
else
task = tasks.detect { |t| t.id == attributes[:id].to_i }
task.attributes = attributes
end
end
end
def tag_attributes=(tag_attributes)
tag_attributes.each do |attributes|
if attributes[:id].blank?
tag.build(attributes)
else
tag = tags.detect { |t| t.id == attributes[:id].to_i }
tag.attributes = attributes
end
end
end
"maybe I will make it [display error messages] a futur episode depending on the demand"
... there is demand ;-)
beside "the demand" there is an other reason to go into this topic: "additional options for presenting model errors -(Changeset #7870)"
I've got problems with validation of tasks. The has_many relation silently puts up additional validators, which are invoked at the time you try to save the projects model.
The problem is that these hidden validators affect the errors of the project by adding messages like "tasks is invalid" for each invalid task. (In fact, in my case the model names are different, so I'm giving the approximate messages.)
Now, I am seeing two issues with that. First, is there any way to localize the messages produced by has_many? Second, is it anyhow possible to disable adding these messages at all (I want only one message for all failures and I want it customized).
I have the same question. It seems there is a lot of refactor that needs to be done to accommodate multiple sub models.
I posted this question to "refactor my code" to see what the community can come up with. Here is the URL. http://refactormycode.com/codes/87-combine-contact-attributes-in-model
There is a bug in railscasts code. When you point your browser to non existent episode (for example http://www.railscasts.com/episodes/100), than your application will crash. I think some <tt>begin/rescue/end</tt> could help ;]
I have a problem that I can't figure out for the life of me.
I understand the part about
Task: <%= f.text_field :name, :index => nil %>
I must be formatting something wrong or not writing this part write, but how do you set a select list to :index => nil?
Here is my code:
<%= f.select(:phone_type, [ ['',''], ['Home', 'Home'],
['Work', 'Work'], ['Cell', 'Cell'], ['Fax', 'Fax'],
['Other', 'Other'] ]) %>
I have tried :index => nil, tried :selected => nil after the array, but am receiving 'Conflicting types for parameter containers. Expected an instance of Array, but found an instance of Hash.' in my log file. Any help please?
Great screencast ! But i have one issue, i have read on how to use this technique with select or collection_select, but what about a date_select, the above workaround doesn't work for it ? Any clue to get out of this, anyone plz ?
I stumbled on the problem with select tags, and using html_options instead of options.
Related to that, checkboxes and radio buttons don't have html_options, so you have to manually create the elements (using check_box_tag and radio_button_tag respectively).
Otherwise, because Rails tries to be helpful when dealing with unchecking checkboxes, you get weird behaviour when updating a project, along the lines of getting extra, but blank, tasks.
Thanks for the comments everyone. I'll try to answer some...
@TheCompWiz, there's no need to call update_attributes here because the attributes have already been set earlier. A simple save is all that's needed.
@Ted, yeah, I should have made the save_tasks method private. Good catch.
@Brandon, the show_title? is another helper method which just checks an instance variable I set earlier. Here's the code:
http://pastie.caboo.se/108319
@magistrator, I wish there was a way to disable this automatic validation message but I'm not sure how. Instead what I have done is built a custom error message handler which I can instruct to ignore certain validation error messages.
@Michael, I'll try to put up the full project source code in a day or so.
@Gilles & Darryl, I wish I had an easy answer for the date select or checkbox fields not working, but unfortunately I don't. You'll have to make them manually.
On top of this, checkboxes are tricky because the browser only sends them if they are checked. This means they won't be organized into the proper task attribute hashes. You either need to keep track of them separately or use some javascript to set a hidden field when they are changed.
I am having problems with validations on the task attributes on update. All the validations work wonderful on create. But for some reason, and I am assuming it is because the task_attributes=(attributes) method just assigns all the attributes to a task on update. How can one make sure the same validations pass for a task on update of a project when the task attributes are directly assigned?
Found the problem. validates_associated :tasks in the project.rb file. It is missing from the source listed above, but I was able to catch the error watching the video again. Thanks.
My question - what about transactions? In this simple example, it's probably not a big deal if there's an error saving tasks after the project has updated, but in general, I want all of my models to be saved or none (if failure). The rails before/after methods are convenient, but I see them more as ways to fulfill pre- and post- conditions v. implementing core save operations. I'm in the midst of implementing a multi-model save now and have overridden save/save! to handle the logic of saving all the models in a single transaction. I'm not sure this is the best way, but it allows me to wrap all model updates in a single transaction.
We use acts_as_ferret, which is very slick and convenient but makes me nervous for similar reasons - indexing occurs after save, so if indexing fails it doesn't rollback the database. Thus the index can get out of sync with the model.
These are big issues in Java land, but I rarely hear them talked about in the Rubyverse. Was curious if anyone else shared these concerns. Perhaps they are simply not as important to the types of apps that Rails developers tend to build?
+1 on any future episodes dealing with associations, errors or validation for multi-model save operations. This seems to be a corner of Rails where good practices aren't well documented (if even established).
@Jason Jason, By handling the destroy through hidden fields, it gives the user the opportunity to Cancel all changes. Nothing persists until the form is submitted which makes for a nice user experience. Of course different apps warrant different behaviors.
@Joe, sorry about that, source is updated to include that line now.
@Rich, I haven't tested this fully, but I believe if an exception is raised in a callback (save_tasks method) then it will properly rollback the save of the project and other tasks. Is it not working for you?
Ryan,
Validation and error displays are tricky. Maybe a collection good practices can serve as a valuable episode.
Here're some questions:
1. validates_associated on tasks is nice: what if I want to create a blank project without any task? I don't want it preventing me from creating a project; and later I want to edit this blank project and add tasks.
2. Will "task.new_record?" in the partial fails in a blank project case (no task) if you try to edit it, since task will be a nil object?
For those who are dealing with date_select tag with this method of updating related models in one form, one easy workaround i have found is to use a text_field instead of the date_select to enter your date data … it's not a perfect one, but …
You can also do it by hand by usind select_date_tag and so on …
Thanks for the feedback, Ryan, I think you're right. It never occurred to me that save() and the accompanying life-cycle methods were wrapped in a transaction w/in the framework, but after your comment I did some experimenting and that appears to be the case. (Adding an after_save method that raises an exception and calling save() rolls back a model update that otherwise saves successfully.)
It's news to me (good news, I think, though I'm still digesting the implications) that ActiveRecord is running save() + life cycle methods in a transaction. Will have to dig deeper into the ActiveRecord code - thanks for the insight!
@john, if the project has no tasks then it is treated as an empty array so neither of the issues you mentioned should be a problem. But I haven't tested it extensively, does it not work?
@Gilles, I'm pretty sure you can adapt this to work with HABTM. One thing you'll have to do is decide how the removal process will work. Does it just remove the join or the end model? Everything else is very similar.
@Skyblaze, no PDF, but I'll be writing a few tutorials on railsforum.com in the near future showing this technique with possible additions.
Just to close out the transaction discussion, this from the Rails doc:
Save and destroy are automatically wrapped in a transaction.
Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction depend on or you can raise exceptions in the callbacks to rollback.
I think that we could simplify this example a lot when not worrying weather task is new or updated.
On each update we could first delete all existing tasks and then add all submitted:
def task_attributes=(task_attributes)
tasks.clear
task_attributes.each do |attributes|
tasks.build(attributes)
end
end
This way we could remove all marking of new and deleted records and save_tasks method.
We should probably had to mark tasks association as :dependent => :delete_all and add transaction in controller so we don't lose tasks if some validation fails.
More on complex forms and validation is very welcome. I'm trying to do the reverse of your relationship here. I'm trying to create a User that belongs_to a group. I've discovered how to do that with "build_group", but I need validate the created group before saving the user, if the user is creating a new group. Otherwise, I just want to validates_presence_of the group selected in a drop down. The whole validation system, with callbacks and conditionals, is pretty hard to get a good overview handle on.
@Igor, interesting idea. Destroying the tasks each time would probably work, however I hesitate to do this. There may be extra state (such as created_at) that we need to stay persistent. There might be other associations with the task that would be lost as well.
@RainChen, from my experience calling @project.save will only save the tasks if the project itself is new as well. This is why I only handle the callback on after_update. Although I haven't tested this extensively.
@tonghoho, I don't think having two "[]" in a form field name will work properly. Instead you'll have to keep track of the sub tasks separately. Sorry I can't go into detail.
Thank you for the cast. Instead of complicating things with the destroy logic and doing all nasty stuff, how about a call to link_to_remote to destroy the element?
For future Googlers, I didn't find the real solution, but what I ended up doing was leveraging the Chronic gem to parse natural language date-time expressions.
I've using the code from this episode, weirdly enough, no matter I try to remove a task or edit task value, the code "@project.update_attributes(params[:project]) " in project controller has always turned out to be false, therefore redirect me to the edit page, where the task values ain't changed. I've spent a day to figure out what's wrong, still got no clue.
Let me just say Ryan: thou are the man. My need for this and your creation of these podcasts came hand in hand. Props. Also, thanks a lot @Ilya for the error fixes when using the latest edge.
@Joan: try 'puts params[:project]' before that line of code in your controller and then check your development.log to see what it's spitting out. If it's not normal, then my guess if that you have a form builder problem in one of your views. Good Luck!
if you use the after_update instead of the after_save callback and you are creating a project for the first time (aka, not updating) and you create a task then delete it and then hit save the deleted task will still be created. this happens because save_tasks callback is not called on the create of the project model so you don't do the check for should_destroy?. one fix is to just use after_save instead of after_update, although i guess this results in a double save on the initial creation of a project.
another fix is to add a before_create alongside the after_update:
before_create :bc
def bc
fields_to_destroy = []
fields.each do |field|
fields_to_destroy << field if field.marked_for_destroy?
end
fields_to_destroy.each {|field| fields.delete(field) }
end
i think this is optimal because it won't even hit the db for new tasks on a new project that are marked for delete. but changing the callback to before_save is much simpler and less prone to bugs
I have items that belong to a studyform. I am trying to save both an item name and and item type into each studyform. My _item partial has:
<% fields_for "studyform[item_attributes][]", item do |f| %>
<p>Item: <%= f.text_field :name, :index => nil %>
Type: <%= f.text_field :type, :index => nil %>
<% if item.new_record? %>etc
The name saves into the items database but the type is always null. Any help would be appreciated.
The helper inserts a hidden field after checkboxes to have it send 0 if the box isn't checked.
Unfortunately it means that when it is checked both the 1 and the 0 get sent and it thinks there are 2 sets of attributes to save. It can also mean the values get applied to the wrong rows of data.
You shouldn't use .detect to pull up the appropriate task. To keep the task associated with the Project, use the association finder method tasks.find_by_id(params[:id])
This way you avoid instantiating a lot of unnecessary task models. This is essential when a model may have thousands of sub-models.
@Ryan: I may have an easier method of removing tasks, but I'll need to test it when combined with the concepts in this set of screen casts.
I'm using line items of an invoice, but I'll use your terminology. So I have a collection of @tasks from the database, and a new empty array: new_tasks. I loop through the posted params, searching for an existing entry in @tasks, which I then update. If not found, I build a new task instead. Either way I append that task to the new_tasks array.
When I assign the @project.tasks = new_tasks and save @project, Rails handles the removals for me, simply by the fact that they don't exist in the collection... smart! They don't exist in the collection because they weren't posted and added to new_tasks. So I have no need to mark for destroy or even call destroy.
I have a bunch of code in the controller, and I'm not using the virtual attributes you mentioned... so I'm really looking forward to combining these to methods and seeing if it all works!
For me it works perfectly but now i have to add several "third" models that are related to my second model. How can i do that. Anyway an issue with this code is the fact that we will have input fields with same ids...
Ryan -- Thanks for the screencasts. They are extremely helpful.
@Paul, I made it work with checkboxes. I used check_box_tag. I use the checkbox value in the model (in def choice_attributes) to set the value of the field I want set, rather than setting it directly with the checkbox, like so:
It's a workaround but it works (and work s for radios, too). Then on the form end, I manually check the box like so:
<% is_checked = choice.correct == 1 ? true : false; %> and render the check box this way:
<%= check_box_tag('question[correct_choice_id][]',choice.id, is_checked,{}) %>
(that's on edit -- on new, when I don't have question.correct.choice I have named the checkbox with a counter from within the partial, and run essentially the same thing. I'm not sure if this will be clear or not but it does work for me so hopefully it helps.
I've found a couple of issues here that maybe only affect me, but I'll post in case someone else experiences this.
First, the "should_destroy" attribute was showing up in the first item regardless of which item was removed. The issue was that the 'should_destroy' hidden fields had no value and so did not end up in the correct array. I switched to hidden_field_tag with a default 0 value and it's fixed.
Second, when passing strings with non-alpha chars the URL encoding for javascript was running once for each layer of the array. So '-n/a-' was becoming "-n%25252Fa-", I did a 3.times{ attr[:string] = URI.unescape(attr[:string])} within the task_attributes= method. Hackish, but it works.
I was a bit confused by the workaround in the should_destroy? method, until I remembered that MySQL lacks a genuine BOOLEAN datatype.
With PostgreSQL, you can define a boolean field in your model (without a trailing ?), and a predicate method (with the trailing ?) is automatically there in your model. Handy.
I can edit and delete tasks with no problem, but when I try to create a new task (or several at once) the very first of the previously saved tasks gets wiped out. I copied Ryan's code line for line and am running this in Rails 2.0.
ALmost forgot to mention, I only have this problem when I have the marked_for_destroy hidden field and js link in that partial. I remove them and can add tasks with no problem. Odd..
How do I reference the project[task_attributes][] fields for auto_complete_field functions... Does anyone have an example using auto_complete_field and fields_for?
Only one question.. I'm trying to do this with a habtm relationship (possible topic for another screenie? ;)) and unfortunately it doesn't save the linking table when updating attributes. :(
Does anyone have an idea how to tell rails to manually save it?
Ryan, this was a terrific tutorial. I'd definitely be interested in seeing more on validations, etc.
I'm also curious as to how you might take a progressive enhancement approach. The current method seems to require Javascript. I tried using a TaskController to provide web services, but if I make it polymorphic (Project and ToDoList), all hell breaks loose... or at least, it looks really ugly.
I like the fact that nothing is lost until the entire project is saved, so I'd prefer to keep it that way, but adding and removing tasks becomes difficult without Javascript.
Thanks so much for another great screencast series. However, I found it a little bit complicated and cluttered with all these hidden fields, javascript and such. So I took a slightly different approach. First, I modifed the task partial so that it passes always a model id:
I find this series very useful. I've implemented some stuff using the technique you've described and it works great, but I'm interested how could I make file upload using this very technique (gmail like upload). Can you help me please?
I found this railscast series to be particularly useful! After stumbling across Jamis Buck's post about moving associated creations to the model your screen cast really cleared up the concept for me.
Validations had me puzzled, but I finally figured it out. I've posted what worked for me in my situation over on my <a href='http://psifire.livejournal.com/119168.html'>Live Journal</a> page... since I don't have a real blog.
@ Gernot Kogler: I think that your idea can not work - I have been experimenting quite a lot with it and I think that all duplicate named imputs will get lost (for example if you have twho or more new tasks). The trick of putting the new items to the array only works if there is bare [] in the name of INPUT field. SO I suggest changing your conditional so it gives empty string for new tasks.
I unfortunately ran into a problem while using the code. My Task model has more than one field (name, priority and estimated cost) and they all are required (i.e. there is a validates_presence_of rule for each of them).
The problem happens when the user updates an existing task. More precisely when the user deletes the content of one of the text field and then clicks the remove link for that task.
As you can guess, since the remove links just hides the task, an incomplete task is sent to the controller and since validation happens before the after_callback is called, validation fails, the task is not destroyed and the controller redirects to the update page (with an error saying that the task is invalid)
Fortunately, the solution is not too difficult, all we need is to add a method in the Task class that returns false if the task should be destroyed and the pass that method as an "if" parameter to all validation rules for the task.
I believe that you should delete :id from parameters before model going to save or you will get error like this: "Can't mass-assign these protected attributes: id".
Here is the code of method in the Project model that will fix that issue:
def task_attributes=(task_attributes)
task_attributes.each do |attributes|
unless id = attributes.delete(:id)
tasks.build(attributes)
else
task = tasks.detect { |t| t.id == id.to_i }
task.attributes = attributes
end
end
end
That was probably the most intense Railscast yet. Unbelievable how much ground you covered.
Excellent stuff as always
+1 vote for a future episode on associated validations.
:)
Have you tried out this technique in Rails 2.0 preview? The task_attributes= method doesn't seem to be getting called on @project.update_attributes.
However, the Task#update method is still getting called, so I wonder if this was another change in Rails 2.0, eliminating the need for the after_update filter on Project.
Hi Ryan,
Nice screencast! :)
One thing came to mind, though: instead of sending destroy flags from the client side, wouldn't it be easier to set the flag in Project#task_attributes= for any record for which there's no attributes passed in?
@Ilya & Andrew, I have yet to test this extensively in edge rails and 2.0 preview. I'll do that and try to post the fixes in the show notes.
@George, that's an interesting idea. I hadn't thought of that! I'm wondering if it will be anymore difficult to carry the state across when validation fails. I'll have to play around with it. Thanks!
Just out of curiosity... is there any reason you chose to use "task.save" as opposed to "task.update_attributes"?
Hey Ryan, another great episode.
I hope somebody convinces you do to a part 4 covering the error messages :-)
Just a matter of style preference, shouldn't save_tasks in Project be marked as private? Perhaps it's just my old-school tendencies but I try to make everything private by default and only expose what needs to be exposed. I understand if you just didn't bother in order to keep the episode on topic.
Hey Ryan,
thanks for another curious ingenious screencast. Before I was doing that Javascript stuff with rendering a partial into a string and then inserting this with js. But your method is more rubyish.
But one thing is getting me a headache. You are using :index => nil in the form helpers for text_field. I tried to use that with select but it does not work. The index is rendered anyway. Do you have any clue?
Taalas,
You have to pass an empty hash as an option before the :index => nil
Something like this will work:
f.select :field, [ "option 1", "option 2" ], {}, :index => nil
Figured this out from http://dev.rubyonrails.org/ticket/589
Hi Wes,
Thanks for that. I was stuck often because of that "empty hash" thing...
I don't know why I always tap into that trap.
JS question:
How do I make the mark_for_destroy function more generic so I can apply it to multiple models (ie. phones, email addresses, etc.)
right now it is the following:
$(element).up('.phone').hide();
I'm not proficient in JS so any basic help is appreciated. Thanks!
As I was watching this episode, I notice in your application layout you have a helper called show_title?, related to your "Pretty Page Titles" from episode 30. I was wondering how that particular little snippet works.
I love these screencasts! I've been working on Rails for almost a year now, and you seem to come out with the one that I need just as I need it.
Nice episode. I agree that the ":index => nil" snippets are a bit ugly, so I submitted a patch that adds support to pass the index option to the fields_for helper:
http://dev.rubyonrails.org/ticket/9883
Please feel free to "+1" the patch if this patch looks of interest to you.
Thanks ryan, this has to be one of the longest railscasts.
i would like an episode on error messages. i have two models, one is post, the other is photo. photo is a model using image_column, users can upload 'photo' while they create a post, but it's also important to display the error message of 'photo' while saving, as image_column does validations like format check and size check. i guess quite a lot people are doing it this way, so i really hope there will be an episode on error messages.
once again, thanks ryan for a great episode!!
I love this technique and use it on a few projects I'm working on. A few bits of contention from an accessibility stand point:
1. You should use label tags instead of plain text.
2. One thing that I'd like to see more of is unobtrusive javascript. It's pretty easy to pick up and makes pages more accessible. I think it'd be great for you to push this since you're in a position to reach a lot of people.
Nice tutorial Ryan, I took at stab at pluginizing this technique and have uploaded the plugin and a sample project using it at: http://code.google.com/p/multimodel-forms/
If anyone has any suggestion feel free to email me (my email is on the project home page), or just add it under "Issues". Im currently working on view helpers so try and simplify the views alot.
This is the most awesome yet! I agree with the many others! "Complex Forms Part 4" Single Form Multiple Model Validations! That would be so much help to so many people. Thanks for your amazing work.
I have a question. Let's say my project has many tasks, but it also has many tags and I want the project form to be able to accommodate building two models in reference to a project on the same form. How would one refactor the following *_attributes=(*_attributes) methods to avoid duplication?
def task_attributes=(task_attributes)
task_attributes.each do |attributes|
if attributes[:id].blank?
tasks.build(attributes)
else
task = tasks.detect { |t| t.id == attributes[:id].to_i }
task.attributes = attributes
end
end
end
def tag_attributes=(tag_attributes)
tag_attributes.each do |attributes|
if attributes[:id].blank?
tag.build(attributes)
else
tag = tags.detect { |t| t.id == attributes[:id].to_i }
tag.attributes = attributes
end
end
end
"maybe I will make it [display error messages] a futur episode depending on the demand"
... there is demand ;-)
beside "the demand" there is an other reason to go into this topic: "additional options for presenting model errors -(Changeset #7870)"
I've got problems with validation of tasks. The has_many relation silently puts up additional validators, which are invoked at the time you try to save the projects model.
The problem is that these hidden validators affect the errors of the project by adding messages like "tasks is invalid" for each invalid task. (In fact, in my case the model names are different, so I'm giving the approximate messages.)
Now, I am seeing two issues with that. First, is there any way to localize the messages produced by has_many? Second, is it anyhow possible to disable adding these messages at all (I want only one message for all failures and I want it customized).
Anyway, thanks for the great job, Ryan!
Hi Ryan,
very nice screencast. Can you send me the sourcecode of the rails project, i am so sluggardly ;-)
Michael
@ Joe
I have the same question. It seems there is a lot of refactor that needs to be done to accommodate multiple sub models.
I posted this question to "refactor my code" to see what the community can come up with. Here is the URL. http://refactormycode.com/codes/87-combine-contact-attributes-in-model
Refactored! Check it out
Hi Ryan,
Great screencast! Instead of text boxes, should it just be as easy to use multiple select lists?
Thanks,
Matthew
There is a bug in railscasts code. When you point your browser to non existent episode (for example http://www.railscasts.com/episodes/100), than your application will crash. I think some <tt>begin/rescue/end</tt> could help ;]
Ryan: You didn't post the code for the javascript mark_for_destroy function.
Here it is for everyone who needs it:
function mark_for_destroy(element) {
$(element).next('.should_destroy').value = 1
$(element).up('.task').hide();
}
I have a problem that I can't figure out for the life of me.
I understand the part about
Task: <%= f.text_field :name, :index => nil %>
I must be formatting something wrong or not writing this part write, but how do you set a select list to :index => nil?
Here is my code:
<%= f.select(:phone_type, [ ['',''], ['Home', 'Home'],
['Work', 'Work'], ['Cell', 'Cell'], ['Fax', 'Fax'],
['Other', 'Other'] ]) %>
I have tried :index => nil, tried :selected => nil after the array, but am receiving 'Conflicting types for parameter containers. Expected an instance of Array, but found an instance of Hash.' in my log file. Any help please?
@Joe,
Any luck there Joe with using select lists? I'm also trying this out but I'm getting the same message as yourself.
Any breakthroughs then mail me.
matthew.langm at gmail.com
@Joe,
You might want to try add the :index option to the html_options like this
<%= f.collection_select(:phone_type, @phone_types, "id", "name", {}, :index => nil) %>
Hope this helps!
Great screencast ! But i have one issue, i have read on how to use this technique with select or collection_select, but what about a date_select, the above workaround doesn't work for it ? Any clue to get out of this, anyone plz ?
I stumbled on the problem with select tags, and using html_options instead of options.
Related to that, checkboxes and radio buttons don't have html_options, so you have to manually create the elements (using check_box_tag and radio_button_tag respectively).
Otherwise, because Rails tries to be helpful when dealing with unchecking checkboxes, you get weird behaviour when updating a project, along the lines of getting extra, but blank, tasks.
Thanks for the comments everyone. I'll try to answer some...
@TheCompWiz, there's no need to call update_attributes here because the attributes have already been set earlier. A simple save is all that's needed.
@Ted, yeah, I should have made the save_tasks method private. Good catch.
@Brandon, the show_title? is another helper method which just checks an instance variable I set earlier. Here's the code:
http://pastie.caboo.se/108319
@magistrator, I wish there was a way to disable this automatic validation message but I'm not sure how. Instead what I have done is built a custom error message handler which I can instruct to ignore certain validation error messages.
@Michael, I'll try to put up the full project source code in a day or so.
@Gilles & Darryl, I wish I had an easy answer for the date select or checkbox fields not working, but unfortunately I don't. You'll have to make them manually.
On top of this, checkboxes are tricky because the browser only sends them if they are checked. This means they won't be organized into the proper task attribute hashes. You either need to keep track of them separately or use some javascript to set a hidden field when they are changed.
I am having problems with validations on the task attributes on update. All the validations work wonderful on create. But for some reason, and I am assuming it is because the task_attributes=(attributes) method just assigns all the attributes to a task on update. How can one make sure the same validations pass for a task on update of a project when the task attributes are directly assigned?
I also forgot about the after_update :save_tasks method where the save is set to false. Any ideas?
Found the problem. validates_associated :tasks in the project.rb file. It is missing from the source listed above, but I was able to catch the error watching the video again. Thanks.
Great stuff, Ryan, thank you!
My question - what about transactions? In this simple example, it's probably not a big deal if there's an error saving tasks after the project has updated, but in general, I want all of my models to be saved or none (if failure). The rails before/after methods are convenient, but I see them more as ways to fulfill pre- and post- conditions v. implementing core save operations. I'm in the midst of implementing a multi-model save now and have overridden save/save! to handle the logic of saving all the models in a single transaction. I'm not sure this is the best way, but it allows me to wrap all model updates in a single transaction.
We use acts_as_ferret, which is very slick and convenient but makes me nervous for similar reasons - indexing occurs after save, so if indexing fails it doesn't rollback the database. Thus the index can get out of sync with the model.
These are big issues in Java land, but I rarely hear them talked about in the Rubyverse. Was curious if anyone else shared these concerns. Perhaps they are simply not as important to the types of apps that Rails developers tend to build?
+1 on any future episodes dealing with associations, errors or validation for multi-model save operations. This seems to be a corner of Rails where good practices aren't well documented (if even established).
Quick question:
Why handle the destroying through javascript and hidden fields? Why not quickly trash it at the scene via ajax?
@Jason Jason, By handling the destroy through hidden fields, it gives the user the opportunity to Cancel all changes. Nothing persists until the form is submitted which makes for a nice user experience. Of course different apps warrant different behaviors.
@Joe, sorry about that, source is updated to include that line now.
@Rich, I haven't tested this fully, but I believe if an exception is raised in a callback (save_tasks method) then it will properly rollback the save of the project and other tasks. Is it not working for you?
Ryan,
Validation and error displays are tricky. Maybe a collection good practices can serve as a valuable episode.
Here're some questions:
1. validates_associated on tasks is nice: what if I want to create a blank project without any task? I don't want it preventing me from creating a project; and later I want to edit this blank project and add tasks.
2. Will "task.new_record?" in the partial fails in a blank project case (no task) if you try to edit it, since task will be a nil object?
For those who are dealing with date_select tag with this method of updating related models in one form, one easy workaround i have found is to use a text_field instead of the date_select to enter your date data … it's not a perfect one, but …
You can also do it by hand by usind select_date_tag and so on …
One last question for me about this episode : Is it possible to use this technique with HABTM relationships ?
Thanks for the feedback, Ryan, I think you're right. It never occurred to me that save() and the accompanying life-cycle methods were wrapped in a transaction w/in the framework, but after your comment I did some experimenting and that appears to be the case. (Adding an after_save method that raises an exception and calling save() rolls back a model update that otherwise saves successfully.)
It's news to me (good news, I think, though I'm still digesting the implications) that ActiveRecord is running save() + life cycle methods in a transaction. Will have to dig deeper into the ActiveRecord code - thanks for the insight!
Anyway i still think that short particular arguments like this are better explained in mini-pdf. Will you release them in the future?
@john, if the project has no tasks then it is treated as an empty array so neither of the issues you mentioned should be a problem. But I haven't tested it extensively, does it not work?
@Gilles, I'm pretty sure you can adapt this to work with HABTM. One thing you'll have to do is decide how the removal process will work. Does it just remove the join or the end model? Everything else is very similar.
@Skyblaze, no PDF, but I'll be writing a few tutorials on railsforum.com in the near future showing this technique with possible additions.
Just to close out the transaction discussion, this from the Rails doc:
Save and destroy are automatically wrapped in a transaction.
Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction depend on or you can raise exceptions in the callbacks to rollback.
http://caboo.se/doc/classes/ActiveRecord/Transactions/ClassMethods.html
I think that we could simplify this example a lot when not worrying weather task is new or updated.
On each update we could first delete all existing tasks and then add all submitted:
def task_attributes=(task_attributes)
tasks.clear
task_attributes.each do |attributes|
tasks.build(attributes)
end
end
This way we could remove all marking of new and deleted records and save_tasks method.
We should probably had to mark tasks association as :dependent => :delete_all and add transaction in controller so we don't lose tasks if some validation fails.
@Gilles, for the date_select, use
:index=>''
rather than
:index=>nil
[quote]
after_update :save_tasks
def save_tasks
tasks.each do |t|
if t.should_destroy?
t.destroy
else
t.save(false)
end
end
end
[/quote]
when @project.save, it automatically save the @project.tasks. Then Rails calling the callback save_tasks ?
Would the same task do update twice ?
For moving error messages from failed task validations up to the project error messages, I added this "after_validations" method to Project:
http://pastie.caboo.se/109132
More on complex forms and validation is very welcome. I'm trying to do the reverse of your relationship here. I'm trying to create a User that belongs_to a group. I've discovered how to do that with "build_group", but I need validate the created group before saving the user, if the user is creating a new group. Otherwise, I just want to validates_presence_of the group selected in a drop down. The whole validation system, with callbacks and conditionals, is pretty hard to get a good overview handle on.
I love your work. Thank you!
what about adding task has many sub_tasks
In field_for what name I have to use
"project[task_attributes][][sub_task_attributes][]" is not work.
+1 for another screencast discussing error messages and validations in greater detail.
@Igor, interesting idea. Destroying the tasks each time would probably work, however I hesitate to do this. There may be extra state (such as created_at) that we need to stay persistent. There might be other associations with the task that would be lost as well.
@RainChen, from my experience calling @project.save will only save the tasks if the project itself is new as well. This is why I only handle the callback on after_update. Although I haven't tested this extensively.
@tonghoho, I don't think having two "[]" in a form field name will work properly. Instead you'll have to keep track of the sub tasks separately. Sorry I can't go into detail.
Thank you for the cast. Instead of complicating things with the destroy logic and doing all nasty stuff, how about a call to link_to_remote to destroy the element?
I have a little bit different situation. In this screencast we have project-task but I have task-project situation (contact-group in my case)
So, the form had been saving even though there were validation errors in Group. valid? saved the day
if @contact.group.valid? && @contact.save
flash[:notice] = 'New contact has been created'
redirect_to
end
In the view:
<% if @contact && @contact.errors %>
<%= error_messages_for 'contact' %>
<% if @contact.group %>
<% @group = @contact.group %>
<%= error_messages_for 'group' %>
<% end %>
<% end %>
This might be Edge Rails only, but instead of setting an instance variable for <tt>error_messages_for</tt> you can do this:
<code>
%lt!-- in _task.rhtml -->
<%= error_messages_for(:task, :object => task) %>
</code>
I've noticed that this method really dislikes date-time select boxes.
For future Googlers, I didn't find the real solution, but what I ended up doing was leveraging the Chronic gem to parse natural language date-time expressions.
I've using the code from this episode, weirdly enough, no matter I try to remove a task or edit task value, the code "@project.update_attributes(params[:project]) " in project controller has always turned out to be false, therefore redirect me to the edit page, where the task values ain't changed. I've spent a day to figure out what's wrong, still got no clue.
Let me just say Ryan: thou are the man. My need for this and your creation of these podcasts came hand in hand. Props. Also, thanks a lot @Ilya for the error fixes when using the latest edge.
@Joan: try 'puts params[:project]' before that line of code in your controller and then check your development.log to see what it's spitting out. If it's not normal, then my guess if that you have a form builder problem in one of your views. Good Luck!
Thank you, Henry. I finally was able to found out the problem, it's actually caused by the failure of validation in task model.
nice cast, there is one bug though.
if you use the after_update instead of the after_save callback and you are creating a project for the first time (aka, not updating) and you create a task then delete it and then hit save the deleted task will still be created. this happens because save_tasks callback is not called on the create of the project model so you don't do the check for should_destroy?. one fix is to just use after_save instead of after_update, although i guess this results in a double save on the initial creation of a project.
another fix is to add a before_create alongside the after_update:
before_create :bc
def bc
fields_to_destroy = []
fields.each do |field|
fields_to_destroy << field if field.marked_for_destroy?
end
fields_to_destroy.each {|field| fields.delete(field) }
end
i think this is optimal because it won't even hit the db for new tasks on a new project that are marked for delete. but changing the callback to before_save is much simpler and less prone to bugs
sorry for any confusion: replace 'field' with 'task'
here is a better example:
before_create :bc
def bc
tasks.dup.each do |task|
tasks.delete(field) if task.marked_for_destroy?
end
end
Steve, I believe that issue is actually handled by the logic in the view code:
<% if task.new_record? %>
<%= link_to_function "remove", "$(this).up('.task').remove()" %>
<% else %>
When you're creating a new project, you just delete the entire field for the task rather than marking it for deletion.
I have items that belong to a studyform. I am trying to save both an item name and and item type into each studyform. My _item partial has:
<% fields_for "studyform[item_attributes][]", item do |f| %>
<p>Item: <%= f.text_field :name, :index => nil %>
Type: <%= f.text_field :type, :index => nil %>
<% if item.new_record? %>etc
The name saves into the items database but the type is always null. Any help would be appreciated.
Has anyone made this work with check boxes?
The helper inserts a hidden field after checkboxes to have it send 0 if the box isn't checked.
Unfortunately it means that when it is checked both the 1 and the 0 get sent and it thinks there are 2 sets of attributes to save. It can also mean the values get applied to the wrong rows of data.
You shouldn't use .detect to pull up the appropriate task. To keep the task associated with the Project, use the association finder method tasks.find_by_id(params[:id])
This way you avoid instantiating a lot of unnecessary task models. This is essential when a model may have thousands of sub-models.
Does this still work with the latest Rails trunk? I get this error: Can't mass-assign these protected attributes: id
@Ryan: I may have an easier method of removing tasks, but I'll need to test it when combined with the concepts in this set of screen casts.
I'm using line items of an invoice, but I'll use your terminology. So I have a collection of @tasks from the database, and a new empty array: new_tasks. I loop through the posted params, searching for an existing entry in @tasks, which I then update. If not found, I build a new task instead. Either way I append that task to the new_tasks array.
When I assign the @project.tasks = new_tasks and save @project, Rails handles the removals for me, simply by the fact that they don't exist in the collection... smart! They don't exist in the collection because they weren't posted and added to new_tasks. So I have no need to mark for destroy or even call destroy.
I have a bunch of code in the controller, and I'm not using the virtual attributes you mentioned... so I'm really looking forward to combining these to methods and seeing if it all works!
For me it works perfectly but now i have to add several "third" models that are related to my second model. How can i do that. Anyway an issue with this code is the fact that we will have input fields with same ids...
Ryan -- Thanks for the screencasts. They are extremely helpful.
@Paul, I made it work with checkboxes. I used check_box_tag. I use the checkbox value in the model (in def choice_attributes) to set the value of the field I want set, rather than setting it directly with the checkbox, like so:
attributes[:correct] = correct_choice_id.include?(attributes[:counter_id]) ? 1 : 0
It's a workaround but it works (and work s for radios, too). Then on the form end, I manually check the box like so:
<% is_checked = choice.correct == 1 ? true : false; %> and render the check box this way:
<%= check_box_tag('question[correct_choice_id][]',choice.id, is_checked,{}) %>
(that's on edit -- on new, when I don't have question.correct.choice I have named the checkbox with a counter from within the partial, and run essentially the same thing. I'm not sure if this will be clear or not but it does work for me so hopefully it helps.
I've found a couple of issues here that maybe only affect me, but I'll post in case someone else experiences this.
First, the "should_destroy" attribute was showing up in the first item regardless of which item was removed. The issue was that the 'should_destroy' hidden fields had no value and so did not end up in the correct array. I switched to hidden_field_tag with a default 0 value and it's fixed.
Second, when passing strings with non-alpha chars the URL encoding for javascript was running once for each layer of the array. So '-n/a-' was becoming "-n%25252Fa-", I did a 3.times{ attr[:string] = URI.unescape(attr[:string])} within the task_attributes= method. Hackish, but it works.
I'm having the same problem as Paul, is there an easy solution anywhere?
Thank you so much for putting this together. It was incredibly helpful! Please add my vote to your doing a part 4 on validation.
Thanks a lot for this Ryan.
+1 for a separate validations screencast!
Ryan -
Thanks for the great screencasts.
I was a bit confused by the workaround in the should_destroy? method, until I remembered that MySQL lacks a genuine BOOLEAN datatype.
With PostgreSQL, you can define a boolean field in your model (without a trailing ?), and a predicate method (with the trailing ?) is automatically there in your model. Handy.
Strange problem here..
I can edit and delete tasks with no problem, but when I try to create a new task (or several at once) the very first of the previously saved tasks gets wiped out. I copied Ryan's code line for line and am running this in Rails 2.0.
Anyone else face this issue?
ALmost forgot to mention, I only have this problem when I have the marked_for_destroy hidden field and js link in that partial. I remove them and can add tasks with no problem. Odd..
How do I reference the project[task_attributes][] fields for auto_complete_field functions... Does anyone have an example using auto_complete_field and fields_for?
Awesome screencast series, thank you so much!
Only one question.. I'm trying to do this with a habtm relationship (possible topic for another screenie? ;)) and unfortunately it doesn't save the linking table when updating attributes. :(
Does anyone have an idea how to tell rails to manually save it?
Ryan, this was a terrific tutorial. I'd definitely be interested in seeing more on validations, etc.
I'm also curious as to how you might take a progressive enhancement approach. The current method seems to require Javascript. I tried using a TaskController to provide web services, but if I make it polymorphic (Project and ToDoList), all hell breaks loose... or at least, it looks really ugly.
I like the fact that nothing is lost until the entire project is saved, so I'd prefer to keep it that way, but adding and removing tasks becomes difficult without Javascript.
Thanks so much for another great screencast series. However, I found it a little bit complicated and cluttered with all these hidden fields, javascript and such. So I took a slightly different approach. First, I modifed the task partial so that it passes always a model id:
<%= fields_for "project[tasks][#{task.new_record? ? 0 : task.id}]", task do |task_form| %>
Now I can determine which tasks to create, update and delete in the method task_attributes= like this:
def task_attributes=(task_attributes)
task_attributes.each do |id, attributes|
if id.to_i == 0
tasks.build(attributes)
else
task = tasks.detect{|p| p.id == id.to_i}
tasks.attributes = attributes
end
end
tasks.reject{|t| t.new_record? || task_attributes.has_key?(t.id.to_s)}.each {|t| t.mark_for_destroy}
end
Works like a charm and keeps my views clean.
Hi Ryan.
I find this series very useful. I've implemented some stuff using the technique you've described and it works great, but I'm interested how could I make file upload using this very technique (gmail like upload). Can you help me please?
I found this railscast series to be particularly useful! After stumbling across Jamis Buck's post about moving associated creations to the model your screen cast really cleared up the concept for me.
Validations had me puzzled, but I finally figured it out. I've posted what worked for me in my situation over on my <a href='http://psifire.livejournal.com/119168.html'>Live Journal</a> page... since I don't have a real blog.
@ Gernot Kogler: I think that your idea can not work - I have been experimenting quite a lot with it and I think that all duplicate named imputs will get lost (for example if you have twho or more new tasks). The trick of putting the new items to the array only works if there is bare [] in the name of INPUT field. SO I suggest changing your conditional so it gives empty string for new tasks.
Ryan,
great tutorial.
I unfortunately ran into a problem while using the code. My Task model has more than one field (name, priority and estimated cost) and they all are required (i.e. there is a validates_presence_of rule for each of them).
The problem happens when the user updates an existing task. More precisely when the user deletes the content of one of the text field and then clicks the remove link for that task.
As you can guess, since the remove links just hides the task, an incomplete task is sent to the controller and since validation happens before the after_callback is called, validation fails, the task is not destroyed and the controller redirects to the update page (with an error saying that the task is invalid)
Fortunately, the solution is not too difficult, all we need is to add a method in the Task class that returns false if the task should be destroyed and the pass that method as an "if" parameter to all validation rules for the task.
def do_validation
!should_destroy?
end
validates_presence_of :name, :priority, :estimated_cost, :if => :do_validation
Ryan,
would it be possible to have an RSS feed for tracking the comments for each episode?
In s