looking for gem that flags datetime fields - ruby-on-rails

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

Related

How to add errors before updating attributes?

I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end

Rails4 refactor Model make DRY

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

Recommended practice for passing current user to model

Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append

what implications have redefining *_before_type_cast in an activerecord model

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".

DRY up this model with virtual attributes

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

Resources