Rails Routes - Select controller dynamically - ruby-on-rails

Lets say, for the sake of the question, that I have two user types: type1 & type2. I want Rails to use a controller/module depending on the type of user that is being displayed. For example:
If User(id: 1, type: 'type1') has type1 and User(id: 2, type: 'type2') has type2, going to:
/users/1
would select the Type1::UsersController. And going to:
/users/2
would select the Type2::UsersController.
This will allow me to use different controllers and views for each type.
Note: I don't want the type to be displayed in the URL, I want it to be dynamic.

As GoGoCarl says, this isn't really the Rails way to do things. That said, it's not that difficult to get it to work. You can do something like this in routes.rb:
get 'users/:id', to: 'type1/users#show', constraints: lambda { |request|
_id = request.fullpath.gsub('/users/','').to_i
# Note: there might be an easier way to get ID from the request object
User.find(_id)._type == 'type1'
}
get 'users/:id', to: 'type2/users#show', constraints: lambda { |request|
_id = request.fullpath.gsub('/users/','').to_i
User.find(_id)._type == 'type2'
}
I've renamed your type field to _type in my example (because Rails uses type for Single Table Inheritance). I've tested this and it works as desired.

This is possible, but you'd be doing a lot of (probably) unnecessary fighting against the Rails way. I would think you would want one controller as there's probably quite a bit of shared logic (such as saving, deleting, creation, etc).
To answer your question (because I hate when people leave recommendations instead of answers), then you'll need to create a Module that extends Routing, which will allow you to do custom matching. From there, you can do your checks and route appropriately. Here's an example.
That said, a better route to go (no pun intended) would be to have one controller which has a centralized method that can select views.
def find_view view_name
"#{view_name}#{#user.type}"
end
So, a call to render find_view('new') would attempt to render a view named "new-type1." You can put all your type1 user-specific logic in that view. Same for user type2.
Again, since I would think there would be much overlap in your user code, you may want to push this find_view method to a helper class so you can call it from your views, and do things like render specific partials instead based on the user type. That will allow for more code re-use, which is never a bad thing.
Once you get your head wrapped around having a single controller, there are a number of simple ways that you can push user-type-specific code to different avenues -- the views method explained above, you can push all your relevant code to separate helpers which are dynamically called based on the user type, and I'm sure there's more (probably better ones). But all those have one major thing in common -- you'll be fighting Rails a LOT less, and you will have less duplicate code, if you succumb to letting Rails have its way with one route, one controller.
Good luck, hope that helps.

Related

URL Structure for Comparing Items

What is the best way to structure a route for comparing multiple items?
Here's the URL example: https://versus.com/en/microsoft-teams-vs-slack-vs-somalia
How to achieve this in routes.rb file? Cannot really find anything in Internet regarding ruby gems. The only thing I can think about is url with optional params, however what if the number of params is unlimited?
you're going to have to parse the a-vs-b-vs-c yourself.
So in routes.rb, you'll have something like:
get 'compare/:compare_string', to 'compare#show'
then you'll get a parameter compare_string that you'll have to parse:
#in compare_controller.rb
def show
compare_items = params[:compare_string].split('-vs-')
# generate the comparison from the compare_items array
end
First - you probably shouldn't allow unlimited #'s of parameters in practice. Even something like 100 might break your page and/or cause performance issues and open you up to DOS attacks. I'd choose some kind of sensible/practical limit and document/enforce it (like 10, 12 or whatever makes sense for your application). At around 2k characters you'll start running into URL-length issues.
Next - is there any flexibility in the URL? Names tend to change so if you want URL's to work over time you'll need to slug-ify each of them (with something like friendly-id) so you can track changes over time. For example - could you use an immutable/unique ID AND human-readable names?
In any case, Rails provides a very flexible system for URL routing. You can read more about the various options / configurations with their Rails routing documentation.
By default a Dynamic Segment supports text like in your example, so (depending on your controller name) you can do something like:
get 'en/:items', to: 'items#compare'
If it's helpful you can add a custom constraint regexp to guarantee that the parameter looks like what you expect (e.g. word-with-dashes-vs-another-vs-something-else)
get 'en/:items', to: 'items#compare', constraints: { items: /(?:(?:[A-Z-]+)vs)+(?:[A-Z-]+)/ }
Then, in your controller, you can parse out the separate strings however you want. Something like...
def compare
items = params[:items].split('-vs-')
end

Can I have the same URL, but different dynamic segments?

Is it possible to use the same URL, but with different dynamic segments?
My issue is: I want to be able add object A to objects B and C. So I want to have 2 Rails routes, A/new/:b_id AND A/new/:c_id. Which I tried.
In my routes.rb:
controller :A do
get 'A/new/:b_id', to: 'A#new', as: :new_b_a
get 'A/new/:c_id', to: 'A#new', as: :new_c_a
end
Problem is that the value being passed into the new page is always params[:b_id]! (I can print out the value from the URL using params[:b_id].)
So it seems like maybe I can't have 2 similar routes with different dynamic segments..? If so, how would I do this?
A better way to accomplish this would be using nested resources.
You always get :b_id because Rails matches routes in the order they appear in your file. Since a B ID is an integer indistinguishable from a C ID, there's no way for it to know if you want one or the other.
But, since you do have Bs andCs already, and perhaps those also need to be created, shown, etc., you can differentiate their paths RESTfully, which is what Rails wants you to do.
# config/routes.rb
resources :bs do
resources :as
end
resources :cs do
resources :as
end
This will build you the paths you're creating manually, but turned around a bit:
/bs/:b_id/as/new
/cs/:c_id/as/new
As you can see, the paths now start with the object type you want to add an A too, so Rails can tell them apart. The helper methods generated for this look the same as the ones you're currently defining manually:
new_b_a_path(b)
new_c_a_path(c)
Both paths will route you to the AsController, and then you'll need to look up the correct B or C based on the parameter present:
# AsController#new
#parent = B.find(params[:b_id]) if params[:b_id]
#parent = C.find(params[:c_id]) if params[:c_id]
#a = parent.build_a # Assuming has_one or has_many from B and C to A
Rails has spent a long time developing a particular way to do this sort of thing. You can always dive in and do it a different way, but at best you'll be wasting effort, and at worst you'll be fighting the framework. The framework is less compromising and usually wins.
The routing system works by trying to match the current path to each of the registered routes, from top to bottom. The dynamic :b_id part means "anything that goes here in the path will be passed as a parameter called :b_id to the controller". So making a request to "A/new/anything" will always match the first route, and since you renamed the parameter to :new_b_a, that's how it's called in the params hash.
If you really want to use the same route, you'll need to pass an extra argument specifying the class you want to create the relationship with, though I'd not recommend doing that. It could be something like get 'A/new/:klass/:id', so in the controller you could match the parameter to the desired classes:
def new
case params[:klass]
when 'B' then # do stuff
when 'C' then # do stuff
else raise "Invalid class: #{params[:klass]}"
end
end

Controllers in Rails do not "care" how requests are executed

I was reading an article about Rails controllers, can you help me understand please what is meant by the following phrase:
"The best controller is Dilbert-esque: It gives orders without knowing (or caring) how it gets done."
Is it true, in your opinion?
If, for example, I am accessing the index page associated with the subjects controllers, I would define the index method in the subjects_controller.rb rigorously, so I am confused as to what they mean in the article, as I would have thought the opposite.
Any pointers, please?
Thank you and sorry if this is too interpretable. This is the original article: http://betterexplained.com/articles/intermediate-rails-understanding-models-views-and-controllers/
This article is talking about MVC architecture. What's important to take away from an article like this is the fact that Rails is best written with Fat Models and Thin Controllers. This means that you want to have the bulk of your methods/functions in your Model and want to have calls to the functions from your controller. Index is a bad example since typically you're not going to have a lot going on in there.
Your controller for index will typically look something like this
def index
#subjects = Subject.all
end
If you want to scope order for displaying your subjects though, you would do that in your model with a block as follows:
default_scope { order("id DESC") }
A less contrived example might look something like this: Say for example you have an app that accepts input, takes that input and tallies several counters based on what the user entered. Your controller might be named subject_tally and look like this:
def subject_tally
#subject = Subject.find(params[:id])
#subject.winnings += 1
#subject.total_matches += 1
#subject.win_percentage = #subject.winnings.to_f/#subject.total_matches
redirect_to subjects_path
end
THIS IS WRONG. This is a very fat controller and easily moved to the Model where it should be.
If written properly it would look something like this:
subjects_controller.rb: (The Controller)
def subject_tally
#subject = Subject.find(params[:id])
#subject.subject_tally
redirect_to subjects_path
end
subject.rb: (The Model)
def subject_tally
self.winnings += 1
self.total_matches += 1
self.win_percentage =winnings.to_f/total_matches
end
So as you can see, you make only one call from the controller and it "doesn't care" what is actually going on in the backend. It's literally there to pass a value (in this case, the ID of the subject in question) and direct you to another page, in this case, the index.
Furthermore, if you'll notice, you don't need to add that pesky #subject everywhere in your model's subject_tally function... you can reference the attributes of the object just by using self.winnings where you're assigning to an attribute. Ruby is smart enough to know the current subject the method applies to (since you called that function ON a subject from the controller) and in fact you don't even need the self. if you're just retrieving the attributes instead of assigning them... which is why we didn't need self before winnings.to_f or the last line's total_matches.
Very convenient, less code, less time, yay.
The best controller is Dilbert-esque: It gives orders without knowing
(or caring) how it gets done.
means that you should put less logic as you can in the controller,
the controller should only know what to call to get what it needs, and should not know how to carry out a certain action.
In the "Sandy Metz rules" for rails developers (http://robots.thoughtbot.com/sandi-metz-rules-for-developers), she says:
Controllers can instantiate only one object. Therefore, views can only
know about one instance variable and views should only send messages
to that object
only one object could seem a bit extreme, but makes the idea about how much business logic (no logic) you should put in the controller.

Different view for different subdomains in Rails

I am building a Rails application where I need to do a usability test of three different Views for the same application. My thought is to switch out the default view path depending on the subdomain.
For example, I'd like to be able to define the paths something like:
option1.mysite.com => views/option_1
option2.mysite.com => views/option_2
option3.mysite.com => views/option_3
I'd like to keep the Models and Controllers the same, but switch out the Views depending on the subdomain. What might be the best way to do this?
We do it something like this:
session[:site] = case request.subdomains.last
when "a" then "a"
when "b" then "b"
when "c" then "c"
end
That's part of a set_site method in our application controller. Every request checks to see if session[:site] is set; if not, it calls set_site to set it.
In your case, now you just need to introduce logic in your views to present things differently depending on the value of session[:site], but it's even better if your actual view HTML is the same and the major difference is in the CSS. Then you just load different CSS files depending on the value of session[:site].

break down a complex search query in Rails 3

I have a controller which has a lot of options being sent to it via a form and I'm wondering how best to separate them out as they are not all being used simultaneously. Ie sometimes no, tags, sometimes no price specified. For prices I have a default price set so I can work around with it always being there, but the tags either need to be there, or not. etc.
#locations = Location.find(params[:id])
#location = #locations.places.active.where("cache_price BETWEEN ? AND ?",price_low,price_high).tagged_with([params[:tags]).order(params[:sort]).paginate :page => params[:page]
I haven't seen any good examples of this, but I'm sure it must happen often... any suggestions? Also, even will_paginate which gets tacked on last should be optional as the results either go to a list or to a google map, and the map needs no pagination.
the first thing to do when refactoring a complex search action is to use an anonymous scope.
Ie :
fruits = Fruit.scoped
fruits = fruits.where(:colour => 'red') if options[:red_only]
fruits = fruits.where(:size => 'big') if options[:big_only]
fruits = fruits.limit(10) if options[:only_first]
...
If the action controller still remains too big, you may use a class to handle the search. Moreover, by using a class with Rails 3 and ActiveModel you'll also be able to use validations if you want...
Take a look at one of my plugins : http://github.com/novagile/basic_active_model that allows you to easily create classes that may be used in forms.
Also take a look at http://github.com/novagile/scoped-search another plugin more specialized in creating search objects by using the scopes of a model.

Resources