How can i interpolate ivars into Rails i18n strings during validation? - ruby-on-rails

Suppose I have a model class like this:
class Shoebox < ActiveRecord::Base
validates_inclusion_of :description, :in => ["small", "medium"],
:message => I18n.t("activerecord.errors.models.shoebox.with_name",
:name => name)
end
And some yaml:
en:
activerecord:
errors:
models:
shoebox:
with_name: "the description of %{name} is not in the approved list"
And I create a new Shoebox:
s = Shoebox.new(:description => "large", :name => "Bob")
s.valid?
But when I look at the error (s.errors.first.message), I see:
"the description of Shoebox is not in
the approved list"
and not:
"the description of Bob is not in the
approved list"
I've tried :name => name, :name => :name, :name => lambda{name}, :name => lambda{:name}.
I've tried creating a helper method
def shoebox_name
name
end
And passing :name => shoebox_name, :name => :shoebox_name, :name => lambda{shoebox_name} and :name => lambda {:shoebox_name}.
How can I get the ivar value for name to be interpolated into the string?

Try removing the message option in the validation, and change your yaml to be:
en:
activerecord:
errors:
models:
shoebox:
description:
inclusion: "the description of %{name} is not in the approved list"
See http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models for more details

You can use a custom validation method to achieve what you are trying to do. All the columns are available in the custom validator:
validate :description_in
def description_in
if !(["small", "medium"].include?(description))
errors.add(:base, "The description of #{name} is not in the approved list")
end
end
PS: After a lot of googling around, I realized that it was much easier to implement a custom validator than search around.

Related

CSV Upload in rails

I am trying to add parents and their children data in the parent and child table. I have existing data in these tables and I am trying to add further data and I don't want the data to be repeated. Below is the code I am using to upload data. The child has parent_id.
parent.rb
has_many :children, dependent: :destroy
def self.import(file)
CSV.foreach(file.path, headers:true) do |row|
parent = Parent.find_or_update_or_create_by(
parent_1_firstname: row['parent_1_firstname'],
parent_1_lastname: row['parent_1_lastname'],
address: row['address'],
address_line_2: row['address_line_2'],
city: row['city'],
province: row['province'],
postal_code: row['postal_code'],
telephone_number: row['telephone_number'],
email: row['email'],
family_situation: row['admin_notes'],
gross_income: row['gross_income'],
created_by_admin: row['created_by_admin'],
status: row['status']
)
parent.children.find_or_create_by(
firstname: row['firstname'],
lastname: row['lastname'],
dateofbirth: row['dateofbirth'],
gender: row['gender']
)
end
end
child.rb
belongs_to :parent
The error I am facing is when I choose the csv file to be uploaded below is the error which I am getting.
undefined method `find_or_update_or_create_by' for #<Class:0x00007f8797be74b0> Did you mean? find_or_create_by
I have added a sample csv below. Please help me figure out the issue.
parent_1_firstname,parent_1_lastname,address,address_line_2,city,province,postal_code,telephone_number,email,admin_notes,gross_income, created_by_admin ,status,firstname,lastname,dateofbirth,gender
Nav,Deo,College Road,,Alliston,BC,N4c 6u9,500 000 0000,nav#prw.com,"HAPPY",13917, TRUE , Approved ,Sami,Kidane,2009-10-10,Male
undefined method `find_or_update_or_create_by' for
Class:0x00007f8797be74b0 Did you mean? find_or_create_by
AFAIK, there is no find_or_update_or_create_by method in Rails. Unless you have defined it as a class method in the Parent model, you can't call that method on a class. I believe you meant to use find_or_create_by. Change
Parent.find_or_update_or_create_by
to
Parent.find_or_create_by
Update:
You cannot call create unless the parent is saved
Ok, so the parent isn't saved which could be due to any validations has failed. Change Parent.find_or_create_by to Parent.find_or_create_by!(as #jvillian stated) which will raise an exception with the validation error message. Fix the error and you are good to go.
To not have to hard-code various nested loops doing find_or_create_by logic, there is a gem called DutyFree that makes imports and exports like this fairly painless. It intelligently analyses the has_many and belongs_to associations on models and based on these relationships identifies how to properly save each imported row across multiple destination tables. Either a create or an update is performed based on if the data already exists or not.
To demonstrate your example from above, I wrote an RSpec test based on the CSV data you provided:
https://github.com/lorint/duty_free/blob/master/spec/models/parent_complex_spec.rb
There is also a simpler example available with just 6 columns:
https://github.com/lorint/duty_free/blob/master/spec/models/parent_simple_spec.rb
One nice thing about this gem is that after configuring the column definitions to do an import, you get export for free because everything works from the same template. For this example here's the template which allows the column names from your CSV to line up perfectly with the database columns:
IMPORT_TEMPLATE = {
uniques: [:firstname, :children_firstname],
required: [],
all: [:firstname, :lastname, :address, :address_line_2, :city, :province, :postal_code,
:telephone_number, :email, :admin_notes, :gross_income, :created_by_admin, :status,
{ children: [:firstname, :lastname, :dateofbirth, :gender] }],
as: {
'parent_1_firstname' => 'Firstname',
'parent_1_lastname' => 'Lastname',
'address' => 'Address',
'address_line_2' => 'Address Line 2',
'city' => 'City',
'province' => 'Province',
'postal_code' => 'Postal Code',
'telephone_number' => 'Telephone Number',
'email' => 'Email',
'admin_notes' => 'Admin Notes',
'gross_income' => 'Gross Income',
'created_by_admin' => 'Created By Admin',
'status' => 'Status',
'firstname' => 'Children Firstname',
'lastname' => 'Children Lastname',
'dateofbirth' => 'Children Dateofbirth',
'gender' => 'Children Gender'
}
}.freeze
With this in your parent.rb, you can call Parent.df_import(your_csv_object) or Parent.df_export, and the gem does the rest.

Rails 4 enum validation

This is the first time I'm using enums with rails 4 and I ran into some issues, have couple of dirty solutions in mind and wanted to check are there any more elegant solutions in place :
This is my table migration relevant part:
create_table :shippings do |t|
t.column :status, :integer, default: 0
end
My model:
class Shipping < ActiveRecord::Base
enum status: { initial_status: 0, frozen: 1, processed: 2 }
end
And I have this bit in my view (using simple form for) :
= f.input :status, :as => :select, :collection => Shipping.statuses, :required => true, :prompt => 'Please select', label: false
So in my controller:
def create
#shipping = Shipping.create!(shipping_params)
if #shipping.new_record?
return render 'new'
end
flash[:success] = 'Shipping saved successfully'
redirect_to home_path
end
private
def shipping_params
params.require(:shipping).permit(... :status)
end
So when I submit create form and the create action fire I get this validation error :
'1' is not a valid status
So I thought I knew that the issue was data type so I added this bit in the model :
before_validation :set_status_type
def set_status_type
self.status = status.to_i
end
But this didn't seem to do anything, how do I resolve this ? Has anyone had the similar experience?
You can find the solution here.
Basically, you need to pass the string ('initial_status', 'frozen' or 'processed'), not the integer. In other words, your form needs to look like this:
<select ...><option value="frozen">frozen</option>...</select>
You can achieve this by doing statuses.keys in your form. Also (I believe) you don't need the before_validation.
Optionally, you could add a validation like this:
validates_inclusion_of :status, in: Shipping.statuses.keys
However, I'm not sure that this validation makes sense, since trying to assign an invalid value to status raises an ArgumentError (see this).

undefined method `text?' for nil:NilClass Validate uniqness rails3 ruby 187

I just updated from rails 2.3 to 3, i'm trying to replace this old method with something cleaner, because it's outputting the model and field name, wtf!
However I get the above error when calling validates_uniqueness_of (the presence works fine). I passed in the primary id scope, and still get it. Any help is welcome.
def validate
if org_name.blank?
errors.add(:org_name, :blank, :default => nil)
else
if (org = Organization.find_by_org_name(org_name)) && org != self
errors.add(:org_name, :taken, :default => nil, :value => org_name)
end
end
end
to
validates :org_name, :presence => true
validates_uniqueness_of :org_name, :scope => :org_id
Ths is the Rails 3 syntax for uniqueness validtion:
validates :org_name, uniqueness: {scope: :org_id}
This is easy to fix.
Firstly, analyse the error message:
Org name translation missing:
en.activerecord.errors.models.user.attributes.org_name.blank
This is caused by the following line of code:
errors.add(:org_name, :blank, :default => nil)
When you call the above, you are telling rails to look for a translation whose key is :blank. You probably didn't set that up yet, so to do that, just go into your locales file (config/locales/en.yml), and add the following:
en:
hello: "Hello world"
activerecord:
errors:
models:
organization:
attributes:
org_name:
blank: "can't be blank."
Hopefully that will fix it for you.

Rails 3 custom formatted validation errors?

With this model:
validates_presence_of :email, :message => "We need your email address"
as a rather contrived example. The error comes out as:
Email We need your email address
How can I provide the format myself?
I looked at the source code of ActiveModel::Errors#full_messages and it does this:
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)
options = { :default => "%{attribute} %{message}", :attribute => attr_name }
messages.each do |m|
full_messages << I18n.t(:"errors.format", options.merge(:message => m))
end
end
end
full_messages
end
Notice the :default format string in the options? So I tried:
validates_presence_of :email, :message => "We need your email address", :default => "something"
But then the error message actually appears as:
Email something
So then I tried including the interpolation string %{message}, thus overriding the %{attribute} %{message} version Rails uses by default. This causes an Exception:
I18n::MissingInterpolationArgument in SubscriptionsController#create
missing interpolation argument in "%{message}" ({:model=>"Subscription", :attribute=>"Email", :value=>""} given
Yet if I use the interpolation string %{attribute}, it doesn't error, it just spits out the humanized attribute name twice.
Anyone got any experience with this? I could always have the attribute name first, but quite often we need some other string (marketing guys always make things more complicated!).
Errors on :base are not specific to any attribute, so the humanized attribute name is not appended to the message. This allows us to add error messages about email, but not attach them to the email attribute, and get the intended result:
class User < ActiveRecord::Base
validate :email_with_custom_message
...
private
def email_with_custom_message
errors.add(:base, "We need your email address") if
email.blank?
end
end
Using internationalization for this is probably your best bet. Take a look at
http://guides.rubyonrails.org/i18n.html#translations-for-active-record-models
Particularly this section:
5.1.2 Error Message Interpolation
The translated model name, translated
attribute name, and value are always
available for interpolation.
So, for example, instead of the
default error message "can not be
blank" you could use the attribute
name like this : "Please fill in your
%{attribute}"

Rails 3 - Seed.rb data for Money Class

I am trying out seeds.rb for the first time, and one of my data models uses encapsulation provided by the money gem.
Relevant gems:
money (3.6.1)
rails (3.0.5)
My model thus far:
app/models/list.rb
class List < ActiveRecord::Base
attr_accessible :alias, :unit, :participating_manufacturer, :quantity
:latest_price_cents, :latest_price_currency, :url
belongs_to :user
composed_of :latest_price,
:class_name => "Money",
:mapping => [%w(latest_price_cents latest_price_cents), %w(latest_price_currency currency_as_string)],
:constructor => Proc.new {
|latest_price_cents, latest_price_currency| Money.new(latest_price_cents ||
0, latest_price_currency || Money.default_currency)
},
:converter => Proc.new {
|value| value.respond_to?(:to_money) ? value.to_money : raise(ArgumentError,
"Can't convert #{value.class} to Money")
}
end
1) (Addressed successfully)
2) When I get to writing validations, would it be best to write them for the :latest_price attribute or for the :latest_price_cents & :latest_price_currency attributes seperately?
/db/seeds.rb
users = User.create([{ :name => "Foo", :email => "foo#gmail.com",
:password => "foobar", :password_confirmation => "foobar" }])
# etc, will add more users to the array
list = List.create(:user_id => users.first.id, :alias => "Januvia 100mg",
:unit => "tablet", :participating_manufacturer => "Merck",
:quantity => 30, :latest_price_cents => 7500,
:latest_price_currency => "USD", :url =>
"http://www.foobar.com/januvia/100mg-tablets/")
3) Perhaps it is minutiae, but in the seed, should I be assigning values to the virtual :latest_price attribute or to the latest_price_cents and latest_price_currency attributes directly? Is there any way to use faker rather than /db/seeds.rb to perform this task?
I am new to rails and web development.
I can't see your latest_price attribute anywhere, so I'm not sure how to answer your question. Generally, you should validate the attributes entered in the user form. So if a user enters latest_price_cents and latest_price_currency in a form, then they're the ones which need validating.
There's a bug in your seed file. You want to pass in a hash, not an array, when creating a new user; and users should be an array.
users = []
users << User.create!(:name => "Foo",
:email => "foo#gmail.com",
:password => "foobar",)
:password_confirmation => "foobar")
However, if you're considering faker because you want to create some dummy data, take a look at Machinist or Factory Girl. They're designed for creating dummy data, normally for automated tests.
Once you've set up some blueprints, if you want to create dummy data in your seeds file, you can do something like this in seeds.rb:
20.times { List.make } unless Rails.env.production?

Resources