Pretty Paths in Rails - ruby-on-rails

I have a category model and I'm routing it using the default scaffolding of resources :categories. I'm wondering if there's a way to change the paths from /category/:id to /category/:name. I added:
match "/categories/:name" => "categories#show"
above the resources line in routes.rb and changed the show action in the controller to do:
#category = Category.find_by_name(params[:name])
it works, but the 'magic paths' such as link_to some_category still use the :id format.
Is there a way to do this? If this is a bad idea (due to some possible way in which rails works internally), is there another way to accomplish this? So that /categories/music, for example, and /categories/3 both work?

Rails has a nifty model instance method called to_param, and it's what the paths use. It defaults to id, but you can override it and produce something like:
class Category < ActiveRecord::Base
def to_param
name
end
end
cat = Category.find_by_name('music')
category_path(cat) # => "/categories/music"
For more info, check the Rails documentation for to_param.
EDIT:
When it comes to category names which aren't ideal for URLs, you have multiple options. One is, as you say, to gsub whitespaces with hyphens and vice versa when finding the record. However, a safer option would be to create another column on the categories table called name_param (or similar). Then, you can use it instead of the name for, well, all path and URL related business. Use the parameterize inflector to create a URL-safe string. Here's how I'd do it:
class Category < ActiveRecord::Base
after_save :create_name_param
def to_param
name_param
end
private
def create_name_param
self.name_param = name.parameterize
end
end
# Hypothetical
cat = Category.create(:name => 'My. Kewl. Category!!!')
category_path(cat) # => "/categories/my-kewl-category"
# Controller
#category = Category.find_by_name_param(param[:id]) # <Category id: 123, name: 'My. Kewl. Category!!!'>

If you don't want to to break existing code that relying on model id you could define your to_param like this:
def to_param
"#{id}-#{name}"
end
so your url will be: http://path/1-some-model and you still can load your model with Model.find(params[:id]) because:
"123-hello-world".to_i
=> 123

Although possibly more than you need, you may also want to look into 'human readable urls' support like friendly_id or one of the others (for instance, if you need unicode support, etc.) that are described here at Ruby Toolbox.

Related

rails path helper not recognized in model

In my rails application I have a teams model. My route.rb file for teams looks like this:
resources :teams
In my teams_controller.rb file the line team_path(Team.first.id) works however the team_path url helper is not recognized in my model team.rb. I get this error message:
undefined local variable or method `team_path' for # <Class:0x00000101705e98>
from /usr/local/rvm/gems/ruby-1.9.3-p392/gems/activerecord-4.1.1/lib/active_record/dynamic_matchers.rb:26:in `method_missing'
I need to find a way for the model to recognize the team_path path helper.
You should be able to call the url_helpers this way:
Rails.application.routes.url_helpers.team_path(Team.first.id)
Consider solving this as suggested in the Rails API docs for ActionDispatch::Routing::UrlFor:
# This generates, among other things, the method <tt>users_path</tt>. By default,
# this method is accessible from your controllers, views and mailers. If you need
# to access this auto-generated method from other places (such as a model), then
# you can do that by including Rails.application.routes.url_helpers in your class:
#
# class User < ActiveRecord::Base
# include Rails.application.routes.url_helpers
#
# def base_uri
# user_path(self)
# end
# end
#
# User.find(1).base_uri # => "/users/1"
In the case of the Team model from the question, try this:
# app/models/team.rb
class Team < ActiveRecord::Base
include Rails.application.routes.url_helpers
def base_uri
team_path(self)
end
end
Here is an alternative technique which I prefer as it adds fewer methods to the model.
Avoid the include and use url_helpers from the routes object instead:
class Team < ActiveRecord::Base
delegate :url_helpers, to: 'Rails.application.routes'
def base_uri
url_helpers.team_path(self)
end
end
Models are not supposed to be dealing with things like paths, redirects or any of that stuff. Those things are purely constructions of the view or the controller.
The model really should be just that; a model of the thing that you are creating. It should fully describe this thing, allow you to find instances of it, make changes to it, perform validations upon it... But that model wouldn't have any notion of what path should be used for anything, even itself.
A common saying in the Rails world is that if you're finding it difficult to do something (like call a path helper from a model) you are doing it wrongly. Take this to mean that even if something is possible, if it is hard to do in Rails it is likely not the best way to do it.
to add on the previous answer you can use Rails.application.routes.url_helpers just add in route :as like the following example:
get "sessions/destroy/:param_id", as: :logout
so you can use it as following:
Rails.application.routes.url_helpers.logout_path(:param_id => your_value)
Hopefully, this would help

How to use plain Ruby object with url helpers

I am rendering a page from a simple custom model (not ActiveRecord, plain ActiveModel) and I cannot get the url/path helpers to generate an url with their id, like this:
person_path(model)
# I want: /person/3
# I get: /person
Is there any concrete class I must inherit or function to implement so the url helpers work with my custom model?
I heard about to_param but it is not working, at least not with this:
class Person
include ActiveModel::Model
def id
3
end
def to_param
id.to_s
end
end
According the documentation that should work:
Any class that includes ActiveModel::Model can be used with form_for,
render and any other Action View helper methods, just like Active
Record objects.
But I guess there is still a missing function needed for the url helpers to work
You need to define a persisted? method that returns true: the default implementation always returns false, which causes rails to generate a path with no id.
It would be good if you can share the code from config/routes.rb or at least the result from rake routes.
Double check your routes again. I think you may have defined the route as a singular resource resource :person which will not add an ID to the url.

Overwriting default accessors with options

I am using Ruby on Rails 4 and I would like to know what could be the pitfalls when I overwrite default accessors. The Rails' Official Documentation says (in the initial lines):
The mapping that binds a given Active Record class to a certain
database table will happen automatically in most common cases, but can
be overwritten for the uncommon ones.
More, in that documentation there is the "Overwriting default accessors" section which makes me think that I can do it without any problem. What do you think about?
In my case I would like to overwrite attribute accessors in order to provide some options, something like this:
# Given my Article model has :title and :content attributes
# Then I would like to overwrite accessors providing options this way:
class Article < ActiveRecord::Base
def title(options = {})
# Some logic...
end
def content(options = {})
# Some logic...
end
end
# So that I can run
#article.title # => "Sample Title"
#article.title(:parse => true) # => "Sample *Title*"
#article.content # => "Sample description very long text"
#article.content(:length => :short) # => "Sample description..."
Maybe this is more Ruby than Rails, but will be the #article.title calling the title(options => {}) method or it will call the Rails attribute accessor that access the related database table column value?
Update (after commenting)
Since it seems that in the above code default accessors are not overwritten, is there a way to provide options for those accessors so to reach what I am looking for? If so, how?
#article.title #=> will call your method
#article.title(:parse => true) #=> will call your method
There is no method overloading in ruby if that is what you are looking for.
Looking closer at the official documentation I see where your code diverges.
You forgot "=" when defining your method.
class Foo < ActiveRecord::Base
def self.bar=(value)
#foo = value
return 'OK'
end
end
Foo.bar = 3 #=> 3
WARNING: Never rely on anything that happens inside an assignment method,
(eg. in conditional statements like in the example above)

Overriding Rails Default Routing Helpers

I'm writing an app where I need to override the default routing helpers for a model. So if I have a model named Model, with the corresponding helper model_path() which generates "/model/[id]". I'd like to override that helper to generate "/something/[model.name]". I know I can do this in a view helper, but is there a way to override it at the routing level?
You can define to_param on your model. It's return value is going to be used in generated URLs as the id.
class Thing
def to_param
name
end
end
The you can adapt your routes to scope your resource like so
scope "/something" do
resources :things
end
Alternatively, you could also use sub-resources is applicable.
Finally you need to adapt your controller as Thing.find(params[:id]) will not work obviously.
class ThingsController < ApplicationController
def show
#thing = Thing.where(:name => params[:id).first
end
end
You probably want to make sure that the name of your Thing is unique as you will observe strange things if it is not.
To save the hassle from implementing all of this yourself, you might also be interested in friendly_id which gives you this and some additional behavior (e.g. for using generated slugs)
You need the scope in routes.rb
scope "/something" do
resources :models
end

Rails refactoring: Where would you put hash which maps one table's fields to another

So, I have a database of people on an external system, and I want to set up the code to easily create people records internal to our sysem based on the external system. The field names, of course, are not the same, so I've written some code which maps from one table to the next.
class PeopleController < ApplicationController
...
def new
#person = Person.new
if params[:external_id] then
initialize_from_external_database params[:external_id]
end
end
private
def initialize_form_external_database(external_id)
external = External::Person.find(external_id)
if external.nil?
...
else
#person.name_last = exteral.last_name
#person.name_first = external.first_name
#...
#person.valid?
end
end
end
Okay, so the stuff in the "else" statement I can write as a loop, which would use a hash something like:
FieldMappings = {
:name_last => :last_name,
:name_first => :first_name,
:calculated_field => lambda {|external_person| ... },
...
}
But where would you put this hash? Is it natural to put it in the External::Person class because the only reason we access those records is to do this initialization? Or would it go in the controller? Or a helper?
Added: Using Rails 2.3.5.
I'd put this code in the External::Person to avoid Person even having to know it exists. Use a 'to_person' method (or maybe 'to_internal_person') on External::Person. Keep the Hash in External::Person and use it to perform the generation. Either way as JacobM says, you want this code in your model, not controller.
class PeopleController < ApplicationController
def new
if external = External::Person.find_by_id params[:external_id]
#person = external.to_person
else
#person = Person.new
end
end
end
If you're in Rails 3.x (maybe also in 2.x, I'm not sure), you can put miscellaneous classes and modules in your /extras folder which is included in the autoloader path. This is where I always put things of this nature, but I' not aware of any Rails convention for this sort of thing.
First of all, I would do that work in your (internal) Person model -- give it a class method like create_person_from_external_person that takes the external person and does the assignments.
Given that, I think it would be OK to include the hash within that Person model, or somewhere else, as Josh suggests. What would be particularly cool would be to write a generic create_person_from_external_person method that would ask the external person for a hash and then do the mapping based on that hash; that approach could support more than one type of external person. But that may be overkill if you know this is the only type you have to deal with.
I wouldn't put it in the controller, but, again, I wouldn't do that work in the controller either.
You can put it on a module on the lib directory so you don't mess any of your classes that will be full of awesome code that will probably last many years. Another good reason is you can then include/require your mapping module everywhere you need it (maybe in your tests).
module UserMapping
FIELDS = { :last_name => :name_last, .... }
end
If you drop the module on the lib and you use rails 3 you should put this on your config/application.rb file:
config.autoload_paths += %W(#{config.root}/lib)
On Rails::VERSION::MAJOR < 3 the lib directory is automatically added to the autoload_path

Resources