In Rails the default routes use the internal database id to identify the resource, so you end up with routes like:
/user/1/widget/4
It's possible to change these to use something other than :id easily enough so that you could have routes like:
/user/bob/widget/favorites
But is there a way to have both available? I ask because in my case I'm using the route to create a unique id for use with an external service, but I'd like them to be based on a field other than id because it's more useful to pass these alternative ids to the external service.
I can of course build something custom, but we currently have some code that works as follows (with other convenience functions on top; this is the core functionality) to get most of the functionality I would have to build 'for free' from Rails:
class PathIdParser
def initialize
#context = Application.routes
end
def parse(path)
#context.recognize_path(path)
end
def build(route, params)
#context.named_routes[route].format(params)
end
end
Obviously the build function is easy enough to work with to use other routes by just changing the values passed into the params hash, but is there a way I can get parse to use these alternative fields to look up resources by, since recognize_path seems to work based on the values returned by to_param.
In routes.rb
get 'user/:username/widget/favourites', to: 'users#favourites'
This would route 'user/bob/widget/favourites' to the favourites action of the UsersController and you could access the username via
#username = params[:username]
Use the method to_param() in your model.
It returns a String, which Action Pack uses for constructing an URL to this object. The default implementation returns this record’s id as a String, or nil if this record’s unsaved.
class User < ActiveRecord::Base
def to_param
name
end
end
user = User.find_by_name('Richard')
user_path(user) # => "/users/Richard"
Related
I need to handle a particular case of generating email views with URLs constructed from non-persisted data.
Example : assume my user can create posts, and that triggers a post creation notification email, I'd like to send the user an example of fake post creation. For this, I am using a FactoryGirl.build(:post) and passing this to my PostMailer.notify_of_creation(#post)
In everyday Rails life, we use the route url_helpers by passing as argument the model itself, and the route generator will automatically convert the model into its ID to be used for the route URL generation (in article_path(#article), the routes helper converts #article into #article.id for constructing the /articles/:id URL.
I believe it is the same in ActiveRecord, but anyways in Mongoid, this conversion fails if the model is not persisted (and this is somewhat nice as it prevents the generation of URLs that may not correspond to actual data)
So in my specific case, URL generation crashes as the model is not persisted:
<%= post_url(#post_not_persisted) %>
crashes with
ActionView::Template::Error: No route matches {:action=>"show", :controller=>"posts", :post_id=>#<Post _id: 59b3ea2aaba9cf202d4eecb6 ...
Is there a way I can bypass this limitation only in a very specific scope ? Otherwise I could replace all my resource_path(#model) by resource_path(#model.id.to_s) or better #model.class.name but this doesn't feel like the right situation...
EDIT :
The main problem is
Foo.new.to_param # => nil
# whereas
Foo.new.id.to_s # => "59b528e8aba9cf74ce5d06c0"
I need to force to_param to return the ID (or something else) even if the model is not persisted. Right now I'm looking at refinements to see if I can use a scoped monkeypatch but if you have better ideas please be my guest :-)
module ForceToParamToUseIdRefinement
refine Foo do
def to_param
self.class.name + 'ID'
end
end
end
However I seem to have a small scope problem when using my refinement, as this doesn't bubble up as expected to url_helpers. It works fine when using te refinement in the console though (Foo.new.to_param # => 59b528e8aba9cf74ce5d06c0)
I found a way using dynamic method override. I don't really like it but it gets the job done. I am basically monkeypatching the instances I use during my tests.
To make it easier, I have created a class method example_model_accessor that basically behaves like attr_accessor excepts that the setter patches the #to_param method of the object
def example_model_accessor(model_name)
attr_reader model_name
define_method(:"#{model_name}=") do |instance|
def instance.to_param
self.class.name + 'ID'
end
instance_variable_set(:"##{model_name}", instance)
end
end
Then in my code I can just use
class Testing
example_model_accessor :message
def generate_view_with_unpersisted_data
self.message = FactoryGirl.build(:message)
MessageMailer.created(message).deliver_now
end
end
# views/message_mailer/created.html.erb
...
<%= message_path(#message) %> <!-- Will work now and output "/messages/MessageID" ! -->
I am using string as id in routes (e.g. resource/:id ) but id can also be 'new' (a method in my Controller) which rather than showing the resource with id=new, directs to create new resource. How can I restrict users from choosing id=new while creating new resource?
I can think of three solutions where you can set string instead of id
First: set a combination of the id and the attribute_name, add
in your model
def to_param
return [self.id, self.attr_name].join('-')
end
Second: prevent the user of adding any action method in your controller (that's safer than restricting only the "new" method, you may add other get methods in the future)
validates :attr_name, exclusion: { in: YourController.action_methods.to_a }
Third:
use friendly_id gem
Try to use exclusion validation in the model, http://guides.rubyonrails.org/active_record_validations.html#exclusion
In config.rb, change your route to:
resources :resources
You'll get the routes you need. I have a feeling you'll soon need some of the others that come with it, like create and edit.
Edit: to make life easier, in your model:
def to_param
return self.my_string_id
end
Where my_string_id is the string you are using in the URL as the identifier. That will make the URL use that as the :id param instead of the numeric ID.
See: http://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default
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
When I have a path like this:
user_messages_path(current_user)
it generates following URL:
users/1/listings
I am storing in database users' unique codes, so I wouldn't like to display URLs like
users/ID/listings
but more like
users/CODE/listings
How I need to update routes for using paths with users' codes?
Thanks
user_messages_path(current_user)
is a shortcut for:
user_messages_path(current_user.to_param)
which generally does:
user_messages_path(current_user.id)
You can:
pass any string you want: user_messages_path('foo')
or override to_param in your model
Just beware to update your code responsible to retrieve the object from the params.
in user.rb
def to_param
uuid # or whatever attribute you want to use instead of the id
end
in the controllers instead of User.find(params[:id]):
User.find_by_uuid!(params[:id]) # adjust to the attribute name used in to_param
You can use a custom route, something like the following.
get 'users/:code/:listings' => 'listings#index', as: :user_messages
Then in your params there key :code will return that part of the url e.g. params[:code]
In your controller you could use the following
Rails 3.2
User.find_by_code(params[:code])
Rails 4
User.where(code: params[:code])
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)