How to prevent profile urls from colliding with other routes - ruby-on-rails

If I wanted user urls to look like
http://site.com/foobar
instead of
http://site.com/users/foobar
foobar would be the nickname of a user from the nickname column in the user model. How would I prevent users from registering top level routes? Like contact, about, logout, etc?
I can have a table of reserved names. So when a user registers a nickname, it would check against this table. But is there a more convenient approach?

if(Rails.application.routes.recognize_path('nickname') rescue nil)
# forbid using desired nickname
else
# nickname can be used -- no collisions with existing paths
end
UPD:
If any path seems to be recognized by the recognize_path then you've got something like:
get ':nick' => 'user#show'
at the end of your routes.rb which leads to the situation where any path will be routable. To fix this you have to use constraints. I'll show you an example:
# in routes.rb
class NickMustExistConstraint
def self.matches?(req)
req.original_url =~ %r[//.*?/(.*)] # finds jdoe in http://site.com/jdoe. You have to look at this regexp, but you got the idea.
User.find_by_nick $1
end
end
get ':nick' => 'users#show', constraints: NickMustExistConstraint
This way we put some dynamic into our routing system and if we've got a user with nick jdoe then route /jdoe will be recognized. If we haven't got a user with nick rroe than /rroe path will be unroutable.
BUT if I were you I would simply do two things:
# in User.rb
def to_param
nick
end
# in routing.rb
resources :users, path: 'u'
And it'll give me the ability to get paths like a /u/jdoe (which is quite simple and totally comply with REST).
In this case make sure you are searching your users via User.find_by_nick! params[:id] (yeah, it's still params[:id] although contains a title, unfortunately).

Related

Possible to Change Rails Routing Convention?

I'm wondering if it's possible to edit the default Rails routing convention to fetch a specific record based on a field that is not the ID?
For instance, instead of retrieving a specific record based on ID, with the verb/url combination:
GET /users/:id
Retrieve a specific record based on username, with the verb/url combination:
GET /users/:username
I don't see why this would be a problem theoretically, as long as usernames were required to be unique, but I'm having trouble understanding how to implement it based on the Rails Routing Guide.
I have gathered that I will need to add a line to my routes.rb file, to define a singular resource, just prior to:
resources :users
However, I'm having trouble understanding the syntax to accomplish this. Any help in understanding this would be greatly appreciated.
Yes it is possible and they are called Non Restful Routes in the rails documentation
A trivial example is doing the below in your routes.rb
get ':users/:show/:username', controller: "users", action: "show"
and in your UsersController you have a show action that looks like this:
def show
if params[:id].present?
#user = User.find(params[:id])
elsif params[:username].present?
#user = User.find_by(username: params[:username])
end
end
This way you support showing by id and username, if you want do disable support for either of them, modify the if clause as you wish
I think you are looking to change the to_param method like so:
class User < ActiveRecord::Base
def to_param
"#{id} #{name}".parameterize
end
end
This would give the url as: /user/id-name. If you want to get rid of the id before the name it gets a little more complicated. If you were just to remove it, it will more than likely break since ActiveRecord needs the id first for finds.
To get around this I would suggest using FriendlyId gem: https://github.com/norman/friendly_id
There is also a RailsCast showing how to use Friendly_id but its pretty straight forward.
The routes does not care if it is an ID or username.
It is really how you find it in the controller.
Just in the user show controller:
def show
#user = User.find_by_username params[:id]
end

Case insensitive user names in Rails 3 routes

I am trying to allow users to access their accounts by their user name, regardless of how they capitalize it in the URL. So http://example.com/Username would go to the same page as http://example.com/username.
I am fine with the any other part of the URL being case sensitive (I know that's the standard), but I don't see a need to force a user to get the case of their user name correct in the URL.
Can this be done by adding certain settings in the routes.rb file alone?
I thought this would be straightforward but apparently it isn't.
I found a seemingly simple question here but there's only one answer that I think will help and it's too hacky (by the author's own admission)
I believe this isn't a routing issue and you can address it in your controller code:
User.where("lower(username) = lower(?)", params[:username]).first!
or
User.where("lower(username) = ?", params[:username].downcase).first!
or
User.find(:first, :conditions => [ "lower(username) = ?", params[:username].downcase ])
It should work fine to handle this behaviour in the controller, not in the routes.
# config/routes.rb
match ':username' => 'users#show'
# app/controllers/users_controller.rb
def show
username = params[:username]
user = User.find_by_username(username) || User.find_by_username(username.downcase)
# or something along these lines...
end
An even nicer solution might be to store some kind of slug identification for the user that is always downcased and ready to be used in URLs. You could have a look at the awesome friendly_id gem for that purpose.

Confused about routes setup -- Rails 3.1

I think I'm running across a conflict due to names:
Two models: store coupon
Url needed that will display coupons: http://localhost/coupons/:store_name ('coupons' is written in the url, not replaced with anything)
Controller name: coupons_controller
Here is what I have in my routes right now:
match '/coupons/:store_name' => 'coupons#index', :as => :stores
When I try to do redirect stores_path(store) in another controller, I get this error:
No route matches {:controller=>"coupons"}
Any clues? I'm new to rails so I bet it's a silly mistake.
UPDATE
Is there a central place to tell the dynamic _path() functions to use a specific url structure? i.e. Instead of having to do the following everywhere:
redirect_to stores_path(:store_name => store.store_name)
Instead using just:
redirect_to stores_path(store)
yes you can, redefine to_param in your model:
class Store < ...
def to_param
store_name
end
end
redirect_to stores_path(:store_name => store)
should work if it doesn't (cannot confirm right now), you should be able to do the (little hacky)
redirect_to stores_path+"?store_name=yourstorename"
Doing it the restful way, you should probably have something like this (in your routes):
resources :stores do
resources :coupons # this will give you e.g. /stores/:store_id/coupons for the coupons#index action
end
If you want to use the store name instead of the ID, just search SO for using "slug" or have a look here: getting a 'name' based URL in RESTful routes instead of an id based url or ID + Slug name in URL in Rails (like in StackOverflow)

Rails 3 Routing

I have a community_users model that I route in the following way:
resources :communities do
resources :users
end
This creates the route /communities/:id/users/.
I'd like to configure this route so that only the name of the community with the corresponding :id is shown.
In other words, if a community has an id of '1' and the name 'rails-lovers' - the route would read:
/rails-lovers
and not:
/communities/1/users/
You might want to check out the gem friendly_id
That will give you the clean URLs you are looking for.
I'm not quite sure if this is what you're looking for, but:
One option would be to create the route
match ':community_name' => 'users#show_users_for_community'
and then in the UsersController have
def show_users_for_community
#community = Community.find_by_name(params[:community_name])
<do what you need to do here>
end
I'm not sure if that route will match too many URLs or not -- it's a very general route. So if you do this, maybe put it low down in your routes file.

Rails - How to fix the generated URL to be a friendly URL

I have models that looks like
search.rb:
id eg: 101
name eg: San Francisco
cars.rb
id
name
The search controller redirects user to cars.
search_controller.rb
if search.search_type=='cars'
redirect_to :controller=>'cars', :action=>'index', :id=>search
end
A query to find list of cars from San Francisco looks like:
http://localhost/cars?id=101
I overrode to_param method in search.rb like:
search.rb
def to_param
normalized_name = name.gsub(' ', '-').gsub(/[^a-zA-Z0-9\_\-\.]/, '')
"#{self.id}-#{normalized_name}"
end
This works to some extent:
It generates URLs that look like:
http://localhost/cars?id=101-San-Francisco
However, I would like is to generate a URL that looks like
http://localhost/cars/San-Francisco
How would I go about doing that?
Do I fix the routes?
Thanks for your help.
This can be tricky if you're not prepared for it. The method that controls how a model shows up in the params is pretty straightforward, as you've identified, but that's only the half of it. Later you'll have to retrieve it, so you need to set up for that:
class Search < ActiveRecord::Base
before_save :assign_slug
def self.from_param(id)
self.find_by_slug(id)
end
def to_param
self.slug
end
protected
def assign_slug
self.slug = self.name.gsub(' ', '-').gsub(/[^\w\-\.]/, '')
end
end
This requires adding a slug column to your Search model in order to make it possible to look up a search by it. It may be a good idea to add an index, possibly a unique one, to help retrieve these later in an efficient manner.
In your controller you don't use find but from_param instead to do the retrieval.
You'll also need to add a custom route that takes this parameter:
match '/cars/:search', :to => 'cars#index', :as => 'cars_search'
As a note you should use the route path generator methods whenever possible because multiple routes may match the same parameters. For instance:
redirect_to cars_search_path(:search => search)

Resources