I'm working with an external framework (redmine) which has one Project model that has_many EnabledModules.
Projects can have EnabledModules "attached" or "removed" via the module names, like this:
class Project < ActiveRecord::Base
...
has_many :enabled_modules, :dependent => :delete_all
...
def enabled_module_names=(module_names)
enabled_modules.clear
module_names = [] unless module_names && module_names.is_a?(Array)
module_names.each do |name|
enabled_modules << EnabledModule.new(:name => name.to_s)
end
end
end
I'd like to detect when new modules are attached/removed via callbacks on EnabledModule, and not modify the "original source code" if possible.
I can detect "attachments" like this:
class EnabledModule < ActiveRecord::Base
belongs_to :project
after_create :module_created
def module_created
logger.log("Module attached to project #{self.project_id}")
end
end
I thought that a before_destroy would work for detecting removals, but it will not.
This happens because the enabled_modules.clear call on Project.enabled_module_names=, doesn't invoke 'destroy' on the modules. It just sets their project_id to nil. So I figured I should use a after_update or before_update.
If I use after_update, how can I get the 'previous' project_id?
If I use before_update, how can I differentiate between modules that are 'just updated' and modules whose project_id is going to be reset to nil?
Should I use a totally different approach here?
EDIT: I just found out that I could get the old values with '_was' (i.e. self.project_was). However, collection.clear doesn't seem to trigger update callbacks. Any other solutions?
EDIT 2: Changed title
It looks like revision 2473 onwards of Redmine should solve your problem. See the diffs here:
http://www.redmine.org/projects/redmine/repository/diff/trunk/app/models/project.rb?rev=2473&rev_to=2319
Basically the code has been modified such that removed modules are destroyed rather than deleted, the difference being that model callbacks are not fired for deletes.
There's another related fix in revision 3036 that seems important (see http://www.redmine.org/issues/4200) so you might want to pick up at least that version.
I ended up reimplementing the enabled_module_names= method of projects, including a file in vendor/plugins/my_plugin/lib/my_plugin/patches/project_patch.rb and alias.
module MyPlugin
module Patches
module ProjectPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.extend(ClassMethods)
base.class_eval do
unloadable # Send unloadable so it will not be unloaded in development
# This replaces the existing version of enabled_module_names with a new one
# It is needed because we need the "destroy" callbacks to be fired,
# and only on the erased modules (not all of them - the default
# implementation starts by wiping them out in v0.8'ish)
alias_method :enabled_module_names=, :sympa_enabled_module_names=
end
end
module ClassMethods
end
module InstanceMethods
# Redefine enabled_module_names so it invokes
# mod.destroy on disconnected modules
def sympa_enabled_module_names=(module_names)
module_names = [] unless module_names and module_names.is_a?(Array)
module_names = module_names.collect(&:to_s)
# remove disabled modules
enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
# detect the modules that are new, and create those only
module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name) }
end
end
end
end
end
I had to include some code on my vendor/plugins/my_plugin/init.rb file, too:
require 'redmine'
require 'dispatcher'
# you can add additional lines here for patching users, memberships, etc...
Dispatcher.to_prepare :redmine_sympa do
require_dependency 'project'
require_dependency 'enabled_module'
Project.send(:include, RedmineSympa::Patches::ProjectPatch)
EnabledModule.send(:include, RedmineSympa::Patches::EnabledModulePatch)
end
Redmine::Plugin.register :redmine_sympa do
# ... usual redmine plugin init stuff
end
After this, I was able to detect deletions on enabled modules (via before_delete) on my patcher.
Regarding:
If I use after_update, how can I get the 'previous' project_id
Maybe try project_id_was, it's provided by ActiveRecord::Dirty
Related
I am trying to write a lib plugin/extension to perform an action where I need to know which Models have been marked for use with this plugin.
Currently, I am marking the models in the fashion of acts_as_something method which is added to each Model intended to be used with the plugin.
The main file of the plugin looks like this
# lib/foo.rb
module Foo
class << self
attr_accessor :models
end
self.models = []
module Model
def acts_as_foo
Foo.models << self
end
end
ActiveSupport.on_load(:active_record) do
extend Foo::Model
end
The intended use is to then call in a controller Foo.perform, which needs to know the marked models in order to carry out the intended action, the idea being getting the list of models from Foo.models.
It works as intended if when config.eager_load is set to true in development.rb , otherwise the files of the models have not been used/loaded yet and therefore Foo.models is an empty Array.
My goal is to be able to add more models to Foo without having to change Foo's code like this.
#app/models/bar.rb
class Bar < ApplicationRecord
acts_as_foo
end
Any ideas on the best way to implement this?
I had a similar problem before in my gem.
I ended up loading ONLY model files (which is the minimum of my requirement same as yours because the DSL code is there like your acts_as_foo), and not immediately eager loading all Rails-related files using Rails.application.eager_load!.
# lib/foo.rb
module Foo
class << self
attr_accessor :models
end
self.models = []
class Engine < Rails::Engine
initializer 'foo.eager_load_models' do |app|
unless app.config.eager_load
models_load_path = File.join(Rails.root, 'app', 'models')
# copied from https://apidock.com/rails/Rails/Engine/eager_load%21/class
matcher = /\A#{Regexp.escape(models_load_path.to_s)}\/(.*)\.rb\Z/
Dir.glob("#{models_load_path}/**/*.rb").sort.each do |file|
app.require_dependency file.sub(matcher, '\1')
end
end
end
end
module Model
def acts_as_foo
Foo.models << self
end
end
end
ActiveSupport.on_load(:active_record) do
extend Foo::Model
end
I would like to setup a before_create for all of my modules
what i have been trying is:
module ActiveRecord
module UserMonitor
require 'securerandom'
before_create :attach_uuid
def attach_uuid
self.uuid = SecureRandom.uuid.gsub("-","")
end
end
end
This does not seem to be working.
if i go into each module and add it in there it works, but i want to do it on a global scale.
Any thoughts or ideas on how i can achieve this in this manner? i know i could do it in triggers and such but i don't want to go that route and i would like to avoid hitting every module/class in case i need to change something.
Currently using Ruby 1.9.3 Can not currently upgrade my app until i make future code changes.
Thanks!
An other solution - I use, is to put the logic for UUID in an own module, that you include. I already have some (class-) methods I add to my AR, like set_default_if, so it was a good place for me.
module MyRecordExt
def self.included base
base.extend ClassMethods # in my case some other stuff
base.before_create :attach_uuid # now add the UUID
end
def attach_uuid
begin
self.uuid = SecureRandom.uuid
rescue
# do the "why dont we have a UUID filed?" here
end
end
# some other things not needed for add_uuid
module ClassMethods
include MySpecialBase # just an eg.
def default_for_if(...)
...
end
end
end
and then
class Articel < ActiveRecord::Base
include MyRecordExt
...
end
In general I avoid doing something for ALL models modifying AR base - I made the first bad experience with adding the UUID to all, and crashed with devise GEMs models ...
If you define attach_uuid in the ActiveRecord module, can't you just call the before_create :attach_uuid at the top of each controller? This is DRY.
Is there a UserMonitor controller that you could add it to?
class UserMonitor < ActiveRecord::Base
before_create :attach_uuid
end
I'm writing a Redmine plugin and added some fields to issues form via hooks (the fields are also added to Issue table), so far so good. Now I want to make those fields mandatory, but can't figure out how to 'override' validates_presence_of behavior for Issue model.
I've created a hook for Issue save method, in order to check presence of my new fields before saving, but not sure if this is the best way to go. Is it possible to just extend Issue model so that it validates for presence of my new fields?
You can add validations on new fields in you plugin. Example is here
# load plugin file(s)
Rails.configuration.to_prepare do
TimeEntry.send(:include, TimeLimitTimeEntryPatch)
end
# in patch file
module TimeLimitTimeEntryPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
unloadable
validates_presence_of :comments
validate :validate_time_limit_allowed_ip
end
end
module InstanceMethods
def validate_time_limit_allowed_ip
# add error if permission is not set and IP is not allowed
if !self.class.have_permissions?(user, project) && !time_limit_allowed_ip
errors.add(:hours, I18n.t(:not_allowed_ip))
end
end
end
end
Alternatively:
1) Create extension somewhere in your lib directory (make sure that it is required):
module IssueExtensions
extend ActiveSupport::Concern
included do
validates_presence_of :new_attr
end
end
2) Send it to Issue model. Good place for this could be config/initializers/extensions.rb (must be initialized after Redmine obviously):
Issue.send(:include, IssueExtensions)
This extension create cache_find method for all models of app (I've create this using this post).
config/active_record_extension.rb
require 'active_support/concern'
module ActiveRecordExtension
extend ActiveSupport::Concern
# add your instance methods here
def flush_find
Rails.cache.delete([self.class.name, :cached_find, id])
end
included do
after_commit :flush_find
end
module ClassMethods
def cached_find id
Rails.cache.fetch([self.name, :cached_find, id]) { self.find(id) }
end
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)
I turned this code into a gem and added to this repo.
So I want to add this methods dynamically, something like this:
class User << ActiveRecord::Base
# id, name, email, age...
cached :find, :find_by_name, :find_by_email
end
and the above code should generate cached_find, flush_find, cached_find_by_name, flush_find_by_name... You get it.
I need help to:
Test Rails.cache methods in model_caching gem.
Create code to dynamically add methods to app models based on cached method arguments.
Some links that helped me but do not meet all:
https://github.com/radar/guides/blob/master/extending-active-record.md
http://railscasts.com/episodes/245-new-gem-with-bundler
http://guides.rubyonrails.org/plugins.html
Fell free to clone and improve gem code.
You don't have to hack ActiveRecord::Base. You can add what Marc-Alexandre said right into your concern, like so:
module ActiveRecordExtension
extend ActiveSupport::Concern
...
module ClassMethods
def cached(*args)
define_method "cached_#{arg.to_s}" do
# do whatever you want to do inside cached_xx
end
define_method "flush_#{arg.to_s}" do
# do whatever you want to to inside flush_xx
end
end
end
end
Also, I would not auto include the extension directly in ActiveRecord, I think it's better to explicitly include it in the models you are going to use it.
To add code dynamically you need to hack the ActiveRecord::Base class. In another file (you usually put in lib/core_ext) you could do as follow :
ActiveRecord::Base.class_eval do
def self.cached(*args)
args.each do |arg|
define_method "cached_#{arg.to_s}" do
# do whatever you want to do inside cached_xx
end
define_method "flush_#{arg.to_s}" do
# do whatever you want to to inside flush_xx
end
end
end
end
What it does basically is takes all your arguments for cached (:find, :find_by_name, etc) and define the two methods (cache_find, cache_find_by_name) and flush_find, .. etc)
Hope this helps !
i'm playing with run_callbacks and had a problem. Can somebody help me out?
## loveable.rb
module Loveable
extend ActiveSupport::Concern
included do
define_callbacks :love
end
def loved_by!(lover)
run_callbacks :love do
do_love(lover)
end
end
def do_love(lover)
...implementation goes here...
end
end
## product.rb
class Product < ActiveRecord::Base
include Loveable
set_callback :after, :love, :after_love
def after_love
## How to get lover here??
end
end
I need "lover" user in after_love method. How could I achieved that?
For now I using instance variable but don't like the solution.
## loveable.rb
def loved_by!(lover)
#lover = lover
run_callbacks...
end
## product.rb
def after_love
#lover.do_something
end
Any better idea?
Use an instance variable :)
The only serious concern you have with using instance variables in modules is the risk that the variable names used in the module will clash with names in the class itself or other included modules. If you use a sufficiently obscure name, say #_xxx_loveble_lover, that should effectively minimise the risk.
From your code it appears that lover is an attribute associated with each object, so instance variables are the best device for this.