Rails RESTful Routes: override params[:id] or params[:model_id] defaults - ruby-on-rails

I'm trying to understand how to change this rule directly on the map.resources:
supposing I have a route:
map.resource :user, :as => ':user', :shallow => true do |user|
user.resources :docs, :shallow => true do |file|
file.resources :specs
end
end
so I would have RESTful routes like this:
/:user/docs
/docs/:id
/docs/:doc_id/specs
So I see that is difficult to track the params[:doc_id] on this case because sometimes its params[:id] and sometimes its params[:doc_id] and in this case I would like to always call for one specific name so I won't have to create two different declarations for my filters.
Well, I did a little bit of research and I found this patch:
http://dev.rubyonrails.org/ticket/6814
and basically what this does is give you the ability to add a :key parameter on you map.resources so you can defined how you would like to reference it later so we could have something like:
map.resources :docs, :key => :doc ...
so I always would call the param with params[:doc] instead.
But actually this patch is a little bit old (3 years now)
so I was wondering if we don't have anything newer and already built-in for rails to do this task?
P.S I'm not sure about that to_param method defined inside the model, apparently this didn't change anything on my requests, and on the logs I still getting:
Parameters: {"doc_id"=>"6"} or Parameters: {"id"=>"6"} all the time.

One method of making the parameters a little more friendly without writing fully custom routes is
# docs_controller.rb
def show
#doc = Doc.find(params[:doc].to_i)
end
# doc.rb
def to_param
[ id, permalink ].join("-")
# assumes you're storing a permalink formatted version of the name of your doc
end
# routes.rb
map.resources :docs
This will give you URLs that look something like example.com/docs/234-the-name-of-your-doc

Related

Rails 4.1.2 - to_param escapes slashes (and breaks app)

I use in my app to_param to create custom URL (this custom path contains slashes):
class Machine < ActiveRecord::Base
def to_param
MachinePrettyPath.show_path(self, cut_model_text: true)
end
end
The thing is, that since Rails 4.1.2 behaviour changed and Rails doesn't allow to use slashes in the URL (when use custom URL), so it escapes slashes.
I had such routes:
Rails.application.routes.draw do
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
resources :machines, except: :destroy do
collection do
get :search
get 'search/:ad_type(/:machine_type(/:machine_subtype(/:brand)))', action: 'search', as: :pretty_search
get ':subcategory/:brand(/:model)/:id', action: 'show', as: :pretty
patch ':subcategory/:brand(/:model)/:id', action: 'update' # To be able to update machines with new rich paths.
end
end
end
end
I tried by recommendation in the thread to use glob param just for show method to make sure it works:
resources :machines, except: :destroy do
#...
end
scope format: false do
get '/machines/*id', to: "machines#show"
end
But it absolutely doesn't work. I still have such broken links:
http://localhost:3000/machines/tractor%2Fminitractor%2Fmodel1%2F405
Of course, if I replace escaped slashes on myself:
http://localhost:3000/machines/tractor/minitractor/model1/405
And try to visit path, then page'll be opened.
Any ideas how can I fix that?
I've been having the same problem when using the auto-generated url helpers. I used a debugger to trace the new behavior to its source (somewhere around ActionDispatch::Journey::Visitors::Formatter), but didn't find any promising solutions. It looks like the parameterized model is now strictly treated as a single slash-delimited segment of the path and escaped accordingly, with no options to tell the formatter otherwise.
As far as I can tell, the only way to get the url helper to produce the old result is to use your original routes file and pass each segment separately, something like:
pretty_machine_path(machine.subcategory, machine.brand, machine.model, machine.id)
This is ugly as hell and obviously not something you'll want to do over and over. You could add a method to MachinePrettyPath to generate the segments as an array and explode the result for the helper (say, pretty_machine_path(*MachinePrettyPath.show_path_segments(machine))) but that's still pretty verbose.
Between the above headaches and the "You're Doing it Wrong" attitude from the devs in that Rails ticket you linked to, the simplest option for me was to bite the bullet and write a custom URL helper instead of using to_param. I've yet to find a good example of the "right" way to do that, but something like this bare-bones example should serve the purpose:
#app/helpers/urls_helper.rb
module UrlsHelper
def machine_path(machine, options = {})
pretty_machine_path(*MachinePrettyPath.show_path_segments(machine), options)
end
end
#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper :urls #for the views
include UrlsHelper #for controllers
#...
end
If you're sure the returned url is safe you should add .html_safe to the returned string:
MachinePrettyPath.show_path(self, cut_model_text: true).html_safe
(didn't see anywhere else where it can be escaped but check all the flow in your app, maybe manually testing method by by method)
How about defining the url yourself?
def to_param
"#{ subcategory.title.parameterize }/#{ brand.name.parameterize }/#{ model.name.parameterize) }/#{ id }"
end
And then in your routes something like this:
get 'machines/*id', to: "machines#show"
You also have to split your params[:id] when you do a find on your model.
Machine.find( params[:id].split("/").last )

Customize/modify Rails 3 resources routing

I'm currently writing a rails app that has your regular resource like objects. However I would like to make my resources syncable. My web application uses web offline storage to cache results from the server (user's cannot modify data on server making syncing easier). When I fetch from the server, it returns a hash response like:
{
:new => [...]
:updated => [...]
:deleted => [...]
}
This is all well and good until I want to have a regular fetch method that doesn't do any sort of syncing and simply return an array of models
Now my question is I want to create a method in my routes.rb file that sets up routes so that I have a route to synced_index and index. Ideally, I'd be able to do something like this:
synced_resources :plans
And then all of the regular resource routes would be created plus a few extra ones like synced_index. Any ideas on how to best do this?
Note: I do know that you can modify resources with do...end syntax but I'd like to abstract that out into a function since I'd have to do it to a lot of models.
You can easily add more verbs to a restful route:
resources :plans do
get 'synced_index', on: :collection
end
Check the guides for more information on this.
If you have several routes that are similar to this, then sure, you can add a 'synced_resources' helper:
def synced_resources(*res)
res.each do |r|
resources(r) do
get 'synced_index', on: :collection
end
end
end
Wrap above method in a module to be included in ActionDispatch::Routing::Mapper.

Routing Error with Post/Put requests (Passenger Headers)

I've run into a weird problem and after a bunch of research can't get any closer. I've got several forms that upload files via Carrierwave. When I upload the information, part of the route gets cut off (I think).
For example, I have a multi-part form submitting to:
https:/domain/programs/223/add_file as POST
but on submission I get the error
No route matches [POST] "/223/add_file"
even though what's in my address bar is the complete route. And if submit the complete route as a GET request it works fine. When I run rake routes the route shows up just fine.
Here is a subset of my route:
resources :programs do
match "add_file" => "programs#add_file"
If it matters, I'm running Rails 3.2.2 with Passenger on Apache. The problem only happens on this production server, never in development.
Any ideas? I'm stuck on this one as it effects multiple routes and I've tried defining a custom route just for that form with no luck.
Update:
When I remove multi-part => true or the file_field_tag from the form it fixes the problem. It's still an issue but seems to be less about routing than about the form with file uploads.
Create passenger_extension.rb in the lib folder with this code:
Passenger 3
module PhusionPassenger
module Utils
protected
NULL = "\0".freeze
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
end
end
Passenger 5
module PhusionPassenger
module Utils
# Utility functions that can potentially be accelerated by native_support functions.
module NativeSupportUtils
extend self
NULL = "\0".freeze
class ProcessTimes < Struct.new(:utime, :stime)
end
def split_by_null_into_hash(data)
args = data.split(NULL, -1)
args.pop
headers_hash = Hash.new
args.each_slice(2).to_a.each do |pair|
headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
end
return headers_hash
end
def process_times
times = Process.times
return ProcessTimes.new((times.utime * 1_000_000).to_i,
(times.stime * 1_000_000).to_i)
end
end
end # module Utils
end # module PhusionPassenger
And then in 'config/application.rb' do:
class Application < Rails::Application
...
config.autoload_paths += %W(#{config.root}/lib)
require 'passenger_extension'
end
And then restart a webserver.
NOTICE: I'm not sure that this doesn't break any other functionality so use it on your own risk and please let me know if you find any harm from this approach.
One issue here is you're not specifying whether the route is defined on the collection or a member. Which one of these is the correct route?
programs/:id/add_file
programs/add_file
You should construct your routes like this:
resources :programs do
post 'add_file', :on => :member
end
or
resources :programs do
member do
post 'add_file'
end
end
The above will take post requests at programs/:id/add_file and send them to ProgramsController.add_file with the params[:id] as the program id.
If you want this on the collection, you could do:
resources :programs do
post 'add_file', :on => :collection
end
or
resources :programs do
collection do
post 'add_file'
end
end
This would take post requests at programs/add_file and send them to ProgramsController.add_file, but no params[:id] would be set.
In general you should always specify whether routes are on the collection or member, and you should specify which verb a route should accept (ie use 'get' or 'post' etc. instead of 'match').
Try the above and see if that solves your problem, if not please let me know and I'll take another look.
I think you may need to add
:via => [:post]
to your route specification. It seems odd that it'd work on development and not on production, but as I understand rails routing, the matcher that you've added is only going to respond to get.
Try changing your match to
match "add_file" => "programs#add_file", :via => [:post]
Also, based on the answer just submitted by Andrew, you're probably better off using the member specifier to be explicit about the fact that the operation is happening on a particular Program with a particular id, and not the collection. It also should save some code in your add_file method which is probably working hard to get the id parameter from the url.

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)

How to implement "short" nested vanity urls in rails?

I understand how to create a vanity URL in Rails in order to translate
http://mysite.com/forum/1 into http://mysite.com/some-forum-name
But I'd like to take it a step further and get the following working (if it is possible at all):
Instead of:
http://mysite.com/forum/1/board/99/thread/321
I'd like in the first step to get to something like this: http://mysite.com/1/99/321
and ultimately have it like http://mysite.com/some-forum-name/some-board-name/this-is-the-thread-subject.
Is this possible?
To have this work "nicely" with the Rails URL helpers you have to override to_param in your model:
def to_param
permalink
end
Where permalink is generated by perhaps a before_save
before_save :set_permalink
def set_permalink
self.permalink = title.parameterize
end
The reason you create a permalink is because, eventually, maybe, potentially, you'll have a title that is not URL friendly. That is where parameterize comes in.
Now, as for finding those posts based on what permalink is you can either go the easy route or the hard route.
Easy route
Define to_param slightly differently:
def to_param
id.to_s + permalink
end
Continue using Forum.find(params[:id]) where params[:id] would be something such as 1-my-awesome-forum. Why does this still work? Well, Rails will call to_i on the argument passed to find, and calling to_i on that string will return simply 1.
Hard route
Leave to_param the same. Resort to using find_by_permalink in your controllers, using params[:id] which is passed in form the routes:
Model.find_by_permalink(params[:id])
Now for the fun part
Now you want to take the resource out of the URL. Well, it's a Sisyphean approach. Sure you could stop using the routing helpers Ruby on Rails provides such as map.resources and define them using map.connect but is it really worth that much gain? What "special super powers" does it grant you? None, I'm afraid.
But still if you wanted to do that, here's a great place to start from:
get ':forum_id/:board_id/:topic_id', :to => "topics#show", :as => "forum_board_topic"
Take a look at the Rails Routing from the Outside In guide.
maybe try something like
map.my_thread ':forum_id/:board_od/:thread_id.:format', :controller => 'threads', :action => 'show'
And then in your controller have
#forum = Forum.find(params[:forum_id])
#board = #forum.find(params[:board_id])
#thread = #board.find(params[:thread_id])
Notice that you can have that model_id be anything (the name in this case)
In your view, you can use
<%= link_to my_thread_path(#forum, #board, #thread) %>
I hope this helps

Resources