Undefined class method using Rails Concerns - ruby-on-rails

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

Related

Returning Module Class instead of Model Class with self.class Ruby/Rails

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

How can I share data with a concern in Rails 4.2?

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

How to meta program methods from a Class Constant array which is being referenced in a module?

I wrote a basic Eventable module that allows other Classes to create events for itself. All the Models have their own set of events such as a Message being sent or a User having a sign_in event. I would like to create an array of events as a constant at the class level and access that array inside the module to build helper methods for creating events like so:
class User
EVENT_TYPES = ['sign_in', 'sign_out']
include Eventable
end
class Message
EVENT_TYPES = ['sent', 'opened']
include Eventable
end
module Eventable
extend ActiveSupport::Concern
included do
has_many :events, as: :eventable
attr_accessor :metadata
self::EVENT_TYPES.each do |event_type|
define_method "create_#{event_type}_event" do |args={}|
Event.create(eventable: self, event_type: event_type)
end
end
end
end
The issue with this is: the methods are not being defined properly. It cannot reference the respective Model's constant EVENT_TYPES properly.
How can I access a class constant from insude a module?
Using constant like that is not very rubyesque, I'd say. This is much more idiomatic (in rails, at least).
class Message
include Eventable
event_types :sent, :opened
end
And here's the implementation
module Eventable
extend ActiveSupport::Concern
module ClassMethods
def event_types(*names)
names.each do |event_type|
define_method "create_#{event_type}_event" do |args={}|
Event.create(eventable: self, event_type: event_type)
end
end
end
end
end
Constants can be obtained by Module#const_get method. But I would not present the event types for individual classes as constants, but as either class methods or instance variables belonging to the class:
class User
def self.event_types
[:sign_in, :sign_out]
end
end
# or
class User
#event_types = [:sign_in, :sign_out]
class << self
attr_reader :event_types
end
end
It seems that another answer expressed most of the remaining things I wanted to say. But one more thing: I would not name the module Eventable, but eg. HavingEvents or simply Events.

Module classes in lib folder

I have a lib file lister_extension.rb
module ListerExtension
def lister
puts "#{self.class}"
end
end
And Post model
class Post < ActiveRecord::Base
has_many :reviews
extend ListerExtension
def self.puts_hello
puts "hello123123"
end
end
All is good when I call this in rails c:
2.1.1 :003 > Post.lister
Class
=> nil
But what happens when I want to add a class to my module?
For example:
module ListerExtension
class ready
def lister
puts "#{self.class}"
end
end
end
I get this error
TypeError: wrong argument type Class (expected Module)
When I call Post.first in rails c
From the doc for extend:
Adds to obj the instance methods from each module given as a
parameter.
Hence, you can't access this class through extended class. Have a look into including modules instead of extending them (read about ActionSupport::Concern module as well) or have a go with self.extended method (ref here)
TL;DR , in ruby you can´t extend with classes, you extend/include with modules
regards
updated: example for concern
include / extend with activesupport concern
module Ready
extend ActiveSupport::Concern
# this is an instance method
def lister
....
end
#this are class methods
module ClassMethods
def method_one(params)
....
end
def method_two
....
end
end
end
then in a ActiveRecord Model , like Post
class Post < AR
include Ready
end
with this procedure you will get the instance methods and class methods for free, also you can set some macros like when use a included block,
module Ready
extend ActiveSupport::Concern
included do
has_many :likes
end
end
hope that helps,
regards

Layer Supertype in ActiveRecord (Rails)

I'm developing a ruby on rails app and I want to be able to excecute a method on every AR object before each save.
I thought I'd create a layer-super-type like this:
MyObject << DomainObject << ActiveRecord::Base
and put in DomainObject a callback (before_save) with my special method (which basically strips all tags like "H1" from the string attributes of the object).
The catch is that rails is asking for the domain_object table, which I obviously don't have.
My second attempt was to monkeypatch active record, like this:
module ActiveRecord
class Base
def my_method .... end
end
end
And put that under the lib folder.
This doesnt work, it tells me that my_method is undefined.
Any ideas?
Try using an abstract class for your domain object.
class DomainObject < ActiveRecord::Base
self.abstract_class = true
# your stuff goes here
end
With an abstract class, you are creating a model which cannot have objects (cannot be instantiated) and don't have an associated table.
From reading Rails: Where to put the 'other' files from Strictly Untyped,
Files in lib are not loaded when Rails starts. Rails has overridden both Class.const_missing and Module.const_missing to dynamically load the file based on the class name. In fact, this is exactly how Rails loads your models and controllers.
so placing the file in the lib folder, it will not be run when Rails starts and won't monkey patch ActiveRecord::Base. You could place the file in config/initializers, but I think there are better alternatives.
Another method that I used at a previous job for stripping HTML tags from models is to create a plugin. We stripped a lot more than just HTML tags, but here is the HTML stripping portion:
The initializer (vendor/plugins/stripper/init.rb):
require 'active_record/stripper'
ActiveRecord::Base.class_eval do
include ActiveRecord::Stripper
end
The stripping code (vendor/plugins/stripper/lib/active_record/stripper.rb):
module ActiveRecord
module Stripper
module ClassMethods
def strip_html(*args)
opts = args.extract_options!
self.strip_html_fields = args
before_validation :strip_html
end
end
module InstanceMethods
def strip_html
self.class.strip_html_fields.each{ |field| strip_html_field(field) }
end
private
def strip_html_field(field)
clean_attribute(field, /<\/?[^>]*>/, "")
end
def clean_attribute(field, regex, replacement)
self[field].gsub!(regex, replacement) rescue nil
end
end
def self.included(receiver)
receiver.class_inheritable_accessor :strip_html_fields
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
end
end
end
Then in your MyObject class, you can selectively strip html from fields by calling:
class MyObject < ActiveRecord::Base
strip_html :first_attr, :second_attr, :etc
end
The HTML stripping plugin code already given would handle the specific use mentioned in the question. In general, to add the same code to a number of classes, including a module will do this easily without requiring everything to inherit from some common base, or adding any methods to ActiveRecord itself.
module MyBeforeSave
def self.included(base)
base.before_save :before_save_tasks
end
def before_save_tasks
puts "in module before_save tasks"
end
end
class MyModel < ActiveRecord::Base
include MyBeforeSave
end
>> m = MyModel.new
=> #<MyModel id: nil>
>> m.save
in module before_save tasks
=> true
I'd monkeypatch ActiveRecord::Base and put the file in config/initializers:
class ActiveRecord::Base
before_create :some_method
def some_method
end
end

Resources