ActiveModel::Dirty watch only specific fields - ruby-on-rails

I'm not 100% sure about why ActiveModel::Dirty has its name. I'm guessing it is because it is considered as dirty to use it.
But in some cases, it is not possible to avoid watching on specific fields.
Ex:
if self.name_changed?
self.slug = self.name.parameterize
end
Without ActiveModel::Dirty, the code would look like:
if old_name != self.name
self.slug = self.name.parameterize
end
which implies having stored old_name before, and it is not readable, so IMHO, it is dirtier than using ActiveModel::Dirty. It becomes even worse if old_number is a number and equals params[:user]['old_number'] as it needs to be correctly formated (parsed as int), whereas ActiveRecord does this automagically.
So I would find clean to define watchable fields at Model level:
class User < ActiveRecord::Base
include ActiveModel::Dirty
watchable_fields :name
before_save :generate_slug, if: name_changed?
def generate_slug
self.slug = self.name.parameterize
end
end
Or (even better?) at controller level, before assigning new values:
def update
#user = current_user
#user.watch_fields(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.name_changed?
#user.save # etc.
end
The good thing here is that it removes the memory overload produced by using ActiveModel::Dirty.
So my question is:
Can I do that using ActiveRecord pre-built tools, or should I write a custom library to this?
Thanks

If ActiveModel::Dirty solves your problem, feel free to use it. The name comes from the term "dirty objects" and is not meant to imply that it's a dirty/hackish module.
See this answer for more details on dirty objects: What is meant by the term "dirty object"?

Here's what I have ended up doing. I like it:
class User < ActiveRecord::Base
attr_accessor :watched
def field_watch(field_name)
self.watched ||= {}
self.watched[field_name] = self.send(field_name)
return self
end
def field_changed?(field_name)
self.send(field_name) != self.watched(field_name)
end
end
And in the controller
def update
#user = current_user.field_watch(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.field_changed?(:name)
#user.save
end
I'll report here if I take the time to wrap this code in a gem or something.

Related

Ruby how to return instance evaled array

I have method in my rails model which returns anonymous class:
def today_earnings
Class.new do
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.instance_eval do
def to_f
reduce(:+)
end
end
end
def used
all.map(&:amount_used).instance_eval do
def to_f
reduce(:+)
end
end
end
end.new(self)
end
I want to achieve possibility to chain result in that way user.today_earning.unused.to_f and I have some problems with instance eval because when I call to_f on result it's undefined method, I guess it is due to ruby copying returned value so the instance gets changed, is it true? And if I'm correct how can I change the code to make it work. Also I'm wondering if making new model can be better solution than anomyous class thus I need advice if anonymous class is elegant in that case and if so how can I add to_f method to returned values
Yes, Anonymous class makes the code much complex. I would suggest a seperate class. It will solve 2 problems here.
defining some anonymous class again and again when we call the today_earnings method.
Readability of the code.
Now coming to actual question, you can try something similar to hash_with_indifferent_access. The code looks as follows.
class NumericArray < Array
def to_f
reduce(:+)
end
end
Array.class_eval do
def with_numeric_operations
NumericArray.new(self)
end
end
Usage will be:
Class Earnings
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.with_numeric_operations
end
def used
all.map(&:amount_used).with_numeric_operations
end
end
This looks like a "clever" but ridiculously over-complicated way to do something that can be simply and efficiently done in the database.
User.joins(:store_credits)
.select(
'users.*',
'SUM(store_credits.amount_used) AS amount_used',
'SUM(store_credits.amount) - amount_used AS unused',
)
.where(store_credits: { created_at: Time.current.beginning_of_day..Time.current.end_of_day })
.group(:id)

PORO from AR Object

So I got introduced to using PORO instead of AR object for abstraction and size reduction.
But I have so many AR tables didn't make sense to put so much time to build a PORO class for each and every. Would take like a hour or two!! So instead I spent many hours thinking about how can I make this simpler.
And this is what I ended up making:
class BasePORO
def initialize(obj, immutable = true)
self.class::ATTRIBUTES.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
instance_eval("undef #{attr}=") if immutable
end
end
end
class UserPORO < BasePORO
# or plug your own attributes
ATTRIBUTES = User.new.attributes.keys.map(&:to_sym).freeze
attr_accessor(*ATTRIBUTES)
end
But I can't somehow move the attr_accessor into the Base class or even ATTRIBUTES when not given explicitly. Not sure if its possible even.
Can I somehow move attr_accessor and default ATTRIBUTES into the main BasePORO class?
Any pointers or feedback is welcome.
As suggested in the comments, OpenStruct can do most of the heavy lifting for you. One thing to note is that if you don't freeze it, then after it's initialization you'll be able to add more attributes to it throughout its lifetime, e.g.:
struct = OpenStruct.new(name: "Joe", age: 20)
struct.email = "joe#example.com" # this works
p struct.email # => "joe#example.com"
(so essentially it works like a Hash with object-like interface)
This behavior may be undesired. And if you do freeze the struct, it won't allow any more attributes definition, but then you'd also lose the ability to override existing values (which I think you want to do in cases when someone sets immutable to false).
For the immutable flag to work as I understand you to expect it, I'd create a class that uses OpenStruct under its hood, for example like this:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
#data = OpenStruct.new(obj.attributes)
obj.attributes.keys.each do |attr|
self.class.define_method(attr.to_sym) do
#data.send(attr.to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
#data.send("#{attr}=".to_sym, new_value)
end
end
end
end
end
class UserPORO < BasePORO
end
BTW, if you insisted on having a solution similar to the one shown in the question, then you could achieve this with something like that:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
attributes.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
self.class.define_method(attr.to_sym) do
instance_variable_get("##{attr}".to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
instance_variable_set("##{attr}".to_sym, new_value)
end
end
end
end
private
# default attributes
def attributes
[:id]
end
end
class UserPORO < BasePORO
private
# overriding default attributes from BasePORO
def attributes
User.new.attributes.keys.map(&:to_sym).freeze
end
end
So this is what actually ended up with:
class BaseStruct < OpenStruct
def initialize(model, immutable: true, only: [], includes: [])
if only.empty?
hash = model.attributes
else
hash = model.attributes.slice(*only.map!(&:to_s))
end
includes.each do |i|
relation = model.public_send(i)
if relation.respond_to?(:each)
hash[i.to_s] = relation.map{|r| OpenStruct.new(r.attributes).freeze}
else
hash[i.to_s] = OpenStruct.new(relation.attributes).freeze
end
end
super(hash)
self.freeze if immutable
end
end
Feel free to critique or suggest improvements.

Safest way to override the update method of a model

I have the following model:
class TwitterEngagement < ApplicationRecord
end
And I would like to override create (and create!), update (and
update!) methods of it so no one can manually entry fake data. I would like the help of someone more experienced with active record and rails so I don't mess anything up. Right now what I have is:
class TwitterEngagement < ApplicationRecord
belongs_to :page
def create
super(metrics)
end
def update
super(metrics)
end
private
def metrics
client.get_engagements(page.url)
def client
TwitterClient.new
end
end
Thank you.
TL;DR:
class FacebookEngagement < ApplicationRecord
def create_or_update(*args, &block)
super(metrics)
end
Probably depends on your Rails version, but I traced the ActiveRecord::Persistence sometime before in Rails 5, and found out that both create and update eventually calls create_or_update.
Suggestion:
If ever possible, I'll just do a validation, because it kinda makes more sense because you are validating the inputs, and then probably set an optional readonly?, to prevent saving of records. This will also prevent "silent failing" code / behaviour as doing TL;DR above would not throw an exception / populate the validation errors, if say an unsuspecting developer does: facebook_engagement.update(someattr: 'somevalue') as the arguments are gonna basically be ignored because it's instead calling super(metrics), and would then break the principle of least surprise.
So, I'll probably do something like below:
class FacebookEngagement < ApplicationRecord
belongs_to :page
validate :attributes_should_not_be_set_manually
before_save :set_attributes_from_facebook_engagement
# optional
def readonly?
# allows `create`, prevents `update`
persisted?
end
private
def attributes_should_not_be_set_manually
changes.keys.except('page_id').each do |attribute|
errors.add(attribute, 'should not be set manually!')
end
end
def set_attributes_from_facebook_engagement
assign_attributes(metrics)
end
def metrics
# simple memoization to prevent wasteful duplicate requests (or remove if not needed)
#metrics ||= graph.get_object("#{page.url}?fields=engagement")
end
def graph
Koala::Facebook::API.new
end
end

Is this Rails validation thread-safe

This is running in multiple Sidekiq instances and workers at the same time and it seems that is has generated a couple of issues, like instances getting assigned the "It was alerted recently" error when shouldn't and the opposite.
It is rare, but it is happening, is this the problem or maybe it is something else?
class BrokenModel < ActiveRecord::Base
validates_with BrokenValidator
end
class BrokenValidator < ActiveModel::Validator
def validate record
#record = record
check_alerted
end
private
def check_alerted
if AtomicGlobalAlerted.new(#record).valid?
#record.errors[:base] << "It was alerted recently"
end
p "check_alerted: #{#record.errors[:base]}"
end
end
class AtomicGlobalAlerted
include Redis::Objects
attr_accessor :id
def initialize id
#id = id
#fredis = nil
Sidekiq.redis do |redis|
#fredis = FreshRedis.new(redis, freshness: 7.days, granularity: 4.hours)
end
end
def valid?
#fredis.smembers.includes?(#id)
end
end
We were experiencing something similar at work and after A LOT of digging finally figured out what was happening.
The class method validates_with uses one instance of the validator (BrokenValidator) to validate all instances of the class you're trying to validate (BrokenModel). Normally this is fine but you are assigning a variable (#record) and accessing that variable in another method (check_alerted) so other threads are assigning #record while other threads are still trying to check_alerted.
There are two ways you can fix this:
1) Pass record to check_alerted:
class BrokenValidator < ActiveModel::Validator
def validate(record)
check_alerted(record)
end
private
def check_alerted(record)
if AtomicGlobalAlerted.new(record).valid?
record.errors[:base] << "It was alerted recently"
end
p "check_alerted: #{record.errors[:base]}"
end
end
2) Use the instance version of validates_with which makes a new validator instance for each model instance you want to validate:
class BrokenModel < ActiveRecord::Base
validate :instance_validators
def instance_validators
validates_with BrokenValidator
end
end
Either solution should work and solve the concurrency problem. Let me know if you experience any other issues.
I believe there are some thread safety issues in rails but we can overcome them by taking necessary precautions.
The local variables, such as your local var, are local to each particular invocation of the method block. If two threads are calling this block at the same time, then each call will get its own local context variable and those won't overlap unless there are shared resources involved: instance variables like (#global_var), static variables (##static_var), globals ($global_var) can cause concurrency problems.
You are using instance variable, just instantiate it every time you are coming to the validate_record method and hopefully your problem will go away like :
def validate record
#record.errors[:base] = []
#record = record
check_alerted
end
For more details you can visit this detailed link
Or try to study about rails configs here : link

detect if only one attribute is updated in Rails 4 on update_attributes

I am making a blogging app. I need to have two different methods based on how many attributes have been changed. Essentially, if ONLY the publication_date changes I do one thing...even the publication_date and ANYTHING ELSE changes, I do another thing.
posts_controller.rb
def special_update
if #detect change of #post.publication_date only
#do something
elsif # #post changes besides publication_date
elsif #no changes
end
end
One way to approach this is in your model using methods provided by ActiveModel::Dirty, which is available to all your Rails Models. In particular the changed method is helpful:
model.changed # returns an array of all attributes changed.
In your Post model, you could use an after_update or before_update callback method to do your dirty work.
class Post < ActiveRecord::Base
before_update :clever_method
private
def clever_method
if self.changed == ['publication_date']
# do something
else
# do something else
end
end
end
craig.kaminsky's answer is good, but if you prefer to mess with your controller instead of your model, you can do that as well:
def special_update
# the usual strong params thing
param_list = [:title, :body]
new_post_params = params.require(:post).permit(*param_list)
# old post attributes
post_params = #post.attributes.select{|k,v| param_list.include(k.to_sym)}
diff = (post_params.to_a - new_post_params.to_a).map(&:first)
if diff == ['publication_date']
#do something
elsif diff.empty? # no changes
else # other changes
end
end
Or simply compare parameter with existing value
if params[:my_model][:publication_date] != #my_model.publication_date
params[:my_model][:publication_date] = Time.now
end

Resources