How to properly use base scope in Rails - ruby-on-rails

I have two models that share the behavior. Both Post and Comment can have reactions.
# ./app/models/post.rb
class Post < ApplicationRecord
has_many :reactions, as: :reactionable
end
# ./app/models/comment.rb
class Comment < ApplicationRecord
has_many :reactions, as: :reactionable
end
When I decorate them, I end up with a lot of exact same methods.
# ./app/decorators/post_decorator.rb
class PostDecorator < ApplicationDecorator
delegate_all
def reactions_total_count
object.reactions.count
end
def reactions_type(kind)
object.reactions.collect(&:reaction_type).inject(0) {|counter, item| counter += item == kind ? 1 : 0}
end
def likes_count
reactions_type('like')
end
def hearts_count
reactions_type('heart')
end
def wows_count
reactions_type('wow')
end
def laughs_count
reactions_type('laugh')
end
def sads_count
reactions_type('sad')
end
end
# ./app/decorators/comment.rb
class CommentDecorator < ApplicationDecorator
delegate_all
def reactions_total_count
object.reactions.count
end
def reactions_type(kind)
object.reactions.collect(&:reaction_type).inject(0) {|counter, item| counter += item == kind ? 1 : 0}
end
def likes_count
reactions_type('like')
end
def hearts_count
reactions_type('heart')
end
def wows_count
reactions_type('wow')
end
def laughs_count
reactions_type('laugh')
end
def sads_count
reactions_type('sad')
end
end
I want it to look something like this, but don't know where to put the files, and exactly which technique I should use (include vs. extend).
# ./app/decorators/base.rb
module Base
# methods defined here
end
# ./app/decorators/post.rb
class PostDecorator < ApplicationDecorator
delegate_all
include Base
end
# ./app/decorators/comment.rb
class CommentDecorator < ApplicationDecorator
delegate_all
include Base
end
Please advise. I know there is a better approach that I just can't seem to get right.

First of all, I would figure out a better name for the included module. It could be treated as a role, using which you would enrich classes. This role would have declared a bunch of *_count methods that counts smth in reactions.
So I would name it ReactionsCountable and additionally put it into namespace to distinguish from the decorators: Roles::ReactionsCountable.
Then I would put it into:
/app/decorators
/roles
/reactions_countable.rb
/comment.rb
/post.rb
Other soulution would be to use classic inheritence. Here the Base name would make sense IMO:
class BaseDecorator < ApplicationDecorator
# declare `*_count` methods here
class PostDecorator < BaseDecorator
class CommentDecorator < BaseDecorator

I figured it out with Maicher's answer.
If a CommentDecorator is inside of ./app/decorators/comment_decorator.rb, and we want to include a module, ReactionablesCountable, then we need to create a file inside of ./app/decorators/roles/reactions_countable.rb that has module nesting which reflects the path to the file. For example, the first constant in the include, (Roles) points to the folder Rails will look for it, the second (ReactionsCountable), is the name of the file.
The module needs to be nested within this file as well. I've shown it below.
module Roles
module ReactionsCountable
# methods defined here.
end
end

Related

Getting the ActiveRecord_Relation that received the class method call

Having:
class Foo < ApplicationRecord
def self.to_csv
# irrelevant
end
end
Rails allow me to do:
Foo.all.to_csv
But how would I access the collection that received the method call inside to_csv? (all in this case)
This may seem counter intuitive but you can use #all
For example:
class Foo < ActiveRecord::Base
def self.to_csv
all.map(&:convert_to_csv)
end
end
Not only this will work with Foo.all.to_csv but also with Foo.where(...).to_csv
If you look at the source of #all inside ActiveRecord:
def all
if current_scope
current_scope.clone
else
default_scoped
end
end
This means if you have defined a scope with where or limit it will respect it. Or if you're grabbing all records it will just use default_scoped

Trying to move model code to shared concerns

I have a model with the following two methods which are required in another model so I thought I'd try sharing them via a concern instead of duplicating the code.
class Region < ActiveRecord::Base
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
end
I have created a file in app/models/concerns/sets.rb and my new model reads:
class Region < ActiveRecord::Base
include Sets
end
sets.rb is:
module Sets
extend ActiveSupport::Concern
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
module ClassMethods
end
end
Question:
How do I share a method between models when the method references the model such as "Region.where..."
Either by referencing the class of the including model (but you need to wrap the instance methods in an included block):
included do
def ancestors
self.class.where(...) # "self" refers to the including instance
end
end
or (better IMO) by just declaring the method as a class method, in which case you can leave the class itself out altogether:
module ClassMethods
def ancestors
where(...)
end
end

Rails 2.3 - implement dynamic named_scope using mixin

I use the following method_missing implementation to give a certain model an adaptable named_scope filtering:
class Product < ActiveRecord::Base
def self.method_missing(method_id, *args)
# only respond to methods that begin with 'by_'
if method_id.to_s =~ /^(by\_){1}\w*/i
# extract column name from called method
column = method_id.to_s.split('by_').last
# if a valid column, create a dynamic named_scope
# for it. So basically, I can now run
# >>> Product.by_name('jellybeans')
# >>> Product.by_vendor('Cyberdine')
if self.respond_to?( column.to_sym )
self.send(:named_scope, method_id, lambda {|val|
if val.present?
# (this is simplified, I know about ActiveRecord::Base#find_by_..)
{ :conditions => ["#{base.table_name}.#{column} = ?", val]}
else
{}
end
})
else
super(method_id, args)
end
end
end
end
I know this is already provided by ActiveRecord::Base using find_by_<X>, but I'm trying to go a little bit beyond the example I've given and provide some custom filtering taylored to my application. I'd like to make it available to selected models w/o having to paste this snippet in every model class. I thought of using a module and then mixing it in the models of choice - I'm just a bit vague on the syntax.
I've gotten as far as this when the errors started piling up (am I doing this right?):
module GenericFilter
def self.extended(base)
base.send(:method_missing, method_id, *args, lambda { |method_id, args|
# ?..
})
end
end
Then I hope to be able to use it like so:
def Product < ActiveRecord::Base
include GenericFilter
end
def Vendor < ActiveRecord::Base
include GenericFilter
end
# etc..
Any help will be great - thanks.
Two ways of achieving this
module GenericModule
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def methods_missing
#....
end
end
end
class YourModel
include GenericModule
..
end
or
module GenericModule
def method_missing
#...
end
end
class MyModel
extend GenericModule
end
I would suggest using the first one, its seems cleaner to me. And as general advise, I'd avoid overriding method_missing :).
Hope this helps.
You need to define the scope within the context of the class that is including your mixin. Wrap your scopes in including_class.class_eval and self will be correctly set to the including_class.
module Mixin
def self.included(klass)
klass.class_eval do
scope :scope_name, lambda {|*args| ... }
end
end
end
class MyModel
include Mixin
end

Rails: i have a class method and i want to modify something of the instance

Rails: i have a class method and i want to modify something of the instance
something like this:
class Test < Main
template :box
def test
# here I want to access the template name, that is box
end
end
class Main
def initialize
end
def self.template(name)
# here I have to save somehow the template name
# remember is not an instance.
end
end
that is similar to the model classes:
# in the model
has_many :projects
How do I do it?
EDIT:
class Main
def self.template(name)
#name = name
end
def template
Main.instance_eval { #name }
end
end
class Test < Main
template 6
end
t = Test.new.template
t # t must be 6
You have to bite the bullet and learn ruby meta programming. There is a book on it.
http://pragprog.com/titles/ppmetr/metaprogramming-ruby
Here is one way to do it.
class M
def self.template(arg)
define_method(:template) do
arg
end
end
end
class T < M
template 6
end
t = T.new
puts t.template
There are a few different ways to do this. Here is one:
class Main
def self.template(name)
#name = name
end
end
class Test < Main
def test
Main.instance_eval { #name }
end
end
Main.template 5
Test.new.test
==> 5

Virtual attributes in plugin

I need some help with virtual attributes. This code works fine but how do I use it inside a plugin. The goal is to add this methods to all classes that uses the plugin.
class Article < ActiveRecord::Base
attr_accessor :title, :permalink
def title
if #title
#title
elsif self.page
self.page.title
else
""
end
end
def permalink
if #permalink
#permalink
elsif self.page
self.page.permalink
else
""
end
end
end
Thanks
You can run the plugin generator to get started.
script/generate plugin acts_as_page
You can then add a module which defines acts_as_page and extends it into all models.
# in plugins/acts_as_page/lib/acts_as_page.rb
module ActsAsPage
def acts_as_page
# ...
end
end
# in plugins/acts_as_page/init.rb
class ActiveRecord::Base
extend ActsAsPage
end
This way the acts_as_page method is available as a class method to all models and you can define any behavior into there. You could do something like this...
module ActsAsPage
def acts_as_page
attr_writer :title, :permalink
include Behavior
end
module Behavior
def title
# ...
end
def permalink
# ...
end
end
end
And then when you call acts_as_page in the model...
class Article < ActiveRecord::Base
acts_as_page
end
It will define the attributes and add the methods. If you need things to be a bit more dynamic (such as if you want the acts_as_page method to take arguments which changes the behavior) try out the solution I present in this Railscasts episode.
It appears that you want a Module for this
# my_methods.rb
module MyMethods
def my_method_a
"Hello"
end
end
The you want to include it into the classes you want to use it for.
class MyClass < ActiveRecord::Base
include MyMethods
end
> m = MyClass.new
> m.my_method_a
=> "Hello!"
Take a look here for more information on mixing in modules. You can put the module wherever in a plugin if you like, just ensure its named correctly so Rails can find it.
Create a module structure like YourPlugin::InstanceMethods and include it this module like this:
module YourPlugin
module InstanceMethods
# your methods
end
end
ActiveRecord::Base.__send__(:include, YourPlugin::InstanceMethods)
You have to use __send__ to make your code Ruby 1.9 compatible. The __send__ line is usually placed at the init.rb file on your plugin root directory.

Resources