Rails parent child form example preventing mass assignment - ruby-on-rails

I use rails 3.2 and I want to prevent mass-assignment. I have parent-child relationship.
class Parent < ActiveRecord:Base
has_many :children
attr_accessible :name
end
class Child < ActiveRecord:Base
belongs_to :parent
attr_accessible :title
end
In my routes.rb child resource is not nested within parent resource. Now I have a link to create a new child with new_child_path(#parent.id). This directs me to localhost:3000/child/new?parent_id=1 and I end up in new action:
def new
#child = Child.new
#parent = Parent.find(params[:parent_id])
#child.parent = #parent
end
My question is: how to write my _form.html.erb for a child entity? I cannot use f.hidden_field for parent_id because in my create action it would break up because of mass-assignment. On the other hand I need to pass parent_id to know my parent when I save child. I haven't found a good working example for this.

You should read up on Rails' nested resources.
Some links:
http://railscasts.com/episodes/139-nested-resources
-- EDIT 1 --
Based on your comment of not having more than one level of nesting, you could also have the following route configuration:
resources :grandparents do
resources :parents
end
resources :parents do
resources :children
end
This way, you can still have the parent child relationship, without the overheads of multiple levels of nesting. You could also namespace your controllers to keep things clean, eg:
resources :grandparents do
resources :parents, :controller => "grandparent/parent"
end
resources :parents do
resources :children
end

Related

How to use form_with with self join in rails?

I have a User model which can have many child accounts. I have set up the model as below
class User < ApplicationRecord
has_many :child_accounts, class_name: "User", foreign_key: "parent_account_id"
belongs_to :parent_account, class_name: "User", optional: true
end
I have created a ChildAccountsController to handle the child accounts creation etc. and defined routes as below.
resources :users do
resource :child_accounts
end
But I can get form_with to work in this situation. as
form_with(model: [current_user, #child_account], local: true) do
#...
end
form_with infers the url from the model class since both of them are User. the path it infers user_user_path instead of user_child_accounts_path.
So, is there a rails way to create forms with self joins? Or do I have manually handle this case?
To start with you have a pluralization error:
resources :users do
resources :child_accounts
end
resource is used to declare singular resources.
But the polymorphic route helpers will not be able route to that path automatically anyways, when you pass model instances to form_for, form_with, link_to and button_to they deduce the name of route helper method by calling #model_name and using the methods of ActiveModel::Naming. Since #child_account is an instance of User you get user_users_path. The polymorphic routing helpers are not aware of your associations.
It does not matter at all here if you use form_for or form_with as both use the exact same methods to figure out a path from a model or array of models.
You either need to explicitly pass the url:
form_with(model: #child_account, url: user_child_accounts_path(current_user), local: true) do
#...
end
Or use single table inheritance:
class AddTypeToUsers < ActiveRecord::Migration[6.0]
def change
change_table :users do |t|
t.string :type
end
end
end
class User < ApplicationRecord
has_many :child_accounts,
foreign_key: "parent_account_id",
inverse_of: :parent_account
end
class ChildAccount < User
belongs_to :parent_account,
class_name: "User",
inverse_of: :child_accounts
end
class ChildAccountsController < ApplicationController
def new
#child_account = current_user.child_accounts.new
end
def create
#child_account = current_user.child_accounts.new(child_account_params)
# ...
end
private
def child_account_params
params.require(:child_account)
.permit(:foo, :bar, :baz)
end
end

Polymorphic Routes with Custom Actions in Rails

So I am making a chat app, and I want users to be able to leave chat rooms. This would be done by posting a delete request to a route like /users/:id/chat_rooms/leave/:chat_room_id.
The Users model has has_many :chat_rooms, through: chat_room_users while ChatRooms has has_many :users, through chat_room_users. The UsersController has a leave action, which I want to call using this request on this url.
I want to create a link to this url on a view that I have. I already have a variable #user for the current user and #chosen for the current chat room available on the view. So how would I do a link_to and route for this setup? I have delete /users/:id/chat_rooms/leave/:chat_room_id in the routes.rb file, but how would I do the link_to?
Thank you.
You're overcomplicating it.
DELETE /chat_rooms/:chat_room_id/leave
Instead of passing the user id via the URL you should instead get it through the session or a token (if its an API app).
Rule of thumb: resources should never be nested more than 1 level
deep. A collection may need to be scoped by its parent, but a specific
member can always be accessed directly by an id, and shouldn’t need
scoping (unless the id is not unique, for some reason).
http://weblog.jamisbuck.org/2007/2/5/nesting-resources
This is just a loose example of how to solve this:
# routes.rb
resources :chat_rooms do
member do
post :join
delete :leave
end
end
class User
has_many :chat_room_users
has_many :chat_rooms, though: :chats
end
class ChatRoomUser
belongs_to :user
belongs_to :chatroom
end
class ChatRoom
has_many :chat_room_users
has_many :users, though: :chats
end
Putting this in UsersController is pretty questionable. I would instead place it in ChatroomsController.
class ChatroomsController
# ...
# POST /chat_rooms/:chat_room_id/join
def join
#chat_room = ChatRoom.find(params[:id])
#chat = current_user.chat_room_users.new(chat_room: #chat_room)
if #chat_room.create
# ...
else
# ...
end
end
# DELETE /chat_rooms/:chat_room_id/leave
def leave
#chat_room = ChatRoom.find(params[:id])
#chat = current_user.chat_room_users.find_by(chat_room: #chat_room)
#chat.destroy
end
end
<%= button_to 'Join', join_chat_room_path(#chat_room), method: :post %>
<%= button_to 'Leave', leave_chat_room_path(#chat_room), method: :delete %>

Nested Resource Creation in Parent Form

I have a class called Quote which has_many :line_items, as: :line_itemable (line_items are polymorphic). A quote must have at least one line_item upon creation, so in my Quote creation form I have a section dedicated to adding line items. My routes look like this:
resources :quotes, shallow: true do
resources :line_items
end
which means my routes look like this:
POST /quotes/:quote_id/line_items(.:format) line_items#create
new_quote_line_item GET /quotes/:quote_id/line_items/new(.:format) line_items#new
In the line items section of the quote form I have a button that, when clicked, links to the new_quote_line_item controller action to render a line_item creation modal. My issue is that since the quote hasn't been created yet it doesn't have :quote_id to use in the path. How can I go about achieving this the Rails Way™? I was considering using ajax but I'm not sure if that is overkill for this situation. Thanks for your help!
You should user accepts_nested_attributes_for method in your model to accept attributes for LineItem and fields_for helper
Your model should looks like:
class Quote < ActiveRecord::Base
accepts_nested_attributes_for :line_item
...
end
And you template like:
form_for #quote do |f|
f.fields_for :line_items do |f2|
...
end
...
end
Ajax
You wouldn't need ajax functionality for this - Ajax only allows you to pull data from the server asynchronously, which essentially means you don't have to reload the page.
--
Nested Attributes
What you're looking for, as alluded to by atomAltera sounds like accepts_nested_attributes_for - which allows you to create dependent models from the parent
It sounds to me that you'll need to create a quote before you try and populate line_items, which is actually quite simple using ActiveRecord:
#app/models/quote.rb
Class Quote < ActiveRecord::Base
has_many :line_items
accepts_nested_attributes_for :line_items
end
#app/controllers/quotes_controller.rb
Class QuotesController < ApplicationController
def new
#quote = Quote.new
#quote.line_items.build
end
def create
#quote = Quote.new(quote_params)
#quote.save
end
private
def quote_params
params.require(:quote).permit(:quote, :attributes, :new, line_items_attributes: [:line, :items, :attributes])
end
end
--
If you need any further information, please let me know!!

Where to put nested resources delete for many to many relationship

I have Rails 4 app with nested resources and the child(session) also has a many-to-many relationship with another model (speakers).
resources :parent do
resources :child
end
class Parent < ActiveRecord::Base
has_many :children
end
class Child < ActiveRecord::Base
belongs_to :parent
has_and_belongs_to_many :speakers
end
class Speaker < ActiveRecord::Base
has_and_belongs_to_many :children
end
I am trying to figure out which controller should have the delete/add for the relationship(speaker). I could handle this in the SessionController#destroy but would have to handle a special case for this relationship(doesn't feel right). Currently I have a custom route to SessionController#speaker passing in a param of operation(:add, :delete).
I am keeping the records on both sides of the relationship, only remove the relationship
sessions.speakers.delete(speaker)
What do you think are the best approaches for this?
Keep the remove relationship in SessionController in a special route
Add to SpeakerController in a special route
Create a new controller to handle the relationship
ActiveRecord
To add or remove objects from a collection, you have the << and .delete ActiveRecord methods
These work using ActiveRecord objects, and can be called like this:
#apps/controller/posts_controller.rb
def comment
post = Post.find(params[:post_id])
comment = Comment.find(params[:comment_id])
#Add
post.comments << comment
#Delete
post.comments.delete(comment)
end
Controller
In response to your question as to which controller, I'd recommend keeping the code in the "parent" controller (in your case, sessions) & creating a single method to handle the process
Here's live code we created last week:
#config/routes.rb
resources :entries do
match ":category_id", to: :category, via: [:post, :delete], as: "category"
end
#app/controllers/entries_controller.rb
def category
entry = Entry.find(params[:entry_id])
category = Category.find(params[:category_id])
#Actions
entry.categories << category if request.post?
entry.categories.delete(category) if request.delete?
#Return
redirect_to collection_path
end
This allows us to call a single link with different :method to call different actions:
<%= link_to "Add Category", admin_entry_category_path("60", "20"), method: :post %>

Rails accepts_nested_attributes_for child doesn't have parent set when validating

I'm trying to access my parent model in my child model when validating. I found something about an inverse property on the has_one, but my Rails 2.3.5 doesn't recognize it, so it must have never made it into the release. I'm not sure if it's exactly what I need though.
I want to validate the child conditionally based on parent attributes. My Parent model has already been created. If the child hasn't been created when I update_attributes on the parent, then it doesn't have access to the parent. I'm wondering how I can access this parent. It should be easy, something like parent.build_child sets the parent_id of the child model, why is it not doing it when building the child for accepts_nested_attributes_for?
For Example:
class Parent < AR
has_one :child
accepts_nested_attributes_for :child
end
class Child < AR
belongs_to :parent
validates_presence_of :name, :if => :some_method
def some_method
return self.parent.some_condition # => undefined method `some_condition' for nil:NilClass
end
end
My form is standard:
<% form_for #parent do |f| %>
<% f.fields_for :child do |c| %>
<%= c.name %>
<% end %>
<% end %>
With an update method
def update
#parent = Parent.find(params[:id])
#parent.update_attributes(params[:parent]) # => this is where my child validations take place
end
I had basically the same problem with Rails 3.2. As suggested in the question, adding the inverse_of option to the parent's association fixed it for me.
Applied to your example:
class Parent < AR
has_one :child, inverse_of: :parent
accepts_nested_attributes_for :child
end
class Child < AR
belongs_to :parent, inverse_of: :child
validates_presence_of :name, :if => :some_method
def some_method
return self.parent.some_condition # => undefined method `some_condition' for nil:NilClass
end
end
I had a similar problem: Ruby on Rails - nested attributes: How do I access the parent model from child model
This is how I solved it eventually; by setting parent on callback
class Parent < AR
has_one :child, :before_add => :set_nest
accepts_nested_attributes_for :child
private
def set_nest(child)
child.parent ||= self
end
end
You cannot do this because in-memory child doesn't know the parent its assigned to. It only knows after save. For example.
child = parent.build_child
parent.child # => child
child.parent # => nil
# BUT
child.parent = parent
child.parent # => parent
parent.child # => child
So you can kind of force this behavior by doing reverse association manually. For example
def child_with_inverse_assignment=(child)
child.parent = self
self.child_without_inverse_assignment = child
end
def build_child_with_inverse_assignment(*args)
build_child_without_inverse_assignment(*args)
child.parent = self
child
end
def create_child_with_inverse_assignment(*args)
create_child_without_inverse_assignment(*args)
child.parent = self
child
end
alias_method_chain :"child=", :inverse_assignment
alias_method_chain :build_child, :inverse_assignment
alias_method_chain :create_child, :inverse_assignment
If you really find it necessary.
P.S. The reason it's not doing it now is because it's not too easy. It needs to be explicitly told how to access parent/child in each particular case. A comprehensive approach with identity map would've solved it, but for newer version there's :inverse_of workaround. Some discussions like this one took place on newsgroups.
check these sites, maybe they'll help you...
Rails Nested Attributes Association Validation Failing
accepts_nested_attributes_for child association validation failing
http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes
it seems, rails will assign parent_id after child validation succeeds.
(as parent has an id after it's saved)
maybe worth trying this:
child.parent.some_condition
instead of self.parent.some_condition ... who knows...

Resources