6
votes

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?

3

3 Answers

1
votes

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
0
votes

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)

0
votes

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 )