I have this model:
class Event < Registration
serialize :fields, Hash
Activities=['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
CUSTOM_FIELDS=[:activity, :description, :date_from, :date_to, :budget_pieces, :budget_amount, :actual_pieces, :actual_amount]
attr_accessor *CUSTOM_FIELDS
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields
self.fields={}
CUSTOM_FIELDS.each do |cf|
self.fields[cf]=eval("self.#{cf.to_s}")
end
end
def distribute_fields
unless self.fields.nil?
self.fields.each do |k,v|
eval("self.#{k.to_s}=v")
end
end
end
end
I have a feeling that this can be done shorter and more elegant. Does anyone have an idea?
Jacob
BTW. Can anyone tell me what the asterisk in front of CUSTOM_FIELDS does? I know what it does in a method definition (def foo(*args)) but not here...
Alright first off: never 10000000000.times { puts "ever" } use eval when you don't know what you're doing. It is the nuclear bomb of the Ruby world in the way that it can wreak devestation across a wide area, causing similar symptoms to radiation poisoning throughout your code. Just don't.
With that in mind:
class Event < Registration
serialize :fields, Hash
Activities = ['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
CUSTOM_FIELDS = [:activity,
:description,
:date_from,
:date_to,
:budget_pieces,
:budget_amount,
:actual_pieces,
:actual_amount] #1
attr_accessor *CUSTOM_FIELDS #2
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields
CUSTOM_FIELDS.each do |cf|
self.fields[cf] = send(cf) #3
end
end
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v) #3
end
end
end
end
Now for some notes:
By putting each custom field on its own line, you increase code readability. I don't want to have to scroll to the end of the line to read all the possible custom fields or to add my own.
The *CUSTOM_FIELDS passed into attr_accessor uses what is referred to as the "splat operator". By calling it in this way, the elements of the CUSTOM_FIELDS array will be passed as individual arguments to the attr_accessor method rather than as one (the array itself)
Finally, we use the send method to call methods we don't know the names of during programming, rather than the evil eval.
Other than that, I cannot find anything else to refactor about this code.
I agree with previous posters. In addition I would probably move the gather_fields and distribute_fields methods to the parent model to avoid having to repeat the code in every child model.
class Registration < ActiveRecord::Base
...
protected
def gather_fields(custom_fields)
custom_fields.each do |cf|
self.fields[cf] = send(cf)
end
end
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v)
end
end
end
end
class Event < Registration
...
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields(custom_fields = CUSTOM_FIELDS)
super
end
end
You can replace the two evals with send calls:
self.fields[cf] = self.send(cf.to_s)
self.send("#{k}=", v)
"#{}" does a to_s, so you don't need k.to_s
Activities, being a constant, should probably be ACTIVITIES.
For that asterisk *, check out this post: What is the splat/unary/asterisk operator useful for?
Activities=['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
can be written: ACTIVITIES = %w(Annonce, Butiksaktivitet, Salgskonkurrence).freeze since you are defining a constant.
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v) #3
end
end
end
can be written as a one liner:
def distribute_fields
self.fields.each { |k,v| send("#{k.to_s}=", v) } unless self.fields.empty?
end
Ryan Bigg, gave a good answer.
Related
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)
I try to optimize the following Accounting model class and have two questions:
1) How can i replace the multiple attribute setters with a more elegant method?
2) Is there a better way than the if conditional in the replace_comma_with_dot method
class Accounting < ActiveRecord::Base
validates :share_ksk, :central_office, :limit_value, :fix_disagio,
presence: true, numericality: { less_than: 999.99, greater_than_or_equal_to: 0.00 },
format: { with: /\d*\.\d{0,2}$/, multiline: true, message: I18n.t('accounting.two_digits_after_decimal_point')}
def share_ksk=(number)
replace_comma_with_dot(number)
super
end
def central_office=(number)
replace_comma_with_dot(number)
super
end
def limit_value=(number)
replace_comma_with_dot(number)
super
end
def fix_disagio=(number)
replace_comma_with_dot(number)
super
end
def replace_comma_with_dot(number)
if number.is_a? String
number.sub!(",", ".")
elsif number.is_a? Float
number
else
""
end
end
end
As user Pardeep suggested i'm trying to replace my getters with define_method:
[:share_ksk=, :central_office=, :limit_value=, :fix_disagio=].each do |method_name|
self.class.send :define_method, method_name do |number|
replace_comma_with_dot(number)
super
end
end
What am i missing?
You can define methods dynamically using the define_method, you can fine more information here
and you can update your replace_comma_with_dot with this
def replace_comma_with_dot(number)
return number.sub!(",", ".") if number.is_a? String
return number if number.is_a? Float
""
end
end
Instead of having a method in your model, I'd extract the functionality and append it to either the String or Integer classes:
#lib/ext/string.rb
class String
def replace_comma_with_dot
number.sub!(",",".") #Ruby automatically returns so no need to use return
end
end
This - if your number is a string will allow you to do the following:
number = "67,90"
number.replace_comma_with_dot
To use this in the app, the setters are okay. You could achieve your functionality as follows:
def fix_disagio=(number)
self[:fix_disagio] = number.replace_comma_with_dot
end
Your update is okay, except I would shy away from it myself as it creates bloat which doesn't need to be there.
I was looking for a way to set the attributes when you pull from the db, but then I realized that if you're having to set this each time you call the model, surely something will be wrong.
I would personally look at changing this at db level, failing that, you'd probably be able to use some sort of localization to determine whether you need a dot or comma.
There is a good answer here which advocates adding to the ActiveRecord::Base class:
class ActiveRecord::Base
def self.attr_localized(*fields)
fields.each do |field|
define_method("#{field}=") do |value|
self[field] = value.is_a?(String) ? value.to_delocalized_decimal : value
end
end
end
end
class Accounting < ActiveRecord::Base
attr_localized :share_ksk
end
Let's say I want to redefine a method in a model:
class Model < ActiveRecord::Base
attr_accesor :model_attr
def model_attr
'redefined'
end
end
When I access it directly, it works as it is supposed to, but when I call it from the view:
f.text_field :model_attr
It doesn't. But this still works:
f.text_field :model_attr, value: #model.model_attr
So I had to dig into Rails code:
def text_field(object_name, method, options = {})
Tags::TextField.new(object_name, method, self, options).render
end
to
class TextField < Base # :nodoc:
def render
options = #options.stringify_keys
options["size"] = options["maxlength"] unless options.key?("size")
options["type"] ||= field_type
options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file"
options["value"] &&= ERB::Util.html_escape(options["value"])
add_default_name_and_id(options)
tag("input", options)
end
and
def value_before_type_cast(object)
unless object.nil?
method_before_type_cast = #method_name + "_before_type_cast"
object.respond_to?(method_before_type_cast) ?
object.send(method_before_type_cast) :
value(object)
end
end
Okay, so it looks like text_field is not accessing the attribute directly, but rather appending _before_type_cast. I've read the documentation, but still do not understand why this is necessary for #text_field? I can do this, and it works:
class Model < ActiveRecord::Base
attr_accesor :model_atr
def model_attr
'redefined'
end
def model_attr_before_type_cast
model_attr
end
end
If I redefine both methods, can I get in trouble somehow in the future? Is there a better way to do this?
The reason for using *_before_type_cast is found on the description of this commit :
Added use of *_before_type_cast for all input and text fields. This is helpful for getting "100,000" back on a integer-based
+ validation where the value would normally be "100".
I have a bunch of Redis getters and setters like
def share_points= p
$redis.set("store:#{self.id}:points:share", p) if valid?
end
The thing is, ActiveRecord's validation doesn't stop the values from being inserted into redis. How do I go about doing this without adding if valid? on every setter? valid? calculates the validation every time it is called.
What about switching to an after_save callback approach, where you store all the fields that have been changed and just persist them all at once to redis.
Something like:
class Foo < ActiveRecord::Base
after_save :persist_to_redis
attr_accessor :redis_attributes
def share_points=(p)
#redis_attributes ||= {}
#redis_attributes[:share_points] = p
end
def something_else=(p)
#redis_attributes ||= {}
#redis_attributes[:something_else] = p
end
private
def redis_store_share_points(value)
$redis.set("store:#{self.id}:key", value)
end
def redis_store_something_else(value)
$redis.set("something_else:#{self.id}", value)
end
def persist_to_redis
$redis.multi do
#redis_attributes.each_pair do |key, value|
send("redis_store_#{key}".to_sym, value)
end
end
end
end
I think even this could be refactored and cleaned up but you get the idea.
If the model you're editing is derived from active_record, then you probably want to have a specific, wrapped call to redis that does the validation for you. e.g.
class Foo < ActiveRecord::Base
def rset(key,value)
$redis.set("store:#{self.id}:key", value) if valid?
end
def share_points=(p)
rset("points:share", p)
end
end
You could also put that in a module and include it.
If you're not deriving from AR:Base, you might want to come up with a more AR::Base-like structure using ActiveModel as described here: http://purevirtual.de/2010/04/url-shortener-with-redis-and-rails3/
I have a piece of code here that i really could use some help with refactoring. I need the different methods for adding relational data in a form in rails. The code is taken from http://railscasts.com/episodes/75-complex-forms-part-3, my problem is that i need to have the methods fro both the Material model and the Answer model. So i need the exact same code twice with "materials" replaced by "answers".
It seems this should be solved with some dynamic programming? But I have no experience at all with that.
How is this solved?
after_update :save_materials
after_update :save_answers
def new_material_attributes=(material_attributes)
material_attributes.each do |attributes|
materials.build(attributes)
end
end
def existing_material_attributes=(material_attributes)
materials.reject(&:new_record?).each do |material|
attributes = material_attributes[material.id.to_s]
if attributes
material.attributes = attributes
else
materials.delete(material)
end
end
end
def save_materials
materials.each do |material|
material.save(false)
end
end
You might also want to take a look at this site:
http://refactormycode.com/
If I understood you correctly, you want to have the same methods for answers as for materials, but duplicating the least code. The way to do this is by abstracting some private methods that will work for either answers or materials and call them with the appropriate model from the methods specific to those models. I've given a sample below. Note that I didn't do anything with the save_ methods because I felt they were short enough that abstracting them really didn't save much.
after_update :save_materials
after_update :save_answers
// Public methods
def new_material_attributes=(material_attributes)
self.new_with_attributes(materials, material_attributes)
end
def new_answer_attributes=(answer_attributes)
self.new_with_attributes(answers, answer_attributes)
end
def existing_material_attributes=(material_attributes)
self.existing_with_attributes(materials, material_attributes)
end
def existing_answer_attributes=(answer_attributes)
self.existing_with_attributes(answers, answer_attributes)
end
def save_materials
materials.each do |material|
material.save(false)
end
end
def save_answers
answers.each do |answer|
answer.save(false)
end
end
// Private methods
private
def new_with_atttributes(thing,attributes)
attributes.each do |attribute|
thing.build(attribute)
end
end
def existing_with_attributes=(things, attributes)
things.reject(&:new_record?).each do |thing|
attrs = attributes[thing.id.to_s]
if attrs
thing.attributes = attrs
else
things.delete(thing)
end
end
end