I am attempting to use Rails Concerns (or even a bare Module mixin) to share methods across some of my models.
Given a simple model, I am storing some encoded data in one of the
fields:
class DataElement < ActiveRecord::Base
include EmbeddedData
ENCODED = %w(aliases)
end
I’ve then made a concern with the needed methods for managing the data:
module EmbeddedData
extend ActiveSupport::Concern
included do
after_find :decode_fields
before_save :encode_fields
#decoded = {}
end
def decoded(key, value = false)
#decoded[key][:value] if #decoded.has_key? key
end
def decode_fields
#decoded = {} if #decoded.nil?
ENCODED.each do |field|
if attributes[field]
#decoded[field] = {
value: JSON.parse(attributes[field]),
dirty: false
}
end
end
end
def encode_fields
ENCODED.each do |field|
if decoded[field] && decoded[field][:dirty]
attributes[field] = #decoded[field][:value].to_json
end
end
end
end
Given this setup, I get the error uninitialized constant EmbeddedData::ENCODED
If I change the reference to self::ENCODED in the Concern I get the error:
# is not a class/module
I've even tried making a method on the concern register_fields that I can then call from the model, but the model just throws an unknown method error.
Running out of ideas here and looking for help.
So it turns out the way to access the class constant is:
self.class::ENCODED
Related
I'm trying to make a concern that checks if a user is subscribed to an appropriate plan for my SaaS app.
Here's basically what I'm trying to do:
module SubscriptionControlled extend ActiveSupport::Concern
class_methods do
def requires_subscription_to(perm)
##perms = [perm]
end
end
included do
validate :check_subscription
end
def check_subscription
##perms.each do |perm|
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
This provides this api for a model:
class SomeModel < ApplicationModel
include SubscriptionControlled
requires_subscription_to :pro
end
The problem I'm having is that ##perms seems to be scoped to the CONCERN, rather than the MODEL. So this value is the same for all models. So whichever model is loaded last sets this value for all models.
eg: if loaded in this order:
Model1 -> sets ##perms to [:pro]
Model2 -> sets ##perms to [:business]
Both model 1 and model 2 will only require a subscription to :business
Is there a way of storing class-level variables in a concern that take effect on a per-model basis to accomplish this API?
I don't have a Ruby interpreter at hand right now but I'm fairly certain that using a single # in the class method should do the trick. Another thing that comes to mind is something along the lines of
included do
define_singleton_method :requires_subscription_to do |new_perm|
##perms ||= []
##perms << Array(new_perm)
end
end
Since that will create a new method every time the concern is included, it should work. I just seem to remember that methods defined like that are slightly slower - but since it will probably only be called during initialization, it shouldn't pose a problem in any case.
So I found the right way to do this using a ClassMethods module
module SubscriptionControlled extend ActiveSupport::Concern
module ClassMethods
#perms = []
def requires_subscription_to(perm)
#perms = [perm]
end
def perms
#perms
end
end
included do
validate :check_subscription
end
def check_subscription
self.class.perms.each do |perm|
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
this keeps the permissions scoped to the class, not the concern.
I think you're overcomplicating this. You don't need the check_subscription method at all and that method is why you're trying to make ##perms (or #perm) work.
validate is just a class method like any other and you can give validate block. You can use that block to capture the perm and do away with all the extra machinery:
module SubscriptionControlled extend ActiveSupport::Concern
module ClassMethods
def requires_subscription_to(perm)
validate do
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
end
This is running in multiple Sidekiq instances and workers at the same time and it seems that is has generated a couple of issues, like instances getting assigned the "It was alerted recently" error when shouldn't and the opposite.
It is rare, but it is happening, is this the problem or maybe it is something else?
class BrokenModel < ActiveRecord::Base
validates_with BrokenValidator
end
class BrokenValidator < ActiveModel::Validator
def validate record
#record = record
check_alerted
end
private
def check_alerted
if AtomicGlobalAlerted.new(#record).valid?
#record.errors[:base] << "It was alerted recently"
end
p "check_alerted: #{#record.errors[:base]}"
end
end
class AtomicGlobalAlerted
include Redis::Objects
attr_accessor :id
def initialize id
#id = id
#fredis = nil
Sidekiq.redis do |redis|
#fredis = FreshRedis.new(redis, freshness: 7.days, granularity: 4.hours)
end
end
def valid?
#fredis.smembers.includes?(#id)
end
end
We were experiencing something similar at work and after A LOT of digging finally figured out what was happening.
The class method validates_with uses one instance of the validator (BrokenValidator) to validate all instances of the class you're trying to validate (BrokenModel). Normally this is fine but you are assigning a variable (#record) and accessing that variable in another method (check_alerted) so other threads are assigning #record while other threads are still trying to check_alerted.
There are two ways you can fix this:
1) Pass record to check_alerted:
class BrokenValidator < ActiveModel::Validator
def validate(record)
check_alerted(record)
end
private
def check_alerted(record)
if AtomicGlobalAlerted.new(record).valid?
record.errors[:base] << "It was alerted recently"
end
p "check_alerted: #{record.errors[:base]}"
end
end
2) Use the instance version of validates_with which makes a new validator instance for each model instance you want to validate:
class BrokenModel < ActiveRecord::Base
validate :instance_validators
def instance_validators
validates_with BrokenValidator
end
end
Either solution should work and solve the concurrency problem. Let me know if you experience any other issues.
I believe there are some thread safety issues in rails but we can overcome them by taking necessary precautions.
The local variables, such as your local var, are local to each particular invocation of the method block. If two threads are calling this block at the same time, then each call will get its own local context variable and those won't overlap unless there are shared resources involved: instance variables like (#global_var), static variables (##static_var), globals ($global_var) can cause concurrency problems.
You are using instance variable, just instantiate it every time you are coming to the validate_record method and hopefully your problem will go away like :
def validate record
#record.errors[:base] = []
#record = record
check_alerted
end
For more details you can visit this detailed link
Or try to study about rails configs here : link
I did everything pretty much as described here: question
But I keep getting error:
NoMethodError: undefined method `parent_model' for Stream (call 'Stream.connection' to establish a connection):Class
In model/concerns faculty_block.rb
module FacultyBlock
extend ActiveSupport::Concern
included do
def find_faculty
resource = self
until resource.respond_to?(:faculty)
resource = resource.parent
end
resource.faculty
end
def parent
self.send(self.class.parent)
end
end
module ClassMethods
def parent_model(model)
##parent = model
end
end
end
[Program, Stream, Course, Department, Teacher].each do |model|
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end
In initializers:
require "faculty_block"
method call:
class Stream < ActiveRecord::Base
parent_model :program
end
It seems that the Stream is loaded before loading concern, make sure that you have applied the concerns inside the class definition. When rails loader matches class name for Stream constant, it autoloads it before the finishing evaliation of the faculty_block, so replace constants in it with symbols:
[:Program, :Stream, :Course, :Department, :Teacher].each do |sym|
model = sym.to_s.constantize
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end
I am trying to DRY my code by implementing modules. However, I have constants stored in models (not the module) that I am trying to access with self.class.
Here are (I hope) the relevant snippets:
module Conversion
def constant(name_str)
self.class.const_get(name_str.upcase)
end
end
module DarkElixir
def dark_elixir(th_level)
structure.map { |name_str| structure_dark_elixir(name_str, th_level) if constant(name_str)[0][:dark_elixir_cost] }.compact.reduce(:+)
end
end
class Army < ActiveRecord::Base
include Conversion, DarkElixir
TH_LEVEL = [...]
end
def structure_dark_elixir(name_str, th_level)
name_sym = name_str.to_sym
Array(0..send(name_sym)).map { |level| constant(name_str)[level][:dark_elixir_cost] }.reduce(:+) * TH_LEVEL[th_level][sym_qty(name)]
end
When I place the structure_dark_elixir method inside the DarkElixir module, I get an error, "uninitialized constant DarkElixir::TH_LEVEL"
While if I place it inside the Army class, it finds the appropriate constant.
I believe it is because I am not scoping the self.constant_get correctly. I would like to keep the method in question in the module as other models need to run the method referencing their own TH_LEVEL constants.
How might I accomplish this?
Why not just use class methods?
module DarkElixir
def dark_elixir(th_level)
# simplified example
th_level * self.class.my_th_level
end
end
class Army < ActiveRecord::Base
include DarkElixir
def self.my_th_level
5
end
end
Ugh. Method in question uses two constants. It was the second constant that was tripping up, not the first. Added "self.class::" prior to the second constant--back in business.
def structure_dark_elixir(name_str, th_lvl)
name_sym = name_str.to_sym
Array(0..send(name_sym)).map { |level| constant(name_str)[level][:dark_elixir_cost] }.reduce(:+) * self.class::TH_LEVEL[th_lvl][sym_qty(name_str)]
end
I have a function that does this:
def blank_to_negative(value)
value.is_number? ? value : -1
end
If the value passed is not a number, it converts the value to -1.
I mainly created this function for a certain model, but it doesn't seem appropriate to define this function in any certain model because the scope of applications of this function could obviously extend beyond any one particular model. I'll almost certainly need this function in other models, and probably in views.
What's the most "Rails Way" way to define this function and then use it everywhere, especially in models?
I tried to define it in ApplicationHelper, but it didn't work:
class UserSkill < ActiveRecord::Base
include ApplicationHelper
belongs_to :user
belongs_to :skill
def self.splice_levels(current_proficiency_levels, interest_levels)
Skill.all.reject { |skill| !current_proficiency_levels[skill.id.to_s].is_number? and !interest_levels[skill.id.to_s].is_number? }.collect { |skill| {
:skill_id => skill.id,
:current_proficiency_level => blank_to_negative(current_proficiency_levels[skill.id.to_s]),
:interest_level => blank_to_negative(interest_levels[skill.id.to_s]) }}
end
end
That told me
undefined method `blank_to_negative' for #
I've read that you're "never" supposed to do that kind of thing, anyway, so I'm kind of confused.
if you want to have such a helper method in every class in your project, than you are free to add this as a method to Object or whatever you see fits:
module MyApp
module CoreExtensions
module Object
def blank_to_negative
self.is_number? ? self : -1
end
end
end
end
Object.send :include, MyApp::CoreExtensions::Object
There are a few options:
Monkey-patch the method into ActiveRecord and it will be available across all of your models:
class ActiveRecord::Base
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
Add a "concern" module which you then mix into selected models:
# app/concerns/blank_to_negate.rb
module BlankToNegate
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
# app/models/user_skill.rb
class UserSkill < ActiveRecord::Base
include BlankToNegate
# ...
end
Ruby Datatypes functionality can be extended. They are not sealed. Since you wan to use it in all places why not extend FIXNUM functionality and add a method blank_to_negative to it.
Here's what I ended up doing. I put this code in config/initializers/string_extensions.rb.
class String
def is_number?
true if Float(self) rescue false
end
def negative_if_not_numeric
self.is_number? ? self : -1
end
end
Also, I renamed blank_to_negative to negative_if_not_numeric, since some_string.negative_if_not_numeric makes more sense than some_string.blank_to_negative.