Can I have the same URL, but different dynamic segments? - ruby-on-rails

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

Related

Rails new ActiveRecord build based on triangulation of two required 1:n resources?

(Sorry if this has been addressed before, can't find it.)
Let's say I've got three tables. I'll keep it simple (P = Post, C = Comment and U = User, but not what I'm actually developing): P ||-> C <-|| U, where P can have many Cs, and U can have many Cs. I've got my resource routes setup as Ps/[:p_id]/Us/[:u_id]/cs/[:c_id]. I need to create a /new C. From my understanding, typically if I was only building C from only P or U, I would just generate it from an P.c.build/U.c.build. But since I need both, and neither A nor C are directly hierarchical to each other, I'm trying to understand how to do this. I need three things:
Appropriate *_path helper generated somehow with a new_p_u_c(#P, #U)
Necessary .build alternative for triangulating both P and U with C.
Necessary form_with:
model: with #P and #U
url: *_path create helper (p_u_cs(#P, #U)).
Question: Do I use hidden input fields to store P and U, is will that be automatically generated within the forms_with's <form>?
If anyone finds this, hope it helps.
It was indeed as simple as I had posed it: new_p_u_c_path(p_id: #P.id, u_id: #U.id). I wasn't aware that the paths helper was capable of dynamically accepting multiple arguments. Precautions:
Pass them in the exact order as the route resources or (preferably) pass in the named arguments as shown above.
If you don't use named arguments above and you're using a nice URL gem (I'm using friendly_id), you'll need to pass in the #P.id specifically instead of #P, or else the path helper won't be able to find it.
I only used #P.c.build for this. It worked, but I'm not sure if it is even necessary at this point, since the only thing I needed was the #P.id, so see below...
I passed in my form_with(model: #C, ...)
Because the /new route already contained the #P.id and #U.id, the hidden_field automatically accepted p_id and u_id since I had my models setup with appropriate chaining logic.
p_u_cs_path worked without passing any additional model arguments (again, see 3.1. above).
I don't think this is necessary, now I think about it. The URL passed in 3.2. above should already include the IDs that rails will automatically parse.

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

ruby on rails iterating params

I have a client that is sending params such as age, gender, name and so on.
I need to retrieve data from the table based on the params, but first I need to check for the presence of the param(to avoid a null param and therefore an empty result). The params are working as filters, so they can be triggered or they can be left blanck.
What I am doing right now is
#retieve = Student.all
unless params[:age].nil?
#retrieve = #retrieve.where(age: params[:age])
end
unless params[:gender].nil?
#retrieve = #retrieve.where(gender: params[:gender])
end
and so on for every param I receive. This way I check if the filter has been selected, and if it has I use the selection as a parameter for the query
It works, but as Ruby is known for the DRY statement, I am pretty sure someone out there knows a better way for putting this and to make this flexible.
Thank you for whatever answer or suggestion you will provide!
This will work best if all of these filters were in a subhash of params that you can iterate over without including unwanted parameters (eg the :action and :controller parameters that rails adds)
Once you've done that you could do
(params[:filters] || {}).inject(Student.all) do |scope, (key, value)|
scope.where(key => value)
end
There's a few ways to do this sort of thing and you have options for how far you want to go at this stage.
Two big things I'd consider -
1) Make nice scopes that allow you to send a param and ignore it if it's nil. That way you can just append another scope for each param from the form and it will be ignored without using if or unless
2) Move the search into a separate class (a concern) to keep your controller clean.
Here's a blog post that talks about some of the concepts (too much to post in this answer). There is lots of info on the web about this, I searched on the web under "rails search filter params concern" to get an example for you.
http://www.justinweiss.com/blog/2014/02/17/search-and-filter-rails-models-without-bloating-your-controller/

Rails Routes - Select controller dynamically

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.

How to match rails route based on URL ending with an ID?

It seems really easy to match routes using the '/' as a separator, but I'd like to match using a hyphen. Only problem is that there could be hyphens in the dynamic text that I want to appear before the ID:
Format: http://www.cities.com/-
Example: http://www.cities.com/los-angeles-california-123
match '/:description-:id' => 'cities#show' does not work because the :description text can contiain ids.
Is there any way for me to match this format? I've done this before in .NET using Regex, but loving my new life as a Ruby Dev.
Thanks!
I'd avoid using the routes for this: just pass the param :id and split it in the controller.
E.g.
City.find param[:id].split('-').last
More importantly make sure you are able to generate proper ulrs by overriding to_paramin the model:
def to_param
"#{self.name.split(' ').join('-')}-#{self.id}" # FIXME need extra sanity checks
end
One last trick, unless you do need the id as the last arg, you'd better put it at the beginning, e.g. 123-los-angeles, this way you do not need to "parse" the id in your controller, Model.find '123-los-angeles' works out of the box.
Cheers,

Resources