How can I add a property to child association before render? - ruby-on-rails

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.

Related

In Rails, what is the inverse of update_attributes?

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

How to make one Rails 3 route work with 2 controllers depending on database data?

I am developing a Rails 3 app on Heroku, and here is the situation:
There are two models: User and App. Both have "slugs" and can be accessed via the same url:
/slug
Example:
/myuser => 'users#show'
/myapp => 'apps#show'
What is the best practice to handle this? What clean solution should I implement?
The same logic you can see on AngelList. For example, my personal profile is http://angel.co/martynasjocius, and my app can be found at http://angel.co/metricious.
Thank you!
I would think about introducing a third model, ill call it Lookup for the example, but you will probably want to find a better name. I will also assume your User and App models also define a name field.
class Lookup < ActiveRecord::Base
belongs_to :owner, :polymorphic => true
validates_uniqueness_of :name
end
class User < Active::Record::Base
has_a :lookup, :as => :owner, :validate => true
before_create :create_lookup_record
def create_lookup_record
build_lookup(:name => name)
end
end
class App < Active::Record::Base
has_a :lookup, :as => :owner, :validate => true
before_create :create_lookup_record
def create_lookup_record
build_lookup(:name => name)
end
end
LookupsController < ApplicationController
def show
#lookup = Lookup.find_by_name(params[:id])
render :action => "#{#lookup.owner.class.name.pluralize}/show"
end
end
# routes.rb
resources :lookups
I hope this idea helps, sorry if its no use :)
Try this (substituting action for your actions, like show, edit, etc):
class SlugsController < ApplicationController
def action
#object = Slug.find_by_name(params[:slug]).object # or something
self.send :"#{#object.class.to_s.downcase}_action"
end
protected
def app_action
# whatever
end
def user_action
# something
end
end
Separate stuff out into modules as you see fit. You can have objects of each class get their own actions.

How do you concatenate two active record results to return a new result that can further be filtered?

Imagine the scenario...
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
end
#models/account.rb
class Account < ActiveRecord::Base
belongs_to :user
def public_accounts
Account.all :conditions => { public => true }
end
end
Now imagine I want to concatenate User(:id).accounts with Account.public_accounts to show a list of all accounts available to a user.
So you'd think I'd be able to update the User model to look like this.
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
def all_accounts
self.accounts + Account.public
end
end
However, now I won't be able to use the all() method since it's no longer of that type of object.
In the controller I'd like to do this...
#controllers/accounts_controller.rb
def search_all
User.find(params[:user_id]).all_accounts.all(
:offset => params[:offset],
:limit => params[:limit]
)
end
Thoughts?
Update #1:
Scope's won't work for my scenario. I simplified my scenario to try and get my point across. As stated I need a way to combine two active record results and retain the ability to further filter them in my controller.
So the question is, "Why?" The reason is, I am trying to combine two sets of records to form a complete collection and one of the collections is not associated with the user at all.
I have refactored the above scenario to try and show a more precise example without getting overly complicated.
This might be a good scenario to use scopes.
You can define active and inactive scopes in the Account model and then use the following:
User.accounts
User.accounts.active
User.accounts.inactive
You can even chain scopes together, so you could do something like:
User.accounts.active.paid_up
jklina's answer is correct, it's best to use scopes in this situation. Scopes provide a sugary syntax and are more readable. I'll elaborate on the setup:
class User < AR::Base
has_many :accounts
end
class Account < AR::Base
belongs_to :user
scope :active, where(:active => true)
scope :inactive, where(:active => false)
end
You would then access the account scopes as jklina showed: User.find(1).accounts.active, etc.
Accessing all of a user's accounts like: User.find(1).accounts.
UPDATE:
I fixed some mistakes and added more below.
Based on the updates to your question, I think you need to make the public method a method on the class:
class Accounts < AR::Base
...
# This is essentially a scope anyways
def self.public
where(:public => true)
end
end
class User < AR::Base
...
# This should return all the users accounts
# and any public accounts
def all_accounts
Account.where("user_id = ? OR public is true", self.id)
end
end
Lets look at the return values in the chain:
User.find(params[:user_id]) # returns an instance of User class
User.find(params[:user_id]).all_accounts # returns an array
The Array class doesn't have an instance method called all that's why you are seeing this error. This is not a bug.
Why don't you try this:
class User
has_many :accounts, :conditions => { :active => 1 }
has_many :all_accounts :conditions => ["(active = ? OR public = ?)",
true, true]
end
Now you can:
User.find(params[:user_id]).all_accounts.all(:limit => 10, :offset => 2)
You're try to access two distinct tables and apply LIMIT/OFFSET to them as a combined union. That aint gonna happen unless you logically combine them at the SQL layer, not at the ActiveRecord layer.
Sounds like writing out the SQL, maybe using a UNION and then using find_by_sql might be your best best.

Rails 3: Escape characters (\) appearing in part of JSON string

Anyone know why some of my json elements are being backslash(\) escaped while others are not?
{"first":"John","last":"Smith","dogs":"[{\"name\":\"Rex\",\"breed\":\"Lab\"},{\"name\":\"Spot\",\"breed\":\"Dalmation\"},{\"name\":\"Fido\",\"breed\":\"Terrier\"}]"}
Ideally I'd like NONE of them to be escaped...
This was generated by overriding as_json in two models. Person has_many Dogs.
#models/person.rb
class Person < ActiveRecord::Base
has_many :dogs
def as_json(options={})
{
:first => first,
:last => last,
:dogs => dogs.to_json
}
end
end
#models/dog.rb
class Dog < ActiveRecord::Base
belongs_to :people
def as_json(options={})
{
:name => name,
:breed => breed
}
end
end
Check out jonathanjulian.com's Rails to_json or as_json?
Try removing the to_json on dogs.to_json.

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

Resources