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.
Related
I have a model that has several attributes that are provided at creation. The model also has some additional attributes that are derived from the provided attributes, which I also want to calculate at creation. More problematically, I want to be able to run validations on these derived values (since there are inputs that are valid on their own that lead to invalid derived values).
The problem is that when I do this:
class MyClass < ActiveRecord::Base
attr_accessible :given1, :given2, :derived
before_validation :derivation
validates_uniqueness_of :derived
def derivation
self.derived = self.given1 + self.given2
end
end
MyClass.new(:given1 => aNumber, :given2 => otherNumber)
I always get errors saying I can't add nil to nil. Apparently self.attribute is nil until farther into the validation & creation process.
Obviously I could set my derived values in a later stage, and add a custom validation that works on the given attributes, but that would entail doing the derivation twice, which wouldn't be very DRY.
Is there some other way to get at assigned but not yet validated attributes in the before_validates stage?
Edit: To clarify, I want to call MyClass.new(:given1 => aNumber, :given2 => otherNumber) and have the derived value calculated before the validations check, so that the validations check as if I had called MyClass.new(:given1 => aNumber, :given2 => otherNumber, :derived => aNumber + otherNumber). The problem is that I can't seem to access the passed-in values for :given1 and :given2 in a before_validations method.
I wrote my own snippet of code that looks like this:
class User < ActiveRecord::Base
attr_accessible :email, :first_name, :last_name
validates :email, uniqueness: true
before_validation :derivation
def derivation
self.email = self.first_name + self.last_name
end
end
Running the following yielded no errors:
» u = User.new first_name: "leo", last_name: "correa"
=> #<User:0x007ff62dd8ace0> {
:id => nil,
:first_name => "leo",
:last_name => "correa",
:email => nil,
:created_at => nil,
:updated_at => nil,
}
» u.valid?
User Exists (0.9ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = 'leocorrea' LIMIT 1
=> true
Running u.save saved the record successfully and upon repeating the User.new and saving that new record it returned with ROLLBACK because email was already used.
In any case, make sure you are assigning whatever variables you are using to the given1, given2 and whatever the result is make sure is not giving you false either because it will cancel the before_validate callback and the record won't save.
In Rails 2.3.6 I'm storing some serialized data in a database field.
My "feed_event.data" field in my database is stored as text and is (for example) equal to:
{:post=>{:pic=>"http://s3.amazonaws.com/criticalcity/datas/3524/big_thumb/send-a-letter.jpg", :name=>"Un’istruzione perfetta", :id=>1995, :authors=>"Delilah"}, :user=>{:pic=>"http://s3.amazonaws.com/criticalcity/avatars/537/thumb/DSCN2744.JPG", :name=>"Luci!", :id=>537}}
Now I need to output this field as a string (exactly as it is in the database), but when I ask:
puts feed_event.data
outputs:
postpichttp://s3.amazonaws.com/criticalcity/datas/3524/big_thumb/send-a-letter.jpgnameUn’istruzione perfettaid1995authorsDelilahuserpichttp://s3.amazonaws.com/criticalcity/avatars/537/thumb/DSCN2744.JPGnameLuci!
Why?
How can I output it as a yaml string?
UPDATE
In order to create it I have this in my FeedEvent model:
class FeedEvent < ActiveRecord::Base
has_many :user_feed_events, :dependent => :destroy
has_many :users, :through => :user_feed_events
serialize :data
end
And in order to create a new FeedEvent element I do:
feed = FeedEvent.create(:event_type => "comment #{commentable_type}", :type_id => id, :data => {:user => {:id => user.id, :name => user.name, :pic => user.avatar.url(:thumb)}, :comment => {:id => id, :body => body, :commentable_id => commentable_id, :commentable_type => :commentable_type, :commentable_name => commentable.name}})
UPDATE #2
following nzifnab's hint I used the .to_yaml method, but what Rails outputs in this case is:
data: "--- \n:post: \n :pic: http://s3.amazonaws.com/criticalcity/datas/3524/big_thumb/send-a-letter.jpg\n :authors: Delilah\n :name: \"Un\\xE2\\x80\\x99istruzione perfetta\"\n :id: 1995\n:user: \n :pic: http://s3.amazonaws.com/criticalcity/avatars/537/thumb/DSCN2744.JPG\n :name: Luci!\n :id: 537\n"
Also commenting "serialize :data" in the model outputs the same.
Thanks,
Augusto
When you call feed_data.data rails has automatically de-serialized your string. You could print it out like this:
feed_data.data.inspect to get the ruby hash representation as a string, but since it's already de-serialized it for you do you need to do anything else?
you can call everything on it like feed_data.data[:post][:pic]
I'm not sure what method you can use to grab the raw serialized string from the record, but usually you don't need to.
By default, serialization is made in a Hash.
Simply loop it to display it's content:
<% feed_event.data.each do |key, value| %>
<%= "#{key}: #{value}" %>
<% end %>
I'm just unsure about nesting level here but you've got the idea.
as you mentioned in your Update, the right way to do this is to put "serialize :data" in your model.
Then, you can access the data attribute as a Hash, that's the default, and it gets automatically persisted when you save your object.
Important Note:
One important thing for this to work is that you define the database field as text or string -- not as a binary field -- otherwise this will not work correctly!
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?
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.
I want to use find_or_create_by, but this statement does NOT work. It does not "find" or "create" with the other attributes.
productproperty = ProductProperty.find_or_create_by_product_id(:product_id => product.id, :property_id => property.id, :value => d[descname])
There seems to be very little, or no, information on the use of dynamic finders in Rails 3. "and"-ing these together gives me a an unknown method error.
UPDATE:
Originally I couldn't get the following to work. Please assume I'm not an idiot and "product" is an instance of Product AR model.
product.product_properties.find_or_create_by_property_id_and_value(:property_id => 1, :value => "X")
The error methods was:
no such keys: property_id, value
I couldn't figure that out. Only this morning did I find the reference to passing the values like this instead:
product.product_properties.find_or_create_by_property_id_and_value(1, "X")
And voilá, it works fine. I would have expected a hash to work in the same situation but I guess not.
So I guess you get a down vote if you miss something on the internet?
If you want to search by multiple attributes, you can use "and" to append them. For example:
productproperty = ProductProperty.find_or_create_by_product_id_and_property_id_and_value(:product_id => product.id, :property_id => property.id, :value => d[descname])
There is one minor catch to be aware of. It will always return the object you've specified, even if that object can't be saved due to validation errors. So make sure you check to see if the returned object has an id (or is_valid?). Don't assume its in the database.
Alternatively, you can use the 'bang' version of the method to raise an error if the object cannot be saved:
http://guides.rubyonrails.org/active_record_querying.html#find-or-create-by-bang
This applies to Rails 3.
See http://api.rubyonrails.org/classes/ActiveRecord/Base.html:
With single query parameter:
productproperty = ProductProperty.find_or_create_by_product_id(product.id) { |u| u.property_id => property_id, u.value => d[descname] } )
or extended with multiple parameters:
productproperty = ProductProperty.find_or_create_by_product_id(:product_id => product.id, :property_id => property_id, :value => d[descname]) { |u| u.property_id => property_id, u.value => d[descname] } )
Would work with:
conditions = { :product_id => product.id,
:property_id => property.id,
:value => d[descname] }
pp = ProductProperty.find(:first, :conditions => conditions) || ProductProperty.create(conditions)
In Rails 4, you can use find_or_create_by(attr1: 1, attr2: 2) to find or create by multiple attributes.
You can also do something like:
User.create_with(
password: 'secret',
password_confirmation: 'secret',
confirmation_date: DateTime.now
).find_or_create_by(
email: 'admin#domain.com',
admin: true
)
If you need to create the user with some attributes, but cannot search by those attributes.
You could also use where(...).first_or_create - ActiveRecord::Relation#first_or_create.
product_property_attrs = { product_id: product.id,
property_id: property.id,
value: d[descname] }
product_property = ProductProperty.where(product_property_attrs).first_or_create
I've found in Rails 3.1 you do not need to pass the attributes in as a hash. You just pass the values themselves.
ProductProperty.find_or_create_by_product_id_and_property_id_and_value(
product.id, property.id, d[descname])