How to structure namespaced modules - ruby-on-rails-4

I am having trouble with Ruby class (constant) lookup within the context of a Rails engine gem.
I have a gem MyGem that is a Rails engine. It defines non-namespaced models that are expected to be overridden by the MainApp that would include the gem and namespaced modules, which are included in gem's and main_app's models to have a DRY way of defining reusable code.
Here is a sample code structure:
Two models
# in /app/models/user.rb
class User < ActiveRecord::Base
include MyGem::User::CommonExt
end
# in /app/models/comment.rb
class Comment < ActiveRecord::Base
include MyGem::Comment::CommonExt
end
Their two modules
# in /app/models/concerns/my_gem/user/common_ext.rb
module MyGem::User::CommonExt
def load_comment(id)
return Comment.find(id)
end
end
# in /app/models/concerns/my_gem/comment/common_ext.rb
module MyGem::Comment::CommonExt
def load_user(id)
return User.find(id)
end
end
Now, if I call
User.new.load_comment(1)
I get undefined method #find for MyGem::Comment::Module
I think I understand why this is happening - in the context of #load_comment definition, which is namespaced under MyGem, Comment constant lookup returns MyGem::Comment, rather than the more distant ::Comment
I would prefer not to have to prepend every model instance with ::.
Is there a file structure, model/class definition or configuration change I could use to make a call to Comment return the model Comment, not the MyGem::Comment module?

I would use inheritance instead of mixin in this case.
So in your gem/engine you could define your common class similar to this:
module MyGem
module Common
class Base < ActiveRecord::Base
# common functionality goes here
def load(record_type, id)
record_type.find(id)
end
end
end
end
Then in your main_app code:
class User < MyGem::Common::Base
...
end
Now you could do this:
User.new.load(Comment, 1)
This violates the Law of Demeter but hopefully it illustrates the point.
Doing it like this is DRY and has the added benefit that it prevents your gem from having to know about classes which are outside it's own scope.

Related

How to namespace folder in Rails 4?

how would one put everything in app/presenters under Presenters namespace?
so app/presenters/admin/article.rb containing
module Presenters
module Admin
class Article
end
end
end
may be available as Presenters::Admin::Article ?
Here's the working answer:
app/presenters/admin/article.rb
module Admin
class Article
end
end
access like: Admin::Article.new

correct way to use routes.rb namespace rails?

I'm trying to create a back-end area to my application, so I've created a folder named backend and backend_controller.rb inside it. I need the folder because the backend area will have multiple folders, so it's better-separated from my others.
my routes.rb looks like:
namespace :backend do
get 'index'
end
my backend_controller.rb:
class BackendController < ApplicationController
def index
end
end
But in this mode Rails will search for my backend_controller.rb inside the controllers folder, not in controller>backend. I've tried many variations, and I get routing errors.
So what is correct way to do that? To set the path /backend to index action instead of /backend/index?
Thanks
What i've done:
based on all answers, principally the one from Cyril DD
I've created the backend_controller.rb on the app/controller folder and in the sub-folder app/controller/backend i created the static_pages_controller.rb and all files looks like this:
app/controllers/backend_controller.rb:
class BackendController < ApplicationController
end
app/controller/backend/static_pages_contter.rb:
class Backend::StaticPagesController < BackendController
def dashboard
end
end
routes.rb:
namespace :backend do
resource :static_pages, path: '', only: [] do
root to:'static_pages#dashboard'
end
this works fine, but cause i'm newbie on rails i must ask. This is a good or a conventional way to do that? to administrate the permissions which user can see on the backend i use the backend_controller.rb right? and at last wy i must use resource: instead just get ''
Answering your question
Alright, namespace :something is a shorthand for scope 'something', module: 'something', as: 'something'
Now your declaration is very ambiguous, because you don't specify a controller. Typical declarations look like (assume you have a controller controllers/backend/some_resources_controller.rb and you want to generate default routes)
namespace :backend do
resources :some_resources
end
Now what you did
namespace :backend
get 'index'
end
is really ambiguous and I'm not surprised it doesn't do what you want. Basically you just tell rails to "look inside subfolder 'backend' and define the route 'index'". oO ? Which file/controller are we even talking about ?
What is your backend_controller.rb supposed to do ? Is it some kind of Control Panel ? Dashboard ? If so you're probably gonna have a lot of non-CRUD actions, but anyways you should go for the following syntax
namespace :backend
# Below line of code will auto-generate the `index` for /backend/backend_controller
resource :backend, only: [:index], path: '' do # we need " path: '' " otherwise we'll have https://xxx/backend/backend/dashboard
# If you have non-CRUD actions, put them here !
get 'dashboard' # https://xxx/backend/dashboard
...
end
# However, this will create URLs like "https://xxx/backend/dashboard", etc.
# If you want to redirect https://xxx/backend/ to your backend_controller#index, use root
root to: 'backend#index' # https://xxx/backend/
end
Last thing as mentionned by other guys, when you namespace a file like your Backend_controller inside /backend/ subfolder, you must rename the class like (/controllers/backend/backend_controller)
class Backend::BackendController < ApplicationController
Remark : if you only have like one or two controller actions, instead of using the resource method, you can declare singular resources
namespace :backend do
root to: 'backend#dashboard'
get 'dashboard', to: 'backend#dashboard' # singular resource
end
An Example of what you may actually really want to do...
I'm not sure you are clear yourself about what you want to do. As an example, here is my architecture
Files
/controllers/application_controller.rb
/controllers/backend_controller.rb
/controllers/backend/static_pages_controller.rb
/controllers/backend/***.rb
The class /controllers/backend_controller.rb will not serve any action, but will override ApplicationController to tune it for backend access (but maybe you don't need to do so)
class BackendController < ApplicationController
# Do you need to change user_access method ? Or any other backend-wide config ?
# If so put this config here, otherwise leave empty
end
Now for every file that goes in the /backend/ subfolder, I inherit the backend_controller
class Backend::StaticPagesController < BackendController
def index
end
# Note : if your index is some kind of dashboard, instead I would declare
def dashboard
end
end
class Backend::SomeResourcesController < BackendController
...
end
Routes
namespace :backend do
root to 'static_pages#index' # https://xxxx/backend/
resource :static_pages, only: [:index], path: '' # https://xxxx/backend/index
resources :some_resources
end
If you choose the dashboard solution in your controller, write instead :
namespace :backend do
root to: static_pages#dashboard # https://xxxx/backend/
resource :static_pages, path: '', only: [] do
get 'dashboard' # https://xxxx/backend/dashboard
end
resources :some_resources
end
Then it's simply
# routes.rb
Rails.application.routes.draw do
namespace :backend, shallow: true do
resource :backend, path:''
end
end
Then in your app/controllers/backend/backend_controller.rb, it'd look like this.
class Backend::BackendController < ApplicationController
def index
end
end
When I use rake routes it shows
Prefix Verb URI Pattern Controller#Action
backend_backends GET /backend(.:format) backend/backends#index
Hope this helps.

Calling super in overriden scope defined in concern

I'm looking to chain an additional query onto a scope in a model. The scope is defined in a concern.
module Companyable
extend ActiveSupport::Concern
included do
scope :for_company, ->(id) {
where(:company_id => id)
}
end
end
class Order < ActiveRecord::Base
include Companyable
# I'd like to be able to do something like this:
scope :for_company, ->(id) {
super(id).where.not(:status => 'cancelled')
}
end
However, that understandably throws a NameError: undefined method 'for_company' for class 'Order'
Here's the solution I came up with in my case:
Rather than scope, just go with a regular class method since scope is just "syntactic sugar" for a class method. This is easier to deal with when you need to override using super. In your case it would look like this:
module Companyable
extend ActiveSupport::Concern
module ClassMethods
def for_company(id)
where(:company_id => id)
end
end
end
class Order < ActiveRecord::Base
include Companyable
def self.for_company(id)
super(id).where.not(:status => 'cancelled')
end
end

Observe primary application models from within Rails engine observer

I have a Rails engine that has no models of its own; just controllers, views, and observers that add functionality to the primary application.
I'm attempting to create an observer but can not get Rails to notice it's existence -- the after_create actions and debug statements in the observer are ignored and inserting syntax errors into the file does not raise an error on startup or when insert a row in the observed table.
I've tried all of the techniques mentioned here with no effect.
#/engines/loansengine/lib/loansengine/engine.rb
module Loansengine
class Engine < ::Rails::Engine
isolate_namespace Loansengine
# OBSERVERS
config.active_record.observers = ['Loansengine::TourObserver']
end
end
Observer:
#/engines/loansengine/observers/loansengine/tour_observer.rb
class Loansengine::TourObserver < ActiveRecord::Observer
observe :tours
def after_create(tour)
test_observer(tour)
end
private
def test_observer(tour)
tour.agent_comments = 'pink'
tour.save
end
end
Think I've figured this out:
module Loansengine
class Engine < ::Rails::Engine
isolate_namespace Loansengine
config.before_initialize do
config.active_record.observers << 'Loansengine::TourObserver'
end
end
end

Rails 4 concern errors

I've notice the new features of concern in rails4, and I read the document of it in http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html.
But it seems doesn't work as I expect. Here is my code of my /models/concerns/current_user.rb.
require 'active_support/concern'
module CurrentUser
extend ActiveSupport::Concern
module ClassMethods
def accessor_current_user
attr_accessor :current_user
end
end
end
class ActiveRecord::Base
include CurrentUser
end
You probably notice that last 3 lines of my code, that's because I want all my models can call the method and I think it may be a good way to achieve that. But when i start rails server, it just cannot call the accessor_current_user method. So I am confusing about this. I really don't know the reason. Hope someone can help me. :)
Update!
Finally, i found it's maybe a good way to create a ActiveRecord::Base class in the initializers fold, and then include the CurrentUser in the class.
Try:
require 'active_support/concern'
module CurrentUser
extend ActiveSupport::Concern
included do
attr_accessor :current_user
end
module ClassMethods
end
end
class ActiveRecord::Base
include CurrentUser
end
Then you should be able to do something like this:
foo = Foo.new
foo.current_user
Where Foo is:
class Foo < ActiveRecord:Base
include CurrentUser
end
The way you are calling accessor_current_user is wrong. This way you can call ActiveRecord::Base.accessor_current_user method and it will execute the code without any error. And then you can call
ar = ActiveRecord::Base.new
ar.current_user = "foo"
ar.current_user # this will return 'foo'
But the correct way to implement the attr_accessor using ActiveSupport::Concern is
module CurrentUser
extend ActiveSupport::Concern
included do
attr_accessor :current_user
end
end
If you are writing your code in rails, then you don't need require 'active_support/concern'. Please refer http://blog.neerajk.com/articles/2014-11-26-is-active-support-concern-really-another-concern/