Search code examples
ruby-on-railsrubyruby-on-rails-7

Rails: nested namespace produces a weird path to a partial, resulting a MissingTemplate error


I have an ActiveRecord model named Book, and a model named Book::Author. An author has many books through the Book::Authorship model (one-to-many association).

Off-topic:

The reason to put the Author model under the Book namespace is that I might want to have another author model (e.g. the Post::Author) which is completely unrelated to the Book::Author. With a namespace I can clearly show the relationship, e.g.: example.com/books/authors explicitly states that these are the authors that wrote books (and not posts or anything else).

In my app only administrators can create/update/delete Books and Authors. So I've created a separated namespace Admin for the controllers and the views for the administrators only.

In the routes.rb file I have:

Rails.application.routes.draw do
  # These are for the regular users:
  # only #index and #show actions are defined in the respective controllers:
  namespace :books do
    resources :authors, only: %i[index show]
  end
  resources :books, only: %i[index show]

  # These are for the admins only:
  # all CRUD methods are defined in the respective controllers:
  namespace :admin do
    namespace :books do
      resources :authors
    end

    resources :books
  end
  resources :admin

  root "books#index"
end

Then I've created the Admin::Books::AuthorsController controller:

  • /app/controllers/admin/books/authors_controller.rb

I would expect that the view path follows the same path pattern:

  • /app/views/admin/books/authors/_index.html.erb for the index;
  • /app/views/admin/books/authors/_author.html.erb for the partial.

Unfortunately, it doesn't: the index page does work, but it can't find the _author.html.erb partial.

Here's how I'm trying to render the list of authors in the /app/views/admin/books/authors/_index.html.erb:

  <% @books_authors.each do |book_author| %>
    <%= render book_author %>
  <% end %>

Which gives the following error:

ActionView::MissingTemplate in Admin::Books::Authors#index

Missing partial admin/books/books/author/_author with {...}.

There are two “books” in the path... but why?

The code above works only with an explicit path to the template:

  <% @books_authors.each do |book_author| %>
    <%= render "admin/books/authors/author", book_author: book_author %>
  <% end %>

Which I don't like, since it breaks the convention over configuration paradigm. I don't want to manually type a path in every view under the Admin::Books:: namespace.

I'm looking for a way to achieve the desired functionality without using an explicit path. How I can tell Rails to not include “books” twice in the path when it's looking for a partial?


Solution

  • You controller is namespaced Admin::Books and your model is Books::Author, together you get double "books" path. The logic roughly looks like this:

    [
      File.dirname(Admin::Books::AuthorsController.new.lookup_context.prefixes.first),
      Books::Author.new.to_partial_path
    ].join("/")
    
    #=> "admin/books/books/authors/author"
    

    lookup_context is something you could modify (probably not a good idea):

    # app/controllers/admin/books/authors_controller.rb
    
    def index
      lookup_context.prefixes = ["admin/books", "application"]
      @books_authors = Books::Author.all
    end
    

    Another way would be to drop one of the Books namespaces:

    # either change your controller
    class Admin::AuthorsController < ApplicationController
      def index
        @books_authors = Books::Author.all
      end
    end
    
    # or model, what if someone writes a book and a post?
    class Admin::Books::AuthorsController < ApplicationController
      def index
        @books_authors = Author.joins(:books)
      end
    end
    

    Keeping your models flat is a popular way of sticking to conventions, no namespaces - no problems:

    BookAuthor
    PostAuthor