Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespaced models not working #1291

Open
TrevorHinesley opened this issue Feb 24, 2019 · 20 comments
Open

Namespaced models not working #1291

TrevorHinesley opened this issue Feb 24, 2019 · 20 comments
Labels
bug breakages in functionality that is implemented resolver

Comments

@TrevorHinesley
Copy link

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.

@frm
Copy link
Contributor

frm commented Feb 26, 2019

@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.

@TrevorHinesley
Copy link
Author

TrevorHinesley commented Feb 26, 2019

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?

@coffeejunk
Copy link

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)

@usman-ahmad
Copy link

@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

@coffeejunk
Copy link

@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

@Tashows
Copy link

Tashows commented Jun 20, 2019

@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.

@sedubois
Copy link
Contributor

sedubois commented Jun 25, 2019

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?

@descovi
Copy link

descovi commented Jul 5, 2019

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

This is my very bad fix inside the gem

immagine

@sedubois
Copy link
Contributor

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.

@lukaVarga
Copy link

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

@pablobm
Copy link
Collaborator

pablobm commented Dec 29, 2019

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 #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.

@nickcharlton nickcharlton added bug breakages in functionality that is implemented namespacing models with a namespace labels Jan 2, 2020
charliegucci added a commit to charliegucci/marketplace that referenced this issue Mar 10, 2020
… to see all messages sent by buyers to dog sellers

Note: When generating/creating an `administrate` dashboard
that is namespaced e.g. Buyer::MessageDashboard,
administrate does not seem to put them automatically in the
correct folder/namespace hierarchy to properly autoload the files

… so you need to manually create the proper folder structure as
shown in this open thoughtbot/administrate issue:

thoughtbot/administrate#1291 (comment)
@jeffdeville
Copy link

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?

@andrewdsmith
Copy link

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

@jeffdeville
Copy link

@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) %>

@andrewdsmith
Copy link

@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.

@pablobm
Copy link
Collaborator

pablobm commented Aug 31, 2020

@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?

@khamusa
Copy link

khamusa commented Sep 27, 2022

Related/possible fix for the use cases mentioned here as well: #1037 (comment)

@pablobm
Copy link
Collaborator

pablobm commented Oct 6, 2022

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.
  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.
  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
Copy link
Collaborator

pablobm commented Oct 6, 2022

Regarding the problem with Manyhands::Tenant, it may have been because of the plural namespace. This has been fixed now with #2261

@pablobm
Copy link
Collaborator

pablobm commented Oct 13, 2022

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 pablobm added resolver and removed namespacing models with a namespace labels Apr 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug breakages in functionality that is implemented resolver
Projects
None yet
Development

No branches or pull requests