Rails - Set additional attribute on has_many through record - ruby-on-rails

Using Rails 6.0.3.3 and ruby '2.6.0'.
I have these 3 models connected via a has_many through relation.
class Recipe < ApplicationRecord
has_many :layers
has_many :glazes, through: :layers
end
class Glaze < ApplicationRecord
has_many :layers
has_many :recipes, through: :layers
end
class Layer < ApplicationRecord
belongs_to :recipe
belongs_to :glaze
# there is a column named "coat_type" here that I would like to set
end
Everything is working great for creating a new Recipe and it automagically creating its related Layer record. But now I would like to also set a coat_type attribute on the Layer record when it's created, but I can't seem to figure out how I could do something like that.
The Form View Partial
= form_with model: recipe, local: true do |form|
%p
= form.label :name
%br
= form.text_field :name
%p
= form.label :description
%br
= form.text_area :description
.recipe-glaze
= form.collection_select(:glaze_id, Glaze.all, :id, :name, { prompt: "Select Glaze" }, { name: 'recipe[glaze_ids][]' })
.recipe-glaze
= form.collection_select(:glaze_id, Glaze.all, :id, :name, { prompt: "Select Glaze" }, { name: 'recipe[glaze_ids][]' })
%p
= form.submit
The Controller Create Action (and strong params acton)
def create
#recipe = Recipe.new(recipe_params)
if #recipe.save
redirect_to #recipe
else
render 'new'
end
end
private
def recipe_params
params.require(:recipe).permit(:name, :description, glaze_ids: [])
end
Ideally, I would be able to add another select box in the form and the user would use that to set the coat_type of the Layer record that will be created. But I can't figure out how I could pass that into the controller and have it know what to do with that value.
Is this something that is possible, or am I approaching this incorrectly?

So I actually ended up stumbling upon the "cocoon" gem thanks to this comment. By following the setup instructions for "cocoon", I was able to tweak my code to do what I needed.
My Recipe model changed to this ::
class Recipe < ApplicationRecord
has_many :layers, inverse_of: :recipe
has_many :glazes, through: :layers
accepts_nested_attributes_for :layers, reject_if: :all_blank, allow_destroy: true
end
My controller's strong params action changed to this ::
private
def recipe_params
params.require(:recipe).permit(:name, :description, layers_attributes: [:id, :glaze_id, :coat_type, :_destroy])
end
My form view partial changed to ::
- if recipe.errors.any?
= render partial: 'errors', locals: { recipe: recipe }
%p
= form.label :name
%br
= form.text_field :name
%p
= form.label :description
%br
= form.text_area :description
%h3 Layers
#layers
= form.fields_for :layers do |layer|
= render 'layer_fields', f: layer
.links
= link_to_add_association 'add layer', form, :layers
%p
= form.submit
and the "layer_fields" partial referenced in the form looks like this ::
.nested-fields
.field
= f.label :glaze_id
%br
= f.collection_select(:glaze_id, Glaze.all, :id, :name, { prompt: "Select Glaze" } )
.field
= f.label :coat_type
%br
= f.text_field :coat_type
= link_to_remove_association 'remove layer', f
Making those changes using the "Cocoon" gem, I was able to accomplish what I needed. Hopefully this helps someone else in the future.

Related

Rails: Select multiple tag objects to associate

So I'm working on a project where I have a document object (basically an E-Library application) and I have a bunch of Tag objects that I want to be able to associate with it. Currently I have a has_and_belongs_to_many association between the two. My question is in the form for a tag, what is the best way to select from a list of available tags to associate with that document? And will I have to do any fancy work in the controller to make this happen?
I'm using rails 3.2
Here is some of the code:
# This is the text model
# It will not have an attachment but instead it's children will
class Text < ActiveRecord::Base
attr_accessible :name, :author, :date, :text_langs_attributes, :notes
has_many :text_langs, dependent: :destroy
belongs_to :item
validates :author, presence: true
has_and_belongs_to_many :tags
accepts_nested_attributes_for :text_langs
def get_translations
TextLang.where(:text_id => self.id)
end
def get_language(lang)
TextLang.where(:text_id => self.id, :lang => lang).first
end
end
This is the tag:
# This is the Tags class
# It has and belongs to all of the other file classes
# the tags will need to be translated into four langauges
# Tags will also own themselvea
class Tag < ActiveRecord::Base
attr_accessible :creole, :english, :french, :spanish, :cat,
:english_description, :french_description, :spanish_description,
:creole_description, :parent_id
has_and_belongs_to_many :texts
has_and_belongs_to_many :sounds
belongs_to :parent, :class_name => 'Tag'
has_many :children, :class_name => 'Tag', :foreign_key => 'parent_id'
validates :cat, presence: true, inclusion: { in: %w(main sub misc),
message: "%{value} is not a valid type of tag" }
validates :english, :spanish, :french, :creole, presence: true
TYPES = ["main", "sub", "misc"]
end
This is the form:
= form_for #text do |f|
- if #text.errors.any?
#error_explanation
%h2= "#{pluralize(#text.errors.count, "error")} prohibited this text from being saved:"
%ul
- #text.errors.full_messages.each do |msg|
%li= msg
.field
= f.label :name
= f.text_field :name
.field
= f.label :date
= f.date_select :date
.field
= f.label :author
= f.text_field :author
= f.fields_for :text_langs do |pl|
.field
= pl.label :title
= pl.text_field :title
.field
= pl.label :lang
= pl.text_field :lang
.field
= pl.label :description
= pl.text_field :description, :size => 150
.field
= pl.label :plain_text
= pl.text_area :plain_text
.field
= pl.label :published
= pl.check_box :published
.field
= f.label :txt
= f.file_field :txt
.field
= f.label :notes
= f.text_area :notes, :rows => 10
.actions
= f.submit 'Save'
First of all I would suggest to try simple_form gem it would make your forms DRY and simple. They have very nice features for associations.
You would end doing something like this:
= simple_form_for #text do |f|
...
= f.association :tags, as: :check_boxes
Could be check boxes, radio buttons or maybe a select with multiple values if you need it.
Hope it helps

How do I reference an existing instance of a model in a nested Rails form?

I'm attempting to build a recipe-keeper app with three primary models:
Recipe - The recipe for a particular dish
Ingredient - A list of ingredients, validated on uniqueness
Quantity - A join table between Ingredient and Recipe that also reflects the amount of a particular ingredient required for a particular recipe.
I'm using a nested form (see below) that I constructed using an awesome Railscast on Nested Forms (Part 1, Part 2) for inspiration. (My form is in some ways more complex than the tutorial due to the needs of this particular schema, but I was able to make it work in a similar fashion.)
However, when my form is submitted, any and all ingredients listed are created anew—and if the ingredient already exists in the DB, it fails the uniqueness validation and prevents the recipe from being created. Total drag.
So my question is: Is there a way to submit this form so that if an ingredient exists whose name matches one of my ingredient-name fields, it references the existing ingredient instead of attempting to create a new one with the same name?
Code specifics below...
In Recipe.rb:
class Recipe < ActiveRecord::Base
attr_accessible :name, :description, :directions, :quantities_attributes,
:ingredient_attributes
has_many :quantities, dependent: :destroy
has_many :ingredients, through: :quantities
accepts_nested_attributes_for :quantities, allow_destroy: true
In Quantity.rb:
class Quantity < ActiveRecord::Base
attr_accessible :recipe_id, :ingredient_id, :amount, :ingredient_attributes
belongs_to :recipe
belongs_to :ingredient
accepts_nested_attributes_for :ingredient
And in Ingredient.rb:
class Ingredient < ActiveRecord::Base
attr_accessible :name
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
Here's my nested form that displays at Recipe#new:
<%= form_for #recipe do |f| %>
<%= render 'recipe_form_errors' %>
<%= f.label :name %><br>
<%= f.text_field :name %><br>
<h3>Ingredients</h3>
<div id='ingredients'>
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient_attributes do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name %>
<% end %>
<%= ff.label :amount %>
<%= ff.text_field :amount, size: "10" %>
<%= ff.hidden_field :_destroy %>
<%= link_to_function "remove", "remove_fields(this)" %><br>
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
</div><br>
<%= f.label :description %><br>
<%= f.text_area :description, rows: 4, columns: 100 %><br>
<%= f.label :directions %><br>
<%= f.text_area :directions, rows: 4, columns: 100 %><br>
<%= f.submit %>
<% end %>
The link_to and link_to_function are there to allow the addition and removal of quantity/ingredient pairs on the fly, and were adapted from the Railscast mentioned earlier. They could use some refactoring, but work more or less as they should.
Update: Per Leger's request, here's the relevant code from recipes_controller.rb. In the Recipes#new route, 3.times { #recipe.quantities.build } sets up three blank quantity/ingredient pairs for any given recipe; these can be removed or added to on the fly using the "Add ingredient" and "remove" links mentioned above.
class RecipesController < ApplicationController
def new
#recipe = Recipe.new
3.times { #recipe.quantities.build }
#quantity = Quantity.new
end
def create
#recipe = Recipe.new(params[:recipe])
if #recipe.save
redirect_to #recipe
else
render :action => 'new'
end
end
You shouldn't put the logic of ingredients match into view - it's duty of Recipe#create to create proper objects before passing 'em to Model. Pls share the relevant code for controller
Few notes before coming to code:
I use Rails4#ruby2.0, but tried to write Rails3-compatible code.
attr_acessible was deprecated in Rails 4, so strong parameters are used instead. If you ever think to upgrade your app, just go with strong parameters from the beginning.
Recommend to make Ingredient low-cased to provide uniform appearance on top of case-insensitivity
OK, here we go:
Remove attr_accessible string in Recipe.rb, Quantity.rb and Ingredient.rb.
Case-insensitive, low-cased Ingredient.rb:
class Ingredient < ActiveRecord::Base
before_save { self.name.downcase! } # to simplify search and unified view
validates :name, :uniqueness => { :case_sensitive => false }
has_many :quantities
has_many :recipes, through: :quantities
end
<div id='ingredients'> part of adjusted form to create/update Recipe:
<%= f.fields_for :quantities do |ff| %>
<div class='ingredient_fields'>
<%= ff.fields_for :ingredient do |fff| %>
<%= fff.label :name %>
<%= fff.text_field :name, size: "10" %>
<% end %>
...
</div>
<% end %>
<%= link_to 'Add ingredient', "new_ingredient_button", id: 'new_ingredient' %>
We should use :ingredient from Quantity nested_attributes and Rails will add up _attributes-part while creating params-hash for further mass assignment. It allows to use same form in both new and update actions. For this part works properly association should be defined in advance. See adjusted Recipe#new bellow.
and finally recipes_controller.rb:
def new
#recipe = Recipe.new
3.times do
#recipe.quantities.build #initialize recipe -> quantities association
#recipe.quantities.last.build_ingredient #initialize quantities -> ingredient association
end
end
def create
#recipe = Recipe.new(recipe_params)
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
def update
#recipe = Recipe.find(params[:id])
#recipe.attributes = recipe_params
prepare_recipe
if #recipe.save ... #now all saved in proper way
end
private
def prepare_recipe
#recipe.quantities.each do |quantity|
# do case-insensitive search via 'where' and building SQL-request
if ingredient = Ingredient.where('LOWER(name) = ?', quantity.ingredient.name.downcase).first
quantity.ingredient_id = quantity.ingredient.id = ingredient.id
end
end
end
def recipe_params
params.require(:recipe).permit(
:name,
:description,
:directions,
:quantities_attributes => [
:id,
:amount,
:_destroy,
:ingredient_attributes => [
#:id commented bc we pick 'id' for existing ingredients manually and for new we create it
:name
]])
end
In prepare_recipe we do the following things:
Find ID of ingredient with given name
Set foreign_key quantity.ingredient_id to ID
Set quantity.ingredient.id to ID (think what happens if you don't do that and change ingredient name in Recipe)
Enjoy!

Accessing error messages for nested attribute field

I have a form created using the simple_form gem which populates 2 models using nested attributes. I want to check if there are any errors and display a new block. However, I'm not sure how to correctly access the error message for the location attribute of the Booking model.
class Booking < ActiveRecord::Base
belongs_to :customer
attr_accessible :date_wanted, :location
end
and
class Customer < ActiveRecord::Base
has_many :bookings
accepts_nested_attributes_for :bookings
attr_accessible :name, :phone, :bookings_attributes
validates_presence_of :name, :phone
end
Form view:
simple_form_for #customer, {:html => { :class => "form-horizontal" }} do |f|
= f.input :name
= f.input :phone
= f.simple_fields_for :bookings do |b|
= b.input :date
= b.input :location
- if #customer.errors[:appointments_attributes][:location]
# insert code if any validation errors for the date field were found
= f.button :submit
b is an instance of form builder, holding booking, so you can try:
# ...
if b.object.errors[:location]
# ...

embedded polymorphic association model form

I have these models:
model 1:
module Ems
class Article < ActiveRecord::Base
has_many :images, :as => :imageable
accepts_nested_attributes_for :images
end
end
model 2:
module Ems
class Image < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
end
view:
= form_for #article do |f|
%div
= f.label :title
= f.text_field :title
%div
- f.fields_for :images do |builder|
= builder.label :title
= builder.text_field :title
I don't get any errors however I also dont get the form fields for the embedded image form. All I get is an empty DIV.
Can anyone point me in the right direction?
Thanks
try after replacing - with = at where you are calling fields_for
= f.fields_for :images do |builder|
The fields_for returns form HTML, so you need to render it on UI.

nested attributes with polymorphic has_one model

I am using accepts_nested_attributes_for with the has_one polymorphic model in rails 2.3.5
Following are the models and its associations:
class Address < ActiveRecord::Base
attr_accessible :city, :address1, :address2
belongs_to :addressable, :polymorphic => true
validates_presence_of :address1, :address2, :city
end
class Vendor < ActiveRecord::Base
attr_accessible :name, :address_attributes
has_one :address, :as => :addressable, :dependent => :destroy
accepts_nested_attributes_for :address
end
This is the view:
- form_for #vendor do |f|
= f.error_messages
%p
= f.label :name
%br
= f.text_field :name
- f.fields_for :address_attributes do |address|
= render "shared/address_fields", :f => address
%p
= f.submit "Create"
This is the partial shared/address_fields.html.haml
%p
= f.label :city
%br= f.text_field :city
%span City/Town name like Dharan, Butwal, Kathmandu, ..
%p
= f.label :address1
%br= f.text_field :address1
%span City Street name like Lazimpat, New Road, ..
%p
= f.label :address2
%br= f.text_field :address2
%span Tole, Marg, Chowk name like Pokhrel Tole, Shanti Marg, Pako, ..
And this is the controller:
class VendorsController < ApplicationController
def new
#vendor = Vendor.new
end
def create
#vendor = Vendor.new(params[:vendor])
if #vendor.save
flash[:notice] = "Vendor created successfully!"
redirect_to #vendor
else
render :action => 'new'
end
end
end
The problem is when I fill in all the fileds, the record gets save on both tables as expected.
But when I just the name and city or address1 filed, the validation works, error message shown, but the value I put in the city or address1, is not persisted or not displayed inside the address form fields?
This is the same case with edit action too.
Though the record is saved, the address doesn't show up on the edit form. Only the name of the Client model is shown.
Actually, when I look at the log, the address model SQL is not queried even at all.
Why f.fields_for :address_attributes?
Shouldn't it be:
- f.fields_for :address do |address_fields|
= render "shared/address_fields", :f => address_fields
It's not loading the values on edit and errors because you never load address_attributes with the values from #vendor.address.

Resources