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.
Related
I am using the Friendly-Id gem to create pretty URL's. I would like to create case sensitive URLs and redirect all the other case versions of the same URL to the default one with a 301 status.
/profile/Jack-Wild # default URL
/profile/jack-wild # redirect to /profile/Jack-Wild
/profile/jaCk-Wild # redirect to /profile/Jack-Wild
Friendly_Id downcase's slugs and only matches and accepts downcase URLs, 404ing everything else.
The feasable solution would be to maintain a slug_case_sensitive ( Jack-Wild ) as well as the default downcase version of slug ( jack-wild ).
Then make Friendly_Id search by the slug column with params[:id].downcase and if params[:id] != slug_case_sensitive redirect to the preferred URL ( /profile/slug_case_sensitive ).
This is my idea on how to achieve it, with a few puzzles missing, hoping someone could help filling them in.
Modify the create and update slug methods, to create and update both versions of the slug. This would be only 1 method: set_slug:
# Sets the slug.
def set_slug(normalized_slug = nil)
if should_generate_new_friendly_id?
candidates = FriendlyId::Candidates.new(self, normalized_slug || send(friendly_id_config.base))
slug = slug_generator.generate(candidates) || resolve_friendly_id_conflict(candidates)
send "#{friendly_id_config.slug_column}=", slug
end
end
private :set_slug
Suppose slug is not downcased. Should changing:
send "#{friendly_id_config.slug_column}=", slug
with:
send "#{friendly_id_config.slug_column}=", slug.downcase
send "#{friendly_id_config.slug_column_case_sensitive}=", slug
be enough ? Also, how to add and set friendly_id_config.slug_column_case_sensitive option ?
And then, change user_path(#user) and <%= link_to "My profile", current_user %> to return the case sensitive URL. But no idea of a better way than user_path(id:#user.slug_case_sensitive) which is ugly and also I would have to change it everywhere and not sure if it can be done in 3rd party gems...
Any ideas on this would help and be much appreciated. Thanks!
Maybe different approach could be applied in this situation: most likely you'll use your Rails app behind Nginx as a proxy. Then it's possible to rewrite all requests for profiles with upcased letters in them to canonical variant. It will be faster than making additional DB requests and using whole Rails stack. Also it could be much simpler to implement.
You can override the method normalize_friendly_id in your model:
def normalize_friendly_id(value)
value.to_s.parameterize(preserve_case: true)
end
It relies on ActiveSupport parameterize which accepts a preserve_case option.
I am new to rails, I have one model named "Topic", I want to get users those are currently viewing topic index page.
How do I get those users, can anybody help me?
Thanks in advance
Here is article which describe how to track active/online users use Redis. You can use this approach to track users who are currently viewing your page.
I would do this like so:
Similar to Jack Hardcastle's suggestion, you could have a column in the users table in your db which stores the path part of a user's url, and another column called "last_seen_at"
:string last_viewed_page
:datetime last_seen_at
Then, in your ApplicationController you could have a before_filter like this:
#in ApplicationController
before_action :set_last_seen_at_and_last_viewed_page
def set_last_seen_at_and_last_viewed_page
if current_user && request.method == :get
current_user.update_attributes(:last_seen_at => Time.now, :last_viewed_page => request.path)
end
end
The reason i test that request.method == :get is that this will only include pages that they actually load in the browser, and see the address in their address bar, and not form submission urls which are usually hidden from the user.
So, now you have the data, it's easy to query. We can never know who is on a page at this exact moment, since we don't know what they are actually doing - they might have gone to the toilet or closed their computer. We can only say that "this page is the last page these people looked at, and they did so within the last 15 minutes" or whatever.
So, if we wanted to use that criteria, of the last 15 minutes, we could say
#current_path = request.path
#users_on_current_path = User.where(["last_seen_at > ? and last_viewed_page = ?", 15.minutes.ago, #current_path])
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
StackOverflow seems to have this style of routes for questions:
/questions/:id/*slug
Which is easy enough to achieve, both in routes and to_param.
However, StackOverflow seems to also redirect to that path when just an ID is passed.
Example:
stackoverflow.com/questions/6841333
redirects to:
stackoverflow.com/questions/6841333/why-is-subtracting-these-two-times-in-1927-giving-a-strange-result/
Same goes for any variation of the slug
stackoverflow.com/questions/6841333/some-random-stuff
Will still redirect to the same URL.
My question is: Is this type of redirection typically handled in the controller (comparing the request to the route) or is there a way to do this in routes.rb?
The reason I wouldn't think this possible in the routes.rb file is that typically, you don't have access to the object (so you couldn't get the slug based off the ID, right?)
For anyone interested, Rails 3.2.13 and also using FriendlyID
Ok, so I think I've got this.
I was looking into doing something with middleware, but then decided that's probably not the place for this type of functionality (since we need to access ActiveRecord).
So I ended up building a service object, known as a PathCheck. The service looks like this:
class PathCheck
def initialize(model, request)
#model = model
#request = request
end
# Says if we are already where we need to be
# /:id/*slug
def at_proper_path?
#request.fullpath == proper_path
end
# Returns what the proper path is
def proper_path
Rails.application.routes.url_helpers.send(path_name, #model)
end
private
def path_name
return "edit_#{model_lowercase_name}_path" if #request.filtered_parameters["action"] == "edit"
"#{model_lowercase_name}_path"
end
def model_lowercase_name
#model.class.name.underscore
end
end
This is easy enough to implement into my controller:
def show
#post = Post.find params[:post_id] || params[:id]
check_path
end
private
def check_path
path_check = PathCheck.new #post, request
redirect_to path_check.proper_path if !path_check.at_proper_path?
end
My || in my find method is because in order to maintain resourceful routes, I did something like...
resources :posts do
get '*id' => 'posts#show'
end
Which will make a routes like: /posts/:post_id/*id on top of /posts/:id
This way, the numeric id is primarily used to look up the record, if available. This allows us to loosely match /posts/12345/not-the-right-slug to be redirected to /posts/12345/the-right-slug
The service is written in a universal fashion, so I can use it in any resourceful controller. I have't found a way to break it yet, but I'm open to correction.
Resources
Railscast #398: Service Objects by Ryan Bates
This Helpful Tweet by Jared Fine
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).