I've installed the ActiveRecord Reputation System on my app. How would I go about overriding or adding a callback/method to Evaluation model?
In general how do you add to any model for a gem you installed?
Simply reopen the class:
module ReputationSystem
class Evaluation < ActiveRecord::Base
def my_method_here
puts "Yey!"
end
end
end
You can put this file in config/initializers/my_monkey_patch.rb or in lib/my_monkey_patch.rb, but the later must be loaded into your code.
Related
I'm trying to see if it's possible to inject or execute gem's before filters in the parent app model.
Example:
Gem's gem/app/controllers/test/application_controller.rb:
module Test
class ApplicationController < ActionController::Base
before_update :foo
def foo
puts 'test'
end
end
end
Parent's App business.rb:
class Business < ApplicationRecord
# should call foo before creating the record
# and execute the puts statement
end
When the application executes Business.create(params), the business model should be calling method foo from the gem and executes the puts statement. Currently, I have no way of making this call, b/c foo is not executing before the create method. So I am looking for a way to try and test this. Thanks
Edit 2:
The use case here is each some of the models in the parent apps contain columns created_by and updated_by. We want to update these columns to the user UID that's doing the action on the specific model. I know I can do before_update or before_create in the parents app, but we want to put these actions in the gem so multiple of our applications can use it. The gem we are using is a common gem we wrote that integrated in all our applications. Also, I know there are gems out there like Audit-trail, but we want it simpler.
https://stackoverflow.com/a/35605007/14524531
Pretty much is the same idea with this snippet here, except using it in a gem with multiple models. I hope this makes it more clear. Thanks!
I have decided to test it with a new gem and a clean rails app.
Gem
Gemfile
source 'https://rubygems.org'
gemspec
gem 'activerecord'
lib/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
before_save :foo
def foo
puts 'test'
end
end
Rails app
app/models/business.rb
class Business < ApplicationRecord
end
In rails console I call Business.create and the first output I get is test.
I need some help with my plugin. I want to extend ActiveRecord::Base with a method that initializes another method that can be called in the controller.
It will look like this:
class Article < ActiveRecord::Base
robot_catch :title, :text
...
end
My attempt at extending the ActiveRecord::Base class with robot_catch method looks like following. The function will initialize the specified attributes (in this case :title and :text) in a variable and use class_eval to make the robot? function available for the user to call it in the controller:
module Plugin
module Base
extend ActiveSupport::Concern
module ClassMethods
def robot_catch(*attr)
##robot_params = attr
self.class_eval do
def robot?(params_hash)
# Input is the params hash, and this function
# will check if the some hashed attributes in this hash
# correspond to the attribute values as expected,
# and return true or false.
end
end
end
end
end
end
ActiveRecord::Base.send :include, Plugin::Base
So, in the controller, this could be done:
class ArticlesController < ApplicationController
...
def create
#article = Article.new(params[:article])
if #article.robot? params
# Do not save this in database, but render
# the page as if it would have succeeded
...
end
end
end
My question is whether if I am right that robot_catch is class method. This function is to be called inside a model, as shown above. I wonder if I am extending the ActiveRecord::Base the right way. The robot? function is an instance method without any doubt.
I am using Rails 3.2.22 and I installed this plugin as a gem in another project where I want to use this functionality.
Right now, it only works if I specifically require the gem in the model. However, I want it the functionality to be included as a part of ActiveRecord::Base without requiring it, otherwise I'd have to require it in every model I want to use it, not particularly DRY. Shouldn't the gem be automatically loaded into the project on Rails start-up?
EDIT: Maybe callbacks (http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html) would be a solution to this problem, but I do not know how to use it. It seems a bit obscure.
First, I would suggest you make sure that none of the many many built in Rails validators meet your needs.
Then if that's the case, what you actually want is a custom validator.
Building a custom validator is not as simple as it might seem, the basic class you'll build will have this structure:
class SpecialValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Fill this with your validation logic
# Add to record.errors if validation fails
end
end
Then in your model:
class Article < ActiveRecord::Base
validates :title, :text, special: true
end
I would strongly suggest making sure what you want is not already built, chances are it is. Then use resources like this or ruby guides resources to continue going down the custom validator route.
Answer
I found out the solution myself. Bundler will not autoload dependencies from a gemspec that my project uses, so I had to require all third party gems in an engine.rb file in the lib/ directory of my app in order to load the gems. Now everything is working as it should.
Second: the robot_catch method is a class method.
I have trouble thinking of a way on how to shorten my process on titleizing values upon rendering them in my view.
I did some custom getters for the following attributes that I need to titleize. Here's my example.
user.rb
class User < ActiveRecord::Base
def department
read_attribute(:department).titleize
end
def designation
read_attribute(:designation).titleize
end
end
This method works but it seems a hassle when I want to do this to other models as well.
Is there a more efficient way to handle this which can be used by other models? If you'll mention Draper (since I don't seem to find on how to titleize selected attributes), how can I accomplish using this gem? But, I would prefer not using a gem but instead, create a custom one.
Not tested this, but you could use a Concern with added modules to handle it
--
Modularity
I found a gem called modularity which basically allows you to pass parameters to a concern & other modules. This means if you can pass the params you wish to "titleize", you may be able to pull it off like this:
#Gemfile
gem 'modularity', '~> 2.0.1'
#app/models/concerns/titleize.rb
module Titleize
extend ActiveSupport::Concern
as_trait do |*fields|
fields.each do |field|
define_method("#{field}") do
self[field.to_sym] = field.titleize
end
end
end
end
#app/models/your_model.rb
Class YourModel < ActiveRecord::Base
include Titleize[:your, :params]
end
If you want those value always titleized, what you are doing is fine, but I would actually apply the method on the setters, not on the getters, so you only do it once per record instead of at each read:
def department=(s)
write_attribute(:department, s.to_s.titleize) # The to_s is in case you get nil/non-string
end
If this is purely for presentation (ie, you want the not titleized version in the database, then it can be done in a presenter using Draper:
class UserDecorator < Draper::Decorator
delegate_all
def designation
object.designation.titleize
end
end
(or another rails presenter).
I have a simple class:
class User
include Mongoid::Document
field :name
end
And I would like to reopen it to add a Mongoid callback:
class User
before_create :do_this
def do_this
# do it...
end
end
Unfortunately I got the error: undefined method 'before_create' for User:Class
Any idea how to do this ? Should I use a mixin pattern instead of re-opening ?
UPDATE: I can't change the original class definition, since it's in a shared library. And the load order is tricky, because it's in Rails. The original class is in a file loaded in autoload_path. Where should I reopen it ? And I would rather use a module rather than reopening, but I'm not sure it's possible to include my module "from the outside" !
UPDATE 2: You are right, it's just a load order problem. So now my question becomes: Since Rails' autoload is lazy, how can I force Rails to load my reopening file after it loads the original class file ? :)
Your code above worked for me in the console. I suspect the second class declaration is being loaded first. You might try printing out a message immediately inside each class declaration e.g.
class User
puts "First"
...
end
...
class User
puts "Second"
...
end
and verifying that they load in the correct order.
Also, if you do have access to the first class declaration, you might use a mixin if possible, as it keeps everything for the User class in a single location.
UPDATE: Can you first load/require the shared User class to ensure it is loaded? That is:
require 'app/models/user'
class User
before_create :do_something
def do_something
...
end
end
I've found a way to make this work, but am curious about a better way / the Rails 3 way. (I'm using 2.3.5 still, but hope to migrate around New Year's.)
The situation: I've got two layers of module inheritance, the second layer gets mixed into a Rails model. Both modules define validation methods and I'd like both of them to attach the validations to the base class, but because of the two levels of inheritance, the following doesn't work:
def self.included(base)
base.validate :yadda_yadda
end
When that module is included by another module, the interpreter grinds to a screeching halt because Modules don't know about ActiveRecord::Validations. Including the validations module begs the question of "where is save?" thanks to alias_method.
The following works, as long as you remember to call super whenever you override validate(). I don't trust myself or future maintainers to remember that, so I'd like to use the validate :yadda_yadda idiom instead, if possible.
module Grandpa
def validate
must_be_ok
end
def must_be_ok
errors.add_to_base("#{self} wasn't ok")
end
end
module Dad
include Grandpa
def validate
super
must_be_ok_too
end
def must_be_ok_too
errors.add_to_base("#{self} wasn't ok either")
end
end
class Kid < ActiveRecord::Base
include Dad
validate :must_be_ok_three
def must_be_ok_three
errors.add_to_base("#{self} wasn't ok furthermore")
end
end
Suggestions? Tips for Rails 3 approach? I don't think the validations API has changed that much.
I solved it (when I ran into the same problem, but with something other than validation).
Short answer: you can call send(:included, base) on the module you want to bring in. Within the higher-up included() definition, you need to check whether the base is a Class or a Module.
Why would you ever want to do this? Well, I've got some modules that extract some common functionality out of my models. For instance, the module HasAllocable sets up a polymorphic belongs_to relationship, and a getter/setter pair for a virtual attribute. Now I have another module that needs to pull in HasAllocable, to spare the base classes from having to remember it.
I'd be interested to know whether this smells funny to anyone. I haven't seen anything like it on the web, so I wonder if multiple layers of model inheritance is more of an antipattern.
module Grandpa
def self.included(base)
if base.kind_of?(Class)
base.validate :must_be_ok
end
end
end
module Dad
include Grandpa
def self.included(base)
if base.kind_of?(Class)
# you can do this
#base.send(:include, Grandpa)
# you can also do this
Grandpa.send(:included, base)
# this does not invoke Grandpa.included(Kid)
#super(base)
base.validate :must_be_ok_too
end
end
end
class Kid < ActiveRecord::Base
include Dad
validate :must_be_ok_three
end