administrate
                                
                                 administrate copied to clipboard
                                
                                    administrate copied to clipboard
                            
                            
                            
                        Show page with HasMany calls the wrong _collection view for the related class
I believe this has been submitted before and closed but not fixed. https://github.com/thoughtbot/administrate/issues/1183
When viewing a show page for a class with a HasMany relationship it does not render the _collection partial for the associated class, instead it renders the _collection partial of dashboard class.
Lets say you have an author class with a has_many relationship to books, when the books are listed in the authors show page it will use the _collection partial of the author. Most of the time this wont matter, however, if you have modified the _collection partial of author class it will usually throw an error because you are trying to display an attribute on the book that only exists on the author.
To recreate:
- Generate index views for a class (e.g. author) with a has_many relationship.
- Add a custom column to the generated _collection view (views/admin/authors/_collection.html.erb), lets say <% resource.full_address %>
- Include the has_many relationship in the show page of the parent class (books: Field::HasMany) and add it to the SHOW_PAGE_ATTRIBUTES
- Visit the show page of the author and it should throw an error because it is using the _collection partial generated for the author where it is listing the books. Instead it should use the default administrate _collection partial, or if books has _collection partial generated it should use that.
Thank you for the detailed report. I have been able to reproduce this issue, and I agree that it's a bug. I'll look into it, although I don't have much time these days.
In the meantime, we'd be happy to accept a PR. If someone would like to tackle this, but is unconfident about it, a temptative PR with the general idea would be welcome. From there we can iterate into a full-fledged one.
I had a deeper look at what's wrong. I'll describe it here and perhaps someone will be encouraged to try fix it? :slightly_smiling_face: (If not, a maintainer will eventually get to it).
The problem is not so much about which dashboard is used, but about which view template is rendered. The field type HasMany finds the correct dashboard using Associative#associated_dashboard, but when it comes to rendering the template, Rails's defaults play against us.
The relevant code is the following:
https://github.com/thoughtbot/administrate/blob/9e462f7179f47151d1b91aab59291e6884870dab/app/views/fields/has_many/_show.html.erb#L24-L31
The partial template has_many/_show.html.erb renders "collection" (as it should), but it doesn't tell Rails what the context is. Rails proceeds as usual and figures out that the current controller is authors, so that must be the context. Therefore it tries to find the "collection" partial in the following folders: admin/authors, admin/application, administrate/application. Instead, we would expect that the first folder to look up was admin/books, where the correct template lives.
You can see this yourself by doing the following:
- Clone Administrate into your computer.
- Set up your Rails app to use your cloned, local copy of Administrate instead of the downloaded gem. You'll need to set this up in the Gemfilewith a line likegem "administrate", path: "../../path/to/administrate".
- In Administrate's source code, edit app/views/fields/has_many/_show.html.erband change therendercall to render something wrong instead of"collection". For example `render("foobar", etc...)
- Run the app and visit a showpage.
- You will see a ActionView::MissingTemplateerror, listing the folders where rails tried to find this non-existent"foobar"folder. It'll read something like:Missing partial admin/authors/_foobar, admin/application/_foobar, administrate/application/_foobar....
As for how to fix this, I'm not sure. It looks like Rails may have an API that could help here, with the methods prepend_view_path and append_view_path (see the guides). However perhaps this it not the right tool, as this might not be granular enough for what we want here: they might affect the application globally whereas what we want is to affect only a specific context.
I had a quick look at Rails's code and there might be something to learn from seeing how ActionView::LookupContext works (the @prefixes might be what we need to play with), and how it's used from ActionView::Base.
But I'm not sure of any of it!
@pablobm you are spot on, on the issue, I have made a workaround by making below change in the has_many/_show.html.erb:
<%= render( 
     "/admin/#{field.name}/collection", 
     collection_presenter: field.associated_collection(order), 
     collection_field_name: field.name, 
     page: page, 
     resources: field.resources(page_number, order), 
     table_title: field.name, 
   ) %> 
(Comment originally published at https://github.com/thoughtbot/administrate/issues/2370#issuecomment-1635644394, but moved here as that issue is a duplicate).
I have been looking into this. It's a tricky one, not just in terms of code but more importantly in terms of functionality. We need to put on our Product Manager hats and think of what we want to offer here.
We have a resource Customer for which we want to render a custom collection template. The bug here is that other collections for other resources in the Customer show page (present as HasMany fields) also use the custom collection template intended only for Customer.
From a functionality point of view, the "obvious" fix here is to ensure that each resource type should use its own collection type. Cool. But we have to think about the different variations of this pattern and make sure that our solution caters for all of them.
These are template variations that I foresee users wanting to customise:
- Resource collection on the resource's own index page (the basic one).
- Any "has many" collection on any resource's show page (a "has many" template different from the one used in index pages).
- The "has many" collection of a specific resource on any resource's show page (the "has_many" template of Customerin any show page)
- Any "has many" collection on the show page of a specific resource (all the "has_many" templates in a Customershow page)
- A specific "has many" collection on the show page of a specific resource (the "has many" template for Orderwhen it appears in theCustomershow page).
- Generalisation to other field types, possibly with some sort of inheritance mechanism. Eg: has_many_variation or nested_has_many to try a template of its own field type, then fall back on the has_many template, all while respecting the above variations!
High-level view of the code
I found it useful at this point to see where Rails expects to find partial templates. For example, if instead of collection_header_actions I change the template to render foo, I get this error:
# ActionView::MissingTemplate:
#   Missing partial
       admin/customers/_foo
       admin/application/_foo
       administrate/application/_foo
    with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby]}.
#
#   Searched in:
#     * "$ROOT/spec/example_app/app/views"
#     * "$GEMS/administrate-field-image-1.2.0/app/views"
#     * "$ROOT/app/views"
#     * "$GEMS/kaminari-core-1.2.2/app/views"
This shows us that there are two "levels" of where to find the templates: the specific template names (listed under Missing Partial:), and the directory paths where Rails looks for template files (listed under Searched in:). Plus details like locale, formats, handlers, which we'll have to make sure not to break either.
Back to collection_header_actions, this is where Rails looks for the partial when rendering a collection of customers on the customers index page:
Partials:
- admin/customers/_collection_header_actions
- admin/application/_collection_header_actions
- administrate/application/_collection_header_actions
Paths:
- "$ROOT/spec/example_app/app/views"
- "$GEMS/administrate-field-image-1.2.0/app/views"
- "$ROOT/app/views"
- "$GEMS/kaminari-core-1.2.2/app/views"
Let's not thing about the Paths: list for now as I think those are ok as they are (shout if you think otherwise!) I'm going to focus on the Partials: list.
Now, let's say we want to improve this list to allow all the variations I listed above. Where should Rails look for partials when asked to render collection_header_actions for a Customer.has_many :orders?
Partials:
- admin/customers/has_many/orders/_collection_header_actions
- admin/customers/has_many/_collection_header_actions
- admin/has_many/orders/_collection_header_actions
- admin/orders/_collection_header_actions
- admin/application/_collection_header_actions
- admin/has_many/_collection_header_actions
- administrate/application/_collection_header_actions
So far, does this make sense? Please shout if you need clarification, or if you think it should be different.
Implementation
I've been experimenting with ActionView::LookupContext (available in views as lookup_context). My current intuition is that we can use it to implement our own render helper that will look for templates in the way we want. This isn't going to be simple! I wish there was another way to do this, but I'm running out of other ideas after kicking Rails's tires for a bit.
I've been able to do something like this:
locations = %W[admin/customers admin/application administrate/application]
template = lookup_context.find("collection", locations, true)
# With `template` an instance of `ActionView::Template`
Then we need to actually render that template, for which I believe we'd have to use view_renderer (still investigating). There's some information at https://medium.com/rubyinside/disassembling-rails-template-rendering-2-a99214c6fde8, but there's some additional digging to do.
OK, that's me for today. Let's see if I can do a bit more of this next week.
Here is the solution we are using...
First we check to see if a template exists for this field type. If so, we explicitly set the partial path. If not, we fall back to the default behavior.
  <%
    field_path = "#{namespace}/#{field.data.klass.to_s.try(:underscore).try(:pluralize)}"
    collection_template = if lookup_context.exists?("collection", field_path, true)
      "#{field_path}/collection"
    else
      "collection"
    end
  %>
  <%= render(
          collection_template,
          collection_presenter: field.associated_collection(order),
          collection_field_name: field.name,
          page: page,
          resources: field.resources(page_number, order),
          table_title: field.name
          ) %>