In my application I have a user model with a has many relationship to a status_updates model.
Currently, my database only has 1 user and zero status_updates saved.
What is bizarre is that when I search for status_updates or for a relationship between these objects in consol, affirms that are nil status_updates in the database.
StatusUpdate.count
(0.2ms) SELECT COUNT(*) FROM "status_updates"
=> 0
StatusUpdate.any?
(0.1ms) SELECT COUNT(*) FROM "status_updates"
=> false
user.status_updates.count
(0.2ms) SELECT COUNT(*) FROM "status_updates" WHERE "status_updates"."user_id" = 1
=> 0
user.status_updates.any?
(0.2ms) SELECT COUNT(*) FROM "status_updates" WHERE "status_updates"."user_id" = 1
=> false
So, that's clear for me.
BUT, when in my application I write the following, a status_update object is returned!
def end_date
if self.status_updates.any? == false
return self.status_updates.any?
elsif self.status_updates.any? == true
return self.status_updates.first
end
end
This is the call in my view
current_user.end_date
And this is what is returned to the view:
#<StatusUpdate:0x007fa99765d6f8>
And if I change the call in the view to this:
current_user.status_updates.first
=> <StatusUpdate:0x007fa99740b5f8>
But, if I call this:
current_user.status_updates.count
=> 0
current_user.status_updates.any?
=> true
In the view when I just use current_user.status_updates it returns
[#<StatusUpdate id: nil, created_at: nil, updated_at: nil, user_id: 1, current_weight: 0.0, current_bf_pct: 0.0, current_lbm: 0.0, current_fat_weight: 0.0, change_in_weight: nil, change_in_bf_pct: nil, change_in_lbm: nil, change_in_fat_weight: nil, total_weight_change: nil, total_bf_pct_change: nil, total_lbm_change: nil, total_fat_change: nil>]
What's going on here?!
User Model relationship
has_many :status_updates, dependent: :destroy
Status Update Model Relationship
belongs_to :user
Status Update Model
class StatusUpdate < ActiveRecord::Base
belongs_to :user
after_initialize :default_values
before_save :sanitize
attr_accessible :current_weight,
:current_bf_pct,
:current_lbm,
:current_fat_weight,
:change_in_weight,
:change_in_bf_pct,
:change_in_lbm,
:change_in_fat_weight,
:total_weight_change,
:total_bf_pct_change,
:total_lbm_change,
:total_fat_change,
:created_at
validates :user_id, presence: true
validates :current_bf_pct, presence: true, numericality: true, length: { minimum: 2, maximum:5 }
validates :current_weight, presence: true, numericality: true, length: { minimum: 2, maximum:5 }
validates :current_lbm, presence: true
validates :current_fat_weight, presence: true
def sanitize
if self.current_bf_pct >= 0.5
self.current_bf_pct /= 100
if self.current_bf_pct <= 0.04
self.current_fb_pct *= 100
end
end
self.current_fat_weight = self.current_weight * self.current_bf_pct
self.current_lbm = self.current_weight - self.current_fat_weight
end
def default_values
if self.created_at == nil
self.current_bf_pct = 0.20
self.current_weight = 0
self.current_lbm = 0
self.current_fat_weight = 0
self.change_in_weight = 0
self.change_in_bf_pct = 0
self.change_in_lbm = 0
self.change_in_fat_weight = 0
self.total_weight_change = 0
self.total_bf_pct_change = 0
self.total_lbm_change = 0
self.total_fat_change = 0
end
end
def previous_status_update
previous_status_update = user.status_updates.where( "created_at < ? ", self.created_at ).first
if previous_status_update == nil
return self
else
previous_status_update
end
end
default_scope order: 'status_updates.created_at DESC'
end
User Model:
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
before_create :sanitize
has_many :status_updates, dependent: :destroy
has_many :meals, dependent: :destroy
has_many :custom_foods, dependent: :destroy
has_many :meal_foods, through: :meals
# after_initialize :default_values
attr_accessor :user_password, :user_password_confirmation, :current_password
attr_accessible :email,
:password,
:password_confirmation,
:current_password,
:goal,
:measurement,
:bmr_formula,
:fat_factor,
:protein_factor,
:remember_me,
:deficit_amnt,
:target_bf_pct,
:activity_factor,
:current_password
validates :email, presence: true
validates :target_bf_pct, presence: true, on: :update, length: { minimum: 3, maximum: 4 }
validates :activity_factor, presence: true, on: :update
validates :deficit_amnt, presence: true, on: :update
validates :fat_factor, presence: true, on: :update
validates :protein_factor, presence: true, on: :update
def new?
self.created_at <= 1.minutes.ago.to_date ? true : false
end
def sanitize
#inputs
self.activity_factor = 1.3
self.deficit_amnt = 1
self.target_bf_pct = 10
self.fat_factor = 0.45
self.protein_factor = 1
end
def end_date
if self.status_updates.any? == false
#Time.now
self.status_updates.any?
elsif self.status_updates.any? == true
#(self.start_date + self.weeks_to_goal.to_i.weeks).strftime("%m/%d/%Y")
self.status_updates
end
end
def start_date
if self.status_updates.any? == true
self.status_updates.first.created_at
end
end
def daily_caloric_deficit
self.tdee.to_d - self.daily_intake.to_d
end
def current_fat_weight
BigDecimal(self.latest_status_update.current_fat_weight, 4)
end
def current_lbm
BigDecimal(self.latest_status_update.current_lbm, 4)
end
def current_bf_pct
BigDecimal(self.latest_status_update.current_bf_pct * 100, 4)
end
def current_weight
BigDecimal(self.latest_status_update.current_weight, 4)
end
def total_weight
self.latest_status_update.current_weight
end
# def lbm
# self.latest_status_updates.current_lbm
# end
def recent_weight_change
BigDecimal(self.latest_status_update.current_weight - self.latest_status_update.previous_status_update.current_weight, 2)
end
def recent_lbm_change
BigDecimal(self.latest_status_update.current_lbm - self.latest_status_update.previous_status_update.current_lbm, 2)
end
def recent_fat_change
BigDecimal(self.latest_status_update.current_fat_weight - self.latest_status_update.previous_status_update.current_fat_weight, 3)
end
def total_lbm_change
BigDecimal(self.latest_status_update.current_lbm - self.oldest_status_update.current_lbm, 3)
end
def total_fat_change
BigDecimal(self.latest_status_update.current_fat_weight - self.oldest_status_update.current_fat_weight, 3)
end
def total_weight_change
BigDecimal(self.latest_status_update.current_weight - self.oldest_status_update.current_weight, 3)
end
def last_date
self.status_updates.last.created_at.strftime("%m/%d/%Y")
end
def beginning_date
self.status_updates.first.created_at.strftime("%m/%d/%Y")
end
def latest_status_update
self.status_updates.first
end
def oldest_status_update
self.status_updates.last
end
def bmr
cur_lbm = self.current_lbm
cur_lbm *= 0.45
'%.2f' % (370 + (21.6 * cur_lbm.to_d))
end
def target_weight
tar_bf_pct = self.target_bf_pct /= 100
'%.2f' % ((self.total_weight * tar_bf_pct)+ self.current_lbm)
end
def fat_to_burn
'%.2f' % (self.total_weight.to_d - self.target_weight.to_d)
end
def tdee
'%.2f' % (self.bmr.to_d * self.activity_factor.to_d)
end
def deficit_pct
daily_cal_def = ((self.deficit_amnt.to_f * 3500)/7)
(daily_cal_def.to_d/self.tdee.to_d)
end
def daily_calorie_burn
'%.2f' % (self.tdee.to_d * self.deficit_pct.to_d)
end
def weekly_calorie_burn_rate
'%.2f' % (self.daily_calorie_burn.to_d*7)
end
def weeks_to_goal
'%.2f' % (self.fat_to_burn.to_d*3500/self.weekly_calorie_burn_rate.to_d)
end
def daily_intake
'%.2f' % (self.tdee.to_d - self.daily_calorie_burn.to_d)
end
def total_grams_of(macro)
self.meal_foods.map(¯o).inject(:+)
end
def pct_fat_satisfied
#how much of a macro is needed?
fat_needed = self.fat_factor * self.current_lbm
#how much is in the meal?
fat_provided = self.total_grams_of(:fat)
#percent needed
pct_fulfilled = fat_provided.to_f/fat_needed.to_f
BigDecimal(pct_fulfilled, 2)*100
end
def pct_protein_satisfied
#how much protien is needed?
protein_needed = self.protein_factor * self.current_lbm
#how much protien is provided?
protein_provided = total_grams_of(:protien)
#pct of protien satisfied?
pct_fulfilled = protein_provided.to_f/protein_needed.to_f
BigDecimal(pct_fulfilled, 2)*100
end
def pct_carbs_satisfied
#how many carbs are needed?
cals_required = self.tdee.to_f - (self.tdee.to_f * self.deficit_pct.to_f)
fat_cals = total_grams_of(:fat) * 9
protien_cals = total_grams_of(:protien) * 4
#how many carbs are provided?
cals_provided = fat_cals + protien_cals
cals_balance = cals_required - cals_provided
carbs_needed = cals_balance/4
carbs_provided = total_grams_of(:carbs)
BigDecimal(carbs_provided / carbs_needed, 2) * 100
end
end
I've tried resetting my database and the same issue comes up.
To recap:
It seems that a status_update is being initialized, and this initialized version of the object is what is available to the view. All the attributes are the initialized ones, which is why it doesn't have an id, or a timestamp for when it was created... because it never was created.
however when I try to access it from console I realized that it's giving me 'nil' and all the predictable values because in console it's looking for an already created and object relationship in the relationship.
So the more precise question here is the view returning the initialized version of the object instead of what console returns?
When the app hits a method within the user model it throws an error because the status_update doesn't exist when it calls something like
self.status_updates.first
You're probably doing
#status_update = user.status_updates.build
in your controller - this is pretty common place so that you can then put a form on the page that allows the user to submit a new status update.
This object is unsaved, so
user.status_updates.count
which is guaranteed to hit the database (and run select count(*) ...) doesn't count it. It is however in the in memory cache of the status updates association for that user and so user.status_updates.any? returns true.
You might also note that it's extremely unidiomatic to compare the return value of predicate methods with true or false. It reads funny and methods like this may return a value with equivalent truthiness other than true or false (for example nil instead of false).
This line:
#<StatusUpdate id: nil, created_at: nil, updated_at: nil, user_id: 1, current_weight: 0.0, current_bf_pct: 0.0, current_lbm: 0.0, current_fat_weight: 0.0, change_in_weight: nil, change_in_bf_pct: nil, change_in_lbm: nil, change_in_fat_weight: nil, total_weight_change: nil, total_bf_pct_change: nil, total_lbm_change: nil, total_fat_change: nil>]
The id: nil indicates that this model hasn't been saved yet. Models only get an id once you save them. The fact that it has user_id: 1 indicates that you probably created this through the user, as Frederick indicated. So somewhere you're creating this instance and not saving it.
PS, it's broadly considered poor ruby form to check if booleans are false or true. e.g.
don't do this:
self.status_updates.any? == false
do
self.status_updates.any?
which is true or false anyway, then just use if and unless as your checks. Better still, self is assumed, so in your case, you can just do:
status_updates.any?
PPS active_record defines a new? method for exactly this use case. A record that has not yet been saved is new and so new? will return true. I strongly discourage you from overwriting this method, or anything else defined by active_record.
Related
I am making a custom dashboard for a school application that requires me to calculate some KPIs, the way am doing it right now is calling several class methods from the Opportunity class in the dashboard/index action from the controller, and storing each method result in a variable that is going to be used in a tile. So each variable is a different tile of the dashboard.
The methods belong to the Opportunity class shown below:
class Opportunity < ApplicationRecord
belongs_to :organization
belongs_to :opportunity_status
has_many :tasks, dependent: :destroy
has_many :opportunity_status_logs, dependent: :destroy
before_create :create_status_log
after_update :create_status_log, if: :opportunity_status_id_changed?
validates :name, :description, :revenue, :opportunity_status_id, :closing_date, presence: true
validates :name, :description, format: { with: /\A[[:alpha:]a-zA-Z0-9ñÑ#()\-.,\s]+\z/ }
validates :revenue, numericality: true
validates :closing_date, inclusion: { in: (Time.zone.today..Time.zone.today+5.years) }
def create_status_log
OpportunityStatusLog.create(opportunity_id: self.id, opportunity_status_id: self.opportunity_status_id)
end
def status_updated_by(user)
#status_log = self.opportunity_status_logs.last
#status_log.user_id = user.id
#status_log.save!
end
def self.actives
self.where.not(opportunity_status_id: [11,12])
end
def self.won
self.where(opportunity_status_id: 11)
end
def self.lost
self.where(opportunity_status_id: 12)
end
def self.average_revenue
self.won.average(:revenue)
end
def self.minimum_revenue
self.won.minimum(:revenue)
end
def self.maximum_revenue
self.won.maximum(:revenue)
end
def self.filter_by_status(status_id)
self.where(opportunity_status: status_id)
end
def self.relative_percentage(item_amount, total)
item_amount * 100 / total
end
def self.conversion_rate
self.won.count / self.all.count.to_f * 100
end
def self.potential_revenue
self.actives.sum(:revenue)
end
end
and this is the way the controller is structured:
class DashboardController < ApplicationController
before_action :authenticate_user!
def index
#opportunities = Opportunity.includes(:opportunity_status).all
#actives = Opportunity.actives.count
#won = Opportunity.won.count
#lost = Opportunity.lost.count
#average_revenue = Opportunity.average_revenue
#minimum_revenue = Opportunity.minimum_revenue
#maximum_revenue = Opportunity.maximum_revenue
#in_appreciation = Opportunity.filter_by_status(6).count
#in_value_proposition = Opportunity.filter_by_status(7).count
#in_management_analysis = Opportunity.filter_by_status(8).count
#in_proposal = Opportunity.filter_by_status(9).count
#in_review = Opportunity.filter_by_status(10).count
#app_perc = Opportunity.relative_percentage(#in_appreciation, #opportunities.count)
#vp_perc = Opportunity.relative_percentage(#in_value_proposition, #opportunities.count)
#ma_perc = Opportunity.relative_percentage(#in_management_analysis, #opportunities.count)
#pp_perc = Opportunity.relative_percentage(#in_proposal, #opportunities.count)
#rw_perc = Opportunity.relative_percentage(#in_review, #opportunities.count)
#conversion_rate = '%.2f' % [Opportunity.conversion_rate]
#potential_revenue = Opportunity.potential_revenue
end
end
Even though it works as expected, it looks like the controller is a bit too fat and I feel that with the current approach if the app scales it will be very slow due to the amount of queries that are being done. So, is there a way to refactor this in order to optimize the data retrieval and the displaying of the KPIs?
Thanks in advance
You can try implementing Facade Pattern in Rails. It will make your controller skinny but on the query part you will still be needing to make those queries, there is no way to skip that.
You can try to optimize db by adding index and creating sql views in future when you get performance lag, at this time it will be like premature optimization
I Have a Model
module Interspire
class InterspireLead < ActiveRecord::Base
before_create :update_contactable
def update_contactable
self.contactable = false #contactable datatype is boolean
end
end
end
But when i create a object.
a = Interspire::InterspireLead.create(:email => "abc#gmail.com")
a.valid?
#=> true
a.errors.full_messages
#=>[]
a.save
#=> ROLLBACK
How to fix this erorrs?
Return true in your update_contactable method:
def update_contactable
self.contactable = false #contactable datatype is boolean
true
end
You need to use another callback
before_save :update_contactable if: new_record?
not
before_create
Your callback needs to be private. (Edit: This is wrong. It is not a requirement!)
An example from the docs:
class Subscription < ActiveRecord::Base
before_create :record_signup
private
def record_signup
self.signed_up_on = Date.today
end
end
false stops object from being created without any error
as zhaqiang mentioned:
def update_contactable
self.contactable = false
1 == 1
end
I recived Task:
Add a method to the Profile class, called get all profiles, which:
• accepts a min and max for the birth year
• issues a BETWEEN SQL clause in a where clause to locate Profiles with birth years that are between min
year and max year
• defends itself against SQL injection when applying the parameters to the SQL clauses
• returns a collection of Profiles in ASC birth year order
Profile Class:
class Profile < ActiveRecord::Base
belongs_to :user
validates :first_name, presence: true
validates :last_name, presence: true
validates :gender, inclusion: %w(male female)
validate :first_and_last
validate :male_Sue
def first_and_last
if (first_name.nil? and last_name.nil?)
errors.add(:base, "Specify a first or a last.")
end
end
def male_Sue
if (first_name == "Sue" and gender == "male")
errors.add(:base, "we are prevent male by name Sue.")
end
end
def get_all_profiles
end
end
How can complete this task? explanation appriciating...
I should pass this rspec test:
context "rq14" do
context "Profile has a get_all_profiles method" do
subject(:profile) { Profile.new }
it { is_expected.to respond_to(:get_all_profiles) }
end
it "will return a list of profiles between requested birth years in ascending order" do
user = User.create(:username=>"testUser", :password_digest=>"xxxx")
startYear = 1960
endYear = 2000
testYear = 1985
testCount = 0
(0..20).each do |i|
birthYear = startYear + rand(0..(endYear - startYear))
if (birthYear <= testYear)
testCount = testCount + 1
end
profile = Profile.create(:user_id=>user.id, :gender=>"male", :birth_year=>birthYear, :first_name=>"User #{i}", :last_name=>"Smith#{i}")
end
profileGroup = Profile.new.get_all_profiles(startYear, testYear)
expect(profileGroup.length).to be(testCount)
# test that results are sorted by birthyear and are ascending
year = startYear
profileGroup.each do |t|
expect(t.birth_year).to be >= year
year = t.birth_year
end
end
end
end
Thanks, Michael.
It's the answer:
def get_all_profiles(start_year, end_year)
Profile.where(:birth_year => start_year..end_year).order(:birth_year )
end
It's more Rails style to use a scope:
scope :all_profiles -> (date_from, date_to) { where birth_date: date_from..date_to }
I have this variable opinions I want to store as an instance variable in my model... am I right in assuming I will need to add a column for it or else be re-calculating it constantly?
My other question is what is the syntax to store into a column variable instead of just a local one?
Thanks for the help, code below:
# == Schema Information
#
# Table name: simulations
#
# id :integer not null, primary key
# x_size :integer
# y_size :integer
# verdict :string
# arrangement :string
# user_id :integer
#
class Simulation < ActiveRecord::Base
belongs_to :user
serialize :arrangement, Array
validates :user_id, presence: true
validates :x_size, :y_size, presence: true, :numericality => {:only_integer => true}
validates_numericality_of :x_size, :y_size, :greater_than => 0
def self.keys
[:soft, :hard, :none]
end
def generate_arrangement
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#arrangement = Array.new(y_size) { Array.new(x_size) }
#arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
rand_opinion = Simulation.keys[rand(0..2)]
#arrangement[y_index][x_index] = rand_opinion
#opinions[rand_opinion] += 1
end
end
end
def verdict
if #opinions[:hard] > #opinions[:soft]
:hard
elsif #opinions[:soft] > #opinions[:hard]
:soft
else
:push
end
end
def state
#arrangement
end
def next
new_arrangement = Array.new(#arrangement.size) { |array| array = Array.new(#arrangement.first.size) }
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#seating_arrangement.each_with_index do |array, y_index|
array.each_with_index do |opinion, x_index|
new_arrangement[y_index][x_index] = update_opinion_for x_index, y_index
#opinions[new_arrangement[y_index][x_index]] += 1
end
end
#arrangement = new_arrangement
end
private
def in_array_range?(x, y)
((x >= 0) and (y >= 0) and (x < #arrangement[0].size) and (y < #arrangement.size))
end
def update_opinion_for(x, y)
local_opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
for y_pos in (y-1)..(y+1)
for x_pos in (x-1)..(x+1)
if in_array_range? x_pos, y_pos and not(x == x_pos and y == y_pos)
local_opinions[#arrangement[y_pos][x_pos]] += 1
end
end
end
opinion = #arrangement[y][x]
opinionated_neighbours_count = local_opinions[:hard] + local_opinions[:soft]
if (opinion != :none) and (opinionated_neighbours_count < 2 or opinionated_neighbours_count > 3)
opinion = :none
elsif opinion == :none and opinionated_neighbours_count == 3
if local_opinions[:hard] > local_opinions[:soft]
opinion = :hard
elsif local_opinions[:soft] > local_opinions[:hard]
opinion = :soft
end
end
opinion
end
end
ActiveRecord analyzes the database tables and creates setter and getter methods with metaprogramming.
So you would create a database column with a migration:
rails g migration AddOpinionToSimulation opinion:hash
Note that not all databases support storing a hash or a similar key/value data type in a column. Postgres does. If you need to use another database such MySQL you should consider using a relation instead (storing the data in another table).
Then when you access simulation.opinion it will automatically get the database column value (if the record is persisted).
Since ActiveRecord creates a setter and getter you can access your property from within the Model as:
class Simulation < ActiveRecord::Base
# ...
def an_example_method
self.opinions # getter method
# since self is the implied receiver you can simply do
opinions
opinions = {foo: "bar"} # setter method.
end
end
The same applies when using the plain ruby attr_accessor, attr_reader and attr_writer macros.
When you assign to an attribute backed by a database column ActiveRecord marks the attribute as dirty and will include it when you save the record.
ActiveRecord has a few methods to directly update attributes: update, update_attributes and update_attribute. There are differences in the call signature and how they handle callbacks.
you can add a method like
def opinions
#opinions ||= Hash[ Simulation.keys.map { |key| [key, 0] }
end
this will cache the operation into the variable #opinions
i would also add a method like
def arrangement
#arrangement ||= Array.new(y_size) { Array.new(x_size) }
end
def rand_opinion
Simulation.keys[rand(0..2)]
end
and then replace the variables with your methods
def generate_arrangement
arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
arrangement[y_index][x_index] = rand_opinion
opinions[rand_opinion] += 1
end
end
end
now your opinions and your arrangement will be cached and the code looks better. you didn't have to add a new column in you table
you now hat to replace the #opinions variable with your opinions method
I have a validation method that has to verify values assigned in another method, how can i get it to recognise those values before validation? the pay_must_be_same_to_amount method needs some values from the create_items_from_readings method
class Invoice < ActiveRecord::Base
attr_accessible :approved_by, :due_date, :invoice_date, :reading_ids, :terms, :customer_id, :customer, :status, :reference_no, :payment_method, :amount, :payment_date
has_many :invoice_items, :dependent => :destroy
belongs_to :customer, :inverse_of => :invoices
validate :pay_must_be_same_to_amount
def create_item_from_readings
item = invoice_items.new
item.rate = customer.unit_cost
readings_in_this_period = customer.unbilled_readings.where('date_of_reading <= ?', invoice_date).order(:date_of_reading)
return nil if readings_in_this_period.empty?
self.reading_ids = readings_in_this_period.collect(&:id).join(',')
total_units = 0
readings_in_this_period.each do |reading|
total_units = total_units + reading.units_used1 + reading.units_used2 + reading.units_used3
end
item.amount = total_units * customer.unit_cost * customer.ct_ratio
item.tax_amount = (item.amount * Settings.vat) if customer.pays_vat
invoice_from_reading = readings_in_this_period.first.previous_reading
invoice_from_reading ||= readings_in_this_period.first
invoice_to_reading = readings_in_this_period.last
#Select Item description based on Phase type
if customer.phase_type == 'Single Phase'
item.description = "Electricity used from #{invoice_from_reading.date_of_reading.strftime('%d/%m/%Y')} with readings #{invoice_from_reading.reading1} to #{invoice_to_reading.date_of_reading.strftime('%d/%m/%Y')} with reading #{invoice_to_reading.reading1} - #{total_units.to_i} total units"
else
item.description = "Electricity used from #{invoice_from_reading.date_of_reading.strftime('%d/%m/%Y')} with readings, R1: #{invoice_from_reading.reading1}, R2: #{invoice_from_reading.reading2}, R3: #{invoice_from_reading.reading3} to #{invoice_to_reading.date_of_reading.strftime('%d/%m/%Y')} with readings, R1: #{invoice_to_reading.reading1}, R2:#{invoice_to_reading.reading2}, R3: # {invoice_to_reading.reading3}- #{total_units.to_i} total units"
end
end
end
and the validation method is below, it needs to compare the item.amount above to the amount in the class Invoice
def pay_must_be_same_to_amount
if item.amount < self.amount && item.amount != self.amount
self.errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end
end
A few comments: create_item_from_readings is way too complicated. I can't tell what it's supposed to do, but if you run it, I believe it will return a string (one of the two from the last if statement).
If all you need to do is compare item.amount to the invoice amount attribute, that's simple. You can use your validation method almost as you've written it, plus a few other methods as needed.
def item_amount
total_units * customer.unit_cost * customer.ct_ratio
end
def total_units
readings_in_this_period.inject(0) {|total,r| total + r.units_used1 + r.units_used2 + r.units_used3 }
end
def pay_must_be_same_to_amount
if item_amount != amount
errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end
The code for both of those supplementary methods is simply modified code from your longer method.
A good rule of practice is that if a method is longer than one line, and you can't tell what it's for by glancing at it, it's too long (this isn't always true, but it's worth considering for complicated methods).
The solution to the question is
def pay_must_be_same_to_amount
sum = 0
self.invoice_items.each do |invoice_item|
sum = sum + invoice_item.amount
end
if sum != self.amount
self.errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end