Implementation Value Object pattern for ActiveRecord attribute - ruby-on-rails

The task was allow user to view, create, edit records in different units, i.e. altitude in meters and feets, speed in m/s, knots, km/h, mi/h.
I've read a lot about Value Objects, composed_of and why we should not use it and use serialize instead and came with solution below.
It seems like complex solution for me.
Could you please point me what can I refactor and direction for it?
app/models/weather.rb
class Weather < ActiveRecord::Base
attr_accessor :altitude_unit, :wind_speed_unit
attr_reader :altitude_in_units, :wind_speed_in_units
belongs_to :weatherable, polymorphic: true
before_save :set_altitude, :set_wind_speed
validates_presence_of :actual_on, :wind_direction
validate :altitude_present?
validate :wind_speed_present?
validates_numericality_of :altitude, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :altitude_in_units, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_speed, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_speed_in_units, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_direction, greater_than_or_equal_to: 0, less_than: 360, allow_nil: true
serialize :altitude, Distance
serialize :wind_speed, Velocity
def wind_speed_in_units=(value)
#wind_speed_in_units = value_from_param(value)
end
def altitude_in_units=(value)
#altitude_in_units = value_from_param(value)
end
private
def value_from_param(value)
return nil if value.is_a?(String) && value.empty?
value
end
def altitude_present?
return if altitude.present? || altitude_in_units.present?
errors.add :altitude, :blank
end
def wind_speed_present?
return if wind_speed.present? || wind_speed_in_units.present?
errors.add :wind_speed, :blank
end
def set_altitude
return if altitude_in_units.blank? || altitude_unit.blank?
self.altitude = Distance.new(altitude_in_units, altitude_unit)
end
def set_wind_speed
return if wind_speed_in_units.blank? || wind_speed_unit.blank?
self.wind_speed = Velocity.new(wind_speed_in_units, wind_speed_unit)
end
end
app/model/distance.rb
class Distance < DelegateClass(BigDecimal)
FT_IN_M = 3.280839895
def self.load(distance)
new(distance) unless distance.nil?
end
def self.dump(obj)
obj.dump
end
def initialize(distance, unit = 'm')
value = convert_from(BigDecimal.new(distance), unit)
super(value)
end
def dump
#delegate_dc_obj
end
def convert_to(unit)
method = "to_#{unit}"
raise ArgumentError, "Unsupported unit #{unit}" unless respond_to? method
send method
end
def convert_from(val, unit)
method = "from_#{unit}"
raise ArgumentError, "Unsupported unit #{unit}" unless respond_to? method
send method, val
end
def to_m
#delegate_dc_obj
end
def to_ft
#delegate_dc_obj * FT_IN_M
end
def from_m(val)
val
end
def from_ft(val)
val / FT_IN_M
end
end
app/models/velocity.rb is almost the same as distance.

Related

undefined method `__metadata' for #<Participant:0x00000001076da378> with rails 6 / mongoid

I have the follow code that is working in rails 5. Updagrate to 6 I get the error undefined method `__metadata'.
Here's the problematic code
*
def nature
self.__metadata.key.to_s.singularize.to_sym #
end
*
Have try to use method but it doesn't return what it does in rails 5 / mongoid. Mongoid version is '~> 7.0'
Complete class code
# Participant model class definition
class Participant
include Mongoid::Document
include Mongoid::Timestamps
include DryValidation
field :address
field :identifier
field :name
field :birthdate, type: Date
field :sex
field :profession
field :phone
field :email
field :ownership_percentage
field :contribution_amount
field :category
field :group
field :registered_on, type: Date
field :retired, type: Boolean
field :retired_on, type: Date
field :committee
# Callbacks
before_save :generate_identifier
# Relations
embedded_in :book, inverse_of: :shareholders
embedded_in :book, inverse_of: :directors
embedded_in :book, inverse_of: :employees
embedded_in :book, inverse_of: :committee_members
embeds_many :participant_files
accepts_nested_attributes_for :participant_files, allow_destroy: true
#Validations
validates :name, presence: true
validates :email, allow_blank: true, format: { with: /\A\S+#\S+\.\S+\z/i }
validates :registered_on, presence: true, non_existent_date: true
validates :birthdate, non_existent_date: true
validates :retired_on, non_existent_date: true
validate :registered_on_date
def self.options_for(field_name)
case field_name.to_sym
when :category then [nil, :founders, :actives, :participants]
when :sex then [nil, :male, :female]
when :group then [nil, :legal, :accounting, :human_resources, :consumer, :employee,
:management_and_administration, :communication_and_marketing,
:ethic_and_gouvernance, :other]
else []
end
end
def self.ordered
# This should be as simple as .order_by(:retired_on.desc, :registered_on.asc)
# but the registered_on parameters is never ordered correctly so I had to do this ugly thing :(
self.all.sort_by{ |a| [ (a.retired_on ? a.retired_on.strftime('%Y%m%d') : 0), (a.registered_on ? a.registered_on.strftime('%Y%m%d') : 0) ].join }
end
def self.ordered_by_name
participants = self.active.sort_by{ |p| p.name.downcase }
participants += self.inactive.sort_by{ |p| p.name.downcase }
participants
end
def self.active
now = Time.now.strftime('%Y%m%d')
self.all.select do |a|
if a.registered_on
if a.retired_on
a.retired_on.strftime('%Y%m%d') >= now && a.registered_on.strftime('%Y%m%d') <= now
else
a.registered_on.strftime('%Y%m%d') <= now
end
end
end
end
def self.inactive
now = Time.now.strftime('%Y%m%d')
self.all.select do|a|
(a.retired_on && a.retired_on.strftime('%Y%m%d') < now) ||
(a.registered_on && a.registered_on.strftime('%Y%m%d') > now)
end
end
def book
self._parent
end
def committee_member?
self.nature == :committee_member
end
def director?
self.nature == :director
end
def employee?
self.nature == :employee
end
def nature
self.__metadata.key.to_s.singularize.to_sym #
end
def active?
!retired?
end
def retired?
self.retired_on && self.retired_on <= Time.zone.today
end
def shareholder?
self.nature == :shareholder
end
def securities
self.book.transactions.any_of({from: self.id}, {to: self.id}).asc(:transacted_on)
end
def save_files
self.participant_files.each do |pf|
pf.save
end
delete_objects_without_file
end
def has_shares?
book.share_categories.each do |sc|
return true if total_shares(sc) > 0
end
false
end
def total_shares(share_category)
total = 0
securities.each do |s|
if s.share_category == share_category
if s.nature == 'issued' or (s.nature == 'transfered' and self.id.to_s == s.to.to_s)
total += s.quantity if s.quantity
elsif s.nature == 'repurchased' or (s.nature == 'transfered' and self.id.to_s == s.from.to_s)
total -= s.quantity if s.quantity
end
end
end
total
end
def share_class_percentage(sc)
book.share_class_quantity(sc) > 0 ? self.total_shares(sc)/book.share_class_quantity(sc).to_f*100 : 0
end
def acceptance_documents
self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_accept'}
end
def resignation_documents
self.book.documents.select{|document| document.participant_id == self.id && document.nature == 'dir_resig'}
end
private
def existing_identifier?
participant_type = self.__metadata.key.to_sym
identifiers = book.send(participant_type).map{ |p| p.identifier if p.id != self.id }.compact
identifiers.include? self.identifier
end
def generate_identifier
self.identifier = self.name.parameterize if self.identifier.blank?
i = 1
while existing_identifier?
self.identifier = "#{self.identifier}-#{i}"
i += 1
end
end
def registered_on_date
unless registered_on.nil? || retired_on.nil?
if registered_on > retired_on
errors.add(:registered_on, I18n.t("mongoid.errors.models.participant.attributes.registered_on.greater_than_retired_on"))
end
end
end
def delete_objects_without_file
self.participant_files.each do |pf|
pf.delete if pf.pdf_file.file.nil?
end
end
end```

Can you help me understand why this boolean method is returning false?

I have a User model with an account_type attribute that is either "Student" or "Partner". I have created boolean methods in my User model to determine if a user record is either a student or partner (see below).
def student?
self.account_type == "Student"
end
def partner?
self.account_type == "Partner"
end
In rails console, when I set user equal to an instance of User that has a student account type and enter user.account_type == "Student", I get true but when I call user.student?, I get false. Is there an issue with how I've set up these methods? They seem pretty straight forward so I'm not following why true isn't returned for the record.
Console Output:
user = User.last
#<User id: 18, first_name: "gjalrgkj", last_name: "kgjrlgakjrl", email: "terajglrkj#gmail.com", password_digest: "$2a$10$WF.Rw3PzlWilH0X.Nbfxfe5aB18WW6J7Rt4SAKQEwI8...", remember_digest: nil, activation_digest: "$2a$10$/bXG4/nKCiiZHWailUPAmOZj7YhCjKhPm4lUW6nPC3N...", activated: nil, activated_at: nil, reset_digest: nil, reset_sent_at: nil, account_type: "Student", created_at: "2018-07-02 04:21:07", updated_at: "2018-07-02 04:21:07">
>> user.account_type
=> "Student"
>> user.account_type = "Student"
=> "Student"
>> user.student?
=> false
User Model:
class User < ApplicationRecord
has_one :personal_information
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
validates :first_name, presence: true, length: { maximum: 50 }
validates :last_name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
validates :account_type, presence: true
def User.new_token
SecureRandom.urlsafe_base64
end
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
def create_reset_digest
self.reset_token = User.new_token
update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
end
def authenticated?(attribute, token)
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
def forget
update_attribute(:remember_digest, nil)
end
def provide_age
now = Time.now.utc.to_date
if self.birthday.nil?
nil
else
self.age = now.year - self.birthday.year - ((now.month > self.birthday.month || (now.month == self.birthday.month && now.day >= self.birthday.day)) ? 0 : 1)
update_attribute(:age, self.age)
end
end
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
def activate
update_columns(activated: true, activated_at: Time.zone.now)
end
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
private
def downcase_email
self.email.downcase!
end
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
User Helper
def account_type
[
['Student'],
['Partner'],
['School Administrator'],
['Philanthropist']
]
end
def student?
self.account_type == "Student"
end
def partner?
self.account_type == "Partner"
end
end
Your 2 methods student? and partner? belong in the user model.
When you are doing user.student? it looks for student? method as a instance method in the user model.
self in helper is not the user instance it points to your helper module.
Hope this helps.
try to put this code into your User Model (not in Helper)
def student?
self.account_type == "Student"
end
def partner?
self.account_type == "Partner"
end
and this is a good idea to create a setter to set the account_type, example
def set_account_type(type)
self.account_type = type
end

shoulda-matchers fail when attribute saved in a model's callback

class StudentPiggyBank < ActiveRecord::Base
PERIODS = [['tydzień', :week], ['miesiąc', :month], ['trzy miesiące', :three_months]]
RATES_MULTIPLIERS = {week: 1, month: 1.5, three_months: 2}
INTEREST_RATE_PRECISION = 2
before_validation :set_interest_rate
validates :completion_date, presence: true
validates :balance, numericality: {greater_than_or_equal_to: 0,
message: I18n.t('errors.messages.negative_piggy_bank_balance')}
validates :interest_rate, numericality: {greater_than_or_equal_to: 0,
message: I18n.t('errors.messages.negative_interest_rate')}
def self.date_from_param(period_param)
case period_param
when 'week'
1.week.from_now
when 'month'
1.month.from_now
when 'three_months'
3.months.from_now
end
end
protected
def set_interest_rate
num_of_days = completion_date - Date.today
if num_of_days >= 90
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:three_months], INTEREST_RATE_PRECISION)
elsif num_of_days >= 30
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:month], INTEREST_RATE_PRECISION)
else
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:week], INTEREST_RATE_PRECISION)
end
end
end
This code works. However, when testing with shoulda-matchers
describe StudentPiggyBank do
it { should validate_numericality_of(:interest_rate).is_greater_than_or_equal_to(0) }
it { should validate_numericality_of(:balance).is_greater_than_or_equal_to(0) }
end
I get errors for the line num_of_days = completion_date - Date.today:
NoMethodError:
undefined method `-' for nil:NilClass
Why completion_date is nil?
Well, it will basically do a described_class.new, so you won't have a completion_date. You can fix it like this:
describe StudentPiggyBank do
context 'with a completion date' do
before { subject.completion_date = 7.days.from_now }
it { should validate_numericality_of(:interest_rate).is_greater_than_or_equal_to(0) }
end
end

Before Validation loop through self attributes for modification

I have created a simple before_validation:
before_validation :strip_tabs
def strip_tabs
end
In my class I want to loop through all my attributes and remove tabs from each value. Most posts I found on SO are people who want to set 1 attribute. But I want to edit all my values.
Question:
How can I loop all self attributes of a model and edit them.
Friend suggested this, but content_column_names does not exist:
self.content_column_names.each {|n| self[n] = self[n].squish}
UPDATE 1: More code:
class PersonalInfo
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
extend ActiveModel::Translation
extend ActiveModel::Callbacks
include Sappable
require 'ext/string'
attr_accessor \
:first_name, :middle_name, :last_name,:birthdate,:sex,
:telephone,:street,:house_number,:city,:postal_code,:country,
:e_mail, :nationality, :salutation, :com_lang
validates :e_mail, :email => {:strict_mode => true}
validate :validate_telephone_number
validate :age_is_min_17?
before_validation :strip_tabs
def strip_tabs
binding.remote_pry
end
def age_is_min_17?
birthdate_visible = PersonalField.not_hidden.find_by_name "BIRTHDATE"
if birthdate_visible && birthdate && birthdate > (Date.current - 17.years)
#errors.add(:birthdate, I18n.t("apply.errors.birthdate"))
end
end
def validate_telephone_number
telephone_visible = PersonalField.not_hidden.find_by_name "TELEPHONE"
telephone_current = telephone.dup
if telephone_visible && telephone_current && !telephone_current.empty?
if telephone_current[0] == '+' || telephone_current[0] == '0'
telephone_current[0] = ''
#errors.add(:telephone, I18n.t("apply.errors.telephone")) if !telephone_current.is_number?
else
#errors.add(:telephone, I18n.t("apply.errors.telephone"))
end
end
end
def initialize(hash)
simple_attributes = [:first_name, :middle_name, :last_name,:birthdate,:sex,
:telephone,:street,:house_number,:city,:postal_code,:country,
:e_mail, :nationality, :salutation, :com_lang]
simple_attributes.each do |attr|
set_attr_from_json(attr, hash)
end
set_attr_from_json(:birthdate, hash) {|date| Date.parse(date) rescue nil}
end
end
Update 2: Rails Version:
I'm using Rails '3.2.17'
You can do as following:
before_validation :strip_tabs
def strip_tabs
self.attributes.map do |column, value|
self[column] = value.squish.presence
end
end
But I think that .squish will not work on created_at, updated_at, id, ... Because they are not String!
def strip_tabs
self.attributes.map do |column, value|
self[column] = value.kind_of?(String) ? value.squish.presence : value
end
end
Since your class is not a Rails model (ActiveRecord::Base), you can do as following:
def strip_tabs
self.instance_variables.map do |attr|
value = self.instance_variable_get(attr)
value = value.squish if value.kind_of?(String)
self.instance_variable_set(attr, value)
end
end
This should work
def strip_tabs
self.attributes.each do |attr_name, attr_value|
modified_value = ... # calculate your modified value here
self.write_attribute attr_name, modified_value
end
end
Because it's not an ActiveRecord model you won't have attributes or column_names, but you already have an array of your attribute names in your initialize function. I would suggest making that into a constant so you can access it throughout the model:
class PersonalInfo
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
extend ActiveModel::Translation
extend ActiveModel::Callbacks
include Sappable
require 'ext/string'
SIMPLE_ATTRIBUTES = [:first_name, :middle_name, :last_name,:birthdate,:sex,
:telephone,:street,:house_number,:city,:postal_code,:country,
:e_mail, :nationality, :salutation, :com_lang]
attr_accessor *SIMPLE_ATTRIBUTES
before_validation :strip_tabs
def strip_tabs
SIMPLE_ATTRIBUTES.each{ |attr| self[attr] = self[attr].squish }
end
...
def initialize(hash)
SIMPLE_ATTRIBUTES.each do |attr|
set_attr_from_json(attr, hash)
end
set_attr_from_json(:birthdate, hash) {|date| Date.parse(date) rescue nil}
end
end

Is possible to use Rails ActiveModel::Validations per instance?

theres an excerpt of my code:
module Configuracao
extend self
class Key
include ActiveModel::Validations
attr_accessor :name, :type, :default, :validations, :group, :available_values
def initialize(params)
params.symbolize_keys!.assert_valid_keys(:name, :type, :default, :validations, :group, :available_values)
#group = params[:group]
#name = params[:name]
#type = params[:type]
#available_values = params[:available_values]
#default = params[:default]
#validations = params[:validations]
#in this way each validation is being added for all keys
Configuracao::Key.class_eval do
validates :value, params[:validations]
end
end
end
end
so for every instance key i will have a diferent validation passed in a hash, example:
Key.new( validations: { presence: true, numericality: true } )
Key.new( validations: { length: { maximum: 30 } } )
There's a way to do it?
well i found a solution, maybe not so elegant or best way to do, but it works
def initialize(params)
params.symbolize_keys!.assert_valid_keys(:name, :type, :default, :validations, :group, :available_values)
#group = params[:group]
#name = params[:name]
#type = params[:type]
#available_values = params[:available_values]
#default = params[:default]
##current_validations = nil
##current_validations = #validations = params[:validations]
class << self
validates :value, ##current_validations unless ##current_validations.blank?
end
end
now each time i instantiate a Key, the class will be modified only for that instance
Will this work?
...
validates :all_hash_validations_pass
...
def all_hash_validations_pass
...iterate through the hash here, and validate each of them
end
If not, you should be able to use a custom validator for more control.

Resources