Virtual attribute not moved to the model hash inside params - ruby-on-rails-3

I'm having a problem in my Rails 3.2 app where a virtual attribute sent restfully via JSON is not in the right place in the params hash. Well, it isn't where I expect. It remains to be seen if my expectations are correct. :)
I have a model using the standard virtual attribute pattern, like this:
class Track < ActiveRecord::Base
def rating
# get logic removed for brevity
end
def rating=(value)
# set logic
end
def as_json(options={}) # so my method is in the JSON when I use respond_with/to_json
super(options.merge(methods: [:rating]))
end
end
The JSON sent to my controller looks like this:
{"id":1,"name":"Icarus - Main Theme 2","rating":2}
To be clear, name and id are not virtual, rating is.
I end up with this in the params hash, after rails does its magic:
{"id"=>"1", "name"=>"Icarus - Main Theme 2", "rating"=>2, "track"=>{"id"=>"1", "name"=>"Icarus - Main Theme 2"}}
As you can see, id and name make it to the nested :track hash, but rating does not. Is this expected behavior? It breaks the (somewhat) standard practice of using the nested hash in the controller because the nested hash does not contain all the parameters I need.
Track.update(params[:id], params[:track]) # :track is missing rating
Thanks for your help!

I recently ran into this gotcha as well. The problem is, the params wrapper is looking at your model Track.attribute_names to determine how to map the data into a :track => {params} hash. If you don't have a model associated, the default will be to wrap the params based on the controller name, and include all of the values:
class SinglesController < ApplicationController
def create
#params[:single] will contain all of your attributes as it doesn't
# have an activerecord model to look at.
#track_single = Track.new(params[:single])
end
end
You can call wrap_parameters in your controller to tell action controller what attributes to include when its wrapping your params, like so:
class TracksController < ApplicationController
wrap_parameters :track, :include => :rating
#other controller stuff below
end
See more here: http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html

Maybe if you assign the rating virtual attribute inside the nested hash like this:
def as_json(options={})
super(options.merge(:track => {:methods => #rating}))
end
It would behave the way you expected.

Just ran across this problem and figured out a pretty decent solution. Add the following to your ApplicationController
wrap_parameters exclude: [:controller, :action, :format] + ActionController::ParamsWrapper::EXCLUDE_PARAMETERS
This way, everything is nested under your resource (except for stuff Rails adds to the params hash) and you won't ever have to append to a controller specific call of wrap_parameters again. :D

Related

Rails nested resources and routing - how to break up controllers?

I have the following models:
Post
Tag
TaggedPost (from which Post and Tag derive their associations by has_many :through)
And I have the following routes.rb file:
resources :tags
resources :posts do
resources :tags
end
So when I navigate to, say, /posts/4/tags, that will shoot me into the index action for the Tag controller with the post_id value set in the parameters array. Cool.
My question is though, now that I'm accessing the nested tags resource under posts, should I be hitting the Tags controller still? Or should I setup some other controller to handle the nested nature of tags at this point? Otherwise I have to build additional logic into the Tags controller. This can be done of course, but is this the common way of handling nested routes and resources? The code I have in the index action for the Tags controller is as follows:
TagsController.rb
def index
if params[:post_id] && #post = Post.find_by_id(params[:post_id])
#tags = Post.find_by_id(params[:post_id]).tags
else
#tags = Tag.order(:name)
end
respond_to do |format|
format.html
format.json {render json: #tags.tokens(params[:q]) }
end
end
I can see the code in this controller growing increasingly large, as I plan for many additional resources to be associated with tag resources. Thoughts on how to break this out?
Summary of questions:
If a resource is nested, should the nested resource be going through a different controller representing the nested nature of the resource? This is opposed to going through the normal controller as I am in the code example that I provided.
If so, how should these controllers be named and setup?
Let me know if you need more information.
I think the best solution is to split up controllers:
resources :tags
resources :posts do
resources :tags, controller: 'post_tags'
end
And then you have 3 controllers. Optionally, you can inherit
PostTagsController from TagsController to do something like:
class PostTagsController < TagsController
def index
#tags = Post.find(params[:post_id]).tags
super
end
end
If the difference is only the retrieval of tags, you can:
class TagsController < ApplicationController
def tags
Tag.all
end
def tag
tags.find params[:id]
end
def index
#tags = tags
# ...
end
# ...
end
class PostTagsController < TagsController
def tags
Product.find(params[:product_id]).tags
end
end
Use that methods and simply override tags in the inheriting controllers ;)
All you are doing with nested resources is changing the routing URL. Only thing you would have to do is make sure you are passing the proper id (in your case post)to the tag controller. Most common error is the Can't Find *** ID.
If you don't nest a profile route into a user route it would look like this
domain.com/user/1
domain.com/profile/2
When you nest the routes it would be
domain.com/user/1/profile/2
That is all that it is doing and nothing else. You don't need additional controllers. Doing nested routing is just for looks. allowing your user to follow the association. The most important thing about nesting routes is that you make sure you make the link_to's to the right path.
When not nested: it would be
user_path
and
profile_path
when it is nested you would need to use
user_profile_path
rake routes is your friend to find out how the routes have changed.
Hope it helps.

What is the best way to translate slugs in Rails routes

I'm trying to achieve full internationalization of my routes in a Rails3.1 app. I'm already using Francesc Pla's rails-translate-routes to localize route actions and resources. The last step is to be able to translate slugs for some of my models.
Route to be translated:
http://myhost.com/products/pharmacy --> http://myhost.com/productos/farmacia
I have a nested route of the form
# routes.rb
match 'products/:category_slug' => "products#index"
I have a model Category with an instance #<Category id: 1, slug: "pharmacy"> and I do find_by_slug category in my ProductsController.
Any ideas on how to do translate the :category_slug part of the route?
As far as I'm aware, you can call translation helpers directly from your controller as long as you namespace correctly with I18n.
So your ProductsController could contain something like the following:
class ProductsController < ApplicationController
def index
i18n_slug = I18n.t("locale.category.#{params[:category_slug]}")
#category = Category.find_by_slug(i18n_slug)
end
end
You should probably inform yourself as to potential security risks of passing the params directly into the translation engine, though I'm not aware of any. You might also consider
moving that into a before filter or into the application controller if it will be used in multiple controller actions.

Track dirty for not-persisted attribute in an ActiveRecord object in rails

I have an object that inherits from ActiveRecord, yet it has an attribute that is not persisted in the DB, like:
class Foo < ActiveRecord::Base
attr_accessor :bar
end
I would like to be able to track changes to 'bar', with methods like 'bar_changed?', as provided by ActiveModel Dirty. The problem is that when I try to implement Dirty on this object, as described in the docs, I'm getting an error as both ActiveRecord and ActiveModel have defined define_attribute_methods, but with different number of parameters, so I'm getting an error when trying to invoke define_attribute_methods [:bar].
I have tried aliasing define_attribute_methods before including ActiveModel::Dirty, but with no luck: I get a not defined method error.
Any ideas on how to deal with this? Of course I could write the required methods manually, but I was wondering if it was possible to do using Rails modules, by extending ActiveModel functionality to attributes not handled by ActiveRecord.
I'm using the attribute_will_change! method and things seem to be working fine.
It's a private method defined in active_model/dirty.rb, but ActiveRecord mixes it in all models.
This is what I ended up implementing in my model class:
def bar
#bar ||= init_bar
end
def bar=(value)
attribute_will_change!('bar') if bar != value
#bar = value
end
def bar_changed?
changed.include?('bar')
end
The init_bar method is just used to initialise the attribute. You may or may not need it.
I didn't need to specify any other method (such as define_attribute_methods) or include any modules.
You do have to reimplement some of the methods yourself, but at least the behaviour will be mostly consistent with ActiveModel.
I admit I haven't tested it thoroughly yet, but so far I've encountered no issues.
ActiveRecord has the #attribute method (source) which once invoked from your class will let ActiveModel::Dirty to create methods such as bar_was, bar_changed?, and many others.
Thus you would have to call attribute :bar within any class that extends from ActiveRecord (or ApplicationRecord for most recent versions of Rails) in order to create those helper methods upon bar.
Edit: Note that this approach should not be mixed with attr_accessor :bar
Edit 2: Another note is that unpersisted attributes defined with attribute (eg attribute :bar, :string) will be blown away on save. If you need attrs to hang around after save (as I did), you actually can (carefully) mix with attr_reader, like so:
attr_reader :bar
attribute :bar, :string
def bar=(val)
super
#bar = val
end
I figured out a solution that worked for me...
Save this file as lib/active_record/nonpersisted_attribute_methods.rb: https://gist.github.com/4600209
Then you can do something like this:
require 'active_record/nonpersisted_attribute_methods'
class Foo < ActiveRecord::Base
include ActiveRecord::NonPersistedAttributeMethods
define_nonpersisted_attribute_methods [:bar]
end
foo = Foo.new
foo.bar = 3
foo.bar_changed? # => true
foo.bar_was # => nil
foo.bar_change # => [nil, 3]
foo.changes[:bar] # => [nil, 3]
However, it looks like we get a warning when we do it this way:
DEPRECATION WARNING: You're trying to create an attribute `bar'. Writing arbitrary attributes on a model is deprecated. Please just use `attr_writer` etc.
So I don't know if this approach will break or be harder in Rails 4...
Write the bar= method yourself and use an instance variable to track changes.
def bar=(value)
#bar_changed = true
#bar = value
end
def bar_changed?
if #bar_changed
#bar_changed = false
return true
else
return false
end
end

rails 3 response format and versioning using vendor MIME type in the Accept header

Preamble:
I investigated how to version an API and found several ways to do it. I decided to try peter williams' suggestion and created new vendor mime types to specify version and format. I could find no definitive write-up for doing this following "the rails way" so I pieced together info from several places. I was able to get it working, but there is some goofiness in the way the renderers handle Widget array vs Widget instance in respond_with.
Basic steps & problem:
I registered mime types and added renderers for version 1 in both xml and json to ApplicationController, the renderers call to_myproj_v1_xml and to_myproj_v1_json methods in the model. respond_with(#widget) works fine but respond_with(#widgets) throws an HTTP/1.1 500 Internal Server Error saying that the "Template is missing".
Workaround:
"Template is missing" means that no render was called and no matching template exists. by accident, I discovered that it is looking for a class method... so I came up with the code below which works but I'm not really happy with it. The goofiness is mostly in and related to xml = obj.to_myproj_v1_xml(obj) and the duplication in the model.
My question is - has anyone done anything similar in a slightly cleaner fashion?
-= updated code =-
config/initializers/mime_types.rb:
Mime::Type.register 'application/vnd.com.mydomain.myproj-v1+xml', :myproj_v1_xml
Mime::Type.register 'application/vnd.com.mydomain.myproj-v1+json', :myproj_v1_json
app/controllers/application_controller.rb:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :authenticate
ActionController.add_renderer :myproj_v1_xml do |obj, options|
xml = obj.to_myproj_v1_xml
self.content_type ||= Mime::Type.lookup('application/vnd.com.mydomain.myproj-v1+xml')
self.response_body = xml
end
ActionController.add_renderer :myproj_v1_json do |obj, options|
json = obj.to_myproj_v1_json
self.content_type ||= Mime::Type.lookup('application/vnd.com.mydomain.myproj-v1+json')
self.response_body = json
end
end
app/models/widget.rb:
class Widget < ActiveRecord::Base
belongs_to :user
V1_FIELDS = [:version, :model, :description, :name, :id]
def to_myproj_v1_xml
self.to_xml(:only => V1_FIELDS)
end
def to_myproj_v1_json
self.to_json(:only => V1_FIELDS)
end
def as_myproj_v1_json
self.as_json(:only => V1_FIELDS)
end
end
app/controllers/widgets_controller.rb:
class WidgetsController < ApplicationController
respond_to :myproj_v1_xml, :myproj_v1_json
def index
#widgets = #user.widgets
respond_with(#widgets)
end
def create
#widget = #user.widgets.create(params[:widget])
respond_with(#widget)
end
def destroy
#widget = #user.widgets.find(params[:id])
respond_with(#widget.destroy)
end
def show
respond_with(#widget = #user.widgets.find(params[:id]))
end
...
end
config/initializers/monkey_array.rb
class Array
def to_myproj_v1_json(options = {})
a = []
self.each { |obj| a.push obj.as_myproj_v1_json }
a.to_json()
end
def to_myproj_v1_xml(options = {})
a = []
self.each { |obj| a.push obj.as_myproj_v1_json } # yes this is json instead of xml. as_json returns a hash
a.to_xml()
end
end
UPDATE:
Found another solution that feels better but still a little weird (I'm still not completely comfortable with monkey patches), probably ok though... basically moved building the response data from the class method to_myproj_v1_json to a monkey patch on Array. This way when there is an Array of Widgets, it calls the instance method as_myproj_v1_json on each Widget and returns the whole Array as desired format.
One note:
as_json has nothing to do with json format, just creates a hash. Add custom formatting to as_myproj_v1_json (or an as_json override if you aren't using custom mime types), then to_json will change a hash to a json string.
i have updated the code below to be what is currently used, so the original question may not make sense. if anyone wants the original question and code shown as was and fixed code in a response i can do that instead.
For the answer: see the question :-)
In short, there are different solutions, of which one is in the question above:
Monkey-patch Array to implement a method that will give the (old) v1 JSON back
I haven't seen this content type trick used anywhere in a Rails project before so this is new to me. The way I've typically seen it done is to define a route namespace (e.g. /api/v1/) which goes to a controller (say, Api::Version1Controller).
Also, I know you want to do things the "Rails way", and maybe this sounds crotchety coming from a guy who has been with Rails since 1.3, but the whole respond_with / respond_to stuff is rather magic to me. I didn't know that respond_to looks for a to_XXX method when it serializes objects, for instance (maybe I need to read up on that). Having to monkey-patch Array like that seems rather silly. Besides, for an API, formatting the model data is really the view's job, not the model's. I might look into something like rabl in this case. There's a good writeup about it here.

How to slugify a parent model with fields from a child model?

I'd like to slugify the urls for a model in Rails 3, using Mongoid. The problem is the fields I want to use in the slug are located in a child model. I am using mongoid-slug gem to find a solution to this and my attempt so far is this:
class Building
references_one :address
def to_param
address.to_param
end
end
class Address
referenced_in :building
field :houseno
field :street
slug :houseno, :street
end
While this allows me to form the correct url by calling building_path(building), the page does not contain the correct values. Error message complains that object id is incorrect, and I'm not sure how to get Rails to listen and find the record by to_param.
For the curious, here is how I solved my own problem. I realized that I needed to use change the show action from
#building = Building.find(params[:id])
to
#building = Address.find_by_slug(params[:id]).building
And voila! It works.