Building association proxies with through - ruby-on-rails

Taking this article as a starting point:
Rails Way Blog - Association Proxies
and looking specifically at
def create
#todo_list = current_user.todo_lists.build params[:todo_list]
if #todo_list.save
redirect_to todo_list_url(#todo_list)
else
render :action=>'new'
end
end
This is a way of making sure you assign ownership to the user
BUT supposing the ToDo list was a many-many relationship associated in a has_many through i.e.
def User < AR:Base
has_many :user_todos
has_many :todo_lists, :through => :user_todos
end
At this point...
#todo_list = current_user.todo_lists.build params[:todo_list]
still works but only the todo_list is saved and not the join. How can I get the joy of association proxies without having lots of nested if/else should the join or Todo instance not validate when saved.
I was thinking of something along the lines of...
#todo_list = cu.user_todos.build.build_to_do(params[:todo_list])
but as I mentioned above the user_todos don't get saved.
Your help is greatly appreciated!
Kevin.

Try doing something along the lines of:
current_user.todo_lists << TodoList.new(params[:todo_list])

I got it working...
At first I had:
#todo_list = #user.todo_lists.build params[:todo_list]
#todo_list.save!
This works when TodoList simply belongs_to User but not when it's a many-to-many relationship. When I changed the relationship I had to do...
#todo_list = #user.todo_lists.build params[:todo_list]
#user.save!
This saved the #user, the join and the #todo_list.
Obvious I suppose but then I wonder if the original example should also
current_user.save!
instead of
#todo_list.save!
I thought the idea behind build was that it validates and saves the associations when you save the parent so does the example at Rails Way miss the point? or more likely am I?

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.

Reassociate all related models in rails

Ok, We f&^%$**&ed up.
We lost a bunch of user records. At some point, an integration file ran which re-inserted some of the lost records.
The problem is that the new users have a different ID than the original user, so all the existing related content for the old User id has been orphaned. I now need to go back in and reassociate all the orphaned stuff to the new User id. It won't be enough to simply give the new user the old Id from backup, because there will be new content associated to the new User Id.
We know the reflect_on_all_associations method, but that is hard to use for finding stuff. However, this could be a starting point for a script of some kind.
Any clues on how to have a method return all models related to a particular model based on associations, without having to specify or know those associations?
Here's a way to use reflect_all_associations: You can iterate through the associations, select only the has_many and has_one, and then update those records. Here's a helper class to do the job which you can execute by calling AssociationFixer.new(user_to_destroy, original_user_id).fix_associations:
class AssociationFixer
USER_ASSOCIATIONS = User.reflect_on_all_associations
def initialize(user_to_destroy, original_user_id)
#user_to_destroy = user_to_destroy
#original_user_id = original_user_id
end
def fix_associations
USER_ASSOCIATIONS.each do |association|
next if association.options.has_key? :through
if association.macro == :has_many
fix_has_many(association)
elsif association.macro == :has_one
fix_has_one(association)
end
end
end
def fix_has_many(association)
#user_to_destroy.send(association.name).each do |record|
if association.options.has_key? :foreign_key
record.send(assignment_method(association.foreign_key), #original_user_id)
else
record.user_id = #original_user_id
end
record.save
end
end
def fix_has_one(association)
if association.options.has_key? :foreign_key
#user_to_destroy.send(association.name).send(assignment_method(association.foreign_key), #original_user_id)
else
#user_to_destroy.send(assignment_method(association.name.user_id), #original_user_id)
end
record.save
end
def assigment_method(method_name)
method_name.to_s + '='
end
end
This sounds like a problem for SQL. I would look at importing your backup tables into the database in a separate table, if it's possible to join them on a column, maybe email or user_name or something. Then you can run a select based on the old id and update to the new id. Best of luck!

Deleting a record in a has_and_belongs_to_many table - Rails

I've been looking, and can't find a good answer for how to delete records in a HABTM table. I assume a lot of people have this same requirement.
Simply, I have Students, Classes, and Classes_Students
I want a student to be able to drop a class, or delete the HABTM record that has signed that student up for that class.
There must be a simple answer to this. Does anyone know what it is?
The reason why .destroy or .delete does not work on this situation is due to the missing primary key in the middle table. However, our parent objects have this really cool method called {other_obj}_ids. It is a collection of ids on the left table object, of the right table object. This information is of course populated from our middle table.
So with that in mind, we have 2 object classes (Student, and Classes). Active record magic can generally figure out the middle table if you are not doing anything fancy, but it is recommended to use has_many :through.
class Student < ActiveRecord::Base
has_and_belongs_to_many :classes
end
class Classes < ActiveRecord::Base
has_and_belongs_to_many :students
end
What we can now do in terms of the middle table with this setup...
student = Student.find_by(1)
student.classes # List of class objects this student currently has.
student.class_ids # array of class object ids this student currently has
# how to remove a course from the middle table pragmatically
course = Course.find_by({:name => 'Math 101'})
# if this is actually a real course...
unless course.nil?
# check to see if the student actually has the course...
if student.class_ids.include?(course.id)
# update the list of ids in the array. This triggers a database update
student.class_ids = student.class_ids - [course.id]
end
end
I know this is a little late to answer this, but I just went through this exact situation tonight and wanted to share the solution here.
Now, if you want this deleted by the form, since you can now see how it is handled pragmatically, simply make sure the form input is nested such that it has something to the effect of:
What kind of trouble are you having? Do you have the appropriate :dependent=>:destroy and :inverse_of=>[foo] on your relations?
Let's say a class had a course title. You can do:
student.classes.find_by_course_title("Science").delete
So the proper answer here is to do something like this in your view:
<%= link_to 'Remove', cycle_cycles_group_path(#cycle, cycle), method: :delete %><br />
cycle is from a block the above code is within.
#cycle is an instance variable from the join models controller.
cycle_cycles_group_path is the nested join table "cycles_groups" under the model "Cycle" in the routes.rb file:
resources :cycles do
resources :cycles_groups do
end
end
and the join model controller looks like this:
def destroy
#cycles_group = CyclesGroup.find(params[:id])
#cycle = #cycles_group.cycle
#cycles_group.destroy
puts "cycle: #{#cycle}"
respond_to do |format|
format.html {redirect_to cycle_path(#cycle), notice: 'Training Week was successfully removed!'}
end
end

Check if a User has a Game

I've done some searching here and I've not been able to find anything that quite answers what I'm looking for. If I failed in my search I apologize.
Moving on, I am new to Rails and I'm working on an application to test the waters if you will. I'm using Devise for authentication and it's proving quite useful. That said, I've run into a big of a road block with where a certain check for data would go, and how I would go about it.
I have three tables: users, games, and users_games (I read that this was an acceptable naming convention for relational tables, correct me if I'm wrong). On a Games page I would like to display a certain message if the currently logged in User has this Game added to their account (in users_games). I'm unsure of where to perform this check, or if it even matters at all.
As for the actual checking, my initial idea would be something along the lines of:
games_controller.rb
class GamesController < ApplicationController
def index
#games = Game.all
end
def show
#game = Game.find(params[:id])
#user_owns = UsersGames.where(:game_id => #game.id, :user_id => current_user.id)
end
end
Then on the view checking if #user_owns has a value or not.
Thanks in advance for any insight or wisdom you can offer.
What about this way, may be you don't need users_games
if game has_many users and user belongs_to game
def show
#game = Game.find_by_user_id(current_user.id)
end
Then on the view checking if #game has a value or not.
If your Users<->Games relationship is a simple HABTM with no additional attributes on the join table, i.e.
class User < AR::Base
has_and_belongs_to_many :games
class Game < AR::Base
has_and_belongs_to_many :users
you don't need to have a separate model for the join table, provided that you follow the Rails naming convention that requires you to follow the lexicographical order when naming your join table, i.e. in your case it would be games_users, not the other way around like you have it now.
Going back to your original question, I think it can be as simple as this:
def show
#game = Game.find(params[:id])
#game_owned = current_user.games.include? #game
end
You can also make this a method on your User model:
class User < AR::Base
...
def owns_game?(game)
self.games.include?(game)
end
end
and then call current_user.owns_game?(#game) in your controllers/views.

How does one intercept nested_attributes for further processing?

tl;dr: Is it possible to intercept posted values from a nested model for further processing? I've tried everything I can think of to access the nested attributes to use for a before_save callback, but that may be only a testament to the limits of my imagination.
I'm writing a library application, where books can have many authors and vice versa. That bit is working just fine with a has_many_through and accepts_nested_attributes_for. The app saves all the book and author information just fine. Except... I can't seem to write a working before_save on either the Author or Book model to check if the Author that we're trying to create exists. Here's what I already have:
class Book < ActiveRecord::Base
has_many :authorships
has_many :authors, :through => :authorships
accepts_nested_attributes_for :authors, :authorships
end
class Author < ActiveRecord::Base
has_many :authorships
has_many :books, :through => :authorships
before_save :determine_distinct_author
def determine_distinct_author
Author.find_or_create_by_author_last( #author )
end
end
## books_controller.rb ##
def new
#book = Book.new
#book.authors.build
respond_to do |format|
format.html
format.xml { render :xml => #book }
end
end
def create
#book = Book.new(params[:book])
#author = #book.authors #I know this line is wrong. This is the problem (I think.)
# more here, obviously, but nothing out of ordinary
end
When I post the form, the dev log passes on this:
Parameters: {"commit"=>"Create Book",\
"authenticity_token"=>"/K4/xATm7eGq/fOmrQHKyYQSKxL9zlVM8aqZrSbLNC4=",\
"utf8"=>"✓", "book"{"title"=>"Test", "ISBN"=>"", "genre_id"=>"1", "notes"=>"", \
"authors_attributes"=>{"0"{"author_last"=>"McKinney", "author_first"=>"Jack"}}, \
"publisher"=>"", "pages"=>"", "language_id"=>"1", "location_id"=>"1"}}
So... the data's there. But how do I get to it to process it? When I post the data, here's that log:
Author Load (0.4ms) SELECT `authors`.* FROM `authors` WHERE\
`authors`.`author_last` IS NULL LIMIT 1
I've done a fair bit of searching around to see what there is out there on nested attributes, and I see a lot on the form side, but not much on how to get to the assets once they're submitted.
Any explanation of why the solution which works does actually work would also be appreciated. Thanks.
First, this line
#author = #book.authors
assigns an array of authors to #author, but in your before_save you seem to be expecting a single author model. Secondly, the #author variable is scoped to the controller and will be empty in your model's scope.
Remove that line and try this in your Book class (not tested):
def determine_distinct_author
authors.each do |author|
Author.find_or_create_by_author_last( author )
end
end
1) The nested attributes is an array, and should be accessed as an array. Getting the submission from the one model to the next still presents quite a problem.
2) accepts_nested_attributes_for is probably not going to help out a whole lot, and there will undoubtedly be a lot of custom processing which will have to happen between now and when the whole system is fully functional. Probably best to ditch it now and move on.
In that direction: It looks like Ryan Bates has done a recent bit on the jQuery TokenInput plugin which will take care of a lot of the autocomplete feature on the author. More to the larger problem (and a bit harder to find), is a follow up that he posted to some issues with the plugin on working with new entries.
I am not quite sure what you are trying to accomplish, but inside your Author you have a before_save callback, and that is obviously giving the error.
To me it seems you are looking for an author with the given name, and then want to use that? Is that correct?
Using a nested form, you will always create a new author, unless you want to use something like a select-box, and then select an author-id (which is part of the book, so not nested), instead of the authors details.
UX-wise, i would offer the option to either select an existing author (use an autocomplete field), or create a new one (using the nested fields option --without your before_save callback).

Resources