Accessing URI params in the controller - ruby-on-rails

(Learning RoR on my own, so pls forgive me if this is an obvious question)
I have an app to track books stored on shelves in libraries. A request should come in like:
GET books/library_id => shows all books on every shelf in a library
GET books/library_id/shelf_id => shows all books on one shelf in a library
GET books/library_id/shelf_id/book_id => shows a particular book
POST would use similar formats, except I will be using JSON in the POST body to supply the information (author, pub date, length, etc) for the book, shelf, or library
My question is, [:params] passed in to my controller seems to hold query (anything after a ?) and POST body parameters, but not the URL, which I need to parse and use to determine what to show. Is there a way to get this out of the parameters? I'm trying to avoid something like GET /books/?library_id/shelf_id

You can set up a route so that params will contain specific URL fragments in addition to the query string and/or post data.
In config/routes.rb:
get 'books/:library_id(/:shelf_id(/:book_id))', to: 'books#show'
In app/controllers/books_controller.rb:
class BooksController < ApplicationController
def show
library_id = params[:library_id]
shelf_id = params[:shelf_id] # may be nil
book_id = params[:book_id] # may be nil
# TODO: Do something with library_id, shelf_id (if present),
# book_id (if present).
end
end
Alternatively, if you wanted to process the URL with some very custom logic, you could have a wildcard route like get 'books/*sometext', to: 'books#show'. Then, in your controller action you could manually parse params[:sometext]. This would be considered "not the Rails way" but it's there if you need complete flexibility.
Finally, maybe it is worth mentioning that in your controller action you can get information about the request such as request.path, request.fullpath, request.url. But it doesn't sound like you need this in your case.

Related

How to get an organized search URL result with slashes orders (/)?

I want a search section on the "index" from books_controller with some filter options from different authors, categories and other attributes. For example, I can search for a category "romance" and max pages = 200. The problem is that I'm getting this (with pg_search gem)
http://localhost:3000/books?utf8=%E2%9C%93&query%5Btitle%5D=et&button=
but I want this:
http://localhost:3000/books/[category_name]/[author]/[max_pages]/[other_options]
In order that if I want to disable the "max_pages" from the same form, I will get this clean url:
http://localhost:3000/books/[category_name]/[author]/[other_options]
It'll work like a block that I can add and remove.
What is the method I should use to get it?
Obs: this website, for example, has this kind of behavior on the url.
Thank you all.
You can make a route for your desired format and order. Path parameters are included in the params passed to the controller like URL parameters.
get "books/:category_name/:author/:max_pages/:other_options", to: "books#search"
class BooksController < ApplicationController
def search
params[:category_name] # etc.
end
end
If other options is anything including slashes, you can use globbing.
get "books/:category_name/:author/:max_pages/*other"
"/books/history/farias/100/example/other"
params[:other]# "example/other"
So that gets you the basic form, now for the other you showed it could just be another path since the parameter count changed.
get "books/:category_name/:author/*other_options", to: "books#search"
params[:max_pages] # nil
If you have multiple paths with the same number of parameters, you can add constraints to separate them.
get "books/:category_name/:author/:max_pages/*other", constraints: {max_pages: /\d+/}
get "books/:category_name/:author/*other"
The Rails guide has some furth information, from "Segment Contraints" and "Advanced Constraints": http://guides.rubyonrails.org/routing.html#segment-constraints
If the format you have in mind does not reasonably fit into the provided routing, you could also just glob the entire URL and parse it however you wish.
get "books/*search"
search_components = params[:search].split "/"
#...decide what you want each component to mean to build a query
Remember that Rails matches the first possible route, so you need to put your more specific ones (e.g. with :max_pages and a constraint) first else it might fall through (e.g. and match the *other).

How to redirect old url to new url in rails using just routes?

The old sitemap of my application has already been indexed to Google now. For some one visiting my rails app with old url shouldn't go to 404.
The old url looked like this
/search?sub_category=210
Now after making them friendly, it looks like this:
/search?sub_category=milling-and-drilling.
I tried redirecting it from controller but it causes too much issues on other things. Such as filters which are using the same params. Is there a way I can do it from routes file?
Instead of using redirect_to route_path(id) you would do redirect_to route_path(object.find_by_id(id).name)
How to redirect old url to new url in rails using just routes?
AFAIK (As far as I know), no. (if through params)
The main job of routes.rb is to determine what "code" will handle the request, particularly that which matches request.url and the request.method. It does not concern yet of the request parameters nor its values: these would be handled in the controllers itself. Although, you can route based on parameters (or any information about the "request") through a routes constraint
Alternative Solution:
Instead of finding by ID, now find by "Slug name or ID".
In your models, particularly in this specific example of yours, add something like:
class SubCategory < ApplicationRecord
# if you're using a gem for the "friendly" urls, you don't need this method, and just use theirs.
def self.friendly_find!(friendly_id)
# this may not necessarily be `name`, maybe `slug`? or depending on your case
find_by(name: friendly_id) || find(friendly_id)
end
end
And in your controllers, just replace wherever you're finding a record (maybe also to other models not just SubCategory as needed be), particularly in your search action:
def search
sub_category = SubCategory.friendly_find!(params[:sub_category])
end
NOTE: friendly_find! will raise an Error if no record is found.

Changing urls in ruby on rails depending on different conditions

I'm new to ruby on rails....I wanted to know if there is a way to change the URL displayed depending on the client's response. I mean... here's an example:
I'm making a project showing listings in various places...
Now in general I have a home page, a search page, and a detail page for listings. So, respective URLs are officespace/home, officespace/search?conditions, officespace/detailpage?id=(controller-officespace)[&Conditions eg.---price,size,place,type...]
So, every time the client makes a request for search, the same URL is shown, of course with the given conditions.
Now I want that if the client asks for only the place and mentions nothing about size, price, etc., the url should be /listing/location_name.
If he mentions other conditions, then it'll be listing/(office_type)/size(x sq feet)_office_for_rent_in_locationname)
B.t.w. (I already have a controller named listings and its purpose is something else.)
And so on ........... Actually, I want to change URLs for a number of things. Anyway, please help me. And please don't refer me to the manuals. I've already read them and they didn't give any direct help.
This is an interesting routing challenge. Essentially, your goal is to create a special expression that will match the kinds of URL's you want to display in the user's browser. These expressions will be used in match formulas in config/routes.rb. Then, you'll need to make sure the form actions and links on relevant search pages link to those specialized URL's and NOT the default pages. Here's an example to get started:
routes.rb
match "/listing/:officeType/size/:squarefeet/office_for/:saleOrRent/in/:locationName" => "searches#index"
match "/listing/*locationName" => "searches#index"
resources :searches
Since you explicitly mentioned that your listings controller is for something else, I just named our new controller searches. Inside the code for the index method for this controller, you have to decide how you want to collect the relevant data to pass along to your view. Everything marked with a : in the match expressions above will be passed to the controller in the params hash as if it were an HTTP GET query string parameter. Thus we can do the following:
searches_controller.rb
def index
if params[:squarefeet] && params[:officeType] && params[:locationName]
#listings = Listing.where("squarefeet >= ?", params[:squarefeet].to_i).
where(:officeType => params[:officeType],
:locationName => params[:locationName])
elsif params[:locationName]
#listings = Listing.where(:locationName => params[:locationName])
else
#listings = Listing.all
end
end
And to send the user to one of those links:
views/searches/index.html.erb
<%= link_to "Click here for a great office!", "/listing/corporate/size/3200/office_for/rent/in/Dallas" %>
The above example would only work if your Listing model is set up exactly the same way as my arbitrary guess, but hopefully you can work from there to figure out what your code needs to look like. Note that I wasn't able to get the underscores in there. The routes only match segments separated by slashes as far as I can tell. Keep working on it and you may find a way past that.

How can I make rails route helpers always use to_param to generate the path, even when I just pass in an ActiveRecord ID?

So, I'm implementing a pretty/SEO-friendly URL scheme for my rails app. I have a model called Artist, and I would like the Rails artist_path helper to always generate the friendly version of the path.
In my routes.rb file, I have the following line:
get 'artists/:id(/:slug)', :to => 'artists#show', :as => 'artist'
If the slug is left out, or is incorrect (it's calculated by the artist name), the controller 301 redirects to the correct URL. However, for SEO reasons, I want to ensure that all links internal to my site have the correct URL to start with.
The Artist model has the two following (very simple) functions to allow this to work:
def slug
name.parameterize
end
def to_param
"#{id}/#{slug}"
end
If I call artist_path with an artist object, this works as intended:
> app.artist_path(Artist.find 1234)
=> "/artists/1234/artist-name"
However, when I use call it with just the ID, it does not seem to use to_param at all:
> app.artist_path(id: 1234)
=> "/artists/1234"
tl;dr: How can I force Rails to always instantiate the object and call to_param on it when artist_path is called, even when only the ID is specified?
As far as I'm aware, the reason why what you're asking won't work is because when you pass in values to the built-in/automatic URL helpers (like an ID, per your example), those values just get used to "fill in the blanks" in the route URL.
If you pass an array, the values from the array will get used in order until the URL is filled in. If you pass a hash, those properties will get replaced into the URL. An object, like your model, will use it's to_param method to fill in the values... etc.
I understand your concern regarding "having knowledge of the limitations of that model," however, this behavior is standard in Rails and I don't believe it would really throw anyone. When you pass in an ID, as you do in your example, you're not telling the URL helper to "lookup a record via the model using this ID," you're simply telling it to "replace ':id' in the URL string with the ID you're providing." I'm fairly certain the built-in URL helpers wouldn't even know how to lookup the record - what model to use, etc. - other than maybe inferring from the route/controller name.
My best suggestion is to always use the instantiated model/record. If you were hoping the URL Helper would look that up for you, then there's no extra overhead as far as querying the database goes. If there's some additional reason you want to avoid instantiating the record yourself, I'd be glad to hear it and possibly provide other suggestions.

In RESTful design, what's the best way to support different kinds of GETs?

In a current project I need to support finding a User by login credentials and also by email address. I know that in RESTful design you use a GET to find resources. In Rails...
GET /users # => UsersController.index -- find all the users
GET /users/1 # => UsersController.show -- find a particular user
But I also need something akin to...
GET /users?username=joe&password=mysterio
GET /users?email=foo#bar.com
Is it conventional to add additional routes and actions beyond index and show?
Or is it more common to put conditional logic in the show action to look at the params and detect whether we're finding by one thing or another?
There's a similar issue with PUT requests. In one case I need to set a User to be "active" (user.active = true), and in another case I just need to do a general form-based editing operation.
Thanks guys. Eventually I'm going to figure out this REST stuff.
I'm new to SO, so I can't comment, but the checked green answer is not RESTful.
In a RESTful world, your controller grabs all the parameters and passes it to the model layer for processing. Typically, you shouldn't create another action.
Instead, you should do do something like this:
def show
#user = User.find_by_login_or_email(params[:user])
... #rest of your action
end
Your model can have a method like this:
class User
self.find_by_login_or_email(params)
return find_by_login(params[:login]) unless params[:login].blank?
return find_by_email(params[:email]) unless params[:email].blank?
nil #both were blank
end
end
Your view could look like this:
<%= f.text_field :user, :email %>
or
<%= f.text_field :user, :login %>
Note: untested code, so may be buggy...but the general line of thinking is usually not to create new actions for every one-off rule. Instead, look to see if you can push the logic into the models. If your controllers start to have too many non-standard actions, then it may be time to re-evaluate your domain modeling, and perhaps it's refactor the actions to some new models.
ps: you should never pass in passwords via a GET like that
I don't know how much of this is convention, but this is what I would do. I
would add another action, as long as it's specifically related to that
resource. In your example, show is a find by userid, so it makes sense as
another action on UsersController. You can turn it into a sentence that makes
sense, "get me the user with this email address"
For the other one, GET /users?username=joe&password=mysterio, I would do
that as another resource. I assume you're thinking that action would log in
the user if the password were correct. The verb GET doesn't make sense in that
context.
You probably want a 'session' resource (BTW, this is how restful_auth works).
So you would say "create me a session for this user", or something like POST
/sessions where the body of the post is the username & password for the user.
This also has the good side effect of not saving the password in the history
or letting someone capture it on the HTTP proxy.
So your controller code would look something like this:
class UsersController < ActionController::Base
def show
#user = User.find_by_id(params[:id])
# etc ...
end
def show_by_email
#user = User.find_by_email(params[:email)
end
end
class SessionsController < ActionController::Base
def create
# ... validate user credentials, set a cookie or somehow track that the
# user is logged in to be able to authenticate in other controllers
end
end
You would set up your routes like this:
map.connect "/users/byemail", :controller => "users", :action => "show_by_email", :conditions => { :method => :get }
map.resources :users
map.resources :sessions
That will get you URLs like /users/byemail?email=foo#example.com. There are
issues with encoding the email directly in the URL path, rails sees the '.com'
at the end and by default translates that into the :format. There's probably a
way around it, but this is what I had working.
Also like cletus says, there are ways to make your route match based on the format of the parts of the URL, like all numbers or alphanumeric, but I don't know off hand how to make that work with the dots in the url.
The first thing you can do is make your GETs as smart as possible. In your example, this can be handled programmatically. The argument can be processed this way:
Is a number? It's a userid;
Has a # in it? It's an email;
Otherwise? It's a username.
But I assume that you're not just talking about this example and want something to handle the general case rather than just this specific one.
There are basically two ways of dealing with this:
Add extra path information eg /users/email/me#here.com, /users/name/cletus; or
Be more specific in your "top-level" URL eg /user-by-email/me#here.com, /user-by-name/cletus.
I would handle it programmatically if you can.
Regarding the "ByEmail" request, have you considered creating a new email resource.
GET /email/foo_at_bar_dot_com
The response could contain a link to the related user.
I see so many people trying to apply RESTful design principles to their URL structure and then mapping those urls to procedural handler code. e.g. GET = Show, or is it GET = Index or ShowByEmail. By doing this you are really just pretending to do a RESTful design and then trying to create a mapping between a resource oriented URL space and procedurally oriented implementation. That is really hard to do and the procedural nature keeps leaking out into the URLs.
Resource oriented design often requires a very different way of thinking about problems that we are used to and unfortunately many of the frameworks out there keep sucking us back into the RPC model.
You might be able to set up different routes for different tasks. So for this case you could have one route to a method in UserControll dedecated to getting a user by email, and another for getting the information by credentials.

Resources