Activeadmin: Routing Errors When to_param is Overwritten

Created on 11 Jul 2011  路  35Comments  路  Source: activeadmin/activeadmin

I overwrote to_param in a model to implement friendly urls. Active Admin can no longer link to the admin urls that require ids without throwing a routing error.

party.rb:

# Should give me urls like this: /parties/23/name-of-party
def to_param
  "#{id}/#{slug}"
end

def slug
  self.name.parameterize
end

routes.rb:

resources :parties, :constraints => { :id => /[0-9]+\/.+/ }

I believe this a different problem than the other to_param issues listed below as I'm getting a routing error, not a active record loading error. Specifically, I get a routing error when any active admin page attempts to build a link that requires an id.

Is there any way to force active admin to ignore the overwritten to_param method?

Other issue that involve to_param:

35: https://github.com/gregbell/active_admin/issues/35

62: https://github.com/gregbell/active_admin/issues/62

191: https://github.com/gregbell/active_admin/issues/191

bug

Most helpful comment

This worked flawlessly for me. Thanks watson.

ActiveAdmin.register Post do
  before_filter do
    Post.class_eval do
      def to_param
        id.to_s
      end
    end
  end
end

UPDATE: It works sporadically.

UPDATE 2: The following seems to be more stable.

ActiveAdmin.register Category do
    controller do
        defaults :finder => :find_by_slug
    end
end

All 35 comments

It doesn't sound that different, but here's a different solution: Run-time redefine the to_param method like this:

ActiveAdmin.register Post do
  before_filter do
    Post.class_eval do
      def to_param
        id.to_s
      end
    end
  end
end

In production, rails cache the classes.
therefore, when you override the to_param method it's affect the whole site.

The best solution I found is to use around_filter to override to_param again to the original state

around_filter do |controller, action|
  User.class_eval do
    def to_param
      id.to_s
    end
  end

  begin
    action.call
  ensure
    User.class_eval do
      def to_param
        username
      end
    end
  end
end

I actually was running into the same issues which was resulting in me having to change the way my application functioned to get around this, which I did not like one bit.

So...I wrote a little gem that covers generating these slugs that works very well with activeadmin and the rest of my application. Available at https://github.com/HuffMoody/has_unique_slug if anyone is interested.

anyone found a solution to this?

Just submitted pull request #785 for this issue here: https://github.com/gregbell/active_admin/pull/785

Hi Matt. That's a clean fix, and submitted with tests too. Nice one.

Until the patch above is merged, here is an improvement on the overriding approach which doesn't require redefining the custom to_param semantics:

  around_filter do |controller, action|
    MyModel.class_eval do
      alias :__active_admin_to_param :to_param
      def to_param() id.to_s end
    end

    begin
      action.call
    ensure
      MyModel.class_eval do
        alias :to_param :__active_admin_to_param
      end
    end
  end

+1

Any solution to this yet?

This worked flawlessly for me. Thanks watson.

ActiveAdmin.register Post do
  before_filter do
    Post.class_eval do
      def to_param
        id.to_s
      end
    end
  end
end

UPDATE: It works sporadically.

UPDATE 2: The following seems to be more stable.

ActiveAdmin.register Category do
    controller do
        defaults :finder => :find_by_slug
    end
end

+1 for armstrjare's solution.

+1 thanks for the solution, was the only thing that worked for me

+1 thanks armstrjare's

+1 for mdoyle13 (UPDATE 2). That works well when to_param returns something find_by-able.

+1 for Update 2 by mdoyle13. Worked perfectly.

The issue roots all the way down to inherited resources which offers a solution to this built in.

The best way to solve this is to add defaults for inherited resources in the controller block provided by active admin.


ActiveAdmin.register Article do

  controller do
    defaults finder: :find_by_slug
  end

end

Where slug is the name of the column I am using to store the slug/permalink/etc. You can pass any method on the model into here to allow active admin to find the resource.

Hope this helps!

just adding my +1 here

one thing though my application doesn't respond to

defaults finder: :find_by_id

at all

@gilsilas 's patch worked like a charm though

Setting a defaults finder works in some cases.

But it won't work if you have a validation error on the to_param field.

E.g.

class MyModel < ActiveRecord::Base
  validates_format_of :slug, with: /^mandatory_prefix[0-9]+/
end

irb > MyModel.create!(slug: "mandatory_prefix_1")

Go to /admin/my_models/mandatory_prefix_1/edit

Set the slug to "wrong_prefix_2" and save
Page reloads, with the slug validation error (as expected)
Change the slug to "mandatory_prefix_2" submit again and you'll get:

ActiveRecord::RecordNotFound 
Couldn't find MyModel with slug = wrong_prefix_2

Because the form_for was rendered using the invalid (unsaved) MyModel#slug

:+1: for solving this issue

Does someone want to open a pull request for this?

controller do
defaults finder: :find_by_slug
end
:+1:

I'm working on an app that makes use of @armstrjare's solution with class_eval and we're noticing some very strange behavior. Occasionally, visitors are reporting seeing links like

/pages/3 (which 404s)

instead of

/pages/about

I suspect this may be related to the fact that class_eval is temporarily redefining Page#to_param to use the id instead of the slug.

@stevegrossi that behavior could definitely happen if you're using a multithreaded server.

You might have better luck with the "defaults finder: :find_by_slug" approach.

Indeed, the site's served up by Puma. I didn't realize the threading connection here, so thanks for the tip.

I don't think any of the above solutions will work if you're using friendly_id's "scope" feature, as the slug for a resource is not guaranteed to be unique. Has anyone found a decent way to deal with this?

I encountered a similar problem to this, a code base I was working with had a slug on a Foo model (provided by friendly_id) that was defined as being scoped to another model:

class Foo < ActiveRecord::Base
  friendly_id :name, use: :scoped, scope: :bar_category
  # ...
end

I won't go into why this isn't being treated as a nested resource in the active admin setup we are using, but because it isn't we found that active admin ended up using the non-unique slug for its urls, i.e. a url for a 'foo' object might be http://localhost:3000/admin/foos/not-a-unique-slug/edit and this meant that which object actually got modified was unpredictable.

My solution was to override the inherited_resources resource path/url helpers in the controller so that the urls instead used the id of the model, i.e:

ActiveAdmin.register Foo do

  # ...

  controller do
    # Overriding resource url helpers so that this admin controller uses the id
    # for urls, rather than the slug (which can have duplicates):
    def resource_path(*given_args)
      given_options = given_args.extract_options!
      admin_foo_path((given_args.first || @foo).id, given_options)
    end
    def edit_resource_path(*given_args)
      given_options = given_args.extract_options!
      edit_admin_foo_path((given_args.first || @foo).id, given_options)
    end
    def resource_url(*given_args)
      given_options = given_args.extract_options!
      admin_foo_url((given_args.first || @foo).id, given_options)
    end
  end
end

This is a bit brittle as it relies on knowledge of the inner workings of both active_admin and inherited_resources, but it does fix the problem for us.

I hope this might help others looking at this issue, but I'm open to suggestions of a better way to handle this.

@elsurudo I had the same problem and fixed it with the following code:

ActiveAdmin.register Foobar do
  before_filter do
    Foobar.class_eval do
      def to_param
        id.to_s
      end
    end
  end
end

Source: http://stackoverflow.com/questions/7684644/activerecordreadonlyrecord-when-using-activeadmin-and-friendly-id

@tomgrim this also worked for me, but I also needed to add an after_filter so that it reverted the change. Otherwise, the change remains active and it can affect the rest of the application (that is, links on the public site using ID for the param).

Better than that, I preferred to use an around_filter, like this:

# app/models/foobar.rb
# In my app I use a generated token as the ID
class Foobar < ActiveRecord::Base
  def to_param
    token
  end
end

# app/admin/foobars.rb
ActiveAdmin.register Foobar do
  controller do

    around_filter :use_id_instead_of_token_as_param

    def use_id_instead_of_token_as_param
      Foobar.class_eval do
        def to_param
          id
        end
      end

      yield

      Foobar.class_eval do
        def to_param
          token
        end
      end
    end

  end
end

This works perfectly for me :+1:

Thanks @mdoyle13

controller do
  defaults :finder => :find_by_slug
end

I'm closing this, while it's a inherited_resources bug and not a ActiveAdmin one.

This should be the correct way to handle it:

controller do
  defaults finder: :find_by_slug
end

I think there's more than one problem being discussed on this issue, and defaults finder: :find_by_slug only addresses one of them.

As I see it:

  1. Some people have a custom to_param which they need to tie back into their ActiveAdmin pages, so that things can be looked up properly.
  2. Some people have a custom to_param but don't want it to have any effect on ActiveAdmin pages, so that everything is based on ID.

(2) causes a bunch of problems. It means that you have to specify .id on any link_tos in the view templates, but it also means that for stuff like index blocks, you have to reimplement the link to "View" a particular record.

column "" do |foo|
  link_to "View", admin_foo_path(foo.id)
end

That's very hacky and isn't very sustainable in the face of future changes to how ActiveAdmin renders the index table, or handling permissions for which links are visible by a particular admin user. (It's also fairly cringeworthy that other solutions posted here include dynamically changing method definitions on the fly.)

So basically while (1) is solved by specifying a :finder, I don't think (2) has been addressed. Does it not belong in this issue?

@aprescott If you use a custom to_param you need to use it everywhere. To build a config.use_to_param = false would be a massiv code blow up, that we can't do. Thats way we use the Rails url helpers under the hood and they use the to_param method. You have two choices, customize to_param or not. Customize to_param, but not for ActiveAdmin is not and probably will not be supported way.

Here are some DIY ideas:

  1. You can use Rubys Refinements to redefine url_for in ActiveAdmin to use .id instate of .to_params.
  2. You can work with ViewModels to have a User, a FrontendUser and a BackendUser class. Then you can define to_params only on the FrontendUser. (depend on what you wan't to do, you don't need all three classes)
  3. Maybe a Decorator can help you.

BTW: It's not only a ActiveAdmin problem, we use Formtastic and inherited_resources which both need to change too.

I see this is an old thread but I wanted to comment on a solution that worked for me while hitting this same problem. I had redefined to_param on a Job model and what I did was make an AdminJob model that inherits from Job and defines to_param the standard way (return instance id). That is:

class Job < ActiveRecord::Base
end

class AdminJob < Job
  def to_param
    id.to_s
  end
end

Then on Active Admin's side I did:

ActiveAdmin.register AdminJob, as: "Job" do
end

I currently haven't find any problems with this, it seems ActiveRecord is quite smart with inheritance.

Cheers

+1 for mdoyle13 (UPDATE 2). Works for me.

defaults finder: :find_by_slug works, however, if I have a child belongs_to resource within the parent resource, I again get an error:

Couldn't find Foo with 'id'=bar

This happens in:

def scoped_collection
  super.includes(:bazes)
end

This method is defined within the controller block of the child resource. So I guess, there must be something like:

defaults parent_finder: :find_by_slug

OK, so it seems that the resources configurations for a parent resources are set by the options of the belongs_to. So in order to be able to use a custom finder for the parent resource, you need to do this:

belongs_to :foo, finder: :find_by_slug
Was this page helpful?
0 / 5 - 0 ratings