rails3 i18n accepts_nested_attributes_for how-to translate? - ruby-on-rails-3

I'm using Rails 3.2.6 and this is an example case:
class Man < ActiveRecord::Base
has_many :eyes
accepts_nested_attributes_for :eyes
end
class Eye < ActiveRecord::Base
belongs_to :man
validates_inclusion_of :color, in: { %w[brown green blue] }
end
Views (in HAML):
= form_for #man do |f|
- if #man.errors.any?
#error_explanation
%h2= t 'errors.messages.record_invalid', count: #man.errors.count
%ul
- #man.errors.full_messages.each do |msg|
%li= msg
= f.fields_for(:eyes) do |b|
.field
= b.label :color
= b.text_field :color
.actions
= f.submit :submit
it.yml:
it:
activerecord:
attributes:
customer:
eyes: Occhi
customer/eyes:
color: Colore
errors:
models:
man/eyes:
attributes:
color:
inclusion: non valido
However the label of color is not translated (but it is with 'actviterecord.attributes.eye.color'), the attribute in the error message is only "Occhi" and the rest is errors.model.eyes.attributes.color.inclusion instead of errors.models.man/eyes.attributes.color.inclusion
The error message is the errors.model.man.attributes.eyes.inclusion, but how can I differentiate it? It should be something like "Occhi Colore non valido" instead of "Occhi non valido"

Try this out:
it:
activerecord:
attributes:
# set the name used in nested attribute error messages
customer/eyes:
color: Occhi Colore
errors:
models:
# change the error message for eye color not included in the list
eye:
attributes:
color:
inclusion: non valido
messages:
# change the inclusion message globally
inclusion: non valido
helpers:
label:
# set the label used by form builder for labels
man[eyes_attributes]:
color: Occhi Colore

Related

Undefined method "id" trying to save nested attributes in the same form

I am developing a web application with Rails in which I need to save two models with the same form. One of the models (Characteristic) belongs to the other (Facilities), so I decided to use a accepts_nested_attributes_for for the contained model. In the view, I use form_for to save the parent model (Characteristic) and another form_for for the contained model (Facilities). However, I always obtain the same error:
Started PUT "/facilities/537f8adfb4f2d7c124000056" for 127.0.0.1 at 2014-05-31 20:00:23 +0200
Processing by FacilitiesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"xr+cGlb9onx4o13IaS3K5UfYzmrb6pMdKljBc8byKdY=", "facilities"=>{"description"=>"Services", "characteristics"=>[{"id"=>"537f8adfb4f2d7c124000057", "title"=>"Room", "description"=>"Free"}]}, "commit"=>"Send", "id"=>"537f8adfb4f2d7c124000056"}
MOPED: 127.0.0.1:27017 COMMAND database=admin command={:ismaster=>1} (1.2872ms)
MOPED: 127.0.0.1:27017 QUERY database=hotel_abadi_development collection=facilities selector={"_id"=>"537f8adfb4f2d7c124000056"} flags=[:slave_ok] limit=0 skip=0 batch_size=nil fields=nil (0.4916ms)
MOPED: 127.0.0.1:27017 QUERY database=hotel_abadi_development collection=admins selector={"$query"=>{"_id"=>"537f8ad9b4f2d7c124000001"}, "$orderby"=>{:_id=>1}} flags=[:slave_ok] limit=-1 skip=0 batch_size=nil fields=nil (0.7987ms)
MOPED: 127.0.0.1:27017 QUERY database=hotel_abadi_development collection=admins selector={"$query"=>{"_id"=>"537f8ad9b4f2d7c124000001"}, "$orderby"=>{:_id=>1}} flags=[:slave_ok] limit=-1 skip=0 batch_size=nil fields=nil (0.7885ms)
MOPED: 127.0.0.1:27017 QUERY database=hotel_abadi_development collection=facilities selector={"$query"=>{"admin_id"=>"537f8ad9b4f2d7c124000001"}, "$orderby"=>{:_id=>1}} flags=[:slave_ok] limit=-1 skip=0 batch_size=nil fields=nil (0.8206ms)
Completed 500 Internal Server Error in 502.0ms
NoMethodError (undefined method `id' for #<ActiveSupport::HashWithIndifferentAccess:0xa6ae334>):
app/controllers/facilities_controller.rb:21:in `update'
Rendered /home/jesus/.rvm/gems/ruby-2.0.0-p353/gems/actionpack-3.2.15/lib/action_dispatch/middleware/templates/rescues/_trace.erb (2.5ms)
Rendered /home/jesus/.rvm/gems/ruby-2.0.0-p353/gems/actionpack-3.2.15/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb (2.4ms)
Rendered /home/jesus/.rvm/gems/ruby-2.0.0-p353/gems/actionpack-3.2.15/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (17.7ms)
In order to solve the problem, I have also tried to use fields_for with the nested attributes but I obtain the same error. The main files which defines the application are the next:
app/models/facilities.rb
class Facilities
...
field :description, type: String
field :language, type: Symbol, default: :es
has_many :characteristics, dependent: :destroy
accepts_nested_attributes_for :characteristics, allow_destroy: true
...
end
app/models/characteristic.rb
class Characteristic
...
field :title, type: String
field :description, type: String
field :language, type: Symbol, default: :es
belongs_to :admin
has_one :upload, dependent: :destroy
accepts_nested_attributes_for :upload, allow_destroy: true
...
end
app/controllers/facilities_controller.rb
class FacilitiesController < ApplicationController
load_and_authorize_resource
respond_to :json, :html
...
def update
#facilities.update_attributes!( params[:facilities] )
respond_with #facilities, api_template: :general, location: hotel_path
end
...
end
app/views/facilities.html.haml
= form_for facilities, url: facilities_path( facilities ) do |f|
= f.text_area :description
.facilities_form
- facilities.characteristics.each_with_index do |char, index|
= form_for characteristic, url: characteristic_path( characteristic ), html: { method: :put } do |d|
= d.hidden_field :id, name: 'facilities[characteristics][][id]'
= d.text_field :title, width: 20, size: 20, name: 'facilities[characteristics][][title]'
= d.text_area :description, width: 20, rows: 4, cols: 22, name: 'facilities[characteristics][][description]'
= f.submit "Send"
Solved:
In the nested attributes, I manually put the name of the fields because I am using another form_for for them. In that names, I use "facilities[characteristics][][name_of_field]", but when we need to use nested attributes, we have to put "characteristics_attributes", so the correct name is "facilities[characteristics_attributes][][name_of_field]".
I've spotted that you use facilities instead of facility both in view and controller.
I assume the problem is load_and_authorize_resource in FacilitiesController try to load #facility extracting id from params. So, change facilities_path( facilities ) to facilities_path(#facility) or something similar.

Translating custom error messages

I have a form (using simple_form) which I want to implement support for translated error messages. All my translations appear with the exception of the error message.
My Customer model is:
class Customer < ActiveRecord::Base
attr_accessible :name, :phone, :email, :contact_method
validates_presence_of :phone, :email, :contact_method, :message => I18n.t(:required)
end
My fr.yml file
fr:
name: 'Nom'
phone: 'Téléphone'
email: 'Courriel'
contact_method: 'Méthode de contact'
required: 'Requis'
My form is as follows:
= simple_form_for #customer do |f|
= f.input :name, label: t(:name)
= f.input :phone, label: t(:phone)
= f.input :email, label: t(:email)
Is there something I'm missing?
At first, you should use a Symbol with validates_presence_of. Don't translate it with I18n manually:
validates_presence_of :phone, :email, :contact_method, :message => :required
Secondly, add translation for your error message to your locale file like this:
activerecord:
errors:
models:
customer:
required: 'Requis'

sending specific data into a collection partial

I have a User class with a has_many :messages and a Message class which belongs_to :user. In the Message controller's show action has the corresponding view:
<% if #messages.any? %>
<ol class="microposts">
<%= render partial: 'shared/message', collection: #messages %>
</ol>
<% end %>
And the shared/_message.html.erb template looks like this:
<li id="<%= message.id %>">
<span class="content"><%= message.body %></span>
<% user_id = message.from %>
<% user = User.find(user_id) %>
From: <%= user.name %>
</li>
I feel like the following two lines should be done in the Messages controller from what I read in tutorials on Rails:
<% user_id = message.from %>
<% user = User.find(user_id) %>
But how would I pass each message's corresponding from value (which stores user.id) into the partial?
thanks,
mike
So far i have understood that you have a from column in your messages table which stores the user_id. So if you call message.from it returns the user_id. Then you can do the following
class Message < AR::Base
belongs_to :user, :foreign_key=>'from'
end
In your controller
#messages = Message.all.includes(:user)
#It will load messages and will associated users eager loaded.
Now in your view you can call message.user.name to get the user's name directly. You dont need these following two lines in your view at all
<% user_id = message.from %>
<% user = User.find(user_id) %>
Let me know if ive missed anything.
UPDATE:
Here is what Ive tried with
User Model
class User < ActiveRecord::Base
attr_accessible :age, :name
has_many :sent_messages, :class_name=>'Message', :foreign_key => 'from'
has_many :received_messages,:class_name=>'Message', :foreign_key => 'user_id'
end
#Contains Data:
#<User id: 1, name: "p1", age: 27, ... ...>,
#<User id: 2, name: "p2", age: 25, ... ...
Message Model
class Message < ActiveRecord::Base
attr_accessible :body, :sender, :title, :receipent
belongs_to :sender, :class_name => 'User', :foreign_key => 'from'
belongs_to :receipent, :class_name => 'User', :foreign_key => 'user_id'
end
Now say p1 is sending a message to p2
p1.sent_messages.create(title: "test title", body: "test body", receipent: p2)
#Result:
#<Message id: 1, title: "test title", body: "test body", user_id: 2, from: 1 ... ... >
Now for a message, you can get both sender and receipent directly like
#messages = Message.includes(:sender, :receipent) # You can load only sender or only receipent
#In View
message.sender.name
#OR
message.receipent.name

Rails 3 and has_many :through: automagically set/initialize attributes on join model

I deeply searched the web in order to find a clean and simple way to deal with attributes initialization on the join model of a has_many :through relation, but I did not find a best solution for my need.
In the exaple I provide below, I need to automatically set the attribute role of the Training join model when I create or update a Course object.
This is my model:
QUALIFICATIONS = ["Theoretical Instructor", "Practical Instructor"]
class Course < ActiveRecord::Base
has_many :trainings, dependent: :destroy
has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Theoretical Instructor" }
accepts_nested_attributes_for :theoretical_instructors
has_many :practical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Practical Instructor" }
accepts_nested_attributes_for :practical_instructors
end
class Trainer < ActiveRecord::Base
has_many :trainings, dependent: :destroy
has_many :courses, through: :trainings
end
class Training < ActiveRecord::Base
belongs_to :trainer
belongs_to :course
# Join model has the :role attribute, that I wish I could validate this way:
# validates :role, presence: true, inclusion: { in: QUALIFICATIONS }
end
The rationale behind this model is that I want to save Training objects in a single table. I don't want to create the TheoreticalInstructor and the PracticalInstructor join models (potentially exploding the number of tables) to solve this problem.
This view provides the form to submit a new Course:
<%= form_for #course do |course_form| %>
<%- # fields for course attributes, as usual... %>
<%= course_form.label :theoretical_instructor_ids %><br />
<%= course_form.select :theoretical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, { }, { multiple: true } %>
<%= course_form.label :practical_instructor_ids %><br />
<%= course_form.select :practical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, { }, { multiple: true } %>
<%= course_form.submit %>
<% end%>
The question is: what can I do in order to make #course = Course.new(params[:course]) the only line of code in the Course controller needed to save this association on submit of the previous form?
Differently from this question I don't want to create new Trainer objects when I create a new Course: I want to choose them from those already present in the DB (through a multiselect input field).
What I need is that something like #course.theoretical_instructor_ids = [1, 2] creates two Training objects with the role attribute set to Theoretical Instructor
I'm thinking on an after_initialize callback on Training that set role basing on the relation name (:theoretical_instructors and :practical_instructors), but I really don't know how to do it. Any advice? Am I missing some point?
Thank you guys!
EDIT 1 from oli-g
This question deals with a similar problem: the difference is that I don't want to build Trainer objects when I create a new Course, but I simply want to associate existing Trainer objects to a new Course.
EDIT 2 from oli-g
Basing on this (a 5 years old post) and this blog posts, I've changed the Course model in this way:
class Course < ActiveRecord::Base
has_many :trainings, dependent: :destroy
has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Theoretical Instructor"] do
def <<(theoretical_instructor)
Training.send(:with_scope, create: { role: "Theoretical Instructor" }) { self.concat theoretical_instructor }
end
end
accepts_nested_attributes_for :theoretical_instructors
has_many :practical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Practical Instructor"] do
def <<(practical_instructor)
Training.send(:with_scope, create: { role: "Practical Instructor" }) { self.concat practical_instructor }
end
end
accepts_nested_attributes_for :practical_instructors
end
This code enables me to do a thing like this
:001 > c = Course.first
=> #<Course id: 1>
:002 > t1 = Trainer.first
=> #<Trainer id: 1, name: "Tom">
:003 > c.theoretical_instructors << t1
=> #<Trainer id: 1, name: "Tom">
:004 > Training.all
=> [#<Training id: 1, role: "Theoretical Instructor", trainer_id: 1, course_id: 1>]
This is an acceptable workaround, even if in my controller I still can't do just #course = Course.new(params[:course]), but I have to create Training objects iterating on params[:course][:theoretical_instructor_ids] and params[:course][:practical_instructor_ids].
But I am curious, so the question remains open: what can I do in order to enable #course = Course.new(params[:course]) to build Training objects along with the Course?
Now... I think I discovered a bug in Rails:
:005 > c.practical_instructors
=> [] # correct
:006 > c.practical_instructor_ids
=> [] # obviously
:007 > c.reload
=> #<Course id: 1>
:008 > c.practical_instructor_ids
=> [1] # WRONG!!!
:009 > c.practical_instructors
=> [] # now it's correct...
:010 > c.practical_instructor_ids
=> [] # WTF!?
I think I will report this at github issues...
EDIT 3 by oli-g
Bug reported at github
Your issue is that you won't be able to add associations until after your record has been created. In this case, the Training associations are stored using the Course record id, and the Course id isn't defined until after the Course is saved for the first time. What you'll want to do is to use the after_create callback to call a function after the record has been created.
Add this to the end of your Course model:
# Use attr accessors to store the initial values so they won't conflict with the *_instructor_ids methods defined above
attr_accessor :create_theoretical_instructors
attr_accessor :create_practical_instructors
# This will call the create_training_records function after the record is created
after_create :create_training_records
private
def create_training_records
create_theoretical_instructors.each do |instructor_id|
self.theoretical_instructors << Instructor.find(instructor_id)
end
create_practical_instructors.each do |instructor_id|
self.practical_instructors << Instructor.find(instructor_id)
end
save!
end
And change the form in your view to use the new attr_accessors:
<%= course_form.label :create_theoretical_instructors %><br />
<%= course_form.select :create_theoretical_instructors, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, { }, { multiple: true } %>
<%= course_form.label :create_practical_instructors %><br />
<%= course_form.select :create_practical_instructors, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, { }, { multiple: true } %>
Now when you submit the form, it will write the instructor ids to the new Course instance variables; after the Course has been validated and saved, it will automatically create the new associations.

Suppress "base" in error text for custom validation of Rails nested attributes

I have the following models:
class Evaluation < ActiveRecord::Base
attr_accessible :product_id, :description, :evaluation_institutions_attributes
has_many :evaluation_institutions, :dependent => :destroy
accepts_nested_attributes_for :evaluation_institutions, :reject_if => lambda { |a| a[:token].blank? }, :allow_destroy => true
validate :requires_at_least_one_institution
private
def requires_at_least_one_institution
if evaluation_institution_ids.nil? || evaluation_institution_ids.length == 0
errors.add_to_base("Please select at least one institution")
end
end
end
class EvaluationInstitution < ActiveRecord::Base
attr_accessible :evaluation_institution_departments_attributes, :institution_id
belongs_to :evaluation
has_many :evaluation_institution_departments, :dependent => :destroy
accepts_nested_attributes_for :evaluation_institution_departments, :reject_if => lambda { |a| a[:department_id].blank? }, :allow_destroy => true
validate :requires_at_least_one_department
private
def requires_at_least_one_department
if evaluation_institution_departments.nil? || evaluation_institution_departments.length == 0
errors.add_to_base("Please select at least one department")
end
end
end
class EvaluationInstitutionDepartment < ActiveRecord::Base
belongs_to :evaluation_institution
belongs_to :department
end
I have a form for Evaluation that includes nested attributes for EvaluationInstitution and EvaluationInstitutionDepartment, so my form is nested to 3 levels. The 3rd level is giving me a problem.
The errors are triggered as expected, but when the error triggers for requires_at_least_one_department, the text reads
Evaluation institutions base Please
select at least one department
The message should read "Please select at least one department".
How do I remove "Evaluation institutions base"?
In Rails 3.2, if you take a look at the implementation of method full_message, you will see that it displays error messages through I18n with format "%{attribute} %{message}".
It means that you can customize the displayed format in your I18n locales as follows:
activerecord:
attributes:
evaluation_institutions:
base: ''
That would get rid of the prefix "Evaluation institutions base" as you wanted.
If anyone is looking for a solution for this that doesn't involve monkey patching, here's what I did in my errors partial. I simply look for "base" in the name of the attribute with the error and if it exists, I only post the message, otherwise I build the full_message. Now this won't work if you have attributes that have base in the name, but I don't so this works for me. It's a little hacky but so are the other solutions to this issue.
<% if object.errors.any? %>
<div id="error-explanation">
<div class="alert alert-error">
<ul>
<% object.errors.each do |atr, msg| %>
<li>
<% if atr.to_s.include? "base" %>
<%= msg %>
<% else %>
<%= object.errors.full_message(atr, msg) %>
<% end %>
</li>
<% end %>
</ul>
</div>
</div>
<% end %>
Adding the following monkey patch to initializers did the job for me in 3.2.3 with dynamic_form:
class ActiveModel::Errors
#exact copy of dynamic_form full_messages except 'attr_name = attr_name.sub(' base', ':')'
def full_messages
full_messages = []
each do |attribute, messages|
messages = Array.wrap(messages)
next if messages.empty?
if attribute == :base
messages.each {|m| full_messages << m }
else
attr_name = attribute.to_s.gsub('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
attr_name = attr_name.sub(' base', ':')
options = { :default => "%{attribute} %{message}", :attribute => attr_name }
messages.each do |m|
if m =~ /^\^/
options[:default] = "%{message}"
full_messages << I18n.t(:"errors.dynamic_format", options.merge(:message => m[1..-1]))
elsif m.is_a? Proc
options[:default] = "%{message}"
full_messages << I18n.t(:"errors.dynamic_format", options.merge(:message => m.call(#base)))
else
full_messages << I18n.t(:"errors.format", options.merge(:message => m))
end
end
end
end
full_messages
end
end
If you aren't using dynamic_form, try the following instead (unless your form gem overrides errors.full_messages like dynamic_form does):
class ActiveModel::Errors
#exact copy of Rails 3.2.3 full_message except 'attr_name = attr_name.sub(' base', ':')'
def full_message(attribute, message)
return message if attribute == :base
attr_name = attribute.to_s.gsub('.', '_').humanize
attr_name = #base.class.human_attribute_name(attribute, :default => attr_name)
attr_name = attr_name.sub(' base', ':')
I18n.t(:"errors.format", {
:default => "%{attribute} %{message}",
:attribute => attr_name,
:message => message
})
end
end
The only change to original code is the following line:
attr_name = attr_name.sub(' base', ':')
Suggestions welcome.
This is an older question, but this issue just bit me again in Rails 6, so posting my solution here, since this is the most relevant SO post that covered the issue.
Example: Saving a top level class: 'Parent' containing a collection of 'Child', where 'Child' has a custom validation method:
e.g.
class Parent < ActiveRecord::Base
has_many :children
accepts_nested_attributes_for :children, allow_destroy: true, reject_if: :all_blank
end
class Child < ActiveRecord::Base
belongs_to :parent
validate :custom_method
def custom_method
errors.add(:base, :error_symbol)
end
end
The following is needed:
Providing the locale entry for 'error_symbol'
Preventing the error for children rendering as 'Child Base ...'
Solution
First, add config.active_model.i18n_customize_full_message = true to your application.rb file.
Then, the following locale file works to override both the message and prevent 'Base' being prepended to the collection.
# config/locales/en.yml
en:
activerecord:
errors:
models:
parent/children:
format: 'Child: %{message}'
child:
error_symbol: "Error message goes here"
Interestingly, there seems to be some interaction here with accepts_nested_attributes_for, as I only was able to reproduce this issue when creating the parent and children with a single params object.
Should this not work for you, or you have a more complex issue, taking a look in ActiveModel/lib/active_model/errors.rb at the full_message method.
This should tell you whether:
The class in question is correctly picking up the i18n formatting for that class
What keys it is using to look in the locale files.