using ruby 1.9.2 and rails 3, i would like to limit the fields returned when a record is accessed as json or xml (the only two formats allowed).
this very useful post introduced me to respond_with and i found somewhere online that a nice way to blanket allow/deny some fields is to override as_json or to_xml for the class and set :only or :except to limit fields.
example:
class Widget < ActiveRecord::Base
def as_json(options={})
super(:except => [:created_at, :updated_at])
end
def to_xml(options={})
super(:except => [:created_at, :updated_at])
end
end
class WidgetsController < ApplicationController
respond_to :json, :xml
def index
respond_with(#widgets = Widgets.all)
end
def show
respond_with(#widget = Widget.find(params[:id]))
end
end
this is exactly what i am looking for and works for json, but for xml "index" (GET /widgets.xml) it responds with an empty Widget array. if i remove the to_xml override i get the expected results. am i doing something wrong, and/or why does the Widgets.to_xml override affect the Array.to_xml result?
i can work around this by using
respond_with(#widgets = Widgets.all, :except => [:created_at, :updated_at])
but do not feel that is a very DRY method.
In your to_xml method, do the following:
def to_xml(options={})
options.merge!(:except => [:created_at, :updated_at])
super(options)
end
That should fix you up.
Related
I've got two models.
Class ModelA < ActiveRecord::Base
has_many :model_bs
end
Class ModelB < ActiveRecord::Base
belongs_to :model_a
def as_json(options = {})
{
:whatever => 'hello world'
}
end
end
When I call model_a.as_json(:include => :model_b), I want it to return a json which includes all model_bs, which it does, but employing my as_json redefinition, which it does not as it just uses the default one. Is there any way to use my own method rather than the original one? Thanks
In Rails 3, as_json method invokes serializable_hash to obtain the attributes hash. And they share the same 'options' parameter. In your case, overwritting serializable_hash would give the expected result.
def serializable_hash(options = {})
{:whatever => 'hello world'}
end
But, My suggestion is that instead of overwriting the convention, operate on the result of "super", which is like:
def serializable_hash(options = {})
hash = super
has[:name] = "hello world"
hash
end
I'm trying to implement the Railscast 340 that demos how to use DataTables, which looks like an awesome gem for my project.
My model is different, of course; but the datatables class the Mr Bates builds (very quickly), in order to do server-side processing, is rather complicated to follow. I got the source code, and basically attempted to follow along. My view comes up with zero records (but there are > 10,000 records), but does not break.
However, here is what the error message output from the rails server says just before it stops:
NameError (undefined local variable or method `genotypes' for #<GenotypesDatatable:0xa9e852c>):
app/datatables/genotypes_datatable.rb:12:in `as_json'
app/controllers/genotypes_controller.rb:8:in `block (2 levels) in index'
app/controllers/genotypes_controller.rb:6:in `index'
Just before this, there appears to be this JSON error, which starts:
Started GET "/genotypes.json?sEcho=1&iColumns=8&sColumns=&iDisplayStart=0&iDisplayLength=10&mDataProp_0=...
The relevant part of the genotypes controller looks like this:
def index
respond_to do |format|
format.html
format.json { render json: GenotypesDatatable.new(view_context) }
end
end
And my genotypes model looks like:
class Genotype < ActiveRecord::Base
attr_accessible :allele1, :allele2, :run_date
belongs_to :gmarkers
belongs_to :gsamples
end
My datatables class is given below. This is from Mr Bates code, modified (most likely incorrectly) to replace his Products model with my Genotypes model:
class GenotypesDatatable
delegate :params, :h, :link_to, to: :#view
def initialize(view)
#view = view
end
def as_json(options = {})
{
sEcho: params[:sEcho].to_i,
iTotalRecords: Genotype.count,
iTotalDisplayRecords: genotypes.total_entries,
aaData: data
}
end
private
def data
genotypes.map do |genotype|
[
link_to(genotype.name, genotype),
h(genotype.category),
h(genotype.released_on.strftime("%B %e, %Y")),
genotype.run_date
]
end
end
def Genotypes
#Genotypes ||= fetch_Genotypes
end
def fetch_genotypes
genotypes = Genotype.order("#{sort_column} #{sort_direction}")
genotypes = genotypes.page(page).per_page(per_page)
if params[:sSearch].present?
genotypes = genotypes.where("name like :search or category like :search", search: "%#{params[:sSearch]}%")
end
genotypes
end
def page
params[:iDisplayStart].to_i/per_page + 1
end
def per_page
params[:iDisplayLength].to_i > 0 ? params[:iDisplayLength].to_i : 10
end
def sort_column
columns = %w[gmarker gsample allele1 allele2 run_date]
columns[params[:iSortCol_0].to_i]
end
def sort_direction
params[:sSortDir_0] == "desc" ? "desc" : "asc"
end
end
Any hints on how to troubleshoot (or fix!) this error much appreciated! (Getting this working for my project would be awesome!)
TIA,
rixter
I'm not sure if this is it, but your class has a Genotype method with capital G, it should be all lowercase.
I'm using Rails 3.1 and Mongoid. What would be the proper way to enforce that a field of my model is saved to lowercase? I don't see this in the Mongoid documentation but I was wondering if there is a clean way I should know about. Thanks much.
Ok so I read the documentation more thoroughly, which I should have done initially. And this works for me now.
in the model.rb:
...
before_create :drop_the_case
protected
def drop_the_case
self.MYMODELFIELD = self.MYMODELFIELD.downcase
end
"drop_the_case" being my own arbitrary name for this.
Thanks.
In your model you can use
def before_save
self.your_model_field = your_model_field.downcase
end
or
def before_save
self.your_model_field.downcase!
end
Take a look at http://www.ruby-forum.com/topic/109091 This should work !!
The accepted answer with the before_create callback has some big issues, especially if you use certain constraints like validates_uniqueness_of. Use the before_validation callback instead when possible.
class Safe
include Mongoid::Document
field :foo, type: String
validates_uniqueness_of :foo
before_validation :drop_the_case
protected
def drop_the_case
self.foo = self.foo.downcase
end
end
class Dangerous
include Mongoid::Document
field :foo, type: String
validates_uniqueness_of :foo
before_create :drop_the_case
protected
def drop_the_case
self.foo = self.foo.downcase
end
end
dangerous = Dangerous.create!(name: 'BAR')
safe = Safe.create!(name: 'BAR')
dangerous.update(name: 'BAR') # dangerous.name => BAR
safe.update(name: 'BAR') # safe.name => bar
Dangerous.create!(name: 'BAR') # => true, unique constraint ignored
Safe.create!(name: 'BAR') # throws exception
I'm trying to find an elegant (standard) way to pass the parent of a polymorphic model on to the view. For example:
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true
end
class Employee < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
class Product < ActiveRecord::Base
has_many :pictures, :as => :imageable
end
The following way (find_imageable) works, but it seems "hackish".
#PictureController (updated to include full listing)
class PictureController < ApplicationController
#/employees/:id/picture/new
#/products/:id/picture/new
def new
#picture = imageable.pictures.new
respond_with [imageable, #picture]
end
private
def imageable
#imageable ||= find_imageable
end
def find_imageable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end
end
Is there a better way?
EDIT
I'm doing a new action. The path takes the form of parent_model/:id/picture/new and params include the parent id (employee_id or product_id).
I'm not sure exactly what you're trying to do but if you're trying to find the object that 'owns' the picture you should be able to use the imageable_type field to get the class name. You don't even need a helper method for this, just
def show
#picture = Picture.find(params[:id])
#parent = #picture.imagable
#=> so on and so forth
end
Update
For an index action you could do
def index
#pictures = Picture.includes(:imagable).all
end
That will instantiate all 'imagables' for you.
Update II: The Wrath of Poly
For your new method you could just pass the id to your constructor, but if you want to instantiate the parent you could get it from the url like
def parent
#parent ||= %w(employee product).find {|p| request.path.split('/').include? p }
end
def parent_class
parent.classify.constantize
end
def imageable
#imageable ||= parent_class.find(params["#{parent}_id"])
end
You could of course define a constant in your controller that contained the possible parents and use that instead of listing them in the method explicitly. Using the request path object feels a little more 'Rails-y' to me.
I just ran into this same problem.
The way I 'sort of' solved it is defining a find_parent method in each model with polymorphic associations.
class Polymorphic1 < ActiveRecord::Base
belongs_to :parent1, :polymorphic => true
def find_parent
self.parent1
end
end
class Polymorphic2 < ActiveRecord::Base
belongs_to :parent2, :polymorphic => true
def find_parent
self.parent2
end
end
Unfortunately, I can not think of a better way. Hope this helps a bit for you.
This is the way I did it for multiple nested resources, where the last param is the polymorphic model we are dealing with: (only slightly different from your own)
def find_noteable
#possibilities = []
params.each do |name, value|
if name =~ /(.+)_id$/
#possibilities.push $1.classify.constantize.find(value)
end
end
return #possibilities.last
end
Then in the view, something like this:
<% # Don't think this was needed: #possibilities << picture %>
<%= link_to polymorphic_path(#possibilities.map {|p| p}) do %>
The reason for returning the last of that array is to allow finding the child/poly records in question i.e. #employee.pictures or #product.pictures
class ArticlesController < ApplicationController
def index
#articles = Article.by_popularity
if params[:category] == 'popular'
#articles = #articles.by_popularity
end
if params[:category] == 'recent'
#articles = #articles.by_recent
end
if params[:category] == 'local'
index_by_local and return
end
if params[:genre]
index_by_genre and return
end
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => #articles }
end
end
def index_by_local
# 10 lines of code here
render :template => 'articles/index_by_local'
end
def index_by_genre
# ANOTHER 10 lines of code here
render :template => 'articles/index_by_genre'
end
end
As you can see from above. My controller is not exactly thin. What its doing is, depending on the params that were passed, it interacts with the model to filter out records.
And if params[:local] or params[:genre] was passed. Then it calls its own methods respectively (def index_by_local and def index_by_genre) to do further processing. These methods also load their own template, instead of index.html.erb.
Is this quite typical for a controller to look? Or should I be refactoring this somehow?
We can move the first few lines into the model(article.rb):
def get_by_category(category)
# Return articles based on the category.
end
In this way we can completely test the article fetching logic using unit tests.
In general move all the code related to fetching records inside model.
Controllers in general
should authorize the user
get records using the params and assign them to instance variables
[These must typically be function
calls to model]
Render or redirect
I would define scopes for each of the collections you want to use.
class Article < ActiveRecord::Base
...
scope :popular, where("articles.popular = ?", true) # or whatever you need
scope :recent, where(...)
scope :by_genre, where(...)
scope :local, where(...)
...
def self.filtered(filter)
case filter
when 'popular'
Article.popular, 'articles/index'
when 'recent'
Article.recent, 'articles/index'
when 'genre'
Article.by_genre, 'articles/index_by_genre'
when 'local'
Article.local, 'articles/index_by_local'
else
raise "Unknown Filter"
end
end
end
Then in your controller action, something like this:
def index
#articles, template = Article.filtered(params[:category] || params[:genre])
respond_to do |format|
format.html { render :template => template }
format.xml { render :xml => #articles }
end
end