So im having this weird issue for some reason i dont know why it is happening.
So, i have this extension in conifg/initializers/has_friendly_token.rb
ActiveRecord::Base.define_singleton_method :has_friendly_token do
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
define_method :set_token? do
new_record? || saved_change_to_token?
end
define_method :set_unique_token do
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
And I'm using it in two different models
class Business < ApplicationRecord
include Colorful, Channels, Rails.application.routes.url_helpers
has_friendly_token
end
and
class Conversation < ApplicationRecord
has_friendly_token
include AASM, Colorful, Rails.application.routes.url_helpers
end
the thing is, whenver i try to to run i get this error
`method_missing': undefined local variable or method `has_friendly_token' for Business (call 'Business.connection' to establish a
connection):Class (NameError)
Did you mean? has_secure_token
but it works in conversation. Any ideas why this happenes?
I can't see a good reason to use an initializer to monkeypatch ActiveRecord::Base here in the first place. Instead create a module and extend your ApplicationRecord class with it as this is simply a "macro" method.
module FriendlyToken
def has_friendly_token
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
define_method :set_token? do
new_record? || saved_change_to_token?
end
define_method :set_unique_token do
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
end
class ApplicationRecord < ActiveRecord::Base
extend FriendlyToken
end
Unlike a monkeypatch its easy to test this code and it will be picked up by documentation tools like rdoc or yard as well as coverage tools.
You could also just simply write it as a concern since your macro method does not take any arguments anyways:
module FriendlyToken
extend ActiveSupport::Concern
included do
extend FriendlyId
friendly_id :token
before_validation :set_unique_token, if: :set_token?
end
def set_token?
new_record? || saved_change_to_token?
end
def set_unique_token?
table_name = self.model_name.route_key
scope = persisted? ? self.class.where("#{table_name}.id != ?", id) : self.class.default_scoped
while token.nil? || scope.where(token: token).exists?
self.token = SecureRandom.base58(24)
end
end
end
class Business < ApplicationRecord
include Colorful,
Channels,
Rails.application.routes.url_helpers,
FriendlyToken
end
The advantage here is easier debugging since the methods are not defined dynamically.
See 4 Ways To Avoid Monkey Patching.
Related
I am creating an ActiveRecord class dynamically, and have code along the lines of the following:
new_klass = Class.new ActiveRecord::Base do
cattr_accessor :model_name
self.abstract_class = false
self.table_name = "foo"
# do more stuff...
end
Object.const_set "Foo", new_klass
How can I add a callback to this class, for example, a before_save callback?
Don't see your problem you don't need anything since you are in the class scope, just call the callback methods you want to register ...
Below i've added a validation callback to prevent instance validation :
new_klass = Class.new ActiveRecord::Base do
cattr_accessor :model_name
self.abstract_class = false
self.table_name = "items"
validate do
errors.add :base, "not good"
end
end
> instance = new_klass.new
> instance.valid?
=> false
> instance.errors.full_messages
=> ["not good"]
try including a concern to your dynamic class, and then handle it there.
new_klass = Class.new ActiveRecord::Base do
#...
include MyConcern
#...
end
# app/models/concerns/my_concern.rb
module MyConcern
extend ActiveSupport::Concern
included do
before_save ...
end
end
I am fairly new to Rails validations. I have an Activity model that has many attributes (listed in attributes array below). I need to validate that every activity has a name and a at least one of the other attributes. I was think of something like the following but it looks a little messy. Any advice?
class Activity < ActiveRecord::Base
belongs_to :user
validate :valid_activity
def valid_activity
attributes = [reps, distance, meal, post_meal, yoga, reminder, duration]
if name.present? && self.include? (activity)
end
end
end
You would create a new validator class like so
class ActivityValidator < ActiveModel::Validator
def valid_activity
attributes = [:reps, :distance, :meal, :post_meal, :yoga, :reminder, :duration]
unless name.present? && attributes.any?{ |a| self.activity == a }
errors[:user] << 'Need to add an activity'
end
end
end
Then in your user.rb file, include the validator module and use the validates_with method.
include ActiveModel::Validations
validates_with ActivityValidator
I have this private method in my User model
class User < ActiveRecord::Base
attr_accessible :email, :fullname, :password, :username
has_secure_password
before_create { generate_token(:remember_token) }
.
.
private
def generate_token(col)
begin
self[col] = SecureRandom.urlsafe_base64
end while User.exists?(col => self[col])
end
end
How can i make generate_token available In other models?
Thanks
many options here are some:
use a plain, simple ruby module.
module TokenRememberable
private
def generate_token(col)
begin
self[col] = SecureRandom.urlsafe_base64
end while User.exists?(col => self[col])
end
end
class User < ActiveRecord::Base
include TokenRememberable
before_create { generate_token(:remember_token) }
end
for more complex functionnality, use ActiveSupport::Concern :
module TokenRememberable
extend ActiveSupport::Concern
# thanks to Concern, this block wil be evaluated
# in the context of the including class
included do
before_create { generate_token(:remember_token) }
end
private
def generate_token(col)
begin
self[col] = SecureRandom.urlsafe_base64
end while self.class.exists?(col => self[col])
end
end
class User < ActiveRecord::Base
include TokenRememberable
end
extract the functionnality in a dedicated class, and possibly use composition
# we only need a class method here, but you can also build
# full-fledged objects as you need
class TokenGenerator
def self.generate_token
# your generation logic here
end
end
class User
def after_initialize
#token_generator = TokenGenerator # or TokenGenerator.new( self ), for instance
end
attr_reader :token_generator
delegate :generate_token, to: :token_generator # optionnaly use delegation
end
NOTE :
this is not really a rails issue, more a Ruby one. You should document yourself more on the language you are using... Modules are a very common idiom. I can give you some good reference books if you need
a few approaches:
slightly dependent:
do:
class User < ActiveRecord::Base
before_create { User::Helpers.new(self).generate_token(:remember_token) }
class Helpers < Struct.new(:user)
def generate_token(col)
begin
user.send("#{col}=", SecureRandom.urlsafe_base64)
end while User.exists?(col => user.send(col))
end
end
end
independent: don't pass the user object to the class, just make the method render a random token and assign it from the model.
Hey folks, following problem with Rails and STI:
I have following classes:
class Account < AC::Base
has_many :users
end
class User < AC::Base
extend STI
belongs_to :account
class Standard < User
before_save :some_callback
end
class Other < User
end
end
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
type.new(*args, &block)
end
end
end
And now the problem:
Without rewriting User.new (in module STI), the callback inside User::Standard gets never called, otherwise the account_id is always nil if I create users this way:
account.users.create([{ :type => 'User::Standard', :firstname => ... }, { :type => 'User::Other', :firstname => ... }])
If I'm using a different approach for the module like:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
Then instance variables are not shared, because it's creating a new object.
Is there any solution for this problem without moving the callbacks to the parent class and checking the type of class?
Greetz
Mario
Maybe there's something I don't know, but I've never seen Rails STI classes defined in that manner. Normally it looks like...
app/models/user.rb:
class User < AC::Base
belongs_to :account
end
app/models/users/standard.rb:
module Users
class Standard < User
before_save :some_callback
end
end
app/models/users/other.rb:
module Users
class Other < User
end
end
It looks as though you are conflating class scope (where a class "lives" in relation to other classes, modules, methods, etc.) with class inheritance (denoted by "class Standard < User"). Rails STI relationships involve inheritance but do not care about scope. Perhaps you are trying to accomplish something very specific by nesting inherited classes and I am just missing it. But if not, it's possible it's causing some of your issues.
Now moving on to the callbacks specifically. The callback in Standard isn't getting called because the "account.users" relationship is using the User class, not the Standard class (but I think you already know that). There are several ways to deal with this (I will be using my class structure in the examples):
One:
class Account
has_many :users, :class_name => Users::Standard.name
end
This will force all account.users to use the Standard class. If you need the possibility of Other users, then...
Two:
class Account
has_many :users # Use this to look up any user
has_many :standard_users, :class_name => Users::Standard.name # Use this to look up/create only Standards
has_many :other_users, :class_name => Users::Other.name # Use this to look up/create only Others
end
Three:
Just call Users::Standard.create() and Users::Other.create() manually in your code.
I'm sure there are lots of other ways to accomplish this, but there are probably the simplest.
So I solved my problems after moving my instance variables to #attributes and using my second approach for the module STI:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
class User < AR:Base
extend STI
belongs_to :account
validates :password, :presence => true, :length => 8..40
validates :password_digest, :presence => true
def password=(password)
#attributes['password'] = password
self.password_digest = BCrypt::Password.create(password)
end
def password
#attributes['password']
end
class Standard < User
after_save :some_callback
end
end
Now my instance variable (the password) is copied to the new User::Standard object and callbacks and validations are working. Nice! But it's a workaround, not really a fix. ;)
I am trying to have a pack of very generic named scopes for ActiveRecord models like this one:
module Scopes
def self.included(base)
base.class_eval do
named_scope :not_older_than, lambda {|interval|
{:conditions => ["#{table_name}.created_at >= ?", interval.ago]
}
end
end
end
ActiveRecord::Base.send(:include, Scopes)
class User < ActiveRecord::Base
end
If the named scope should be general, we need to specify *table_name* to prevent naming problems if their is joins that came from other chained named scope.
The problem is that we can't get table_name because it is called on ActiveRecord::Base rather then on User.
User.not_older_than(1.week)
NoMethodError: undefined method `abstract_class?' for Object:Class
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:2207:in `class_of_active_record_descendant'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1462:in `base_class'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1138:in `reset_table_name'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1134:in `table_name'
from /home/bogdan/makabu/railsware/startwire/repository/lib/core_ext/active_record/base.rb:15:in `included'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:92:in `named_scope'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `call'
from /var/lib/gems/1.8/gems/activerecord-2.3.5/lib/active_record/named_scope.rb:97:in `not_older_than'
How can I get actual table_name at Scopes module?
Try using the #scoped method inside a class method of ActiveRecord::Base. This should work:
module Scopes
def self.included(base)
base.class_eval do
def self.not_older_than(interval)
scoped(:conditions => ["#{table_name}.created_at > ?", interval.ago])
end
end
end
end
ActiveRecord::Base.send(:include, Scopes)
Rails 5, ApplicationRecord (Hope it helps others)
# app/models/concerns/not_older_than.rb
module NotOlderThan
extend ActiveSupport::Concern
included do
scope :not_older_than, -> (time, table = self.table_name){
where("#{table}.created_at >= ?", time.ago)
}
end
end
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include NotOlderThan
end
# app/models/user.rb
class User < ApplicationRecord
# Code
end
# Usage
User.not_older_than(1.week)
In Rails 5, all models are inherited from ApplicationRecord by default. If you wan to apply this scope for only particular set of models, add include statements only to those model classes. This works for join queries and chained scopes as well.
Additional useful scopes below :
module Scopes
def self.included(base)
base.class_eval do
def self.created(date_start, date_end = nil)
if date_start && date_end
scoped(:conditions => ["#{table_name}.created_at >= ? AND #{table_name}.created_at <= ?", date_start, date_end])
elsif date_start
scoped(:conditions => ["#{table_name}.created_at >= ?", date_start])
end
end
def self.updated(date_start, date_end = nil)
if date_start && date_end
scoped(:conditions => ["#{table_name}.updated_at >= ? AND #{table_name}.updated_at <= ?", date_start, date_end])
elsif date_start
scoped(:conditions => ["#{table_name}.updated_at >= ?", date_start])
end
end
end
end
end
ActiveRecord::Base.send(:include, Scopes)
Here is an updated, Rails4 compatible solution.
I am told defining global scopes like this can lead to conflicts, caveat emptor and all that, but sometimes you just need a simple scope on all your models, right?
Define a module.
# in /app/models/concerns/global_scopes.rb
module GlobalScopes
def self.included(base)
base.class_eval do
def self.in_daterange(start_date, end_date)
all.where(created_at: start_date.to_date.beginning_of_day..end_date.to_date.end_of_day)
end
end
end
end
Have the module included in ActiveRecord::Base.
# in /config/initializers/activerecord.rb
ActiveRecord::Base.send(:include, GlobalScopes)
That's it! Notice that in Rails4 you do not have to mess with :scoped, but instead you use :all and chain your query to it.