Rails mountable engines helper methods not found when routing from main app - ruby-on-rails

I have a module as a mountable engine mounted to a main app
through
mount MyEngine::Engine, :at => '/myengine'
I have everything namespaced in the negine and the engine has it's own views in engine/app/views/myengine/
Everything works fine when I run rails server then try to access
localhost:3000/myengine
first then go to the root of the main app and come back to the engine through a link in the index view of the main app
However when I start the server, go to localhost:3000 and from there click on the link to the engine module it tries to fetch the views correctly however the methods contained in the engine's helpers raises an error upon call for them being undefined.
I am on rails 4

I used eager_load to load the mountable engine in it's initialized in the main app and everything seems to be working now.
Myengine::Engine.eager_load!

I just ran into this issue as well. I had defined an ApplicationController inside my engine and was seeing NoMethodErrors when I tried to use helper methods inside one of my engine's controllers.
The Problem
The code looked something like this:
in my_engine/app/controllers/my_engine/application_controller.rb
module MyEngine
class ApplicationController < ActionController::Base
helper ApplicationHelper
end
end
in my_engine/app/controllers/my_engine/projects_controller.rb
module MyEngine
class ProjectsController < ApplicationController
def new
# some action code here
end
end
end
in my_engine/app/helpers/my_engine/application_helper.rb
module MyEngine
module ApplicationHelper
def translations_include_tag
javascript_include_tag "translations-#{I18n.locale}.js"
end
end
end
If I navigated to a route in the host rails app, then clicked a link that navigated to my_engine/projects/new, I would get a NoMethodError saying translations_include_tag didn't exist.
The Explanation
I think this was happening because the application had two classes with the same name: ApplicationController in MyEngine and ApplicationController in the host app. When the first route outside the engine is visited, Rails autoloads the ApplicationController from the host app. What that means is Rails associates the main app's ApplicationController with the name "ApplicationController". When you navigate to my_engine/projects/new, Rails lazy loads ProjectsController which also inherits from ApplicationController. Rails/ruby thinks you're referring to the same ApplicationController it has already loaded, so in effect ProjectsController ends up inheriting from the host app's ApplicationController instead of the one in the engine. Since the translations_include_tag method isn't defined in the host app's ApplicationController, ruby raises a NoMethodError when the view tries to call it.
The Solution
I was able to fix the problem by explicitly inheriting from MyEngine's ApplicationController:
in my_engine/app/controllers/my_engine/projects_controller.rb
module MyEngine
# changed from just plain 'ol ApplicationController
class ProjectsController < ::MyEngine::ApplicationController
def new
# some action code here
end
end
end
After fixing the inheritance issue, the NoMethodError went away.
The accepted answer says to use eager loading, which will also fix the problem because it forces the engine's ApplicationController to load first. Since the engine's ApplicationController is namespaced inside MyEngine, it's full name is MyEngine::ApplicationController, which doesn't conflict with the main app's ApplicationController because the constant names are different.

You need to include the helpers explicitely in your lib/engine.rb file:
initializer 'your-gem-name.setup_helpers' do |app|
app.config.to_prepare do
ActionController::Base.send :include, HelperModuleName
end
end
end

Related

Render a view in Rails 5 API

I generated an API-only rails app with Rails 5 via rails new <application-name> --api. I've decided I want to include a view for testing some things and am having issues getting a view to load.
I created a users/index.html.erb file with some text and my controller is now simply def index; end but there is nothing appearing when I hit the /users URL. I also tried commenting out the # config.api_only = true in config/application.rb but that didn't affect anything. Any suggestions on how to proceed?
You don't need to uncomment config.api_only = true for this purpose, just inherit your controller from ActionController::Base, or do it in your ApplicationController (default for common rails generation).
Code:
For this controller only YourController < ActionController::Base
For all apllication ApplicationController < ActionController::Base
this is from the ActionController::Metal docs https://apidock.com/rails/ActionController/Metal
it says:
ActionController::Metal by default provides no utilities for rendering >views, partials, or other responses aside from explicitly calling of >response_body=, content_type=, and status=. To add the render helpers >you’re used to having in a normal controller, you can do the following:
class HelloController < ActionController::Metal
include AbstractController::Rendering
include ActionView::Layouts
append_view_path "#{Rails.root}/app/views"
def index
render "hello/index"
end
end
So I've tried it myself and adding just by adding the two modules actually work just fine when using it for ActionController::API

Rails module scope

Given the following controller structure:
# application_controller.rb
class ApplicationController < ActiveController::Base; end
# pages_controller.rb
class PagesController < ApplicationController; end
# admin/application_controller.rb
module Admin
class ApplicationController < ::ApplicationController; end
end
# admin/pages_controller.rb
module Admin
class PagesController < ApplicationController; end
end
One would expect Admin::PagesController to inherit from Admin::ApplicationController and it does. But I have noticed that sometimes it inherits from ::ApplicationController.
So I decided not to risk it and changed declaration of all controllers in /admin to specifically target Admin::ApplicationController
# admin/pages_controller.rb
module Admin
class PagesController < Admin::ApplicationController; end
end
Okay that works, but from what I know it was correct in the first place. Why Rails inherits from a wrong controller sometimes?
Admin::PagesController sometimes inherits from ApplicationController instead of Admin::ApplicationController despite both being in the same module Admin
The problem here is rails' development mode code loading: in general code is loaded when you try to do something with a constant (eg subclass from it) and that constant doesn't exist. This results in const_missing being called and rails uses it this to try to load the class (for a detailed description see the guide).
If neither ApplicationController nor Admin::ApplicationController exist then when you access your admin pages controller ruby will hit that const_missing and try to load admin/application_controller.rb
However if ApplicationController is already loaded then ruby won't fire const_missing since it perfectly legal for a class in the admin module to inherit from something at the toplevel.
The solution as you say is to make explicit what you are inheriting from. Personally in my own apps I use Admin::BaseController as the base class.
Another option is to use require_dependency to point Rails to the correct file:
# admin/application_controller.rb
require_dependency 'admin/application_controller'
module Admin
class PagesController < ApplicationController
end
end

Using helpers from mounted engine in parent Rails app

I mounted engine to the application, and I want to use one of the engine's helper all over the parent application.
In my Dummy app I put this in Dummy's application_controller.rb:
class ApplicationController < ActionController::Base
include Sourcebuster::CookieSettersHelper
before_filter :set_sourcebuster_data
helper_method :extract_sourcebuster_data
private
def set_sourcebuster_data
set_sourcebuster_cookies
end
end
And it was ok.
But when I mounted it to the real app, helper from engine just don't work.
How to load it, so it can do the stuff all over the parent app?
Got it.
I tried to set_sourcebuster_data inside namespaced engine (http://lvh.me:3000/sourcebuster).
before_filter in application's application_controller.rb don't work in this case.
Added it to engine's application_controller.rb, now it's doing the stuff all over the app)

CanCan: load_and_authorize_resource in namespace other than that of MainApp

I'm using CanCan for permissions in my Rails application in which I have built my own engine for some generic form functionality. I would like to lock down permissions in my system so that users cannot freely access my engine controllers' actions. These controllers, for the most part, just make use of the 7 REST actions, so I would like to use CanCan's load_and_authorize_resource at the top of each controller.
However, when I write my code like this:
module MyEngine
class FormController < ApplicationController
load_and_authorize_resource
...
end
end
I get this error:
uninitialized constant Form
My guess is that the automatic loader in load_and_authorize_resource is tied to my MainApp namespace, and does not recognize that I am calling it in a different namespace, and so does a call like Form.find(params[:id]) rather than MyEngine::Form.find(params[:id]).
If this is the case, how can I fix this? It's not a huge deal because authorize! still works properly, so I can define the authorization in each action individually, but it would feel much cleaner if I were able to use the load_and_authorize_resource method.
CanCan can not find namespaced models. Try to specify class:
load_and_authorize_resource class: MyEngine::Form
It seems to be a bug in CanCan::ControllerResource#namespace:
def namespace
#params[:controller].split("::")[0..-2]
end
As you see, it tries to split controller path by :: but it comes in the form of my_engine/my_controller.
So the fix is dumb simple:
def namespace
#params[:controller].split("/")[0..-2]
end
Wonder how they could miss such a stupid bug for so long. Shall send them a pull request.
P.S. Have just signed up to answer 8)
If the model class is namespaced differently than the controller, you will need to specify the :class option.
module MyEngine
class FormController < ApplicationController
load_and_authorize_resource :class => MyEngine::Form
...
end
end

Missing devise routes helpers inside of rails engine views

I'm building a Rails engine called Engrave.
I have the engine mounted like so:
# Routes.rb of the host app
mount Engrave::Engine => "/engrave", :as => "engrave_engine"
Within this engine I have a controller called "PostsController". When I navigate to this controller to view a post like so: /engrave/posts/1 I get this error:
undefined local variable or method `new_user_session_path'
The PostsController in the engine is inheriting from the engine controller, which is inheriting from the application controller, like so:
module Engrave
class PostsController < ApplicationController
...
end
class Engrave::ApplicationController < ApplicationController
end
The new_user_session_path is being defined by devise, which I have setup like:
devise_for :users
The call to new_user_session_path is in the layouts/application.html.erb template file in the host app
I cannot figure out why this route helper isn't available in this context. What am I doing wrong?
Use
main_app.new_user_session_path
that should work
I've had success doing the following in the main app's application_helper.rb:
module ApplicationHelper
# Can search for named routes directly in the main app, omitting
# the "main_app." prefix
def method_missing method, *args, &block
if main_app_url_helper?(method)
main_app.send(method, *args)
else
super
end
end
def respond_to?(method)
main_app_url_helper?(method) or super
end
private
def main_app_url_helper?(method)
(method.to_s.end_with?('_path') or method.to_s.end_with?('_url')) and
main_app.respond_to?(method)
end
end
I've used this in mountable engines, so you do not have to sacrifice those features.
Working off of this response, I include all helpers found in application_helpers.rb by stating helper "manager/application" inside the controller (if 'manager' is the current namespace of your mountable engine. Just use 'application' if you are calling this from a standard application).

Resources