I'm frequently building controllers where i would like multiple methods
(in addition to index, edit, show, etc.). Most of the time the actions i
desire could be lumped into show as they are simple GET operations,
however I don't want to put too much logic in any one controller action.
Here is a quick example of two different ways to achieve the same
thing...
class TwitterFriendController < ApplicationController
## lump everything into show?
def show
if params[:id] == "follow"
users = current_user.following
elsif params[:id] == "follow_me"
users = current_user.users_who_follow_me
elsif params[:id] == "following_follow_me"
users = current_user.following_who_follow_me
elsif params[:id] == "following_who_do_not_follow_me"
users = current_user.following_who_do_not_follow_me
...
end
respond_with do |format|
format.json do {...}
end
end
## or split everything out into separate methods, this requires
additional routing
def following
...
end
def users_who_follow_me
...
end
def following_who_follow_me
...
end
def following_who_do_not_follow_me
...
end
end
Everything in show
a ton of logic in one method
DRY ? # lots of extra code needed for logic
Less routing
Seperate Methods
More routing
not DRY
Easy method lookup
Easier to read individual methods
So again the real question is, which one of those techniques are less
bad.
I would do something like:
FOLLOW_WHITELIST = %w[ follow follow_me following_follow_me following_who_follow_me following_who_do_not_follow_me ]
def show
if FOLLOW_WHITELIST.include? params[:id]
users = current_user.send params[:id].to_sym
end
respond_with do |format|
format.json do {...}
end
end
This will call whatever method is passed in params[:id], as long as it's in the whitelist (to prevent arbitrary code injection).
If having separate routes was a plus to you (nicer urls?), you could also dynamically generate the methods and routes with something like this:
class TwitterFriendController < ApplicationController
FOLLOW_ACTIONS = %w[ follow follow_me following_follow_me following_who_follow_me following_who_do_not_follow_me ]
FOLLOW_ACTIONS.each do |action|
define_method action do
users = current_user.send action.to_sym
respond_with do |format|
format.json do {...}
end
end
end
end
And then in routes.rb:
FOLLOW_ACTIONS.each do |action|
match action.to_sym => "controller##{action}"
end
Related
I have a controller action (favorites) in my Rails app that returns a JSON object with two keys (companies and jobs). Each key represents a collection of Company or JobDescription objects. What I want to know is if there is a clean way I can serialize both #companies and #jobs. Here is my code:
def favorites
#companies = current_user.companies
#jobs = current_user.job_descriptions
respond_to do |format|
format.html
format.json { render json: {companies: #companies, jobs: #jobs}, root: false }
end
end
I could always refactor my code into two separate JSON calls (one for jobs, one for companies), but I'd prefer to stick with a single call to favorites.
You can use Rails Presenters here!
So, you can have two presenters: CompaniesPresenter and JobsPresenter which will be responsible for building the #companies and jobs objects respectively.
So, in your controller, you would have something like:
#companies = CompaniesPresenter.new(current_user).companies
#jobs = JobsPresenter.new(current_user).job_descriptions
For example, your CompaniesPresenter would look like this:
class CompaniesPresenter
attr_reader :current_user
def initialize(current_user)
#current_user = current_user
end
def companies
# build the companies JSON here
end
end
Here is a tutorial with Rails Presenter Pattern that might be useful.
And, here is an useful video. Hope this helps.
This example works, are you just trying to change the json format? If so...
In the company or job model, you can add an as_json method and format the output as you want.
def as_json(options = {})
{ :name => name }
end
I have a rails app where many of the models are editable using best_in_place, so I have a lot of controllers that look partially like this:
before_action :find_objects, except: [:new, :create]
def update
#object.update_attributes(object_params)
respond_to do |format|
format.json { respond_with_bip #object}
end
end
private
def object_params
params.require(:object).permit(:foo, :bar)
end
def find_objects
#object = Object.find(params[:id])
end
How do I move this particular repeated piece into a controller concern, given that the object being updated is going to come in with a particular name in the params hash, and object_params and find_objects should call their proper versions based on the model name? Is there some elegant meta-magic that'll sort this all out?
I think this is a case where your code could be "too DRY". You can certainly accomplish this using meta-magic, but it could make your code confusing in the long run.
If you want to do the meta-magic, one trick is to use params[:controller] to get the name of the model. For example, if you have a PostsController, then:
params[:controller] # => "posts"
params[:controller].classify # => "Post"
Taking this a step further, you could write a generic find_object like this:
def find_object
model_class = params[:controller].classify.constantize
model_instance = model_class.find(params[:id])
instance_variable_set("##{model_class.name.underscore}", model_instance)
end
But as I said at the beginning, I'm not sure I would recommend this amount of abstraction just for the sake of DRY-ing your controller code.
I'm creating an API on my application. I currently overrided the as_json method in my model in order to be able to get attached files as well as logo from Paperclip :
def as_json( options = {} )
super.merge(logo_small: self.logo.url(:small), logo_large: self.logo.url(:large), taxe: self.taxe, attachments: self.attachments)
end
Then within my controller, I'm doing :
def index
#products = current_user.products
respond_with #products
end
def show
respond_with #product
end
The problem is that on the index, I don't want get all the attachments. I only need it on the show method. So I tried it :
def index
#products = current_user.products
respond_with #products, except: [:attachments]
end
But unfortunately it's only working on default product attributes (everyting that I merged seems not to be consider). How can I do to not send :attachments?
Thanks
I'd recommend you have a look at active_model_serializers. It will provide a nice and OOP way of handling the kind of object decoration you need - selectively excluding attributes - and much more. There's even a Railscast!
I have a couple different user types (buyers, sellers, admins).
I'd like them all to have the same account_path URL, but to use a different action and view.
I'm trying something like this...
class AccountsController < ApplicationController
before_filter :render_by_user, :only => [:show]
def show
# see *_show below
end
def admin_show
...
end
def buyer_show
...
end
def client_show
...
end
end
This is how I defined render_by_user in ApplicationController...
def render_by_user
action = "#{current_user.class.to_s.downcase}_#{action_name}"
if self.respond_to?(action)
instance_variable_set("##{current_user.class.to_s.downcase}", current_user) # e.g. set #model to current_user
self.send(action)
else
flash[:error] ||= "You're not authorized to do that."
redirect_to root_path
end
end
It calls the correct *_show method in the controller. But still tries to render "show.html.erb" and doesn't look for the correct template I have in there named "admin_show.html.erb" "buyer_show.html.erb" etc.
I know I can just manually call render "admin_show" in each action but I thought there might be a cleaner way to do this all in the before filter.
Or has anyone else seen a plugin or more elegant way to break up actions & views by user type? Thanks!
Btw, I'm using Rails 3 (in case it makes a difference).
Depending on how different the view templates are, it might be beneficial to move some of this logic into the show template instead and do the switching there:
<% if current_user.is_a? Admin %>
<h1> Show Admin Stuff! </h1>
<% end %>
But to answer your question, you need to specify which template to render. This should work if you set up your controller's #action_name. You could do this in your render_by_user method instead of using a local action variable:
def render_by_user
self.action_name = "#{current_user.class.to_s.downcase}_#{self.action_name}"
if self.respond_to?(self.action_name)
instance_variable_set("##{current_user.class.to_s.downcase}", current_user) # e.g. set #model to current_user
self.send(self.action_name)
else
flash[:error] ||= "You're not authorized to do that."
redirect_to root_path
end
end
I have a fairly simple model; Users have_many products. I would like to be able to view a list of all products as well as a list of the products associated with a given user. My routes are set up like this:
/products
/products/:id
/users
/users/:id
/users/:id/products
The catch here is that I'd like to display the product list differently in the product#index view and the user/products#index view.
Is there a 'correct' way to do this? My current solution is to define products as a nested resource inside users, and then to check for params[:user_id] - if its found I render a template called 'index_from_user', otherwise I just render the typical 'index' template.
This is a situation I'm running into a lot - if there's a preferred way to do it I'd love to know...
You can declare two "products" routes - one under users, and one independent of users eg:
map.resources :products
map.resources :users, :has_many => :products
They will both look for "ProductsController#index" but the second will have the "user_id" pre-populated from the route (note: "user_id" not just "id")
So you can test for that in the index method, and display different items depending on whether it is present.
You will need to add a before_filter to the ProductController to actually instantiate the user model before you can use it eg:
before_filter :get_user # put any exceptions here
def index
#products = #user.present? ? #user.products : Product.all
end
# all the other actions here...
# somewhere near the bottom...
private
def get_user
#user = User.find(params[:user_id])
end
If you really want to display completely different views, you can just do it explicitly in the index action eg:
def index
#products = #user.present? ? #user.products : Product.all
if #user.present?
return render(:action => :user_view) # or whatever...
end
# will render the default template...
end