I have the following code to pull out and instantiate Rails controllers:
def get_controller(route)
name = route.requirements[:controller]
return if name.nil?
if name.match(/\//)
controller_name = name.split('/').map(&:camelize).join('::')
else
controller_name = name.camelize
end
p controller_name: controller_name
controller = Object.const_get("#{controller_name}Controller")
p controller: controller
controller.new
end
some routes are single names - "users", "friends", "neighbors", "politicians", etc...
other routes are nested, such as "admin/pets", "admin/shopping_lists", "admin/users", etc...
The above code works (in that it properly builds and instantiates the controller) in most of the cases mentioned, except for one - in this example, "admin/users"
from the puts statements, I'm getting the following:
{:controller_name=>"Admin::Users"}
{:controller => UsersController}
You'll notice that the namespace Admin is getting cut off. My guess is that since this is only the case for controllers which share a name in multiple namespaces (users and admin/users), it has something do to with Rails autoloading (?). Any idea about what is causing this?
As per the comment from lx00st, I should also point out that I've tried various forms of getting these constants, another attempt was as follows:
sections = name.split('/')
sections.map(&:camelize).inject(Object) do |obj, const|
const += "Controller" if const == sections.last
obj.const_get(const)
end
The same problem was encountered with this approach.
This was solved by an answer from user apneadiving which can be found here
Be aware that there are vicious cases in Rails development mode. In order to gain speed, the strict minimum is loaded. Then Rails looks for classes definitions when needed.
But this sometimes fails big time example, when you have say ::User already loaded, and then look for ::Admin::User. Rails would not look for it, it will think ::User does the trick.
This can be solved using require_dependency statements in your code.
Personally I think this is a bug, not a feature, but...so it goes. It solves the problem.
First of all, this code is superfluous:
if name.match(/\//)
controller_name = name.split('/').map(&:camelize).join('::')
else
controller_name = name.camelize
end
The only string would perfectly handle both cases:
controller_name = name.split('/').map(&:camelize).join('::')
Then, you probably want to handle namespaces properly:
n_k = controller_name.split('::')
klazz = n_k.last
namespace_object = if n_k.length == 1
Object
else
Kernel.const_get(n_k[0..-2].join('::'))
end
controller = namespace_object.const_get("#{klazz}Controller")
Hope that helps.
Related
Is it possible for the route below to dynamically select different controllers or at least for a single controller to dynamically call another controller?
get '*path' => 'routing#show
For example:
/name-of-a-person => persons#show
/name-of-a-place => places#show
I recall reading something about Rails 5 that would enable this but I can't find it again to save my life. It's possible I imagined it.
Another options is to have a RoutingController that depending on which path is received will call different controllers.
The use case is I have URLs in the database with a type, and the controller depends on what type is the URL. I'm thinking something like this:
get '*path' do |params|
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
'persons#show'
when 'place'
'places#show'
end
end
I post my second best solution so far; still waiting to see if anyone knows how to do this efficiently within the routes.
class RoutingController < ApplicationController
def show
url = Url.find_by!(path: params[:path])
url.controller_class.dispatch('show', request, response)
end
end
Hat tip to André for the idea.
You could define one controller and inside its action make something like this:
def generic_show
url = Url.find_by!(path: params[:path])
case url.type
when 'person'
controller = PersonController.new
controller.request = request
controller.response = response
controller.show
when 'place'
...
end
end
However, I would recommend you to move the code you want to reuse to other classes and use them in both controllers. It should be easier to understand and maintain.
I think you may be able to do it using advanced routing constraints.
From: http://guides.rubyonrails.org/routing.html#advanced-constraints
If you have a more advanced constraint, you can provide an object that responds to matches? that Rails should use. Let's say you wanted to route all users on a blacklist to the BlacklistController. You could do:
class BlacklistConstraint
def initialize
#ips = Blacklist.retrieve_ips
end
def matches?(request)
#ips.include?(request.remote_ip)
end
end
Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
I don't think the Rails guide example is particularly good, because this problem could essentially be solved in your application controllers before_action.
In this example, the constraint is used for IP filtering, but you could also implement matches? to check if it's a person. I would imagine something like
def matches?(request)
Person.where(slug: request.params[:path]).any?
end
And as such, the Rails router can decide whether or not to dispatch the request to the persons#show action.
I've got this helper method in my application controller:
def current_team
#current_team ||= Team.find(params[:team_id])
end
Problem is, it works for urls of the format:
/teams/20/members/11
but it doesn't work for:
/teams/20
In order to get it to work for those, I have to change :team_id to be :id.
How can I tidy it up so it 'just works'?
Thanks!
Set instance variables (#current_team) in controllers, never in helpers. It's not what helpers are for.
If you follow this advice, you will naturally use params[:id] in TeamsController, but params[:team_id] in MembersController.
(Some people even go on to say that you shouldn't use helpers at all. For facilitating presentation (custom links, buttons, tables, etc), they propose to use Presenter pattern. But you don't have to listen to them. :))
It is not the best thing to do, but to accomplish that you can do the following:
def current_team
#current_team ||= Team.find(params[:team_id].presence || params[:id])
end
Documentation about the Object.presence method:
http://api.rubyonrails.org/classes/Object.html#method-i-presence
#SergioTulentsev is right, you shall not set instance variables in helpers, only in controllers.
I'm assuming you have other resources besides just Team. Rails is going to use the :id param for all of your resources. You will need to look into customizing the routes for your teams#show action. Easier in Rails 4 than in Rails 3.
Have a look at this post for the gory details: Change the name of the :id parameter in Routing resources for Rails
I wouldn't do params[:team_id] || params[:id], because of course in some controller contexts you'd get an id parameter that represents the id for something other than a Team. Assuming that the /teams/:id route is handled by the TeamsController, then you could do the following (to keep your method in ApplicationController and avoid repeating yourself in different controllers):
def current_team
id = controller_name == "teams" ? params[:id] : params[:team_id]
#current_team ||= Team.find(id)
end
Alternatively, you could change your routes so that the url to show a Team is /teams/:team_id and leave your helper as-is, but that would go against the grain of Rails routing conventions.
I'm new to Rails (I've worked in MVC but not that much) and I'm trying to do things the "right" way but I'm a little confused here.
I have a site navigation with filters Items by different criteria, meaning:
Items.popular
Items.recommended
User.items
Brand.items # by the parent brand
Category.items # by a category
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
Either I have an action in ItemsController for every filter (big controller) or I put it in ItemsController BrandsController, CategoriesController (repeated logic), but neither provides a "clean" controller.
But I don't know witch one is better or if I should do something else.
Thanks in advance!
You're asking two separate questions. Items.popular and Items.recommended are best achieved in your Item model as a named scope This abstracts what Xavier recommended into the model. Then in your ItemsController, you'd have something like
def popular
#items = Item.popular
end
def recommended
#items = Item.recommended
end
This isn't functionally different than what Xavier recommended, but to me, it is more understandable. (I always try to write my code for the version of me that will come to it in six months to not wonder what the guy clacking on the keyboard was thinking.)
The second thing you're asking is about nested resources. Assuming your code reads something like:
class User
has_many :items
end
then you can route through a user to that user's items by including
resources :users do
resources :items
end
in your routes.rb file. Repeat for the other nested resources.
The last thing you said is
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
If what I've said above doesn't solve this for you (I think it would unless there's a piece you've left out.) this sounds like a case for subclassing. Put the common code in the superclass, do the specific stuff in the subclass and call super.
There's a pretty convenient way to handle this, actually - you just have to be careful and sanitize things, as it involves getting input from the browser pretty close to your database. Basically, in ItemsController, you have a function that looks a lot like this:
def search
#items = Item.where(params[:item_criteria])
end
Scary, no? But effective! For security, I recommend something like:
def search
searchable_attrs = [...] #Possibly load this straight from the model
conditions = params[:item_criteria].keep_if do |k, v|
searchable_attrs.contains? k
end
conditions[:must_be_false] = false
#items = Item.where(conditions)
end
Those first four lines used to be doable with ActiveSupport's Hash#slice method, but that's been deprecated. I assume there's a new version somewhere, since it's so useful, but I'm not sure what it is.
Hope that helps!
I think both answers(#Xaviers and #jxpx777's) is good but should be used in different situations. If your view is exactly the same for popular and recommended items then i think you should use the same action for them both. Especially if this is only a way to filter your index page, and you want a way to filter for both recommended and popular items at the same time. Or maybe popular items belonging to a specific users? However if the views are different then you should use different actions too.
The same applies to the nested resource (user's, brand's and category's items). So a complete index action could look something like this:
# Items controller
before_filter :parent_resource
def index
if #parent
#items = #parent.items
else
#items = Item.scoped
end
if params[:item_criteria]
#items = #items.where(params[:item_criteria])
end
end
private
def parent_resource
#parent = if params[:user_id]
User.find(params[:user_id])
elsif params[:brand_id]
Brand.find(params[:brand_id])
elsif params[:category_id]
Category.find(params[:category_id])
end
end
Note: I'm only posting this question so that others may find it if they ever need to, as I have found a good solution.
In my controller tests, I don't want to commit to the database, but I still want the controllers to use finder methods to get mock objects (by mocking the find method), but then I want to use dom_id on those mocks in assert_select, to verify that they're being displayed.
However, since they're non-saved objects, dom_id keeps returning new_object instead of object_1, object_2, et cetera.
Is there any quick way to get it to work? I really don't want to persist real records in tests.
Suppose you're using ActiveSupport::TestCase and FactoryGirl for building model objects, you can add this to your test/test_helper.rb file:
# Generate model with id - useful for assert_select with dom_id
def model(name, options = {})
#next_id = {} if #next_id.nil?
#next_id[name] = 1 unless #next_id.has_key(name)
m = build(name, options)
m.id = #next_id[name]
#next_id[name] += 1
m
end
Then, suppose you're using RR as your mocking library, you can do something like this:
test 'something' do
posts = [model(:post), model(:post)]
mock(Post).all { posts }
get :index
assert_select "##{dom_id(posts.first)}", posts.first.title
end
right now I am trying to generalize some of my code. So far it went well, I wrote a few mixins which I can dynamically add to Controllers or Models in order to get things done while obeying DRY.
But with my "Searchform-Helper" I hit a corner in which, right now, I am a bit clueless.
I have a mixin 'SearchIndexController' which adds the methods needed to search for data within a searchindex-table.
After including the mixin I can initialize search-actions within the according controller calling this method:
def init_searchaction(object, name=nil)
singular = object.to_s.classify
plural = singular.pluralize
name = "search_#{singular}".to_sym if name.nil?
unless self.respond_to?(name)
define_method(name) do
# init
success=false
#TODO
# >>> DRAW NEW ROUTE TO THIS ACTION <<<
# evaluate searchform input for Searchindex-Call
needle = params[:query]
success, x, notice = execute_search("#{singular}", needle)
# send selected/filtered data to page
respond_to do |format|
format.js {
render :update do |page|
page.call "sidx_updateSearchResultContentAtIdTag", "##{plural.downcase} tbody", "#{render x}" if success
page.call "sidx_updateNotice", success, "#{notice}"
page.call "sidx_stopSpinner"
end
}
end
end
else
logger.warn("#{__FILE__}:#{__LINE__}:#{self.to_s}: search-action for '#{self.class.name}' can not be created, it already exists!")
end
end
So lets say I have a User-Controller. Within the Userform I have the need to search for several objects. Lets assume I want to be able to search for users, departments and clients... with my mixin I'd just have to initialize the searchactions like this:
init_searchaction :user
init_searchaction :department
init_searchaction :client, :find_clients
these would create actions within the including controller that are called
search_user
search_department
find_clients
The only thing missing is a way to get a route for them. I don't want to have to define the route upfront. I just want to 'init_searchaction' and have the mixin create the necessary route.
So... would it be possible to add the route to the accoring search-action from withing the mixins init_searchaction method dynamically? I think the necessary code would be placed at the #TODO mark in the code example above. But I still haven't found out how to do it... I mean, actually I would be surprised if it would not be possible.
Would anyone have an idea as how to do this? Thanks in advance for any idea that leads to the solution!
You can add work around standart dynamic route
match ':controller(/:action(/:id(.:format)))'
change it to your goals and enjoy :)