administrate icon indicating copy to clipboard operation
administrate copied to clipboard

Namespaced models not working

Open TrevorHinesley opened this issue 6 years ago • 20 comments

Per #871 and #179, it looks like namespaced models were worked on before, but I'm still getting the same issue. Going to the "New " page or trying to edit an object that has PaperTrail results in:

uninitialized constant PaperTrail::VersionDashboard

Which looks to be the same namespacing issue as before. I'm on version 0.11.0 of the gem, as well as Rails 5.2.1 and Ruby 2.5.1.

TrevorHinesley avatar Feb 24 '19 20:02 TrevorHinesley

@TrevorHinesley can you provide an example app where you get that error?

The example app included in this repo has namespaced models (blog/post.rb and dashboards/blog/post_dashboard.rb and it seems to be working fine.

frm avatar Feb 26 '19 19:02 frm

If you were to bundle PaperTrail into that example app and include it in a model, you'd get the same problem. Maybe it's PaperTrail specific, but it seems tied to namespacing somehow, because PaperTrail isn't doing anything special under the hood. Perhaps it's namespaced associations that are the problem?

TrevorHinesley avatar Feb 26 '19 19:02 TrevorHinesley

I'm running into the same issue at the moment. I've created an example app that only includes paper_trail and administrate here: https://github.com/coffeejunk/administrate-paper_trail

Notes:

  • rails generate administrate:install skips the PaperTrail::Version model (see message here) however, creates the association in the generated dashboard (see diff).
  • manually generating the dashboard for PaperTrail::Version works (see here)

coffeejunk avatar Apr 02 '19 21:04 coffeejunk

@coffeejunk Can you try following routes. You can create new folder paper_trail and keep your files inside that folder i-e app/controllers/admin/paper_trail/versions_controller.rb

namespace :admin do
    namespace :paper_trail do
      resources :versions
    end
end

usman-ahmad avatar Apr 19 '19 15:04 usman-ahmad

@usman-ahmad interesting.. after moving the VersionsController into the paper_trail folder as you suggested I get this error on first load: Screen Shot 2019-04-22 at 5 23 34 PM

and upon a refresh I don't get the error anymore:

Screen Shot 2019-04-22 at 5 25 43 PM

coffeejunk avatar Apr 22 '19 21:04 coffeejunk

@coffeejunk This is what I am experiencing as well. However on production (Heroku) the page is not working no matter how many times I might refresh. Seems like a loading issue. I am not sure if this is a gem-specific issue.

EDIT: Following the blog/posts way as in the example application made it work fine in production. In development I get the refresh issue, but I don't really mind as long as it works fine in production.

@TrevorHinesley can you provide an example app where you get that error?

The example app included in this repo has namespaced models (blog/post.rb and dashboards/blog/post_dashboard.rb and it seems to be working fine.

Tashows avatar Jun 20 '19 15:06 Tashows

I had the same issue as in the OP and could fix it in my case with same code as suggested (and didn't encounter any refresh issues so far):

# config/routes.rb
  namespace :admin do
    ...
    namespace :paper_trail do
      resources :versions
    end
  end

# app/controllers/admin/paper_trail/versions_controller.rb
module Admin
  class PaperTrail::VersionsController < Admin::ApplicationController
    # To customize the behavior of this controller,
    # you can overwrite any of the RESTful actions. For example:
    #
    # def index
    #   super
    #   @resources = PaperTrail::Version.
    #     page(params[:page]).
    #     per(10)
    # end

    # Define a custom finder by overriding the `find_resource` method:
    # def find_resource(param)
    #   PaperTrail::Version.find_by!(slug: param)
    # end

    # See https://administrate-prototype.herokuapp.com/customizing_controller_actions
    # for more information
  end
end

# app/dashboards/paper_trail/version_dashboard.rb
require "administrate/base_dashboard"

class PaperTrail::VersionDashboard < Administrate::BaseDashboard
  # ATTRIBUTE_TYPES
  # a hash that describes the type of each of the model's fields.
  #
  # Each different type represents an Administrate::Field object,
  # which determines how the attribute is displayed
  # on pages throughout the dashboard.
  ATTRIBUTE_TYPES = {
    item: Field::Polymorphic,
    id: Field::Number,
    event: Field::String,
    whodunnit: Field::String,
    object: Field::Text,
    created_at: Field::DateTime,
  }.freeze

  # COLLECTION_ATTRIBUTES
  # an array of attributes that will be displayed on the model's index page.
  #
  # By default, it's limited to four items to reduce clutter on index pages.
  # Feel free to add, remove, or rearrange items.
  COLLECTION_ATTRIBUTES = [
    :item,
    :id,
    :event,
    :whodunnit,
  ].freeze

  # SHOW_PAGE_ATTRIBUTES
  # an array of attributes that will be displayed on the model's show page.
  SHOW_PAGE_ATTRIBUTES = [
    :item,
    :id,
    :event,
    :whodunnit,
    :object,
    :created_at,
  ].freeze

  # FORM_ATTRIBUTES
  # an array of attributes that will be displayed
  # on the model's form (`new` and `edit`) pages.
  FORM_ATTRIBUTES = [
    :item,
    :event,
    :whodunnit,
    :object,
  ].freeze

  # Overwrite this method to customize how versions are displayed
  # across all pages of the admin dashboard.
  #
  # def display_resource(version)
  #   "PaperTrail::Version ##{version.id}"
  # end
end

Should Administrate generate this automatically?

sedubois avatar Jun 25 '19 09:06 sedubois

I have problem when namespaces are plurals. In my case shops

This is my very bad fix inside the gem

immagine

descovi avatar Jul 05 '19 15:07 descovi

Looks like I reached a workable state in a project combining Globalize v5.3.0, PaperTrail v10.3.1 (which both use namespaced models) with Administrate v0.12.0, using the following steps:

  1. Remove all previous admin-related config (I had a mix of custom admin pages and RailsAdmin). In particular in routes.rb, removed my old namespace :admin do root ‘pages#home’ ... otherwise got error Page not found when running generator.
  2. Run the generator: rails generate administrate:install. NB: it worked but gave WARNING: Unable to generate a dashboard for PaperTrail::Version. Administrate does not yet support namespaced models. WARNING: Unable to generate a dashboard for Thought::Translation. Administrate does not yet support namespaced models, etc (1 warning about PaperTrail and several others about Globalized models).
  3. For each of these warnings:
    • run something like rails generate administrate:dashboard PaperTrail::Version, rails generate administrate:dashboard Thought::Translation, etc
    • A dashboard configuration file like dashboards/translations_dashboard.rb gets generated successfully with a class such as Thought::TranslationDashboard and various configurations inside, but it is in the wrong directory dashboards --> move it to a new sub-directory which matches the namespace: dashboards/thoughts/translations_dashboard.rb, etc
    • similarly, an admin controller gets generated at the wrong level, move it like this: controllers/translations_controllers.rb --> controllers/thought/translations_controllers.rb
    • Add to routes.rb inside the newly generated admin namespace: namespace :paper_trail do resources :versions end, namespace :post do resources :translations end, etc
  4. At this point the app can be started, and all these routes work without giving an error page: http://localhost:3000/admin, http://localhost:3000/admin/thoughts, http://localhost:3000/admin/thoughts/25, http://localhost:3000/admin/thoughts/25/edit, http://localhost:3000/admin/thought/translations, http://localhost:3000/admin/thought/translations/27.
  5. When viewing http://localhost:3000/admin/thought/translations/27/edit (editing a thought translation) there was the error undefined method 'globalized_model_id', this is because Thought::TranslationDashboard was generated with FORM_ATTRIBUTES = %i[ globalized_model thought_id locale quote ]. I removed globalized_model from this array and then the translation edit page could be loaded correctly.

Some other configurations were then also needed (e.g for models using ActiveStorage attachments, or models with custom to_param method, etc), but these were not related to this namespaced models issue.

sedubois avatar Oct 22 '19 16:10 sedubois

In case anyone else is experiencing issues with getting the namespaced models to work both on development and in production, here's what works for me on Rails 5.2.2 Let's say you have a namespaced model called Foo::Bar

Your controller should look like this:

# app/controllers/admin/foo/bars_controller.rb

module Admin::Foo
  class BarsController < Admin::ApplicationController
  end
end

If you are using administrate in namespaced mode (eg. if it's accessible via /manager instead of /admin), then just replace Admin with Manager and move the controller to app/controllers/manager/foo/bars_controller.rb

**NOTE**: If you define your controller as below, it won't work on production, at least not on rails 5.2.2
# app/controllers/admin/foo/bars_controller.rb

module Admin
  class Foo::BarsController < Admin::ApplicationController
  end
end

Your dashboard should look like this:

# app/dashboards/foo/bar_dashboard.rb

require 'administrate/base_dashboard'

class Foo::BarDashboard < Administrate::BaseDashboard
  ATTRIBUTE_TYPES = {...}.freeze

  COLLECTION_ATTRIBUTES = [...].freeze

  SHOW_PAGE_ATTRIBUTES = [...].freeze

  FORM_ATTRIBUTES = [...].freeze
end

Your routes should look like this

# config/routes.rb
namespace :admin do
    namespace :foo do
      resources :bars
    end
end

lukaVarga avatar Dec 09 '19 19:12 lukaVarga

Adding a bit more of context.

The main issue is that Administrate assumes the names of models following Rails conventions only. There isn't an official way of telling a dashboard that it should be serving a model different from the one named in the current URL.

Due to this, if you want to provide a dashboard for a model, you must ensure that all necessary pieces are in place, and have the expected names.

For example, for PaperTrail::Version you will need the following.

The admin controller will be under the Admin namespace (or whatever you call it in your app), as well as the PaperTrail namespace because it's part of the model name:

### app/controllers/admin/paper_trail/versions_controller.rb
module Admin
  module PaperTrail
    class VersionsController < Admin::ApplicationController
    end
  end
end

The route will reflect this namespacing too:

### config/routes.rb
Rails.application.routes.draw do
  namespace :admin do
    resources :products
    namespace :paper_trail do
      resources :versions
    end
  end
end

The dashboard will also need the PaperTrail namespace:

### app/dashboards/paper_trail/version_dashboard.rb
require "administrate/base_dashboard"

module PaperTrail
  class VersionDashboard < Administrate::BaseDashboard
    ATTRIBUTE_TYPES = {
      event: Field::String,
      object: Field::Text,
    }.freeze

    COLLECTION_ATTRIBUTES = ATTRIBUTE_TYPES.keys
    SHOW_PAGE_ATTRIBUTES = ATTRIBUTE_TYPES.keys
    FORM_ATTRIBUTES = ATTRIBUTE_TYPES.keys
  end
end

With any other dashboards referring to your model via associations, use the option class_name to specify the model class:

### app/dashboards/product_dashboard.rb
ATTRIBUTE_TYPES = {
  id: Field::Number,
  name: Field::String,
  code: Field::String,
  price_in_cents: Field::Number,
  stock: Field::Number,
  versions: Field::HasMany.with_options(class_name: 'PaperTrail::Version'),
  created_at: Field::DateTime,
  updated_at: Field::DateTime,
}.freeze

And that should be it.

The situation is not ideal, and it stems from the way that ResourceResolver works, which I think is a bit too clever. It's related to https://github.com/thoughtbot/administrate/issues/405, where I propose a potential solution (which would need a PR). Still, this doesn't mean yet that such a solution is going to happen any time soon. It requires a bit of thought, and for this we require a bit of time. Personally I'd rather prune the long list of issues a bit before introducing big changes. That will help us understand better what sort of use cases people are getting into.

Or, someone could try implement the solution at #405, and at least we'll have real code to discuss. Doesn't mean it will definitely be merged! But at least it helps understanding the problem better.

pablobm avatar Dec 29 '19 22:12 pablobm

In Rails 6.0.3.2 the above solution does not appear to quite work

I'm getting: NoMethodError (undefined method `admin_tenants_path' for #Admin::Manyhands::TenantsController:0x00007f959144c1a8):

This is in the _form.html.erb file, which is receiving:

["admin", #<Manyhands::Tenant: id: nil, created_at: nil, updated_at: nil>]

The problem is here: actionpack-6.0.3.2/lib/action_dispatch/routing/polymorphic_routes.rb:312

if model.persisted?
  args << model
  model.model_name.singular_route_key
else
  @key_strategy.call model.model_name
end

For the 'new' routes, it'll use the key_strategy lambda, which uses the ActiveModel::Name.param_key value ('tenant') in this case.

For routes where the mode is persisted, it'd use singular_route_key (also 'tenant')

Near as I can see, I have 2 options:

  1. I could override the model_name on all of my models. This seems like a terribly confusing plan
  2. I can override the _form.html.erb from administrate, and specify the url: parameter eg:
<%= form_for([namespace, page.resource], html: { class: "form" }) do |f| %>

becomes

<%= form_for(page.resource, url: admin_manyhands_tenants_path, html: { class: "form" }) do |f| %>

NOTE If you're coming behind me w/ the same problem, note that the solution above is not complete, because you have to discern between the plural (POST) and singular (PUT/PATCH etc) paths. If this is the direction I go, I guess I'll create some kind of helper to generate this.

Is there a better way to do this that I'm not seeing?

jeffdeville avatar Aug 26 '20 02:08 jeffdeville

When setting up for ActiveStorage the instructions from Pablo above worked well but I had to scope the routes as follows:

namespace :admin do
  scope module: :active_storage do
    resources :attachments
    resources :blobs
  end
end

andrewdsmith avatar Aug 27 '20 14:08 andrewdsmith

@andrewdsmith - when I change from:

namespace :admin do
  namespace :manyhands do
    resources :tenants
  end
end

to

namespace :admin do
  scope module: :manyhands do
    resources :tenants
  end
end

I wind up with an error on /admin/tenants, where it tells me: undefined method new_admin_manyhands_tenant_path' for #<#Class:0x00007fed7130b2e8:0x00007fed71309a10>` that's true, because the scope call removes it.

This is called from: app/views/administrate/application/index.html.erb:44

    <%= link_to(
      t(
        "administrate.actions.new_resource",
        name: page.resource_name.titleize.downcase
      ),
      [:new, namespace, page.resource_path],
      class: "button",
    ) if valid_action?(:new) && show_action?(:new, new_resource) %>

jeffdeville avatar Aug 27 '20 16:08 jeffdeville

@jeffdeville My solution probably only works because I'm not allowing the creation of new attachments or blobs, so I don't encounter the code that's causing an error in your case. I was really just commenting above to add an option for people to try who end up at this issue while trying to problem solve. I'm just baffled by why I had to switch to scope in my case.

andrewdsmith avatar Aug 28 '20 09:08 andrewdsmith

@jeffdeville - Thank you for your description of the issue. If you run the following on the console, do you get the same I get?

> model = Manyhands::Tenant.first
  Manyhands::Tenant Load (0.8ms)  SELECT "tenants".* FROM "tenants" ORDER BY "tenants"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<Manyhands::Tenant id: 1, name: "Foo">
> model.model_name.singular_route_key
=> "manyhands_tenant"
> model.model_name.param_key
=> "manyhands_tenant"

From what you say, it looks like instead you get "tenant" in both cases. Is that correct?

I'm surprised as I don't think that's the default behaviour. Is it possible that there's something else in your models that makes the result be different?

pablobm avatar Aug 31 '20 14:08 pablobm

Related/possible fix for the use cases mentioned here as well: https://github.com/thoughtbot/administrate/issues/1037#issuecomment-1260082686

khamusa avatar Sep 27 '22 21:09 khamusa

I researched this area recently. The reason why scope module: :active_storage works has to do with complex Rails internals.

  1. ActiveStorage uses isolate_namespace (just as Administrate does).
  2. Among other things, this monkeypatches the target module (ActiveStorage) so that use_relative_model_naming? returns true.
    • See https://github.com/rails/rails/blob/v6.1.6/railties/lib/rails/engine.rb#L401
  3. This is used by ActiveModel::Name to tell whether the "route key" of a model should include its namespace. For ActiveStorage, it causes ActiveStorage::Blob.model_name.route_key to be blobs, instead of active_storage_blobs.
    • See https://github.com/rails/rails/blob/v6.1.6/activemodel/lib/active_model/naming.rb#L297
  4. In turn, this is used by polymorphic_path. Therefore, this generates things like the routes for ActiveStorage::Blob turning up like blob_path instead of active_storage_blob_path.

So using scope module: :active_storage makes it so that Rails understands that the resource is ActiveStorage::Blob while not creating a routing namespace. Therefore the routes can be just blob.

Another workaround I have found is using namespace, along with the little known method direct in the router, in order to declare the routes of these de-namespaced models explicitly:

# config/routes.rb
  direct(:admin_blob) { |blob, options| route_for(:admin_active_storage_blob, blob, options) }
  direct(:admin_blobs) { |opts| route_for(:admin_active_storage_blobs, opts) }
  direct(:edit_admin_blob) { |blob, opts| route_for(:edit_admin_active_storage_blob, blob, opts) }
  direct(:admin_attachment) { |attachment, options| route_for(:admin_active_storage_attachment, attachment, options) }
  direct(:admin_attachments) { |opts| route_for(:admin_active_storage_attachments, opts) }
  direct(:edit_admin_attachment) { |attachment, opts| route_for(:edit_admin_active_storage_attachment, attachment, opts) }

pablobm avatar Oct 06 '22 14:10 pablobm

Regarding the problem with Manyhands::Tenant, it may have been because of the plural namespace. This has been fixed now with https://github.com/thoughtbot/administrate/pull/2261

pablobm avatar Oct 06 '22 14:10 pablobm

By the way, I think the "solution" for this issue is documenting it, probably in the Guides, which I should write more for...

I'll leave this open until I get around to doing it. Also this comment will remind me of what I'm supposed to do.

pablobm avatar Oct 13 '22 14:10 pablobm