Rails many to many model, adding records - ruby-on-rails

Hello im trying to add some records to my database with this model
class Colleagueship < ActiveRecord::Base
belongs_to :employee
belongs_to :colleague, :class_name => 'Employee'
end
class Employee < ActiveRecord::Base
has_many :colleagueships
has_many :colleagues, :through => :colleagueships
# ...
end
but i have no idea in how to start a new form to create new records
im thinking to try something like
def new
employee = ## gotta get the id here in the form
#colleagueship = employee.colleagueships.build(:colleague_id => params[:colleague_id])
#colleagueship.save
end
what do you think? how do i achieve this with a post http method? do i have to save the employee variable with the request and add the employee_id there?

In the controller
def new
end
def create
# inspect submitted params here
puts params
if colleagueship.save
# etc etc
else
# error
end
end
private
def employee
#employee = Employee.find_by(params[:employee_id])
end
def colleagueship
#colleagueship = employee.colleageships.build
end
helper_method :employee, :colleagueship
Your routes should be nested to provide the key you'll use to find the employee.
resources :employees do
# this will generate /employees/:employee_id/colleagues/:id
resources :colleagueships
end
In your view, you will probably use the form_tag helper, as it's easier to customize forms with whatever fields you want, especially if you're avoiding accepts_nested_attributes which you should. You can also include a hidden_field_tag with employee_id if you aren't nested your routes.
= form_tag new_employee_colleague_path do
= text_field_tag 'colleageship[name]', placeholder: 'something...'
Something along these lines should work. Make sure to inspect the params hash to see that the values are formatted correctly.

Related

Rails: Loop through db objects according to model associations

I have two models post.rb and attachment.rb with the following associations:
class Attachment < ApplicationRecord
belongs_to :post, optional: true
end
class Post < ApplicationRecord
has_many :attachments, :dependent => :destroy
end
This is what I have inside the controller action where the view is:
def results_search
if params[:search]
#attachment = Attachment.all
#posts = Post.search(params[:search])
end
end
I'm trying to do this <% #posts.attachments.each do |post|%> but I'm getting the following error undefined method attachments' for #<Post::ActiveRecord_Relation:0x007fa673bef9d0>
I want to loop through all the results from the search (#posts) and show the results alongside the attachments that belong to each post. Each attachment has a post_id assigned to it. Any ideas how I can implement this?
You're going to want to do something like:
#posts.each do |post|
# do some stuff with post (show name, content, etc.)
post.attachments.each do |attachment|
# do stuff with attachment (show, etc.)
end
end
What it really looks like depends on where you're doing this (view?) and what tools you're using (vanilla erb, slim, haml, etc.).
Also, you're going to want to do some eager loading so you don't end up in N+1 land.

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!!

Filling out an inherited mongoid document using nested attributes

Given the following models:
class Company
include Mongoid::Document
has_many :workers, autosave: true
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
class Worker
include Mongoid::Document
field :hours
attr_accessible :hours
belongs_to :company
end
class Manager < Worker
field :order
has_many :contributors, :class_name => "Worker"
attr_accessible :order, :contributors
end
class Contributor < Worker
field :task
belongs_to :manager, :class_name => "Worker"
attr_accessible :task
end
How does one create a manager in a company in the controller and view using nested attributes?
Here's my guess:
def new
#company = Company.new
#company.workers = [Manager.new]
end
def create
#company = Company.new params[:user]
if #company.save
redirect_to root_url, :notice => "Company with manager created."
else
render :new
end
end
= semantic_form_for #company do |f|
= f.semantic_fields_for :workers do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
problem is the order field which specifically belongs to the manager is not persisting after the create. Also when the data is improperly filled there is an error:
undefined method `order' for #<Worker:0x0000000646f018> (ActionView::Template::Error)
So is there a way for nested attributes to handle inheritance in the models from mongoid?
The question is related to Can nested attributes be used in combination with inheritance? except instead of active record using mongoid.
Honestly, this is a paraphrasing of my code... the real code is more complex situation although i believe these are all of the relevant parts. If you have more questions ask.
UPDATE:
I changed the view to the following:
= semantic_form_for #company do |f|
- #company.workers.each do |worker|
- if worker._type == "Manager"
= f.semantic_fields_for :workers, worker do |worker_fields|
= worker_fields.inputs do
= worker_fields.input :hours
= worker_fields.input :order
I do not get the error anymore, however the nested attributes do not update the company object properly. The params are the following:
{"company"=> {"workers_attributes"=>{"0"=>{"hours"=>"30", "order" => "fish", "id"=>"4e8aa6851d41c87a63000060"}}}}
Again edited for brevity. So the key part is that there is a hash between "0" => {data for manager}. The workers data seems to be held in a hash. I would expect the data to look more like the following:
params = { company => {
workers_attributes => [
{ hours => "30", "order" => "fish" }
]}}
This is different because the workers data is held in an array instead of a hash. Is there another step to get the nested attributes to save properly?
Thanks
what version of Mongoid are you using? Because I don't think the use of refereneces_many is encouraged -- Not that that's related to your problem here, just wanted to probe what version you're using. In the doc on the gorgeous Mongoid.org, get this, I had to learn it the hard way, they say for Updating your records, you need to the autossave set to true. That's NOT accurate. You need it for even creating
so:
class Company
include Mongoid::Document
has_many :workers, :autossave => true # your money shot
accepts_nested_attributes_for :workers
attr_accessible :workers_attributes
end
ADDED:
I was re-reading your code, I spotted the following that might be the problem: Your Company model is set to has_many :workers and is set to accept nested attribbutes for Worker when changes come in, correct? And there is a field named Order in your Manager model which is subclassed from Worker. Yet you're having a form whose nested fields part is pointed at Worker not at Manager, the model that actually has the Order field. And that's obviously not enough, because Company isn't having_many :managers yet, you may need to set it to has_many :managers in the Company model as well.

accepts_nested_attributes_for with find_or_create?

I'm using Rails' accepts_nested_attributes_for method with great success, but how can I have it not create new records if a record already exists?
By way of example:
Say I've got three models, Team, Membership, and Player, and each team has_many players through memberships, and players can belong to many teams. The Team model might then accept nested attributes for players, but that means that each player submitted through the combined team+player(s) form will be created as a new player record.
How should I go about doing things if I want to only create a new player record this way if there isn't already a player with the same name? If there is a player with the same name, no new player records should be created, but instead the correct player should be found and associated with the new team record.
When you define a hook for autosave associations, the normal code path is skipped and your method is called instead. Thus, you can do this:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
This code is untested, but it should be pretty much what you need.
Don't think of it as adding players to teams, think of it as adding memberships to teams. The form doesn't work with the players directly. The Membership model can have a player_name virtual attribute. Behind the scenes this can either look up a player or create one.
class Membership < ActiveRecord::Base
def player_name
player && player.name
end
def player_name=(name)
self.player = Player.find_or_create_by_name(name) unless name.blank?
end
end
And then just add a player_name text field to any Membership form builder.
<%= f.text_field :player_name %>
This way it is not specific to accepts_nested_attributes_for and can be used in any membership form.
Note: With this technique the Player model is created before validation happens. If you don't want this effect then store the player in an instance variable and then save it in a before_save callback.
A before_validation hook is a good choice: it's a standard mechanism resulting in simpler code than overriding the more obscure autosave_associated_records_for_*.
class Quux < ActiveRecord::Base
has_and_belongs_to_many :foos
accepts_nested_attributes_for :foos, reject_if: ->(object){ object[:value].blank? }
before_validation :find_foos
def find_foos
self.foos = self.foos.map do |object|
Foo.where(value: object.value).first_or_initialize
end
end
end
When using :accepts_nested_attributes_for, submitting the id of an existing record will cause ActiveRecord to update the existing record instead of creating a new record. I'm not sure what your markup is like, but try something roughly like this:
<%= text_field_tag "team[player][name]", current_player.name %>
<%= hidden_field_tag "team[player][id]", current_player.id if current_player %>
The Player name will be updated if the id is supplied, but created otherwise.
The approach of defining autosave_associated_record_for_ method is very interesting. I'll certainly use that! However, consider this simpler solution as well.
Just to round things out in terms of the question (refers to find_or_create), the if block in Francois' answer could be rephrased as:
self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save!
This works great if you have a has_one or belongs_to relationship. But fell short with a has_many or has_many through.
I have a tagging system that utilizes a has_many :through relationship. Neither of the solutions here got me where I needed to go so I came up with a solution that may help others. This has been tested on Rails 3.2.
Setup
Here are a basic version of my Models:
Location Object:
class Location < ActiveRecord::Base
has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
has_many :city_tags, :through => :city_taggables
accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end
Tag Objects
class CityTaggable < ActiveRecord::Base
belongs_to :city_tag
belongs_to :city_taggable, :polymorphic => true
end
class CityTag < ActiveRecord::Base
has_many :city_taggables, :dependent => :destroy
has_many :ads, :through => :city_taggables
end
Solution
I did indeed override the autosave_associated_recored_for method as follows:
class Location < ActiveRecord::Base
private
def autosave_associated_records_for_city_tags
tags =[]
#For Each Tag
city_tags.each do |tag|
#Destroy Tag if set to _destroy
if tag._destroy
#remove tag from object don't destroy the tag
self.city_tags.delete(tag)
next
end
#Check if the tag we are saving is new (no ID passed)
if tag.new_record?
#Find existing tag or use new tag if not found
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
else
#If tag being saved has an ID then it exists we want to see if the label has changed
#We find the record and compare explicitly, this saves us when we are removing tags.
existing = CityTag.find_by_id(tag.id)
if existing
#Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
if tag.label != existing.label
self.city_tags.delete(tag)
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
end
else
#Looks like we are removing the tag and need to delete it from this object
self.city_tags.delete(tag)
next
end
end
tags << tag
end
#Iterate through tags and add to my Location unless they are already associated.
tags.each do |tag|
unless tag.in? self.city_tags
self.city_tags << tag
end
end
end
The above implementation saves, deletes and changes tags the way I needed when using fields_for in a nested form. I'm open to feedback if there are ways to simplify. It is important to point out that I am explicitly changing tags when the label changes rather than updating the tag label.
Answer by #François Beausoleil is awesome and solved a big problem. Great to learn about the concept of autosave_associated_record_for.
However, I found one corner case in this implementation. In case of update of existing post's author(A1), if a new author name(A2) is passed, it will end up changing the original(A1) author's name.
p = Post.first
p.author #<Author id: 1, name: 'JK Rowling'>
# now edit is triggered, and new author(non existing) is passed(e.g: Cal Newport).
p.author #<Author id: 1, name: 'Cal Newport'>
Oringinal code:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
It is because, in case of edit, self.author for post will already be an author with id:1, it will go in else, block and will update that author instead of creating new one.
I changed the code(elsif condition) to mitigate this issue:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
elsif author && author.persisted? && author.changed?
# New condition: if author is already allocated to post, but is changed, create a new author.
self.author = Author.new(name: author.name)
else
# else create a new author
self.author.save!
end
end
end
#dustin-m's answer was instrumental for me - I am doing something custom with a has_many :through relationship. I have a Topic which has one Trend, which has many children (recursive).
ActiveRecord does not like it when I configure this as a standard has_many :searches, through: trend, source: :children relationship. It retrieves topic.trend and topic.searches but won't do topic.searches.create(name: foo).
So I used the above to construct a custom autosave and am achieving the correct result with accepts_nested_attributes_for :searches, allow_destroy: true
def autosave_associated_records_for_searches
searches.each do | s |
if s._destroy
self.trend.children.delete(s)
elsif s.new_record?
self.trend.children << s
else
s.save
end
end
end

How to access nested params

I would like to get some nested params. I have an Order that has many Items and these Items each have a Type. i would like to get the type_id parameter from the controllers create method.
#order = Order.new(params[:order])
#order.items.each do |f|
f.item_type_id = Item_type.find_by_name(f.item_type_id).id
end
The reason is that i want the user to be able to create new item_types in the view. When they do that i use an AJAX call add them to the db. When they post the form i get names of the item_type in the item_type_id parameter and i want to find the correct item_type and set the id to that
To access the nested fields from params do the following:
params[:order][:items_attributes].values.each do |item|
item[:type_id]
end if params[:order] and params[:order][:items_attributes]
Above solution will work ONLY if you have declared the correct associations and accepts_nested_attributes_for.
class Order < ActiveRecord::Base
has_many :items
accepts_nested_attributes_for :items, :allow_destroy => true
end
class Item < ActiveRecord::Base
belongs_to :order
end

Resources