I have a class (ActorClue) that has three attr_accessor defined within it. There are a couple other common fields needed by other classes, so I put those common fields in a module (BaseClueProperties). I am including that module within my ActorClue class.
Here's the code sampling :
module BaseClueProperties
attr_accessor :image_url
attr_accessor :summary_text
end
################
class BaseClue
# http://stackoverflow.com/questions/2487333/fastest-one-liner-way-to-list-attr-accessors in-ruby
def self.attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def self.attributes
#attributes
end
def attributes
self.class.attributes
end
end
###############
class ActorClue < BaseClue
attr_accessor :actor_name
attr_accessor :movie_name
attr_accessor :directed_by
include BaseClueProperties
.....
end
I instantiate the above with the following :
>> gg = ActorClue.new
=> #<ActorClue:0x23bf784>
>> gg.attributes
=> [:actor_name, :movie_name, :directed_by]
Why is it only returning :actor_name, :movie_name, and :directed_by and not include :image_url and :summary_text?
I modified the BaseClueProperties to read the following :
module BaseClueProperties
BaseClue.attr_accessor :image_url
BaseClue.attr_accessor :summary_text
end
But still with the same result.
Any thoughts as to why my :image_url and :summary_text attributes aren't getting added to my #attributes collection?
I can't promise that my description of the reason is correct but the following code should fix the problem. I believe that your are adding attributes to the Module not to the class that it is included within. Anyhow replacing your module with the following should fix the problem.
module BaseClueProperties
def self.included(base)
base.send :attr_accessor, :image_url
base.send :attr_accessor, :summary_text
end
end
This should cause the including object to define attribute_accessors when it includes the module.
Related
I'm trying to include a method from a helper module into an ActiveModel::Serializer subclass but for some reason the method is not showing up.
Here's my simple helper module:
module Helpers
module Serialisers
def enforce_zulu_time(attribute)
define_method(attribute) do
object.send(attribute).utc.iso8601 unless object.try(attribute).nil?
end
end
end
end
And here's my test serialiser
class TestSerialiser < ActiveModel::Serializer
include Helpers::Serialisers
attributes :updated_at
enforce_zulu_time :updated_at
end
and my simple object to serialise
class TestItem
include ActiveModel::SerializerSupport
attr_reader :updated_at
def initialize
#updated_at = Time.now.utc
end
end
and my test
describe Helpers::Serialisers do
let(:item) { TestItem.new }
let(:serialiser) { TestSerialiser.new(item) }
subject { serialiser.attributes }
it { expect(subject[:updated_at]).to be_zulu_time}
end
results in
`<class:TestSerialiser>': undefined method `enforce_zulu_time' for TestSerialiser:Class (NoMethodError)
However if I just do this in my TestSerialiser instead
class TestSerialiser < ActiveModel::Serializer
attributes :updated_at
['updated_at'].each do |attribute|
define_method(attribute) do
object.send(attribute).utc.iso8601 unless object.send(attribute).blank?
end
end
end
it all works fine.
Why is my enforce_zulu_time method not being included?
Replace include Helpers::Serialisers with extend Helpers::Serialisers since you expect class methods.
Another solution would be to use ActiveSupport::Concern, see doc
Sidenote
In order to have your code flexible for free, I recommend you to create your own base class for your serializers, like:
class BaseSerializer < ActiveModel::Serializer
end
Then have all your serializers inherit from it. This way you can add features easily.
I have an ActiveRecord class called User. I'm trying to create a concern called Restrictable which takes in some arguments like this:
class User < ActiveRecord::Base
include Restrictable # Would be nice to not need this line
restrictable except: [:id, :name, :email]
end
I want to then provide an instance method called restricted_data which can perform some operation on those arguments and return some data. Example:
user = User.find(1)
user.restricted_data # Returns all columns except :id, :name, :email
How would I go about doing that?
If I understand your question correctly this is about how to write such a concern, and not about the actual return value of restricted_data. I would implement the concern skeleton as such:
require "active_support/concern"
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
attr_reader :restricted
private
def restrictable(except: []) # Alternatively `options = {}`
#restricted = except # Alternatively `options[:except] || []`
end
end
def restricted_data
"This is forbidden: #{self.class.restricted}"
end
end
Then you can:
class C
include Restrictable
restrictable except: [:this, :that, :the_other]
end
c = C.new
c.restricted_data #=> "This is forbidden: [:this, :that, :the_other]"
That would comply with the interface you designed, but the except key is a bit strange because it's actually restricting those values instead of allowing them.
I'd suggest starting with this blog post: https://signalvnoise.com/posts/3372-put-chubby-models-on-a-diet-with-concerns Checkout the second example.
Think of concerns as a module you are mixing in. Not too complicated.
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
def restricted_data(user)
# Do your stuff
end
end
end
I'm not sure if macro is even the correct term. Basically, I want to be able to configure ActiveRecord columns easily (using the familiar AR syntax) so that before_save they will always be formatted a certain way by calling an instance method.
I'd like to make all of this accessable from a mixin.
For example:
class MyClass < ActiveRecord::Base
happy_columns :col1, :col2 # I really want this type of convenient syntax
# dynamically created stuff below from a mixin.
before_save :make_col1_happy
before_save :make_col2_happy
def make_col1_happy; self.col1 += " is happy"; end
def make_col2_happy; self.col2 += " is happy"; end
end
try to extend ActiveRecord , a.e.
#in lib/happy_columns.rb
module HappyColumns
def happy_columns(cols)
cols.each do |c|
before_filter "make_#{c}_happy".to_sym
#here you could define your instance methot using define_method
define_method "make_#{c}_happy" do
#your code
end
end
include InstanceMethods
end
module InstanceMethods
#here you could define other your instancemethod
end
end
ActiveRecord::Base.extend HappyColumns
be sure of include the extensions in your load path , then you could use happy_cols in your model.
sorry if there is some mistake , for define_method look at this .
hope this could help.
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
In an effort to reduce code duplication in my little Rails app, I've been working on getting common code between my models into it's own separate module, so far so good.
The model stuff is fairly easy, I just have to include the module at the beginning, e.g.:
class Iso < Sale
include Shared::TracksSerialNumberExtension
include Shared::OrderLines
extend Shared::Filtered
include Sendable::Model
validates_presence_of :customer
validates_associated :lines
owned_by :customer
def initialize( params = nil )
super
self.created_at ||= Time.now.to_date
end
def after_initialize
end
order_lines :despatched
# tracks_serial_numbers :items
sendable :customer
def created_at=( date )
write_attribute( :created_at, Chronic.parse( date ) )
end
end
This is working fine, now however, I'm going to have some controller and view code that's going to be common between these models as well, so far I have this for my sendable stuff:
# This is a module that is used for pages/forms that are can be "sent"
# either via fax, email, or printed.
module Sendable
module Model
def self.included( klass )
klass.extend ClassMethods
end
module ClassMethods
def sendable( class_to_send_to )
attr_accessor :fax_number,
:email_address,
:to_be_faxed,
:to_be_emailed,
:to_be_printed
#_class_sending_to ||= class_to_send_to
include InstanceMethods
end
def class_sending_to
#_class_sending_to
end
end # ClassMethods
module InstanceMethods
def after_initialize( )
super
self.to_be_faxed = false
self.to_be_emailed = false
self.to_be_printed = false
target_class = self.send( self.class.class_sending_to )
if !target_class.nil?
self.fax_number = target_class.send( :fax_number )
self.email_address = target_class.send( :email_address )
end
end
end
end # Module Model
end # Module Sendable
Basically I'm planning on just doing an include Sendable::Controller, and Sendable::View (or the equivalent) for the controller and the view, but, is there a cleaner way to do this? I 'm after a neat way to have a bunch of common code between my model, controller, and view.
Edit: Just to clarify, this just has to be shared across 2 or 3 models.
You could pluginize it (use script/generate plugin).
Then in your init.rb just do something like:
ActiveRecord::Base.send(:include, PluginName::Sendable)
ActionController::Base.send(:include, PluginName::SendableController)
And along with your self.included that should work just fine.
Check out some of the acts_* plugins, it's a pretty common pattern (http://github.com/technoweenie/acts_as_paranoid/tree/master/init.rb, check line 30)
If that code needs to get added to all models and all controllers, you could always do the following:
# maybe put this in environment.rb or in your module declaration
class ActiveRecord::Base
include Iso
end
# application.rb
class ApplicationController
include Iso
end
If you needed functions from this module available to the views, you could expose them individually with helper_method declarations in application.rb.
If you do go the plugin route, do check out Rails-Engines, which are intended to extend plugin semantics to Controllers and Views in a clear way.