I would need some help with restructuring a Rails application. Below you can find the extra details. The main questions:
I modified the code after #Nathan suggestions. Got one step further to heaven but still can smell some sulfur.
SOLVED: Why am I getting the error: "superclass mismatch for class Report"
SOLVED: Can I have some guidelines, how to structure complex namespacings, extensions and STI in Rails4?
After server restart, I get the error on the Spock main page: --"Unable to autoload constant Bridge, expected ...route goes here... app/models/spock/report/bridge.rb to define it"-- After refreshing the page, the error is gone. But still, indicates some problem. This is reproducible after every server restart. HINT: Possibly an autoload issue... Any idea is welcome.
Environment:
WEBrick 1.3.1
Rails 4.0.9
Ruby 2.1.5
Error details:
Unable to autoload constant Bridge, expected ...route goes here... bridge.rb to define it
app/views/spock/spock/index.html.erb:11:in `block in _app_views_spock_spock_index_html_erb___3895075684249237486_132397353980'
app/views/spock/spock/index.html.erb:6:in `map'
app/views/spock/spock/index.html.erb:6:in `_app_views_spock_spock_index_html_erb___3895075684249237486_132397353980'
File structure:
/app/controllers/spock/spock_controller.rb
/app/controllers/spock/marketplaces_controller.rb
/app/controllers/spock/reports_controller.rb
/app/modules/spock/spock_connector.rb
/app/modules/spock/report.rb
/app/modules/spock/report/bridge.rb
/app/modules/spock/report/metric.rb
Files:
# application.rb
...
config.autoload_paths += Dir[Rails.root.join('app', 'models', 'spock', '{**}')]
...
# routes.rb
namespace :spock do
get '/', :to => 'spock#index'
resources :marketplaces, only: [] do
resources :reports, only: [:show]
end
# spock_controller.rb
module Spock
class SpockController < ApplicationController
def index
#report_categories = Spock::Report.distinct.pluck(:category)
...
# marketplaces_controller
module Spock
class MarketplacesController < SpockController
end
end
# reports_controller
module Spock
class ReportsController < MarketplacesController
helper SpockHelper
...
# spock_connector.rb
module Spock
class SpockConnector < ::ActiveRecord::Base
self.abstract_class = true
establish_connection "spock_#{Rails.env}"
end
end
# report.rb
module Spock
class Report < SpockConnector
attr_accessor :legal_entity
end
end
# bridge.rb
module Spock
class Bridge < Report
...
# metric.rb
module Spock
class Metric < Report
...
Database:
/* reports table */
id, type, category,
1 Spock::Bridge Bridge
2 Spock::Metric Metric
You are opening/defining the Report class in 3 separate files.
In report.rb, you specify ::Spock::SpockConnector as the superclass for Report, but in bridge.rb and metric.rb, you are not specifying an explicit superclass, so Ruby uses Object as its superclass.
You cannot have both Object and ::Spock::SpockConnector as the immediate superclass of Report.
Solution:
In all 3 files you need to use class Report < ::Spock::SpockConnector
As soon as you fix that, you will encounter a similar error, because you are opening SpockConnector in several files, once with ::ActiveRecord::Base as the superclass, and other times with implicit Object as the superclass.
This is another contradiction, so in all files that open/define SpockConnector, you need to use ::ActiveRecord::Base as the superclass.
As for your namespacing question, that's opinion-based, but I will say that in Ruby there is no need to nest subclasses inside namespaces.
For example, you could keep Report, SpockConnector, Metric, and Bridge all at the same namespace level:
module Spock
class SpockConnector < ActiveRecord::Base
end
end
module Spock
class Report < SpockConnector
end
end
module Spock
class Metric < Report
end
end
module Spock
class Bridge < Report
end
end
The fully scoped names are then:
Spock::SpockConnector
Spock::Report
Spock::Bridge
Spock::Metric
OK. After a day of trial and error I found the cause of autoload issue.
If you find your self touching the autoload structure, that is a good sign on being on the wrong path.
So I removed the autoload path from the application.rb file.
And modified the STI classes a little bit, to match the Rails autoload logic;
# bridge.rb
module Spock
class Report
class Bridge < Report
...
# metric.rb
module Spock
class Report
class Metric < Report
...
Database:
/* reports table */
id, type, category,
1 Spock::Report::Bridge Bridge
2 Spock::Report::Metric Metric
Including the STI classes in the superclass makes them able to be autoloaded from the subfolder without including explicitly the folder in the application.rb file.
The downside is that the classes absolute paths gets longer...
Related
I'm new in Ruby and RoR and I'd like to create an admin section for a demo app.
From a little research I've done I've found two different options for creating an admin section. Example:
# config/routes.rb
namespace :admin do
resources :users
end
# app/controllers/admin_controller.rb
class AdminController < ApplicationController
before_filter :authorized?
...
end
Which of the two options is the most proper way to define controllers for the admin section, or they are both equally same?
# app/controllers/admin/users_controller.rb
# I think this is what rails generates if I run the "rails g controller admin::users" command
class Admin::UsersController < AdminController
...
end
# or instead
module Admin
class UsersController < AdminController
....
end
end
Both approaches yield to the same result, which is an UsersController which inherits from AdminController and is found in the Admin module (namespace).
Admin::MyClass is just a shortcut for module Admin ... class MyClass, but...
I would however prefer the explicit nested code (with module Admin on its own line), because it does make a different if the Admin-module has never been defined before. This probably won't happen to you when hacking with standard rails, but can happen when you write ruby code outside of rails.
See these examples:
class I::Am::AClass
end
i = I::Am::AClass.new
puts i.inspect
will lead to
i.rb:1:in `<main>': uninitialized constant I (NameError)
if you never declared the I and nested Am modules before in your code.
Whereas
module I
module Am
class AClass
end
end
end
i = I::Am::AClass.new
puts i.inspect
will work:
#<I::Am::AClass:0x00000001d79898>
because the modules are created along the path to AClass (at least this is how I think about it).
If you ever run in that problem and want to save whitespaces (because you will usually indent stuff in a module definition), there are some idioms to use. The one that solves the problem in the most obvious way (again, to me) is the following:
# Predefine modules
module I ; module Am ; end ; end
# Just a shortcut for:
# module I
# module Am
# end
# end
class I::Am::AClass
end
i = I::Am::AClass.new
puts i.inspect
#<I::Am::AClass:0x000000024194a0>
Just found that the nature of your question (it is not about an Admin-Interface, more about Module-Syntax) is also nicely discussed here Ruby (and Rails) nested module syntax . And I would love to see a ruby-bug report/feature-request on this :)
You can also use the administration framework for Ruby on Rails applications like
ActiveAdmin https://github.com/activeadmin/activeadmin
OR
Railsadmin
https://github.com/sferik/rails_admin
I am looking to separate concerns for some subset of function specific to a model.
I have referenced here and followed this pattern
module ModelName::ConcernName
extend ActiveSupport::Concern
included do
# class macros
end
# instance methods
def some_instance_method
end
module ClassMethods
# class methods here, self included
end
end
However, when I try to start the server it would result in the following error
Circular dependency detected while autoloading constant ModelName::ConcernName
I am wondering what is the best way to do concerns for some subset functions of a model.
Edit
Providing the model code:
path: app/models/rent.rb
Now I have a lot of checking logic in my model
class Rent < ActiveRecord::Base
def pricing_ready?
# check if pricing is ready
end
def photos_ready?
# check if photo is ready
end
def availability_ready?
# check if availability setting is ready
end
def features_ready?
# check if features are set
end
end
I want to separate it in concern
class Rent < ActiveRecord::Base
include Rent::Readiness
end
And organise the concern by namespace
path: app/models/concerns/rent/readiness.rb
module Rent::Readiness
extend ActiveSupport::Concern
included do
# class macros
end
# instance methods
def pricing_ready?
# check if pricing is ready
end
...
module ClassMethods
# class methods here, self included
end
end
Now I got it working if I just do class RentReadiness with the path in app/models/concerns/rent_readiness.rb
You can scope it to Rents and place to concerns/rents/readiness.rb:
module Rents
module Readiness
extend ActiveSupport::Concern
included do
# class macros
end
end
end
And in model:
class Rent < ActiveRecord::Base
include Rents::Readiness
end
You can make it work by just moving the folder with model-specific concerns from concerns to models. So, you would have:
models/
rent.rb
rent/
readiness.rb
I like this convention of using the model as the namespace for its concerns because it lets you remove some redundancy from code:
Since you are defining the concern within the model class, in the model you can write include Readiness instead of include Rent::Readiness
When defining the concern, you can use module Rent::Readiness instead of
class Rent < ApplicationRecord
module Readiness
...
Which would be another way of fixing the circular dependency problem you mentioned in your question.
Rails uses activesupport to load classes and modules as they are defined by inferring the file path based on the class or module name, this is done as the Ruby parser loads your files and come across a new constant that has not been loaded yet. In your case, the Rent model is parsed up to the Rent::Readlines reference, at which point activesupport goes off to look for the rent/readlines.rb code file that matches the name. This file is then then parsed by ruby, but on the first line, The still unloaded Rent class is referenced, which triggers activesupport to go off and look for the code file that matches the name.
In my Rails 3.2 app I have a bunch of plain old ruby objects in the /app/models/ directory. I'd like to move some of these into a separate folder, say /app/models/data_presenter/. For one of the objects,
# /app/models/data_presenter.rb
class DataPresenter
# ...
end
I've tried the following
# /app/models/data_presenter/data_presenter.rb
class DataPresenter::DataPresenter
# ...
end
however, I got the TypeError (wrong argument type Module (expected Class)) error. Any suggestions to overcome this (with or without namespaces)? Do I also need to change the corresponding models' tests names and locations?
As #BroiSatse pointed out, the problem was that I had a bunch of subclasses that were inheriting from the base class DataPresenter. For those subclasses I forgot about the namespacing, i.e.
# /app/models/data_presenter/color_data_presenter.rb
class ColorDataPresenter < DataPresenter
# ...
end
should have been
# /app/models/data_presenter/color_data_presenter.rb
class DataPresenter::ColorDataPresenter < DataPresenter::DataPresenter
# ...
end
or similarly
module DataPresenter
class ColorDataPresenter < DataPresenter
# ...
end
end
For the tests, I couldn't find a magick solution so I just wrote
# /test/unit/answers_presenter/color_data_presenter_test.rb
require 'test_helper'
class ColorDataPresenterTest < ActiveSupport:TestCase
should 'do something cool' do
presenter = DataPresenter::ColorDataPresenter.new
assert presenter.do_something_cool
end
end
Here's the setup:
# app_controller.rb
class AppController; end
# org/app_controller.rb
module Org
class AppController < ::AppController; end
end
# org/admin/app_controller.rb
module Org
class Admin::AppController < AppController; end
end
Why does Org::Admin::AppController inherit from AppController, and not Org::AppController, considering that the class definition is namespaced?
This is because by the time you opened Org::Admin::AppController, Org::AppController must not have been defined, but ::AppController must have been . Perhaps your files are not being 'required' in the order you assumed them to be? You might solve this by adding a require <file containing base class> in the file where you create your derived class.
(Minor style guideline: Don't use :: to refer to classes and modules that you are opening for definition.)
Edit reason: I ran some tests and I must have been mistaken.
I have created a simple railtie, adding a bunch of stuff to ActiveRecord:
0 module Searchable
1 class Railtie < Rails::Railtie
2 initializer 'searchable.model_additions' do
3 ActiveSupport.on_load :active_record do
4 extend ModelAdditions
5 end
6 end
7 end
8 end
I require this file (in /lib) by adding the following line to config/environment.rb before the application is called:
require 'searchable'
This works great with my application and there are no major problems.
I have however encountered a problem with rake db:seed.
In my seeds.rb file, I read data in from a csv and populate the database. The problem I am having is that the additions I made to ActiveRecord don't get loaded, and seeds fails with a method_missing error. I am not calling these methods, but I assume that since seeds.rb loads the models, it tries to call some of the methods and that's why it fails.
Can anyone tell me a better place to put the require so that it will be included every time ActiveRecord is loaded (not just when the full application is loaded)? I would prefer to keep the code outside of my models, as it is code shared between most of my models and I want to keep them clean and DRY.
Putting the extend there just adds it to ActiveRecord::Base.
When a model class is referenced, via Rails 3.1 autoloading/constant lookup, it will load the class. At that point, it is pure Ruby (nothing magic) as to what happens, basically. So I think you have at least a few options. The "bad" option that kind of does what you want it to hook into dependency loading. Maybe something like:
module ActiveSupport
module Dependencies
alias_method(:load_missing_constant_renamed_my_app_name_here, :load_missing_constant)
undef_method(:load_missing_constant)
def load_missing_constant(from_mod, const_name)
# your include here if const_name = 'ModelName'
# perhaps you could list the app/models directory, put that in an Array, and do some_array.include?(const_name)
load_missing_constant_renamed_my_app_name_here(from_mod, const_name)
end
end
end
Another way to do it would be to use a Railtie like you were doing and add a class method to ActiveRecord::Base that then includes stuff, like:
module MyModule
class Railtie < Rails::Railtie
initializer "my_name.active_record" do
ActiveSupport.on_load(:active_record) do
# ActiveRecord::Base gets new behavior
include ::MyModule::Something # where you add behavior. consider using an ActiveSupport::Concern
end
end
end
end
If using an ActiveSupport::Concern:
module MyModule
module Something
extend ActiveSupport::Concern
included do
# this area is basically for anything other than class and instance methods
# add class_attribute's, etc.
end
module ClassMethods
# class method definitions go here
def include_some_goodness_in_the_model
# include or extend a module
end
end
# instance method definitions go here
end
end
Then in each model:
class MyModel < ActiveRecord::Base
include_some_goodness_in_the_model
#...
end
However, that isn't much better than just doing an include in each model, which is what I'd recommend.