I'm reading Beginning Rails 3. It creates a blog with Users who can post Articles and also post Comments to these Articles. They look like this:
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation
attr_accessor :password
has_many :articles, :order => 'published_at DESC, title ASC',
:dependent => :nullify
has_many :replies, :through => :articles, :source => :comments
class Article < ActiveRecord::Base
attr_accessible :body, :excerpt, :location, :published_at, :title, :category_ids
belongs_to :user
has_many :comments
class Comment < ActiveRecord::Base
attr_accessible :article_id, :body, :email, :name
belongs_to :article
in app/views/comments/new.html.erb there's a form which begins like this:
<%= form_for([#article, #article.comments.new]) do |f| %>
My confusion lies in why form_for() has two parameters. What do they resolve to and why are they necessary?
thanks,
mike
Actually, in your example, you are calling form_forwith one parameter (which is Array). If you check the documentation you will see parameters it expects: form_for(record, options = {}, &proc).
In this case a record can be ActiveRecord object, or an Array (it can be also String, Symbol, or object that quacks like ActiveRecord). And when do you need to pass it an Array?
The simplest answer is, when you have a nested resource. Like in your example, you have defined Article has many Comments association. When you call rake routes, and have correctly defined routes, you will see that Rails has defined for you different routes for your nested resource, like: article_comments POST /article/:id/comments.
This is important, because you have to create valid URI for your form tag (well not you, Rails does it for you). For example:
form_for([#article, #comments])
What you are saying to Rails is: "Hey Rails, I am giving you Array of objects as a first parameter, because you need to know the URI for this nested resource. I want to create new comment in this form, so I will give you just initial instance of #comment = Comment.new. And please create this comment for this very article: #article = Article.find(:id)."
This is roughly similar to writing:
form_for(#comments, {:url => article_comments_path(#aticle.id)})
Of course, there is more to the story, but it should be enough, to grasp the idea.
This is a form for commenting on an article. So you, you need the Article you're commenting on (#article) and a new Comment instance (#article.comments.new). The form action for this form will be something like:
/articles/1/comments
It contains the id of the article you're commenting on, which you can use in your controller.
If you omit the #article like this: form_for #article.comments.new, the form action will looks like this:
/comments
In the controller you would have no way of knowing to which article the comment belongs.
Note that for this to work, you need to define a nested resource in your routes file.
Related
I am building an e-com application and would like to implement something like a messaging system. In the application, all conversation will be related to either a Product model or an Order model. In that case, I would like to store the relating object (type + id, I supposed) to the Conversation object.
To add the fields, of course I can generate and run a migration, however, since the Model and Controller are included within the gem, how can I declare the relationship? (belongs_to :linking_object, :polymorphic) and the controller? Any idea?
Thank you.
I ended up customizing the Mailboxer gem to allow for a conversationable object to be attached to a conversation.
In models/mailboxer/conversation.rb
belongs_to :conversationable, polymorphic: true
Add the migration to make polymorphic associations work:
add_column :mailboxer_conversations, :conversationable_id, :integer
add_column :mailboxer_conversations, :conversationable_type, :string
In lib/mailboxer/models/messageable.rb you add the conversationable_object to the parameters for send_message:
def send_message(recipients, msg_body, subject, sanitize_text=true, attachment=nil, message_timestamp = Time.now, conversationable_object=nil)
convo = Mailboxer::ConversationBuilder.new({
:subject => subject,
:conversationable => conversationable_object,
:created_at => message_timestamp,
:updated_at => message_timestamp
}).build
message = Mailboxer::MessageBuilder.new({
:sender => self,
:conversation => convo,
:recipients => recipients,
:body => msg_body,
:subject => subject,
:attachment => attachment,
:created_at => message_timestamp,
:updated_at => message_timestamp
}).build
message.deliver false, sanitize_text
end
Then you can have conversations around objects:
class Pizza < ActiveRecord::Base
has_many :conversations, as: :conversationable, class_name: "::Mailboxer::Conversation"
...
end
class Photo < ActiveRecord::Base
has_many :conversations, as: :conversationable, class_name: "::Mailboxer::Conversation"
...
end
Assuming you have some users set up to message each other
bob = User.find(1)
joe = User.find(2)
pizza = Pizza.create(:name => "Bacon and Garlic")
bob.send_message(joe, "My Favorite", "Let's eat this", true, nil, Time.now, pizza)
Now inside your Message View you can refer to the object:
Pizza Name: <%= #message.conversation.conversationable.name %>
Although rewriting a custom Conversation system will be the best long-term solution providing the customization requirement (Like linking with other models for instance), to save some time at the moment I have implement the link with a ConversationLink Model. I hope it would be useful for anyone in the future who are at my position.
Model: conversation_link.rb
class ConversationLink < ActiveRecord::Base
belongs_to :conversation
belongs_to :linkingObject, polymorphic: true
end
then in each models I target to link with the conversation, I just add:
has_many :conversation_link, as: :linkingObject
This will only allow you to get the related conversation from the linking object, but the coding for reverse linking can be done via functions defined in a Module.
This is not a perfect solution, but at least I do not need to monkey patch the gem...
The gem automatically take care of this for you, as they have built a solution that any model in your own domain logic can act as a messagble object.
Simply declaring
acts_as_messagable
In your Order or Product model will accomplish what you are looking for.
You could just use something like:
form_helper :products
and add those fields to the message form
but mailboxer comes with attachment functionality(carrierwave) included
this might help if you need something like attachments in your messages:
https://stackoverflow.com/a/12199364/1230075
I am on Rails3, I have two model, User, and Post. User has Posts as nested attributes. when I try to save user then I am getting Can't mass-assign protected attributes:.....
Try this attr_accessible in your post model
http://railscasts.com/episodes/26-hackers-love-mass-assignment
if the model definitions are like as follows:
user.rb
class User < ActiveRecord::Base
attr_accessible :name, :posts_attributes
has_many :posts
accepts_nested_attributes_for :posts
end
post.rb
class Post < ActiveRecord::Base
attr_accessible :title, :content :user_id
end
then everything should be fine. You can save user with posts as nested attributes.
Here is a sample codes for the beginners :)
https://github.com/railscash/sample_change_user_role
Mass Assignment is the name Rails gives to the act of constructing your object with a parameters hash. It is "mass assignment" in that you are assigning multiple values to attributes via a single assignment operator.
The following snippets perform mass assignment of the name and topic attribute of the Post model:
Post.new(:name => "John", :topic => "Something")
Post.create(:name => "John", :topic => "Something")
Post.update_attributes(:name => "John", :topic => "Something")
In order for this to work, your model must allow mass assignments for each attribute in the hash you're passing in.
There are two situations in which this will fail:
You have an attr_accessible declaration which does not include :name
You have an attr_protected which does include :name
It recently became the default that attributes had to be manually white-listed via a attr_accessible in order for mass assignment to succeed. Prior to this, the default was for attributes to be assignable unless they were explicitly black-listed attr_protected or any other attribute was white-listed with attr_acessible.
I've got a problem with designing my User model and making a decent form for it. I just want to ensure myself that I'm doing it wrong :)
So it goes like this:
User has got two Addresses:
a mandatory Address for identification and billing,
an optional shipping Address that he could fill in or leave blank
I tried like this:
class User < ActiveRecord::Base
has_one :address
has_one :shipping_address, :class_name => 'Address', :foreign_key => 'shipping_address_id'
accepts_nested_attributes_for :address
accepts_nested_attributes_for :shipping_address
#validations for user
end
and:
class Address < ActiveRecord::Base
#validations for address
end
And then I make a form for User using form_for and nested fields_for. Like this:
= form_for #user, :url => '...' do |a|
= f.error_messages
...
= fields_for :address, #user.build_address do |a|
...
But then, despite that f.error_messages generates errors for all models, fields for Addresses don't highlight when wrong.
Also I have problems with disabling validation of the second address when the user chose not to fill it in.
And I have doubts that my approach is correct. I mean the has_one relation and overall design of this contraption.
So the question:
Am I doing it wrong? How would You do that in my place?
What is wrong in your form is that it will build a new address every time the view is rendered, thus losing all validation errors.
In your controller, in the new action you should do something like
#user.build_address
and in your view write:
= fields_for :address, #user.address do |a|
Hope this helps.
I am following Ryan Bate's tutorial: http://railscasts.com/episodes/163-self-referential-association
But my setup is slightly different.
I am making comments that are self-referential so that comments can be commented on.
The form displays in the view, but when I submit, I get this :
Routing Error
No route matches "/conversations"
And my url says this : http://localhost:3000/conversations?convo_id=1
models
#conversation.rb
belongs_to :comment
belongs_to :convo, :class_name => "Comment"
#comment.rb
belongs_to :post
has_many :conversations
has_many :convos, :through => :conversations
My form :
- for comment in #comments
.grid_7.post.alpha.omega
= comment.text
%br/
- form_for comment, :url => conversations_path(:convo_id => comment), :method => :post do |f|
= f.label 'Comment'
%br/
= f.text_area :text
%br/
= f.submit 'Submit'
My conversations_controller:
def create
#conversation = comment.conversations.build(:convo_id => params[:convo_id])
The app fails here in its creation as it never makes it to the redirect portion of the create method.
There are several pieces to work on here, but the good news is I think the answer you're looking for is simpler than what you already have. If I understand you correctly, you want comments to have many child comments of their own. This is how YouTube works, letting members reply to existing comments. For this, you don't need the has_many :through solution you've implemented. You don't need the conversations object at all. A comment may have many replies (child comments), but a reply isn't going to have more than one parent.
The answer for this is using polymorphism, which is easier to implement than it is to pronounce :) You want your comments to either belong to a post, or to another comment. Polymorphism lets an object belong to one of possibly many things. In fact, comments are the most common use for this.
I cover polymorphism with the example of addresses in this blog post:
http://kconrails.com/2010/10/19/common-addresses-using-polymorphism-and-nested-attributes-in-rails/
But I can show you how it applies to your case more specifically. First, drop the conversation model/controller/routes entirely. Then, change your comments table:
change_table :comments do |t|
t.integer :commentable_id
t.string :commentable_type
t.remove :post_id
end
We don't need post_id anymore, because we're going to change how we associate with other tables. Now let's change the models:
# app/models/post.rb
has_many :comments, :as => :commentable
# app/models/comment.rb
belongs_to :commentable, :polymorphic => true
has_many :comments, :as => :commentable
Notice we dropped the comment belonging to a post directly. Instead it connects to the polymorphic "commentable" association. Now you have an unlimited depth to comments having comments.
Now in your Post#show action, you'll want to create a blank comment like so:
get show
#post = Post.find(params[:id])
#comment = #post.comments.build
end
#comment will now have commentable_id and commentable_type set for you, automatically. Now in your show page, using erb:
<% form_for #comment do |f| %>
<%= f.hidden_field :commentable_type %>
<%= f.hidden_field :commentable_id %>
/* other fields go here */
<% end %>
Now when Comments#create is called, it works like you'd expect, and attaches to the right parent. The example above was showing a comment being added directly to a post, but the process is essentially the same for a comment. In the controller you'd call #comment.comments.build, and the form itself would stay the same.
I hope this helps!
The app is failing sooner than you think - it's not finding a route to that action, so it's not reaching it at all. In your routes.rb file, you need to add:
# rails 3
resources :conversations
# rails 2
map.resources :conversations
This should fix it.
I've upgraded to Rails 2.3.3 (from 2.1.x) and I'm trying to figure out the accepts_nested_attributes_for method. I can use the method to update existing nested objects, but I can't use it to create new nested objects. Given the contrived example:
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :notes
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :body
end
If I try to create a new Product, with a nested Note, as follows:
params = {:name => 'Test', :notes_attributes => {'0' => {'body' => 'Body'}}}
p = Product.new(params)
p.save!
It fails validations with the message:
ActiveRecord::RecordInvalid: Validation failed: Notes product can't be blank
I understand why this is happening -- it's because of the validates_presence_of :product_id on the Note class, and because at the time of saving the new record, the Product object doesn't have an id. However, I don't want to remove this validation; I think it would be incorrect to remove it.
I could also solve the problem by manually creating the Product first, and then adding the Note, but that defeats the simplicity of accepts_nested_attributes_for.
Is there a standard Rails way of creating nested objects on new records?
This is a common, circular dependency issue. There is an existing LightHouse ticket which is worth checking out.
I expect this to be much improved in Rails 3, but in the meantime you'll have to do a workaround. One solution is to set up a virtual attribute which you set when nesting to make the validation conditional.
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :unless => :nested
attr_accessor :nested
end
And then you would set this attribute as a hidden field in your form.
<%= note_form.hidden_field :nested %>
That should be enough to have the nested attribute set when creating a note through the nested form. Untested.
check this document if you use Rails3.
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#label-Validating+the+presence+of+a+parent+model
Ryan's solution is actually really cool.
I went and made my controller fatter so that this nesting wouldn't have to appear in the view. Mostly because my view is sometimes json, so I want to be able to get away with as little as possible in there.
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :note
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id unless :nested
attr_accessor :nested
end
class ProductController < ApplicationController
def create
if params[:product][:note_attributes]
params[:product][:note_attributes].each { |attribute|
attribute.merge!({:nested => true})
}
end
# all the regular create stuff here
end
end
Best solution yet is to use parental_control plugin: http://github.com/h-lame/parental_control