I have a model with data that should never be included when it is rendered as json. So I implemented the class' as_json method to behave appropriately. The problem is when other models with associations with this model render json, my custom as_json is not being called.
class Owner < ActiveRecord::Base
has_one :dog
def as_json(options={})
puts "Owner::as_json"
super(options)
end
end
class Dog < ActiveRecord::Base
belongs_to :owner
def as_json(options={})
puts "Dog::as_json"
options[:except] = :secret
super(options)
end
end
Loading development environment (Rails 3.0.3)
ruby-1.9.2-p136 :001 > d = Dog.first
=> #<Dog id: 1, owner_id: 1, name: "Scooby", secret: "I enjoy crapping everwhere">
ruby-1.9.2-p136 :002 > d.as_json
Dog::as_json
=> {"dog"=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1}}
ruby-1.9.2-p136 :004 > d.owner.as_json(:include => :dog)
Owner::as_json
=> {"owner"=>{"id"=>1, "name"=>"Shaggy", :dog=>{"id"=>1, "name"=>"Scooby", "owner_id"=>1, "secret"=>"I enjoy crapping everwhere"}}}
Thanks for the help
This is a known bug in Rails. (The issue is marked closed due to the migration to Github issues from the previous bug tracker, but it's still a problem as of Rails 3.1.)
As acknowledged above, this is an issue with the Rails base. The rails patch here is not yet applied and seems at least slightly controversial, so I'm hesitant to apply it locally. Even if applied as a monkey patch it could potentially complicate future rails upgrades.
I'm still considering RABL suggested above, it looks useful. For the moment, I'd rather not add another view templating language into my app. My current needs are very small.
So here's a workaround which doesn't require a patch and work for most simple cases. This works where the association's as_json method you'd like to have called looks like
def as_json(options={})
super( <... custom options ...> )
end
In my case I've got Schedule model which has many Events
class Event < ActiveRecord::Base
# define json options as constant, or you could return them from a method
EVENT_JSON_OPTS = { :include => { :locations => { :only => [:id], :methods => [:name] } } }
def as_json(options={})
super(EVENT_JSON_OPTS)
end
end
class Schedule < ActiveRecord::Base
has_many :events
def as_json(options={})
super(:include => { :events => { Event::EVENT_JSON_OPTS } })
end
end
If you followed the guideline that anytime you :include an association in your as_json() methods, you define any options you need as a constant in the model to be referenced, this would work for arbitrary levels of associations. NOTE I only needed the first level of association customized in the above example.
I've found that serializable_hash works just as you'd expect as_json to work, and is always called:
def serializable_hash(options = {})
result = super(options)
result[:url] = "http://.."
result
end
I ran into the same issue. I wanted this to work:
render :json => #favorites.as_json(:include => :location)
But it didn't so I ended up adding an index.json.erb with the following:
<% favs = #favorites.as_json.each do |fav| %>
<% fav["location"] = Location.find(fav["location_id"]).as_json %>
<% end %>
<%= favs.to_json.html_safe %>
Not a fix - just a work around. I imagine you did the same thing.
Update #John pointed out this is a known bug in Rails. A patch to fix it appears to be: at https://github.com/rails/rails/pull/2200. Nevertheless, you might try RABL, because its sweet.
I've always been frustrated with passing a complex set of options to create the JSON views I want. Your problem, which I experienced with Mongoid in Rails 3.0.9, prompted me to write JSON templates. But actually, if you're dealing with relations or custom api properties, it turns out that templates are way nicer.
Besides, dealing with different outputs seems like the View layer to me, so I settled on using RABL, the API templating language. It makes it super easy to build valid JSON and include any associations or fields.
Not a solution to the problem, but a better solution for the use case.
This was reported as a bug: http://ternarylabs.com/2010/09/07/migrating-to-rails-3-0-gotchas-as_json-bug/
Related
I have been working on an App called CtrlPanel for the company I work for.
This app was originally running on Ruby v2.2.2 and rails v4.2.1. I could not get that environment to work on ANYTHING; I tried both PC and Linux. Since I couldn't get that environment running and since it needed to be updated to the newest version anyway I figured I would just get it working on the latest version.
I had no idea what I was in for, that was a little over a month ago. I am happy to report I now have everything in the program working with one exception. There is a catalog that displays all of the items and it uses a scope in the model with a lambda expression that is rather complicated (at least to me). I have had to update the syntax ALL over this application due to the older version of Rails and now being on the newest version and this is the only one I can't seem to figure out. I am pretty sure again that it is just a Syntax problem from Rails v4.2.1 to Rails v6.1.3.1 but I just can't seem to figure it out and I am sure people who are more experienced than myself will know what it is.
Here is the model in question:
category.rb
# == Schema Information
#
# Table name: categories
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
class Category < ActiveRecord::Base
attr_accessible :name
# accepts_nested_attributes_for :product_categories
has_many :products, through: :product_categories
has_many :product_categories, dependent: :destroy
default_scope { order("id") }
scope :with_published_products, -> { joins{product_categories.product}.where{products.status.eq "published"}.uniq }
scope :with_matched_search_terms, ->(search_terms) { joins{product_categories.product}.where{products.name.like search_terms}.uniq }
end
The line with the issue is:
scope :with_published_products, -> { joins{product_categories.product}.where{products.status.eq "published"}.uniq }
It is giving the error:
The method .joins() must contain arguments
I had other joins statements that I had to fix (numerous). It was due to syntax for a very BASIC example from joins{arguments} to joins(arguments). I assume that is the same case here. In this case there are many more elements and that lambda expression thrown into the mix which is making it much harder (for me at least) to get it corrected. I have tried every version of changing that line around I can think of and probably many that do not make sense. I have one version where it will get past the line but then when that "with_published_products" is called later in the view it errors out with a bad PG statement and when I traced it back it was due to the very same query. Here is the view involved:
- if current_user
.pull-left{style: "position:absolute; top: 10px; left: 20px;"}
= link_to products_path do
%button.btn{style: "margin-top: 0px; "}
%i.icon-arrow-left.icon-large
%b back to products
.row-fluid
.span12
%h1.bigred DPF Product Catalog
%hr
.row-fluid
.span12
- unless #categories.empty?
.tabbable.tabbable-left
%ul.nav.nav-tabs{style: "text-align:right;"}
%li.active
%a{"data-toggle" => "tab", :href => "#tab-#{#categories.first.id}"} #{#categories.first.name}
- #categories.each do |category|
-unless category == #categories.first
%li
%a{"data-toggle" => "tab", :href => "#tab-#{category.id}"} #{category.name}
.tab-content
- #categories.each_with_index do |category, i|
.tab-pane.fade{id: "tab-#{category.id}", class: "#{ 'in active' if i.zero? }"}
%h1.bigred.professional{style: "text-align:left;"} #{category.name}
%hr
- #params_name.blank? ? #products = category.products.published.includes(:pictures).order("name asc") : #products = category.products.searched(#params_name).published.includes(:pictures).order("name asc")
= render partial: 'product', collection: #products, cache: true
- else
%h2
There were no products matching
%b #{#params_name.gsub(/%/, '')}
Here is the Controller:
class StaticPagesController < ApplicationController
#skip_before_filter :authenticate_user!, :only => [:catalog, :roi]
skip_before_action :authenticate_user!, :only => [:catalog, :roi]
#skip_authorization_check
before_action
def home
end
def help
end
def catalog
if params[:name].present?
#params_name = "%#{params[:name]}%"
#categories = Category.with_published_products.with_matched_search_terms(#params_name)
else
#categories = Category.with_published_products
end
render layout: "catalog"
end
def roi
render layout: "roi"
end
end
Best I can tell the original author is trying to display product_catagoires that have the status of "published". I might be over simplfying the statement but I beleive that is what I understand it to be doing. I know from other joins I had to fix I had to eliminate the .eq and put => in its place which I tried many versions of and it always complained about the syntax. As an example of what I mean this was another section I fixed:
FROM:
cds_item = item.variants.joins{vendor}.where{ vendor.code.eq 'cds' }.first
TO:
cds_item = item.variants.joins(:vendor).where( :vendor_id => 'cds' ).first
I tried to change the line I am having the problem with to a similar format and every combination I could think of I just can't seem to speak to Rails the way it wants.
If there are any other files I need to attach please let me know.
I appreciate any input anyone has to offer.
I am willing to make the scopes less complicated and more verbose if it solves the problem I just am too new to figure this last piece out.
Thank You,
Scott
Thank You #engineersmnky,
I am now getting the following error:
Cannot have a has_many :through association 'Category#products' which goes through 'Category#product_categories' before the through association is defined.
It is referencing this line in the view:
- #params_name.blank? ? #products = category.products.published.includes(:pictures).order("name asc") : #products = category.products.searched(#params_name).published.includes(:pictures).order("name asc")
Sorry if this is not the right way to add more information, I couldn't post the line dealing with the code in a comment.
My guess is that your legacy app used a gem called Squeel which monkeypatched core methods in ActiveRecord like where, joins etc to take a block argument:
Person.joins(:articles => {:comments => :person})
Becomes:
Person.joins{articles.comments.person}
The authors ambition was that Squeel would be incorporated into ActiveRecord but that didn't pass and the fact that the monkeypatches broke with every Rails release burned out the maintainers. Squeel was abandoned after Rails 4.2. But parts of it live on in the baby_squeel gem.
Some of the easier cases are going to be relatively straight forward to unfurl into modern ActiveRecord code. Others like a LIKE query will require some Arel:
class Category < ActiveRecord::Base
# Do not use attr_accessible
# - model level whitelisting was replaced by strong params in Rails 4
# attr_accessible :name
# accepts_nested_attributes_for :product_categories
# You need to declare the relations you are joining through first
has_many :product_categories, dependent: :destroy
has_many :products, through: :product_categories
# default scope is evil!
# default_scope { order("id") }
# use `lambda do` for multi-line lambdas to keep reasonable line lengths
# or just write regular class methods
scope :with_published_products, lambda do
joins(product_categories: :product)
.where( products: { status: "published" } )
.distinct
end
scope :with_matched_search_terms, lambda do |search_terms|
joins(product_categories: :product)
.where(
Product.arel_attribute(:name)
.lower
.matches("%#{search_terms.downcase}%")
.distinct
)
end
end
I'm not sure what #uniq was supposed to do in the Squeel - but I believe it may have added a distinct clause - like ActiveRecord::Relation#uniq which was deprechiated in Rails 5.0.
I made a real basic github project here that demonstrates the issue. Basically, when I create a new comment, it is saved as expected; when I update an existing comment, it isn't saved. However, that isn't what the docs for :autosave => true say ... they say the opposite. Here's the code:
class Post < ActiveRecord::Base
has_many :comments,
:autosave => true,
:inverse_of => :post,
:dependent => :destroy
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
obj.text=val
end
end
class Comment < ActiveRecord::Base
belongs_to :post, :inverse_of=>:comments
end
Now in the console, I test:
p=Post.create(:name=>'How to groom your unicorn')
p.comment="That's cool!"
p.save!
p.comments # returns value as expected. Now we try the update case ...
p.comment="But how to you polish the rainbow?"
p.save!
p.comments # oops ... it wasn't updated
Why not? What am I missing?
Note if you don't use "find_or_initialize", it works as ActiveRecord respects the association cache - otherwise it reloads the comments too often, throwing out the change. ie, this implementation works
def comment=(val)
obj=comments.detect {|obj| obj.posted_at==Date.today}
obj = comments.build(:posted_at=>Date.today) if(obj.nil?)
obj.text=val
end
But of course, I don't want to walk through the collection in memory if I could just do it with the database. Plus, it seems inconsistent that it works with new object but not an existing object.
Here is another option. You can explicitly add the record returned by find_or_initialize_by to the collection if it is not a new record.
def comment=(val)
obj=comments.find_or_initialize_by(:posted_at=>Date.today)
unless obj.new_record?
association(:comments).add_to_target(obj)
end
obj.text=val
end
I don't think you can make this work. When you use find_or_initialize_by it looks like the collection is not used - just the scoping. So you are getting back a different object.
If you change your method:
def comment=(val)
obj = comments.find_or_initialize_by(:posted_at => Date.today)
obj.text = val
puts "obj.object_id: #{obj.object_id} (#{obj.text})"
puts "comments[0].object_id: #{comments[0].object_id} (#{comments[0].text})"
obj.text
end
You'll see this:
p.comment="But how to you polish the rainbow?"
obj.object_id: 70287116773300 (But how to you polish the rainbow?)
comments[0].object_id: 70287100595240 (That's cool!)
So the comment from find_or_initialize_by is not in the collection, it outside of it. If you want this to work, I think you need to use detect and build as you have in the question:
def comment=(val)
obj = comments.detect {|c| c.posted_at == Date.today } || comments.build(:posted_at => Date.today)
obj.text = val
end
John Naegle is right. But you can still do what you want without using detect. Since you are updating only today's comment you can order the association by posted_date and simply access the first member of the comments collection to updated it. Rails will autosave for you from there:
class Post < ActiveRecord::Base
has_many :comments, ->{order "posted_at DESC"}, :autosave=>true, :inverse_of=>:post,:dependent=>:destroy
def comment=(val)
if comments.empty? || comments[0].posted_at != Date.today
comments.build(:posted_at=>Date.today, :text => val)
else
comments[0].text=val
end
end
end
I'm running into an issue where I'm working with the as_json method, and how to efficiently return the object in JSON AND it's belongs_to object as JSON as well, where the belongs_to object has its own belongs_to object. Code would probably explain it better.
The Not-Working Way
Alert class
class Alert < ActiveRecord::Base
belongs_to :message
# for json rendering
def as_json(options={})
super(:include => :message)
end
end
Message class
def as_json(options={})
super( methods: [:timestamp, :num_photos, :first_photo_url, :tag_names],
include: { camera: { only: [:id, :name] },
position: { only: [:id, :name, :address, :default_threat_level ]},
images: { only: [:id, :photo_url, :is_hidden]} })
end
The problem with this first set up is that when I have an Alert object and call
alert.as_json()
I get all the attributes from Alert and all the attributes from Message, but none of the other attributes from Message that I want, like Camera, Position, etc.
Here's the "It's Working, But Probably Not Proper Design Way"
Alert Class
class Alert < ActiveRecord::Base
belongs_to :message
# for json rendering
def as_json(options={})
super().merge(:message => message.as_json)
end
end
Messages Class
# for json rendering
def as_json(options={})
super( methods: [:timestamp, :num_photos, :first_photo_url, :tag_names])
.merge(:camera => camera.as_json)
.merge(:position => position.as_json)
.merge(:images => images.as_json)
end
In this 2nd setup, I get all of Messages's nested attributes like I want.
My question, am I missing some Rails Convention to do this properly? It seems like there would/should be an easier way.
The best answer for me was using serializable_hash. #kikito touched on this in his comment, but there was a typo that prevented it from working. It's not serialized_hash, it's serializable_hash.
Literally just find + replace as_json with serializable_hash and this bug goes away. (It's still not fixed in today's Rails 4.0.2). You also get the benefit of having an easier time implementing an XML API later (some people still use those!).
Which version of Rails are you using? This is a known bug in older versions of Rails, supposedly fixed with this pull request. Your syntax looks right to me, so perhaps this is your problem?
As an aside, you may also want to checkout the new active_model_serializers from Jose Valim (Rails core member). It may at least enable you to work around your issue in a more elegant manner.
I would recommend you to take a look at RABL (stands for Ruby API Builder Language) gem (railscast, github). It offers you a DSL for defining the structure of your JSON (and also XML) response in templates (like Haml or CoffeeScript does). It also supports partials.
I'm using Mongoid and when I .update_attributes on a model that has a references_one using params[:model_name] I get the error...
#model.update_attributes(params[:model_name])
undefined method `associations' for "...":String
I understand why this is happening. Mongoid is trying to map that .association_name to the string value in the params hash when what it wants is a reference to another Mongoid::Document. That I get.
What I'd like to know is if there is a global way to fix this. For the moment I've gotten around this issue by doing something like the following...
model_params = params[:model_name]
if model_params.has_key? :relationship
model_params[:relationship] = RelatedModel.first(:conditions => { :_id => model_params[:relationship] })
end
This works but I'd rather have a fix that fixes it every time so that I'm not manually mapping the related model every time I do an update. That would defiantly be a violation of DRY.
Here's sample module that you could include in all your models
module MyAppBase
def my_update_attributes(model_params,related_model)
if model_params.has_key? :relationship
model_params[:relationship] = related_model.first(:conditions => { :_id => model_params[:relationship] })
end
self.update_attributes(model_params)
end
end
#include it in your model classes
class MyModel < ActiveRecord::Base
include MyAppBase
#normal model code
end
I have a model that uses a serialized column:
class Form < ActiveRecord::Base
serialize :options, Hash
end
Is there a way to make this serialization use JSON instead of YAML?
In Rails 3.1 you can just
class Form < ActiveRecord::Base
serialize :column, JSON
end
In Rails 3.1 you can use custom coders with serialize.
class ColorCoder
# Called to deserialize data to ruby object.
def load(data)
end
# Called to convert from ruby object to serialized data.
def dump(obj)
end
end
class Fruits < ActiveRecord::Base
serialize :color, ColorCoder.new
end
Hope this helps.
References:
Definition of serialize:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb#L556
The default YAML coder that ships with rails:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/coders/yaml_column.rb
And this is where the call to the load happens:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/read.rb#L132
Update
See mid's high rated answer below for a much more appropriate Rails >= 3.1 answer. This is a great answer for Rails < 3.1.
Probably this is what you're looking for.
Form.find(:first).to_json
Update
1) Install 'json' gem:
gem install json
2) Create JsonWrapper class
# lib/json_wrapper.rb
require 'json'
class JsonWrapper
def initialize(attribute)
#attribute = attribute.to_s
end
def before_save(record)
record.send("#{#attribute}=", JsonWrapper.encrypt(record.send("#{#attribute}")))
end
def after_save(record)
record.send("#{#attribute}=", JsonWrapper.decrypt(record.send("#{#attribute}")))
end
def self.encrypt(value)
value.to_json
end
def self.decrypt(value)
JSON.parse(value) rescue value
end
end
3) Add model callbacks:
#app/models/user.rb
class User < ActiveRecord::Base
before_save JsonWrapper.new( :name )
after_save JsonWrapper.new( :name )
def after_find
self.name = JsonWrapper.decrypt self.name
end
end
4) Test it!
User.create :name => {"a"=>"b", "c"=>["d", "e"]}
PS:
It's not quite DRY, but I did my best. If anyone can fix after_find in User model, it'll be great.
My requirements didn't need a lot of code re-use at this stage, so my distilled code is a variation on the above answer:
require "json/ext"
before_save :json_serialize
after_save :json_deserialize
def json_serialize
self.options = self.options.to_json
end
def json_deserialize
self.options = JSON.parse(options)
end
def after_find
json_deserialize
end
Cheers, quite easy in the end!
The serialize :attr, JSON using composed_of method works like this:
composed_of :auth,
:class_name => 'ActiveSupport::JSON',
:mapping => %w(url to_json),
:constructor => Proc.new { |url| ActiveSupport::JSON.decode(url) }
where url is the attribute to be serialized using json
and auth is the new method available on your model that saves its value in json format to the url attribute. (not fully tested yet but seems to be working)
I wrote my own YAML coder, that takes a default. Here is the class:
class JSONColumn
def initialize(default={})
#default = default
end
# this might be the database default and we should plan for empty strings or nils
def load(s)
s.present? ? JSON.load(s) : #default.clone
end
# this should only be nil or an object that serializes to JSON (like a hash or array)
def dump(o)
JSON.dump(o || #default)
end
end
Since load and dump are instance methods it requires an instance to be passed as the second argument to serialize in the model definition. Here's an example of it:
class Person < ActiveRecord::Base
validate :name, :pets, :presence => true
serialize :pets, JSONColumn.new([])
end
I tried creating a new instance, loading an instance, and dumping an instance in IRB, and it all seemed to work properly. I wrote a blog post about it, too.
A simpler solution is to use composed_of as described in this blog post by Michael Rykov. I like this solution because it requires the use of fewer callbacks.
Here is the gist of it:
composed_of :settings, :class_name => 'Settings', :mapping => %w(settings to_json),
:constructor => Settings.method(:from_json),
:converter => Settings.method(:from_json)
after_validation do |u|
u.settings = u.settings if u.settings.dirty? # Force to serialize
end
Aleran, have you used this method with Rails 3? I've somewhat got the same issue and I was heading towards serialized when I ran into this post by Michael Rykov, but commenting on his blog is not possible, or at least on that post. To my understanding he is saying that you do not need to define Settings class, however when I try this it keeps telling me that Setting is not defined. So I was just wondering if you have used it and what more should have been described? Thanks.