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
Related
I want some of my model attributes to predefined dynamically. I have various models.And now I want My Bill model to create objects using other model instances.
Models :
leave.rb # belongs_to :residents
resident.rb # has_many:leaves,has_many:bills,has_one:account
bill.rb # belongs_to:residents
rate_card.rb # belongs_to:hostel
account.rb # belongs_to:resident
hostel.rb
now here is my bills controller create method :
def create
#bill = Resident.all.each { |resident| resident.bills.create(?) }
if #bill.save
flash[:success]="Bills successfully generated"
else
flash[:danger]="Something went wrong please try again !"
end
end
I want to build bill using all of the models eg:
resident.bills.create(is_date:using form,to_date:using form,expiry_date:using form,amount:30*(resident.rate_card.diet)+resident.rate_card.charge1+resident.rate_card.charge2)+(resident.account.leaves)*10+resident.account.fine)
///////Is this possible ?
And how to use strong params here ?
Pls help me out thxx..
I think the Rails way for this logic you want is with callbacks if you want calculated attributes either on create, update or delete, meaning attributes that depend on other models. For instance:
class Bill < ActiveRecord::Base
...
before_create :set_amount
...
protected
def set_amount
self.amount = 30 * self.resident.rate_card.diet + self.resident.rate_card.charge1 + self.resident.rate_card.charge2 + (self.resident.account.leaves) * 10 + self.resident.account.fine
end
end
If you want this logic to be used when updating the record also, then you should use before_save instead of before_create.
After you do this, you should accept the usual params (strong) of Bill model, as in:
def bill_params
params.require(:bill).permit(:is_date, :to_date, :expiry_date)
end
So your create call would be like:
resident.bills.create(bill_params)
Also, be wary of your create action, you should probably create a method either on your Bill or your Resident model that uses transactions to create all bills at the same time because you probably want either every bill created or none. This way you won't have the Resident.all.each logic in your BillsController.
create takes a hash, you can:
create_params = { amount: 30*(resident.rate_card.diet) }
create_params[:some_field] = params[:some_field]
# and so on
resident.bills.create(create_params)
or:
obj = resident.bills.build(your_strong_parameters_as_usual)
obj.amount = # that calculation
obj.save!
I'm confused at your syntax of your controller. #bill is being set to the value of a loop, which feels off. Each loops return the enumerable you cycle through, so you'll end up with #bill = Resident.all with some bills being created on the side.
What your controller really wants to know is, did my many new bills save correctly?
This seems like a perfect place to use a ruby object (or, colloquially, a Plain Old Ruby Object, as opposed to an ActiveRecord object) to encapsulate the specifics of this bill-generator.
If I'm reading this right, it appears that you are generating many bills at once, based on form-inputted data like:
is_date
to_date
expiry_date
...as well as some data about each individual resident.
Here's the model I'd create:
app/models/bill_generator.rb
class BillGenerator
include ActiveModel::Model
# This lets you do validations
attr_accessor :is_date, :to_date, :expiry_date
# This lets your form builder see these attributes when you go form.input
attr_accessor :bills
# ...for the bills we'll be generating in a sec
validates_presence_of :is_date, :to_date, :expiry_date
# You can do other validations here. Just an example.
validate :bills_are_valid?
def initialize(attributes = {})
super # This calls the Active Model initializer
build_new_bills # Called as soon as you do BillGenerator.new
end
def build_new_bills
#bills = []
Resident.all.each do |r|
#bills << r.bills.build(
# Your logic goes here. Not sure what goes into a bill-building...
# Note that I'm building (which means not-yet-saved), not creating
)
end
def save
if valid?
#bills.each { |b| b.save }
true
else
false
end
end
private
def bills_are_valid?
bill_validity = true
#bills.each do |b|
bill_validity = false unless b.valid?
end
bill_validity
end
end
Why all this mess? Because in your controller you can do...
app/controllers/bill_controller.rb
def create
#bill_generator = BillGenerator.new(bill_generator_params)
if #bill_generator.save?
# Redirect to somewhere with a flash?
else
# Re-render the form with a flash?
end
end
def bill_generator_params
params.require(:bill_generator).permit(:is_date, :to_date, :expiry_date)
# No extra garbage. No insecurity by letting all kinds of crud through!
end
...like a BillGenerator is any old object. Did it save? Great. It didn't, show the form again.
Now, my BillGenerator won't just be copy-and-paste. Your 'build_new_bills' probably will have some of that math you alluded to, which I'll leave to you.
Let me know what you think!
you can do it by using params.permit! as this allows any parameters to be passed. here's an example:
def create
...
#bill = Resident.all.each { |resident| resident.bills.create(any_params) }
end
private
def any_params
params.permit!
end
be careful with this of course, as you are opening this up to potential exploits.
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".
In my form I have a virtual attributes that allows me to accept mixed numbers (e.g. 38 1/2) and convert them to decimals. I also have some validations (I'm not sure I'm handling this right) that throws an error if something explodes.
class Client < ActiveRecord::Base
attr_accessible :mixed_chest
attr_writer :mixed_chest
before_save :save_mixed_chest
validate :check_mixed_chest
def mixed_chest
#mixed_chest || chest
end
def save_mixed_chest
if #mixed_chest.present?
self.chest = mixed_to_decimal(#mixed_chest)
else
self.chest = ""
end
end
def check_mixed_chest
if #mixed_chest.present? && mixed_to_decimal(#mixed_chest).nil?
errors.add :mixed_chest, "Invalid format. Try 38.5 or 38 1/2"
end
rescue ArgumentError
errors.add :mixed_chest, "Invalid format. Try 38.5 or 38 1/2"
end
private
def mixed_to_decimal(value)
value.split.map{|r| Rational(r)}.inject(:+).to_d
end
end
However, I'd like to add another column, wingspan, which would have the virtual attribute :mixed_wingspan, but I'm not sure how to abstract this to reuse it—I will be using the same conversion/validation for several dozen inputs.
Ideally I'd like to use something like accept_mixed :chest, :wingspan ... and it would take care of the custom getters, setters, validations, etc.
EDIT:
I'm attempting to recreate the functionality with metaprogramming, but I'm struggling in a few places:
def self.mixed_number(*attributes)
attributes.each do |attribute|
define_method("mixed_#{attribute}") do
"#mixed_#{attribute}" || attribute
end
end
end
mixed_number :chest
This sets chest to "#mixed_chest"! I'm trying to get the instance variable #mixed_chest like I have above.
You're going to want a custom validator
Something like
class MixedNumberValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value.present? && MixedNumber.new(value).to_d.nil?
record.errors[attribute] << (options[:message] || "Invalid format. Try 38.5 or 38 1/2")
end
end
end
Then you can do
validates :chest, mixed_number: true
Note that I'd extract the mixed_to_decimal stuff into a separate class
class MixedNumber
def initialize(value)
#value = value
end
def to_s
#value
end
def to_d
return nil if #value.blank?
#value.split.map{|r| Rational(r)}.inject(:+).to_d
rescue ArgumentError
nil
end
end
and that this definition lets you drop the if statement in the save_chest method.
Now you just need to do some metaprogramming to get everything going, as I suggested in my answer to your other question. You'll basically want something like
def self.mixed_number(*attributes)
attributes.each do |attribute|
define_method("mixed_#{attribute}") do
instance_variable_get("#mixed_#{attribute}") || send(attribute)
end
attr_writer "mixed_#{attribute}"
define_method("save_mixed_#{attribute}") do
# exercise for the reader ;)
end
before_save "save_#{attribute}"
validates "mixed_#{attribute}", mixed_number: true
end
end
mixed_number :chest, :waist, :etc
I got datetime filds order_confirmed_at and completion_confirmed_at
class CoolModel < ActiveRecord::Base
attr_accessible :order_confirmed, completion_confirmed
def order_confirmed
order_confirmed_at.present?
end
def order_confirmed=(state)
if state and order_confirmed_at.blank?
self.order_confirmed_at = Time.now
end
order_confirmed_at.present?
end
def completion_confirmed
completion_confirmed_at.present?
end
def completion_confirmed=(state)
if state and completion_confirmed_at.blank?
self.completion_confirmed_at = Time.now
end
completion_confirmed_at.present?
end
end
...so in my view I can just check checkbox that order was confirmed and completed
Thing is: not only this is duplication, but this stuff obviously looks pretty standard. So in matter saving me time writing gem: is there rails gem/engine doing this (or maybe part of Rails I'm not aware of) ??
class CoolModel < ActiveRecord::Base
#something like this
acts_even_coller_on :order_confirmed_at, :completion_confirmed_at
end
You could just override the setter method.
def confirmed_at=(value)
super value == "true" ? confirmed_at || Time.now : nil
end
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.