How to access nested params - ruby-on-rails

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

Related

Rails many to many model, adding records

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.

Updating a join model relationship in Rails

Basically I have a Shop, Category and a join model ShopCategory with additional attributes
class Shop
has_many :shop_categories
has_many :categories, through: :shop_categories
class Category
has_many :shop_categories
has_many :shops, through: :shop_categories
class ShopCategory
belongs_to :shop
belongs_to :category
I have a shop form which I'd like to create or update the shop through it.
My first thought is to create a virtual attribute called :categories and to have the model handle the setter and getter through it, something like this (pseudocode for simplicity):
def categories=(cats)
cats.each do |c|
check if a ShopCategory exists with this shop (self) and that category.
if doesn't exist, create one, if exists ignore
for all the categories in self that weren't touched, delete that ShopCategory
end
end
but I feel this would cause problems in the long run because of the connection of 3 models and not though a controller
However, I can't seem to think of a simple way to have a create and update methods in the shops_controller for handling this
def update
#shop = Shop.find params[:id]
cats = params[:shop].delete :categories
#shop.update_attributes(shop_params)
## should I have a category update method here? How would I handle errors? This gets complicated
end
It sounds like you want a nested model form, for editing both a Shop and its associated ShopCategories.
Basically, what it entails is on the form for your Shop, you can simply iterate over the associated ShopCategories and print out fields for them, to edit them all together. Rails will automatically handle it all, as long as the parameters are structured correctly.
https://github.com/nathanvda/cocoon is a gem for making nested model forms easier.
There is also a tutorial on Railscasts:
http://railscasts.com/episodes/196-nested-model-form-revised
Collections
I don't know how experienced you are with Ruby on Rails, but you may wish to look at some of the documentation pertaining to collections
What you're looking at is how to populate your collections - which is actually relatively simple:
#app/controllers/shops_controller.rb
Class ShopsController < ApplicationController
def create
#shop = Shop.new(shop_params)
#shop.save
end
private
def shop_params
params.require(:shop).permit(:your, :attributes, category_ids: [])
end
end
This will allow you to use the following form:
#app/views/shops/new.html.erb
<%= form_for #shop do |f| %>
<% Category.all.each do |category| %>
<%= f.check_box :category_ids, category.id %>
<% end %>
<% end %>
--
Modularity
In terms of validating your collections for uniqueness, you will be best using DB, or Association-level validation:
class Shop
has_many :categories, -> { uniq }, through: :shop_categories
This will essentially create only unique categories for your shop, which you can populate with the method described above.

Scope exception rules

I am trying to define an inventory for all my articles but I want to exclude the articles that are sent to me with a parameter.
Here is what the relationship looks like:
Article
has_many :tags, through: :articletags
ArticleTags
belongs_to :article
belongs_to :tags
Tags
has_many :article, through: articletags
Here's a method to define the one without the tags in my models:
def self.by_not_tags(tag)
joins(:tags).where('tags.title != ?', tag)
end
Here's how I call it in my view:
<%= link_to (tag.title), articles_path(:scope => tag.title) %>
Here's my controller:
def custom
if params[:scope].nil?
#articles = Article.all(:order => 'created_at DESC')
else
#articles = Article.by_tags(params[:scope])
#articles2 = Article.by_not_tags(params[:scope])
end
end
The goal is to see all the articles with a tag first, and then to show the other ones without that tag, so I don't have duplicates.
My issue is with the joins, but I am not sure how to find the article without tags. Maybe an except would work, but I am not sure what kind of query would work for it.
Assuming ArticleTag model needs to validate the presence of both article_id and tag_id,
Article.where('article_tag_id is null')
If I do not assume that above validation stated,
Article.where('not exists (select 1 from article_tags where article_id = articles.id)')

How to maintain the ordering for nested attributes when using accepts_nested_attributes_for in a Rails application

Here is the parent model:
class TypeWell < ActiveRecord::Base
...
has_many :type_well_phases, :dependent => :destroy
accepts_nested_attributes_for :type_well_phases, :reject_if => lambda { |a| a[:phase_id].blank? }, :allow_destroy => true
...
end
Here is the nested model:
class TypeWellPhase < ActiveRecord::Base
belongs_to :type_well
belongs_to :phase
end
Here is the Phase model:
class Phase < ActiveRecord::Base
...
has_many :type_well_phases
...
end
I add nested records in child table (TypeWellPhases) by copying ALL records from my phases (Phase model) table in the parent model's controller as shown below:
class TypeWellsController < ResourceController
...
def new
#new_heading = "New Type Well - Computed"
#type_well = TypeWell.new
initialize_phase_fields
end
private
def initialize_phase_fields
Phase.order("id").all.each do |p|
type_well_phase = #type_well.type_well_phases.build
type_well_phase.phase_id = p.id
type_well_phase.gw_heat_value = p.gw_heat_value
end
end
...
end
I do this because I want to maintain a specific order by the children fields that are added. The part of the code Phase.order("id") is for that since the phases table has these records in a specific order.
After this I use the simple_form_for and simple_fields_for helpers as shown below in my form partial:
= simple_form_for #type_well do |f|
...
#type_well_phases
= f.simple_fields_for :type_well_phases do |type_well_phase|
= render "type_well_phase_fields", :f => type_well_phase
Everything works as desired; most of the times. However, sometimes the ordering of Child rows in the form gets messed up after it has been saved. The order is important in this application that is why I explicitly do this ordering in the private method in the controller.
I am using the "cocoon" gem for adding removing child records. I am not sure as to why this order gets messed up sometimes.
Sorry for such a long post, but I wanted to provide all the pertinent details up front.
Appreciate any pointers.
Bharat
I'll explain you in a more generic way. Say, you have Product and Order models:
= form_for #product do |f|
...
= f.fields_for :orders do |order_fields|
= order_fields.text_field :name
If you want your orders to be sorted by name then just sort them :)
Instead of:
= f.fields_for :orders do |order_fields|
put:
= f.fields_for :orders, f.object.orders.order(:name) do |order_fields|
As you see, the f variable that is a parameter of the block of form_for has method object. It's your #product, so you can fetch its orders via .orders and then apply needed sorting via .order(:name) (sorry for this little confusion: order/orders).
The key to your solution that you can pass sorted orders as the second parameter for fields_for.
P.S. Your using the simple_form gem doesn't affect my solution. It'll work if you add 'simple_' to helpers. Just wanted my answer to be more helpful for others and not too task-related.
If you are using Rails 2.3.14 or older you have to use:
f.fields_for :orders, f.object.orders.all(:order => :name) do |order_fields|
I use this way:
class League < ActiveRecord::Base
has_many :rounds, -> { sort_by_number }, dependent: :destroy
end
class League::Round < ActiveRecord::Base
belongs_to :league
scope :sort_by_number, -> { order('league_rounds.number ASC') }
end
In the view
= form_for league do |f|
= f.fields_for :rounds do |round_form|
# Here rounds are sorted by sort_by_number
This approach allows the use of any scope defined in the model. This approach allows the creation of several differently sorted associations.

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

Resources