Summary:
How do I customize the path that respond_to
generates for an ActiveModel object?
Update: I'm looking for a hook, method override, or configuration change to accomplish this, not a workaround. (A workaround is easy but not elegant.)
Context & Example:
Here is an example to illustrate. I have a model, Contract
, which has a lot of fields:
class Contract < ActiveRecord::Base
# cumbersome, too much for a UI form
end
To make the UI code easier to work with, I have a simpler class, SimpleContract
:
class SimpleContract
include ActiveModel::Model
# ...
def contract_attributes
# convert SimpleContract attributes to Contract attributes
end
def save
Contract.new(contract_attributes).save
end
end
This works well, but I have a problem in my controller...
class ContractsController < ApplicationController
# ...
def create
@contract = SimpleContract.new(contract_params)
flash[:notice] = "Created Contract." if @contract.save
respond_with(@contract)
end
# ...
end
The problem is that respond_with
points to simple_contract_url
, but I want it to point to contract_url
instead. What is the best way to do that? (Please note that I'm using ActiveModel.)
(Note: I'm using Rails 4 Beta, but that isn't central to my problem. I think a good answer for Rails 3 will work as well.)
Sidebar: if this approach to wrapping a model in a lightweight ActiveModel class seem unwise to you, please let me know in the comments. Personally, I like it because it keeps my original model simple. The 'wrapper' model handles some UI particulars, which are intentionally simplified and give reasonable defaults.
First, here is an answer that works:
class SimpleContract
include ActiveModel::Model
def self.model_name
ActiveModel::Name.new(self, nil, "Contract")
end
end
I adapted this answer from kinopyo's answer to Change input name of model.
Now, for the why. The call stack of respond_to
is somewhat involved.
# Start with `respond_with` in `ActionController`. Here is part of it:
def respond_with(*resources, &block)
# ...
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
# That takes us to `call` in `ActionController:Responder`:
def self.call(*args)
new(*args).respond
end
# Now, to `respond` (still in `ActionController:Responder`):
def respond
method = "to_#{format}"
respond_to?(method) ? send(method) : to_format
end
# Then to `to_html` (still in `ActionController:Responder`):
def to_html
default_render
rescue ActionView::MissingTemplate => e
navigation_behavior(e)
end
# Then to `default_render`:
def default_render
if @default_response
@default_response.call(options)
else
controller.default_render(options)
end
end
And that is as far as I've gotten for the time being. I have not actually found where the URL gets constructed. I know that it happens based on model_name
, but I have not yet found the line of code where it happens.