nested form & habtm - ruby-on-rails

I am trying to save to a join table in a habtm relationship, but I am having problems.
From my view, I pass in a group id with:
<%= link_to "Create New User", new_user_url(:group => 1) %>
# User model (user.rb)
class User < ActiveRecord::Base
has_and_belongs_to_many :user_groups
accepts_nested_attributes_for :user_groups
end
# UserGroups model (user_groups.rb)
class UserGroup < ActiveRecord::Base
has_and_belongs_to_many :users
end
# users_controller.rb
def new
#user = User.new(:user_group_ids => params[:group])
end
in the new user view, i have access to the User.user_groups object, however when i submit the form, not only does it not save into my join table (user_groups_users), but the object is no longer there. all the other objects & attributes of my User object are persistent except for the user group.
i just started learning rails, so maybe i am missing something conceptually here, but i have been really struggling with this.

Instead of using accepts_nested_attributes_for, have you considered just adding the user to the group in your controller? That way you don't need to pass user_group_id back and forth.
In users_controller.rb:
def create
#user = User.new params[:user]
#user.user_groups << UserGroup.find(group_id_you_wanted)
end
This way you'll also stop people from doctoring the form and adding themselves to whichever group they wanted.

What does your create method look like in users_controller.rb?
If you're using the fields_for construct in your view, for example:
<% user_form.fields_for :user_groups do |user_groups_form| %>
You should be able to just pass the params[:user] (or whatever it is) to User.new() and it will handle the nested attributes.

Expanding on #jimworm 's answer:
groups_hash = params[:user].delete(:groups_attributes)
group_ids = groups_hash.values.select{|h|h["_destroy"]=="false"}.collect{|h|h["group_id"]}
That way, you've yanked the hash out of the params hash and collected the ids only. Now you can save the user separately, like:
#user.update_attributes(params[:user])
and add/remove his group ids separately in one line:
# The next line will add or remove items associated with those IDs as needed
# (part of the habtm parcel)
#user.group_ids = group_ids

Related

How do I replace an ActiveRecord association member in-memory and then save the association

I have a has_may through association and I'm trying to change records in the association in memory and then have all the associations updated in a single transaction on #save. I can't figure out how to make this work.
Here's a simplifiction of what I'm doing (using the popular Blog example):
# The model
class Users < ActiveRecord::Base
has_many :posts, through: user_posts
accepts_nested_attributes_for :posts
end
# The controller
class UsersController < ApplicationController
def update
user.assign_attributes(user_params)
replace_existing_posts(user)
user.save
end
private
def replace_existing_posts(user)
user.posts.each do |post|
existing = Post.find_by(title: post.title)
next unless existing
post.id = existing
post.reload
end
end
end
This is a bit contrived. The point is that if a post that the user added already exists in the system, we just assign the existing post to them. If the post does not already exist we create a new one.
The problem is, that when I call user.save it saves any new posts (and the user_post association) but doesn't create the user_post association for the existing record.
I've tried to resolve this by adding has_many :user_posts, autosave: true to the User model, but despite the documented statement "When :autosave is true all children are saved", that doesn't reflect the behavior I see.
I can make this work, with something hacky like this, but I don't want to save the association records separately (and removing and replacing all associations would lead to lots of callback I don't want to fire).
posts = user.posts.to_a
user.posts.reset
user.posts.replace(posts)
I've trolled through the ActiveRecord docs and the source code and haven't found a way to add records to a has_many through association that create the mapping record in memory.
I finally got this to work, just by adding the association records manually.
So now my controller also does this in the update:
user.posts.each do |post|
next unless post.persisted?
user.user_posts.build(post: post)
end
Posting this as an answer unless someone has a better solution.

Scoping a class method to current_user

I'm working on implementing a tagging system and I'm having problem querying for tagged objects with a scope.
For example, I would like to find all the user's items with a certain tag. With a class method I can currently find all the objects:
def self.tagged_with(name)
Tag.find_by_name(name).items
end
However, this has a problem. If I were to do something like: current_user.items.tagged_with(name) won't this existing method return ALL the items and not just items owned by the current_user? I suppose this is a simply querying issue but I can't figure out how to change a class method into something called on a collection. I have tried going the opposite way, to get a the collection through the tags, something like... tag.items.where(:user_id => current_user.id) but in this case, it's a many-to-many relationship and I haven't been able to get on thumb on this either.
What's the proper way to restrict a query like this?
Create an association on your User class that points to your Tag class.
class User < ActiveRecord::Base
has_many :tags
end
Then you can do:
current_user.tags.where(...)
If you don't already have an association in place, you'll need to create a migration to have the tags table reference your users table with a foreign key.
I think this will help you:
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
So, basically you can define your method tagged_with directly into the association!
This example is took from the documentations ActiveRecord::Associations

Rails: Add an existing child as a new association to parent model via ajax

I try to add an existing child model to the parent via ajax, by only providing the new id.
My models:
class User < ActiveRecord::Base
has_many :books
attr_accessible :books
end
class Book < ActiveRecord::Base
belongs_to: :user
end
In my html form the user is viewing a User and it can select an existing Book to add. This will result in an Ajax request. I would like to only send the new Book, and not all the already assigned books. E.g. the User model has already Books 1 and 2, and now to user selects Book 3 to also be assigned.
I can not find the correct structure of the parameters. If I use the following, it completely overwrites the current associations.
// Ajax parameters
user[books] = [3]
How should is build the parameters such that it only adds the new book? And as a follow-up, how can I build the parameters to remove only a single association?
You have to send only one "book_id" in request.
Then in controller:
# assuming params hash is { :book_id => 3 }
#book = Book.find params[:book_id]
#user.books << #book
...
# Removing
#user.books.delete(#book)
# In `update` action
params[:user][:book_ids] = (#user.book_ids + params[:user][:book_ids]).flatten
#user.update_attributes(params[:user])

Trouble on saving an associated model and then retrieve some data from that

I am using Ruby on Rails 3 and I am trying to retrieve some data from a just saved child model (associated model) in order to store that data in the parent model.
More precisely (in steps) I would like to do:
Save the child model Account of the parent model User
Retrieve the just created Account ID value and save that value in the User model attribute named users_account_id.
... and more explicitly (in values) I would like to have the following scenario after saving the child model Account:
# Account values
Account.id = 222
Account.name = "Test_name"
...
Account.user_id = 111
# User values
User.id = 111
User.users_account_id = 222
I already implemented the first step, but how can I implement the second step?
In order to retrieve the Account ID, I tryed to use an association callback
class User < ActiveRecord::Base
has_one :account, :before_add => :callback_name
validates_associated :account
accepts_nested_attributes_for :account
def callback_name
self.users_account_id = Account.find_by_id(self.id).id
end
end
but I get this error:
Unknown key(s): before_add
This is way overkill. All you need to do is put the user_id in the form of the account that is getting created as a hidden field.
<% form_for(#account) do |f| %>
<%= f.hidden_field :user_id, :value => current_user.id %>
<% end %>
Of course add your other fields that you want for account and you need a current_user object which you will need anyways with your current logic.
I'm going to side step your question a bit, and ask why you need IDs pointing in both directions? I assume you want your User to be related to an Account, and an Account to have one or more Users. The "Rails Way" to do this would be something like the following:
class User < ActiveRecord::Base
belongs_to :account
end
class Account < ActiveRecord::Base
has_many :users
end
In your database the users table will have account_id and the accounts table will not have a user_id of any kind.
This will still allow you to user the association in both directions:
some_user.account # Returns the correct account object
some_account.users # Returns all users for the account
I hope this helps somewhat!

Best practice: How to split up associations-functions in controllers with equal-access models

I have 2 equal-access models: Users and Categories
Each of these should have the standard-actions: index, new, create, edit, update and destroy
But where do I integrate the associations, when I want to create an association between this two models?
Do I have to write 2 times nearly the same code:
class UsersController << ApplicationController
# blabla
def addCategory
User.find(params[:id]).categories << Category.find(params[:user_id])
end
end
class CategoriessController << ApplicationController
# blabla
def addUser
Category.find(params[:id]).users << User.find(params[:user_id])
end
end
Or should I create a new Controller, named UsersCategoriesController?
Whats the best practice here? The above example doens't look very DRY.... And a new controller is a little bit too much, I think?
Thanks!
EDIT:
I need to have both of these associations-adding-functions, because f.e.
#on the
show_category_path(1)
# I want to see all assigned users (with possibility to assign new users)
and
#on the
show_user_path(1)
#I want to see all assigned categories (with possibility to assign new categories)
EDIT:
I'm taking about a HBTM relationship.
If you have a situation where you need to do this with has_and_belongs_to_many, you could take the approach you are currently using, or you could build this into your existing update actions.
When you add a habtm relationship, you will get an additional method on your classes...
class User < ActiveRecord::Base
has_and_belongs_to_many :categories
end
With this, you can do this:
user = User.find(params[:id])
user.category_ids = [1,3,4,7,10]
user.save
The categories with those ids will be set. If you name your form fields appropriately, the update can take care of this for you if you want to use checkboxes or multiselect controls.
If you need to add them one at a time, then the methods you've built in your original post are reasonable enough. If you think the repetition you have is a code smell, you are correct - this is why you should use the approach I outlined in my previous answer - an additional model and an additional controller.
You didn't mention if you are using has_and_belongs_to_many or if you are using has_many :through. I recommend has_many :through, which forces you to use an actual model for the join, something like UserCategory or Categorization something like that. Then you just make a new controller to handle creation of that.
You will want to pass the user and category as parameters to the create action of this controller.
Your form...
<% form_tag categorizations_path(:category_id => #category.id), :method => :post do %>
<%=text_field_tag "user_id" %>
<%=submit_tag "Add user" %>
<% end %>
Your controller...
class CategorizationsController < ApplicationController
def create
if Categorization.add_user_to_category(params[:user_id], params[:category_id])
...
end
end
then your categorization class...
class Categorization
belongs_to :user
belongs_to :category
def self.add_user_to_category(user_id, category_id)
# might want to validate that this user and category exist somehow
Categorization.new(:user_id => user_id, :category_id => category_id)
Categorization.save
end
end
The problem comes in when you want to send the users back, but that's not terribly hard - detect where they came from and send them back there. Or put the return page into a hidden field on your form.
Hope that helps.

Resources