While I suspect this is probably not a “bug” per-se, I’ve experienced some confusing behavior when using nested, namespaced models with the form helper. Here is a complete repro to explain the issue better:
# frozen_string_literal: true
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
gem "sqlite3", "~> 1.4"
end
require "active_record"
require "logger"
require "action_controller/railtie"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :accounts, force: true do |t|
t.string :name
end
create_table :account_entries, force: true do |t|
t.string :name
t.references :account, foreign_key: true
end
end
class Account < ActiveRecord::Base
has_many :entries, class_name: "Account::Entry"
end
class Account::Entry < ActiveRecord::Base
belongs_to :account
end
class TestApp < Rails::Application
config.root = __dir__
config.hosts << "example.org"
config.secret_key_base = "secret_key_base"
config.logger = Logger.new($stdout)
Rails.logger = config.logger
routes.draw do
get "/" => "test#index"
resources :accounts do
resources :entries, module: :account
end
end
end
class TestController < ActionController::Base
include Rails.application.routes.url_helpers
def index
account = Account.create! name: "Test Account"
entry = account.entries.create! name: "Test Entry"
render inline: <<~ERB, locals: { account: account, entry: entry }
<%= form_with model: [account, entry] do |f| %>
<%= f.text_field :name %>
<% end %>
ERB
end
end
require "minitest/autorun"
require "rack/test"
class BugTest < Minitest::Test
include Rack::Test::Methods
def test_generates_wrong_path
begin
get "/"
rescue => exception
assert_match(/ActionView::Template::Error \(undefined method `account_account_entry_path' for an instance of/, exception.message)
end
end
private
def app
Rails.application
end
end
Given this behavior, it leaves me with a couple of questions:
Is overriding the url the correct solution to this? Or is there a less cumbersome way to handle this?
Does this potentially indicate a modeling / routes issue? I’ve placed Entry in the Account::Entry namespace as a way to communicate, “An Entry is highly coupled and relevant to an Account”, but it seems to fight against many of the Rails helpers like this. Is this an improper usage of namespacing?
I tried something like this a few years ago, and after months trying to get it to work on a side branch I abandoned it and went with flat models, and namespaced controllers. I can’t describe how frustrating it was.
I kept finding phantom indications on the internet of people doing this, but they never worked. If you somehow get it please let me know, but i think you’ve isolated the issue quicker than i did. I feel like i finally ran across a thread where DHH advised flat models (the path I ended up taking), and against namespacing models, but i can’t find it.
My constraint here is actually on the model side of things. If I were to make Entry a flat model, it wouldn’t necessarily make much sense in my domain, hence why I’ve namespaced it with Account::Entry, which produces a semantic DB table of account_entries.
I suppose a model of AccountEntry vs. Account::Entry may work better here, but would love to take advantage of namespacing as it does provide good organization to the project and communicate the domain concepts well.
I was surprised how few threads are out there surrounding this topic, leading me to believe I’m either missing something obvious or misusing namespaces entirely.
Yes, that was very much like my consideration, and what you describe is what i ended up doing.
We have a model I wanted to namespace as Location::Description — because there are other models that have Descriptions too, so a flat Description model would not make sense. I remember now, the path helpers were definitely a gotcha. IIRC I tried monkey patching the helper class, but I couldn’t get it to work, plus it felt really hacky… and that’s when I believe I ran across the DHH advice in some thread.
So I went with LocationDescription as my “flat model” name — it inherits from a Description base class, and the db table is still location_descriptions as desired.
I did use namespacing in the controllers, we now have a Locations::DescriptionsController.
Now that i’m searching again, namespacing models does seem possible, just involved. Here’s a good discussion of why it is the way it is, and what’s necessary to override it, by SO Rails guru “max”:
Thanks for the response, this is a really helpful thread to read through! I hadn’t thought about LocationDescription rather than Location::Description. That sounds like a much less frustrating approach.
Another relevant thread suggests adding in use_relative_model_naming?, but this seemed less documented than overriding ActiveModel::Naming to me.
I’ve implemented this:
def model_name
ActiveModel::Name.new(self, Account)
end
And seems to work great!
The output of polymorphic_path([entry.account, entry]) returns the expected account_entry_path according to what I would expect based on these routes:
resources :accounts do
scope module: :account do
resources :entries, only: %i[ edit update show destroy ]
end
end
Thought I’d leave this here for future reference in case others stumble upon it.
In my case, having the /account namespace in the models folder is a huge organizational benefit due to the size of the codebase, so the complexity tradeoff is worth it. Otherwise, I’d probably opt for the AccountEntry approach in the future. Just wish this one hadn’t required so much digging as it seems to be a common confusion in Rails.