In Rails, what is the inverse of update_attributes? - ruby-on-rails

In Rails, what is the inverse of update_attributes! ?
In other words, what maps a record to an attributes hash that would recreate that record and all of it children records?
The answer is not ActiveRecord.attributes as that will not recurse into child object.
To clarify if you have the following:
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars
end
Then you can pass an hash like
{"name" => "a foo", "bars_attributes" => [{"name" => "a bar} ...]}
to update_attributes. But it's not clear how to easily generate such a hash programatically for this purpose.
EDIT:
As I have mentioned in a comment, I can do something like:
foo.as_json(:include => :bars)
but I wanted a solution that uses the accepts_nested_attributes_for :bars declaration to avoid having to explicitly include associations.

Not sure how that would be the "inverse", but while Rails might not "have the answer" per-see, there is nothing stopping you from traversing through an object and creating this VERY efficiently.
Something to get you started:
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for
You'll notice in the accepts_nested_attributes_for method, rails sets a hash for all models nested, in nested_attributes_options. So we can use that to get these nested associations, to populate this new hash.
def to_nested_hash
nested_hash = self.attributes.delete_if {|key, value| [:id, :created_at, :deleted_at].include? key.to_sym } # And any other attributes you don't want
associations = self.nested_attributes_options.keys
associations.each do |association|
key = "#{association}_attributes"
nested_hash[key] = []
self.send(association).find_each do |child|
nested_hash[key] << child.attributes.delete_if {|key, value| [:id, :created_at, :deleted_at].include? key.to_sym }
end
end
return nested_hash
end
OR just thought of this:
Using your example above:
foo.as_json(:include => foo.nested_attributes_options.keys)
One thing to note, this won't give you the bars_attributes where my first suggestions will. (neither will serializable_hash)

You can use the following method to include nested options in hash
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars
def to_nested_hash(options = nil)
options ||= {}
if options[:except]
incl = self.nested_attributes_options.keys.map(&:to_s) - Array(options[:except]).map(&:to_s)
else
incl = self.nested_attributes_options.keys
end
options = { :include => incl }.merge(options)
self.serializable_hash(options)
end
end
If for some situations you don't want bars, you can pass options
foo.to_nested_hash(:except => :bars)
Edit: Another option if you want same behaviour in as_json, to_json and to_xml
class Foo < ActiveRecord::Base
has_many :bars
accepts_nested_attributes_for :bars
def serializable_hash(options = nil)
options ||= {}
if options[:except]
incl = self.nested_attributes_options.keys.map(&:to_s) - Array(options[:except]).map(&:to_s)
else
incl = self.nested_attributes_options.keys
end
options = { :include => incl }.merge(options)
super(options)
end
def to_nested_hash(options = nil)
self.serializable_hash(options)
end
end

Related

How can I add a property to child association before render?

I have a teacher model which has_many students. When I render a student (as json) I want to strip out the teacher_id property and replace it with the name of the teacher in my representation.
What is the best way to achieve this?
Cheers,
Chris
Define a method called teacher_name.
class Student < ActiveRecord::Base
def teacher_name
self.teacher.name
end
end
Include the teacher_name method while invoking to_json:
teacher.to_json(:include => {:students => {:methods=> :teacher_name,
:except => :teacher_id
}
}
)
You can always redefine the to_json method on the model to do whatever you want:
class Student < ActiveRecord::Base
def to_json
self.attributes.merge(
'teacher_id' => self.teacher.name
).to_json
end
end
Update: Based on feedback from kandadaboggu , using a different approach:
class Student < ActiveRecord::Base
def teacher_name
self.teacher and self.teacher.name
end
def to_json(options = { })
super(
{
:methods => :teacher_name,
:except => :teacher_id)
}.merge(options)
)
end
end
Note that when using a brute-force merge like this the result is sometimes less than satisfactory, but will serve for the default case. If you specify :methods or :except options of your own, the defaults will be ignored.

validates_uniqueness_of in destroyed nested model rails

I have a Project model which accepts nested attributes for Task.
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
validates_uniqueness_of :name
end
Uniqueness validation in Task model gives problem while updating Project.
In edit of project i delete a task T1 and then add a new task with same name T1, uniqueness validation restricts the saving of Project.
params hash look something like
task_attributes => { {"id" =>
"1","name" => "T1", "_destroy" =>
"1"},{"name" => "T1"}}
Validation on task is done before destroying the old task. Hence validation fails.Any idea how to validate such that it doesn't consider task to be destroyed?
Andrew France created a patch in this thread, where the validation is done in memory.
class Author
has_many :books
# Could easily be made a validation-style class method of course
validate :validate_unique_books
def validate_unique_books
validate_uniqueness_of_in_memory(
books, [:title, :isbn], 'Duplicate book.')
end
end
module ActiveRecord
class Base
# Validate that the the objects in +collection+ are unique
# when compared against all their non-blank +attrs+. If not
# add +message+ to the base errors.
def validate_uniqueness_of_in_memory(collection, attrs, message)
hashes = collection.inject({}) do |hash, record|
key = attrs.map {|a| record.send(a).to_s }.join
if key.blank? || record.marked_for_destruction?
key = record.object_id
end
hash[key] = record unless hash[key]
hash
end
if collection.length > hashes.length
self.errors.add_to_base(message)
end
end
end
end
As I understand it, Reiner's approach about validating in memory would not be practical in my case, as I have a lot of "books", 500K and growing. That would be a big hit if you want to bring all into memory.
The solution I came up with is to:
Place the uniqueness condition in the database (which I've found is always a good idea, as in my experience Rails does not always do a good job here) by adding the following to your migration file in db/migrate/:
add_index :tasks [ :project_id, :name ], :unique => true
In the controller, place the save or update_attributes inside a transaction, and rescue the Database exception. E.g.,
def update
#project = Project.find(params[:id])
begin
transaction do
if #project.update_attributes(params[:project])
redirect_to(project_path(#project))
else
render(:action => :edit)
end
end
rescue
... we have an exception; make sure is a DB uniqueness violation
... go down params[:project] to see which item is the problem
... and add error to base
render( :action => :edit )
end
end
end
For Rails 4.0.1, this issue is marked as being fixed by this pull request, https://github.com/rails/rails/pull/10417
If you have a table with a unique field index, and you mark a record
for destruction, and you build a new record with the same value as the
unique field, then when you call save, a database level unique index
error will be thrown.
Personally this still doesn't work for me, so I don't think it's completely fixed yet.
Rainer Blessing's answer is good.
But it's better when we can mark which tasks are duplicated.
class Project < ActiveRecord::Base
has_many :tasks, inverse_of: :project
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
belongs_to :project
validates_each :name do |record, attr, value|
record.errors.add attr, :taken if record.project.tasks.map(&:name).count(value) > 1
end
end
Ref this
Why don't you use :scope
class Task < ActiveRecord::Base
validates_uniqueness_of :name, :scope=>'project_id'
end
this will create unique Task for each project.

Ruby on Rails: has_many through frustrations

I'm having a frustrating problem with a has_many through: namely the fact that the through models are not created until save. Unfortunately, I need to set data on these models prior to saving the parent.
Here's the loose setup:
class Wtf < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
class Bar < ActiveRecord::Base
has_many :wtfs
has_many :foos, :through => :wtfs
end
class Foo < ActiveRecord::Base
has_many :wtfs
has_many :bars, :through => :wtfs
def after_initialize
Bar.all.each do |bar|
bars << bar
end
end
end
Everything is fine except that I need to access the "wtf"'s prior to save:
f = Foo.new
=> #
f.bars
=> [list of bars]
empty list here
f.wtfs
=> []
f.save!
=> true
now I get stuff
f.wtfs
=> [list of stuff]
I even went so far as to explicitly create the wtfs doing this:
def after_initialize
Bar.all.each do |bar|
wtfs << Wtf.new( :foo => self, :bar => bar, :data_i_need_to_set => 10)
end
end
This causes the f.wtfs to be populated, but not the bars. When I save and retrieve, I get double the expected wtfs.
Anyone have any ideas?
I think you have the right idea with creating the Wtfs directly. I think it will turn out OK if you just set the bars at the same time:
def after_initialize
Bar.all.each do |bar|
wtfs << Wtf.new(:bar => bar, :data_i_need_to_set => 10) # Rails should auto-assign :foo => self
bars << bar
end
end
Rails should save the records correctly because they are the same collection of objects. The only drag might be that if rails doesn't have the smarts to check if a new Bar record in the bars collection already has a Wtf associated, it might create one anyway. Try it out.
Couldn't you write a before_save handler on Wtf that would set the data you need to set? It would have access to both the foo and bar, if needed.
You could set the method that populates bar to an after_create, like this:
class Foo < ActiveRecord::Base
has_many :wtfs
has_many :bars, :through => :wtfs
after_create :associate_bars
def associate_bars
Bar.all.each do |bar|
bars << bar
end
end
end
This would make the wtfs be already be created when this method is called.

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

How do I save a model with this dynamically generated field?

I have a rails model that looks something like this:
class Recipe < ActiveRecord::Base
has_many :ingredients
attr_accessor :ingredients_string
attr_accessible :title, :directions, :ingredients, :ingredients_string
before_save :set_ingredients
def ingredients_string
ingredients.join("\n")
end
private
def set_ingredients
self.ingredients.each { |x| x.destroy }
self.ingredients_string ||= false
if self.ingredients_string
self.ingredients_string.split("\n").each do |x|
ingredient = Ingredient.create(:ingredient_string => x)
self.ingredients << ingredient
end
end
end
end
The idea is that when I create the ingredient from the webpage, I pass in the ingredients_string and let the model sort it all out. Of course, if I am editing an ingredient I need to re-create that string. The bug is basically this: how do I inform the view of the ingredient_string (elegantly) and still check to see if the ingredient_string is defined in the set_ingredients method?
Using these two together are probably causing your issues. Both are trying to define an ingredients_string method that do different things
attr_accessor :ingredients_string
def ingredients_string
ingredients.join("\n")
end
Get rid of the attr_accessor, the before_save, set_ingredients method and define your own ingredients_string= method, something like this:
def ingredients_string=(ingredients)
ingredients.each { |x| x.destroy }
ingredients_string ||= false
if ingredients_string
ingredients_string.split("\n").each do |x|
ingredient = Ingredient.create(:ingredient_string => x)
self.ingredients << ingredient
end
end
end
Note I just borrowed your implementation of set_ingredients. There's probably a more elegant way to break up that string and create/delete Ingredient model associations as needed, but it's late and I can't think of it right now. :)
The previous answer is very good but it could do with a few changes.
def ingredients_string=(text)
ingredients.each { |x| x.destroy }
unless text.blank?
text.split("\n").each do |x|
ingredient = Ingredient.find_or_create_by_ingredient_string(:ingredient_string => x)
self.ingredients
I basically just modified Otto's answer:
class Recipe < ActiveRecord::Base
has_many :ingredients
attr_accessible :title, :directions, :ingredients, :ingredients_string
def ingredients_string=(ingredient_string)
ingredient_string ||= false
if ingredient_string
self.ingredients.each { |x| x.destroy }
unless ingredient_string.blank?
ingredient_string.split("\n").each do |x|
ingredient = Ingredient.create(:ingredient_string => x)
self.ingredients << ingredient
end
end
end
end
def ingredients_string
ingredients.join("\n")
end
end

Resources