I'm trying to use nested module/class definitions consistently in a Rails app, rather than the compact (::) syntax. However, it doesn't always load the module file itself, which contains the table_name_prefix.
Using Rails 4.1.8 on Ruby 2.1.1...
rails new my_app
...
rails g scaffold User
rails g scaffold Blog::Post
This creates app/models/blog.rb:
module Blog
def self.table_name_prefix
'blog_'
end
end
There seem to be many ways of accidentally preventing Rails from auto-loading blog.rb. The simplest example is via the helpers.
Change app/helpers/blog/posts_helper.rb from:
module Blog::PostsHelper
end
to:
module Blog
module PostsHelper
end
end
Launch the server, visit /users and then visit /blog/posts:
SQLite3::SQLException: no such table: posts: SELECT "posts".* FROM "posts"
Similar problems can occur elsewhere, such as in the model tests. It's not limited to the helpers.
What's the best way of resolving this? Explicitly loading blog.rb and any other namespace modules?
One solution, which doesn't rely on autoloading, is to set the models to inherit from the following, instead of from ActiveRecord::Base directly:
class CustomActiveRecordBase < ActiveRecord::Base
self.abstract_class = true
# If no table name prefix has been defined, include the namespace/module as
# table name prefix, e.g., Blog:: -> blog_
def self.table_name
# If a table_name_prefix has been defined, follow default behaviour
return super if full_table_name_prefix.present?
# Find the prefix, e.g., Blog::Post -> 'blog', User -> ''
prefix = model_name.name.deconstantize.underscore
# If no prefix, follow default behaviour
return super unless prefix.present?
# Otherwise add the prefix with an underscore
"#{prefix}_#{super}"
end
end
Then there is no need to define self.table_name_prefix in blog.rb.
These can be set as defaults for future models through appropriate files in lib/templates/active_record/model/model.rb and module.rb, based on the default active record templates.
This could all be done by monkey-patching ActiveRecord::Base, but this interferes with other classes, such as ActiveRecord::SchemaMigration, which doesn't have a table prefix.
Related
I need to reuse the same method across many migrations. My goal is to avoid code duplication. I tried to do it as shown below, by putting the shared method into file lib/migration_helper.rb and using include MigrationHelper in the migrations that use the shared method.
Is there a more standard way of sharing code in different migrations?
In particular, I put the helper file into lib directory - is this the correct place?
## lib/migration_helper.rb
# Methods shared across migrations.
module MigrationHelper
def my_shared_method
# some shared code
end
end
## db/migrate/do_something.rb
class DoSomething < ActiveRecord::Migration[5.2]
include MigrationHelper
# rubocop:disable Metrics/MethodLength
def up
# some code
my_shared_method
end
# rubocop:enable Metrics/MethodLength
def down
# more code
my_shared_method
end
SEE ALSO:
I got a few ideas from these questions, but they do not fully answer my question:
Custom helper methods for Rails 3.2 Migrations
Rails share code between migrations (aka concerns)
Accessing custom helper methods in rails 3 migrations
This repo has examples of a much more complex version of what I want, with a whole hierarchy of helpers. I need a simpler solution:
https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/database/migration_helpers.rb
https://gitlab.com/gitlab-org/gitlab-foss/-/blob/master/db/migrate/20220808133824_add_timestamps_to_project_statistics.rb
just tested, works in rails 7 (and probably earlier version)
What you could do is:
1 - create your file/class kinda wherever
app/lib/migration/something.rb
db/concerns/something.rb
...
# db/concerns/create_column_alias.rb
module CreateColumnAlias
def create_column_alias(*args)
add_column(*args)
end
end
2 - create an initializer to inject your new helper in the migrations classes (per this gist)
# initializers/extend_migration_with_custom_helpers.rb
require_relative "../../db/concerns/create_column_alias"
ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:include, CreateColumnAlias)
3 - profit.
>$ bin/rails g my_migration
# db/migrate/123345456_my_migration.rb
class MyMigration < ActiveRecord::Migration[7.0]
def change
create_column_alias :tasks, :done, :boolean
end
end
edit
In case you don't want them to be included everywhere, you can skip the serializer and do
require_relative "../concerns/create_column_alias"
# db/migrate/123345456_my_migration.rb
class MyMigration < ActiveRecord::Migration[7.0]
include CreateColumnAlias
def change
create_column_alias :tasks, :done, :boolean
end
end
Though I suggest you not to do that and save you the trouble.
It's ok to have all your helpers available at any time even if you don't use them, especially given this has 0 impacts on production rapidity (only the deployment part, and it's super minimal like if you have 100 helpers you will lose only a few seconds)
I'm trying to call a global method in a file in the lib/ directory from a model. I've already tried with concerns and the problem persists.
The app is developed in Ruby on Rails 5.2.1 and Ruby 2.5.3
# Expense model
class Expense < ApplicationRecord
include Helpers
def self.quantity_this_month
select { |e| year_month(e.date) == year_month(Date.today)
}.count
end
end
# Helper in lib/ directory
module Helpers
def year_month(date)
date.strftime('%Y/%m')
end
end
# in console
Expense.quantity_this_month
Implementing the code of the helper directly in the model method gives the expected result, but right now it shows this error:
undefined method `year_month' for #<Class:0x00007f2f6f0e4b88>
Any ideas?
You need to extend instead of include because the method you want to call it from is a class method.
Also, just a side note, your self.quantity_this_month loads all the Expense records into memory, but it's possible to do the date filtering and counting all within a single SQL query. The below is how it's done in MySQL, this might be slightly different with other databases:
class Expense < ApplicationRecord
extend Helpers # <~~~~~~~ changed to extend
def self.quantity_this_month
where(
"DATE_FORMAT(date, '%Y/%m') = ?",
year_month(Date.today)
).count
end
end
Rails 4.2
I've created a multi-step wizard for a User model i.e. a visitor to the app registers as a User over a 4 step form. Its working in it's current setup.
However, I've had to use require statements to include several of the wizard related ruby files. As a consequence these files are not auto loaded by Rails.
I'd like to refactor the relevant files so that they follow Rails conventions and are able to be auto loaded.
Current Structure - is working
app/wizards/user.rb User wizard model - runs validations on each step etc
module Wizard
module User
STEPS = %w[step1 step2 step3 step4].freeze
# omitted class implementations, not relevant
class Base
end
class Step1 < Base
end
class Step2 < Step1
end
class Step3 < Step2
end
class Step4 < Step3
end
end
end
app/controllers/user_wizards_controller.rb
# I have to require the file above, would like to avoid
require Rails.root.join('app', 'wizards', 'user.rb')
class UserWizardsController < ApplicationController
# I have to specify the template, would like to avoid
def step1
render 'wizards/users/step1'
end
# Notice how I have to refer to module/classes above.
def wizard_user_for_step(step)
raise InvalidStep unless step.in?(::Wizard::User::STEPS)
"Wizard::User::#{step.camelize}".constantize.new(session[:user_attributes])
end
end
app/views/wizards/users/step1.html.erb
app/views/wizards/users/step2.html.erb
Attempted Solution
Based on this statement by xfn
The directories in autoload_paths are considered to be root directories, they do not reflect namespacing. For example, the classes below app/models are not under a Models namespace, any namespace has to go within that directory.
The file app/services/doctor_finder.rb for example does not follow autoloading conventions because of that, since it defines Services::DoctorFinder rather than DoctorFinder. Therefore, that file cannot be autoloaded.
I'm going for...
Models
app/wizards/user/user.rb
app/wizards/user/base.rb
app/wizards/user/step1.rb
app/wizards/user/step2.rb
app/wizards/user/step3.rb
app/wizards/user/step4.rb
However, I'm not getting very far. Any ideas?
If you want to autoload these files move it into e.g services or steps directories like:
app/services/wizards/user/step1
and rails should autoload module:
module Wizards::User
class Step1
end
end
Depend on rails version you will need to add 'services' to autoload path.
Regards views:
render 'wizards/users/step1'
isn't bad and In my opinion could be consider as good practice.(using render method allow you to pass non global variables to view)
If you want to remove this line you should put views for UserWizardsController to user_wizards/step1.html.xxx
or if you want to have view in wizards/users/step1.html.xxx
you should scope your controller in that way:
module Wizards
class UsersController < ApplicationController
end
edn
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 have a namespaced Post controller as below
class Admin::Blog::PostsController < Admin::BaseController
end
and a namespaced model as follows.
class Blog::Post < ActiveRecord::Base
end
But when I try to access the model inside the index action of the post controller as below
def index
#posts = Blog::Post.where(:foo_id => params[:id]).paginate(:page => params[:page], :per_page => 20)
end
I get the following error
LoadError at /admin/blog/posts
Expected/app/models/blog/post.rb to define Post
But when I move the model to Admin::Blog::Post namespace from Blog::Post is works.
I'm bit confused with this and not able to get what is going on with this.
Is it required that Controller and Model should be present in the same namespace ?
Following is the snippet from routes.rb
namespace :admin do
namespace :blog do
resources :posts
resources :categories
end
end
Blog module snippet
module Blog
def self.table_name_prefix
'blog_'
end
end
Preloading controllers and models
config.autoload_paths += Dir["#{Rails.root}/app/models/**/**"]
config.autoload_paths += Dir["#{Rails.root}/app/controllers/**/**"]
config.autoload_paths += Dir["#{config.root}/app/helpers/**/**"]
config.autoload_paths += Dir["#{config.root}/app/tags/**/**"]
config.autoload_paths += %W[ #{Rails.root}/app/extensions #{Rails.root}/app/modules #{Rails.root}/app/drops #{Rails.root}/app/filters #{Rails.root}/app/mailers ]
This is probably caused by rails' autoloader. When doing this :
module Foo
class Bar
end
end
And then trying to use Foo::Bar, the autoloader first tries to locate app/models/foo/bar.rb. The file is loaded, and module Foo is defined here (albeit as a module containing solely Bar) so the autoloader never attempts to load app/models/foo.rb.
This should only happen in development mode, as in production mode all of your files are require'd on startup.
There are two workarounds AFAIK :
Require the module
using require_dependency :
require_dependency 'foo'
module Foo
class Bar
end
end
This is IMHO the right solution, as it does not break the constant lookup, but it is also a bit annoying as you have to add the require statement on top of each namespaced file.
Create Custom Active record Base
This solution doesn't rely on autoloading. Set the models to inherit from the following, instead of from ActiveRecord::Base directly:
class CustomActiveRecordBase < ActiveRecord::Base
self.abstract_class = true
# If no table name prefix has been defined, include the namespace/module as
# table name prefix, e.g., Blog:: -> blog_
def self.table_name
# If a table_name_prefix has been defined, follow default behaviour
return super if full_table_name_prefix.present?
# Find the prefix, e.g., Blog::Post -> 'blog', User -> ''
prefix = model_name.name.deconstantize.underscore
# If no prefix, follow default behaviour
return super unless prefix.present?
# Otherwise add the prefix with an underscore
"#{prefix}_#{super}"
end
end
Then there is no need to define self.table_name_prefix in blog.rb.
This could all be done by monkey-patching ActiveRecord::Base, but this interferes with other classes, such as ActiveRecord::SchemaMigration, which doesn't have a table prefix.
Note :
This bug seems to have been resolved in rails 4. I used the second workaround a lot while on rails 3, but I've tried to reproduce the bug in rails 4 and it does not show up anymore. I think they modified the way the autoloader works... For more info, see the rails guides on autoloading and reloading constants