map.resources with alternate primary key(s) - ruby-on-rails

I have a Rails model Object that does not have an ID column. It instead uses a tuple of primary keys from two other models as its primary key, dependency_id and user_id.
What I want to do is be able to do something like this in routes.rb:
map.resources :object, :primary_key => [:dependency_id, :user_id]
And for it to magically generate URLs like this:
/objects/:dependency_id/:user_id
/objects/:dependency_id/:user_id/1
/objects/:dependency_id/:user_id/1/edit
...Except that I just made that up, and there is no such syntax.
Is there a way to customize map.resources so I can get the RESTful URLs, without having to make custom routes for everything? Or am I just screwed for not following the ID convention?
The :path_prefix option looks somewhat promising, however I would still need a way to remove the id part of the URL. And I'd like to still be able to use the path helpers if possible.

You should override Object model's method to_param to reflect your primary key. Something like this:
def to_param
[dependency_id, user_id].join('-')
end
Then when you'll be setting urls for these objects (like object_path(some_object)) it will automatically gets converted to something like /objects/5-3. Then in show action you'd have to split the params[:id] on dash and find object by dependency_id and user_id:
def show
dep_id, u_id = params[:id].split('-').collect(&:to_i)
object = Object.find_by_dependency_id_and_user_id(dep_id, u_id)
end
You can also look at find_by_param gem for rails.

Related

Using a non-default attribute in path helpers, instead of :id

What I have
I now have a route like
get 'book/:id' => 'books#show', as: :book
The code to create that URL is:
book_path #book
In the controller:
#book = Book.find(params[:id])
So far so good. Now, this id is obviously the internal ActiveRecord or database primary key.
What I want
Instead, I would (in this ficticious example) use the ISBN instead. I could do this like so:
get 'book/:isbn' => 'books#show', as: :book
book_path(isbn: #book.isbn)
#book = Book.find_by(isbn: params[:isbn])
This is a bit verbose; I want the convention "isbn" for this model in the whole application, and nobody should need to care about it, ever. So, I want my 3 lines to look like this:
get 'book/:isbn' => 'books#show', as: :book
book_path #book # (!)
#book = Book.find(params[:isbn]) # (!)
Is it possible to configure the route so that my wish comes true? Obviously, this should automatically work everywhere, at the very least for GET requests where the user sees the ISBN in the URL.
In case it matters, the #isbn attribute is not a primary key in the usual sense, but it is guaranteed that there is only ever one record with a particular ISBN in the table. So at least this part of the Primary Key contract is fulfilled.
To override the Rails default :id parameter, you can use to_param method:
class Book < ActiveRecord::Base
def to_param # overridden
isbn
end
end
And in controller:
book = Book.find_by(isbn: '123456')
If you don't have any problem with using any gem for it, then I suggest you should give a look to friendly_id Gem. This does a good job in such case.
In your routes.rb, use match to match /book/:isbn to book#show
match '/book/:isbn' => 'book#show', via: :get, as: :show_book
In your book.rb, you can override the find method in order find based on isbn instead of id. Something along the lines of:
def self.find(*args)
c = Book.select("id").find_by(name: args[0])
args[0] = c.id
super(args)
end
This will first find by isbn and then using the id, it will call the original find method. (I have not test this, you will have to modify it to make it work with multiple isbns)
But I am not sure if this is recommended and what caveats it will present.
Instead, why don't you use find_by_isbn[params[:isbn]]? It is more readable than find_by(:isbn, params[:isbn]).

Can I use Friendly Id on a Rails model without having to create a slug column on the model table?

On Rails 4.2, I would like to use Friendly Id for routing to a specific model, but dont wan't to create a slug column on the model table. I would instead prefer to use an accessor method on the model and dynamically generate the slug. Is this possible? I couldn't find this in the documentation.
You can not do this directly using friendly id because of the way it uses the slug to query the database (relevant source).
However it is not difficult to accomplish what you want. What you will need are two methods:
Model#slug method which will give you slug for a specific model
Model.find_by_slug method which will generate the relevant query for a specific slug.
Now in your controllers you can use Model.find_by_slug to get the relevant model from the path params. However implementing this method might be tricky especially if your Model#slug uses a non-reversible slugging implementation like Slugify because it simply get rids of unrecognized characters in text and normalizes multiple things to same character (eg. _ and - to -)
You can use URI::Escape.encode and URI::Escape.decode but you will end up with somewhat ugly slugs.
As discussed here I went with the following approach for custom routing based on dynamic slug.
I want custom route like this: /foos/the-title-parameterized-1 (where "1" is the id of the Foo object).
Foo model:
#...
attr_accessor :slug
#dynamically generate a slug, the Rails `parameterize`
#handles transforming of ugly url characters such as
#unicode or spaces:
def slug
"#{self.title.parameterize[0..200]}-#{self.id}"
end
def to_param
slug
end
#...
routes.rb:
get 'foos/:slug' => 'foos#show', :as => 'foo'
foos_controller.rb:
def show
#foo = Foo.find params[:slug].split("-").last.to_i
end
In my show view, I am able to use the default url helper method foo_path.

Rails - overriding name route parameters - GLOBALLY

I have a Rails(4.2.6) application that has tables like companies (id, serial...), user (id, serial, company_id...), where serial is a random generated 5-20 character long string and unique per table. What I am trying to achieve is to have routes like /companies/:serial and /users/:serial...
I have read the documentation, and can do the following in routes.rb:
resources :companies, param: :serial
resources :users, param: :serial
Now, that's not too DRY... Is there a way to do this globally? I know that I could have this in one line (resources :companies, :users, param: :serial), but I have other tables, in other namespaces, to which I would like to apply the rule also.
Another thing that I thought of was to have the serial as primary key but I prefer the auto-incrementing integer, and don't want to litter my db with columns like user.company_serial with 10-20 character long values...
I have tried to make a scope with param: :serial around my resources:
scope param: :serial do
resources :companies
...
end
like with path_names (read here in the documentation) but that didn't had the desired effect, instead added params[:param] with a symbol value :serial for some reason that I don't really understand.
I also know about the existance of the method to_param, but if I understood well, I should use it in the models, so I would have to write the same code as many times as many models I have.
The way you do this is submit serial in a URL as you would have done with the id param, params[:id] and use that in the controller.
Company.find_by_serial(params[:id)
You won't need to change routes, just the controller actions. This can be dried up, too.
You can do this by explicitly passing serial, or by adding the to_param method. If you add to_param to the model, it will always use this in every place it generates the route where it would have used the id and save you some work. If there is ever a place you would rather use the id (I prefer this in some controllers, like admin controllers), then you have to explicitly pass it or work around that.

Rails 4 custom named route not calling correct property of model

As per http://edgeguides.rubyonrails.org/routing.html#overriding-named-route-parameters I am defining a route with a custom named route parameter instead of :id to create friendly URLs. routes.rb looks like:
resources :spaces, param: :name
Running rake routes does indeed give the correct paths with dynamic segments:
space GET /spaces/:name(.:format) spaces#show
But using space_path still tries to retrieve the ID:
irb(main):009:0> app.space_path space
=> "/spaces/1"
were it should give "/spaces/foo" (assuming that the Space with id=1 has name=foo)
I can explicitly do:
irb(main):009:0> app.space_path space.name
=> "/spaces/foo"
But then I lose the whole point of the dynamic paths, and all of my views become far more brittle. Should the dynamic paths not recognize the property to retrieve?
I do know that I can override to_params in the model, but again, isn't that making the model brittle? Shouldn't the dynamic path recognize the name of the dynamic segment and retrieve the correct property off of the model?
app.space_path is going to take the first argument you give it and put it in place of :name. If the argument is an ActiveModel instance, it will call to_param on it, which, unless you override it, will return the value of the id attribute—in this case 1, not foo.
If you want app.space_path(space) to return /spaces/foo you'll need to override Space#to_param:
class Space < ActiveRecord::Base
# ...
def to_param
name
end
end
The only thing :name in your route does is determine what the key for that value in params is in your controller, i.e. if /spaces/foo is requested, params[:name] will be "foo".

If I want to override the resource path (e.g. post_path(#post)), where do I do that?

UPDATE (again) | Ignore the module thing-o I put up here before. Better off just using to_param and making the routes work.
to_param
if slug
"#{id}-#{slug}"
else
super
end
end
match "/posts/:id(-:slug)" => "posts#show", :as => :post, :constraints => { :id => /\d+|\d+-.*/ }
The trick is in that route. It makes -:slug optional, and adds a constraint to :id. The constraint's preference is to match just a numeric value (which is what gets used when a request comes in, so you have :id and :slug in your params), but the pipe allows it to also match a numeric value followed by the slug, for the purpose of route generation. It's a mild hack, but this whole thing is really. Allowing resource to provide a hash of params would solve this. This could be applied to any fancy route you want (e.g. having the :id at the end instead of the start).
Rails 3.0.7.
Take a route like:
match "/posts/:id-:slug" => "posts#show", :as => :post
This provides a post_path(obj) helper for me, but of course, it only wants to generate a single argument from my model, and from what I can tell, there's no way to return multiple values from my models' to_param method. I know I can write to_param like this:
to_param
"#{id}-#{slug}"
end
But this is passed as a single argument to the route, which ultimately doesn't match any routes. Similarly, I know I can remove the "-:slug" from my route, but then the :id parameter contains bogus input and is basically a bit of a hack (though apparently the done thing).
It'd be awesome if a future version of Rails let you return a hash from to_param, like:
to_param
{ :id => id, :slug => slug }
end
then used those when locating the correct route to use.
What I'm trying to figure out, is where I would go about overriding post_path() to provide the correct route. I can put it in a view helper, but then it's not available in my controllers, so I'm guessing this is the wrong place to do it.
Just curious more than anything. I know it works fine if I just omit the :slug on the route, but in a more complex situation I can imagine overriding the default paths to be a useful thing to know. Being able to generate a route just by passing a resource is a pretty nice feature I'd like to adopts, but the marketing dept will always have us filling the URLs will all kinds of keywords mumbo jumbo, so some control would be excellent :)
There are two sides to routing: route generation and route matching.
The route generation, as you point out, is quite easy. Override to_param in any ActiveModel:
# In a model:
def to_param
# Use another column instead of the primary key 'id':
awesome_identifier.to_s
end
The route matching is less obvious. It is possible to specify :awesome_identifier in place of every occurrence of :id in the default resource routes. However, I have found that Rails will give you less resistance if you leave :id in your routes, and only change the logic in your controller. Note that this is a trade-off, because fundamentally the :id in your routes is not really correct.
# In a controller:
def index
#awesome_record = AwesomeModel.find_by_awesome_identifier(params[:id])
# ...
end
As you have noted, Rails has optimised a single use case: if you want the primary key to be used for quick record look-up, but still prefer a slug to be included in the URL for user-friendliness or search engine optimisation. In that case you can return a string of the form "#{id}-#{whatever}" from the to_param method, and everything after the dash will be ignored when feeding the same string back into the find method.

Resources