Between SQL ActiveRecord - ruby-on-rails

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 }

Related

How to test with RSpec on Rails a past date if I cant create an object with past date being prohibited inside the model?

I have a model Appointment that prohibit the object to be created using a past date or update if the field day is in the past.
class Appointment < ApplicationRecord
belongs_to :user
...
validate :not_past, on: [:create, :update]
private
...
def not_past
if day.past?
errors.add(:day, '...')
end
end
end
But I need to make a test file using RSpec to test if it really cannot be edited if the field day is a past date.
require 'rails_helper'
RSpec.describe Appointment, type: :model do
...
it 'Cannot be edited if the date has past' do
#user = User.last
r = Appointment.new
r.day = (Time.now - 2.days).strftime("%d/%m/%Y")
r.hour = "10:00"
r.description = "Some Description"
r.duration = 1.0
r.user = #user
r.save!
x = Appointment.last
x.description = "Other"
expect(x.save).to be_falsey
end
...
end
The trouble is, the test can't be accurate due to an error that prohibit the creation of an Appointment object with the past day.
What should I do to force, or even maybe make a fake object with a past date for I can finally test it?
You can use update_attribute which will skip validations.
it 'Cannot be edited if the date has past' do
#user = User.last
r = Appointment.new
r.day = (Time.now - 2.days).strftime("%d/%m/%Y")
r.hour = "10:00"
r.description = "Some Description"
r.duration = 1.0
r.user = #user
r.save!
x = Appointment.last
x.description = "Other"
r.update_attribute(:day, (Time.now - 2.days).strftime("%d/%m/%Y"))
expect(x.save).to be_falsey
end
Also you have a lot of noise in your test (data which is not asserted) which you should avoid by e.g. creating a helper function or using factories.
it 'Cannot be edited if the date has past' do
appointment = create_appointment
appointment.update_attribute(:day, (Time.now - 2.days).strftime("%d/%m/%Y"))
appointment.description = 'new'
assert(appointment.valid?).to eq false
end
def create_appointment
Appointment.create!(
day: Time.now.strftime("%d/%m/%Y"),
hour: '10:00',
description: 'description',
duration: 1.0,
user: User.last
)
end
Also you test for falsey which will also match nil values. What you want to do in this case is test for false with eq false.

Active Record: find records by ceratin condition

My goal is to find three doctors with more than 1 review and with average rating >= 4
At the moment I'm using this service
class RatingCounterService
def get_three_best_doctors
doctors = find_doctors_with_reviews
sorted_doctors = sort_doctors(doctors)
reversed_hash = reverse_hash_with_sorted_doctors(sorted_doctors)
three_doctors = get_first_three_doctors(reversed_hash)
end
private
def find_doctors_with_reviews
doctors_with_reviews = {}
Doctor.all.each do |doctor|
if doctor.reviews.count > 0 && doctor.average_rating >= 4
doctors_with_reviews[doctor] = doctor.average_rating
end
end
doctors_with_reviews
end
def sort_doctors(doctors)
doctors.sort_by { |doctor, rating| rating }
end
def reverse_hash_with_sorted_doctors(sorted_doctors)
reversed = sorted_doctors.reverse_each.to_h
end
def get_first_three_doctors(reversed_hash)
reversed_hash.first(3).to_h.keys
end
end
Which is very slow.
My Doctor model:
class Doctor < ApplicationRecord
has_many :reviews, dependent: :destroy
def average_rating
reviews.count == 0 ? 0 : reviews.average(:rating).round(2)
end
end
Review model:
class Review < ApplicationRecord
belongs_to :doctor
validates :rating, presence: true
end
I can find all doctors with more than 1 review with this request
doctors_with_reviews = Doctor.joins(:reviews).group('doctors.id').having('count(doctors.id) > 0')
But how can I find doctors with an average rating >= 4 and order them by the highest rating if the "average rating" is an instance method?
Thanks to this answer :highest_rated scope to order by average rating
My final solution is
Doctor.joins(:reviews).group('doctors.id').order('AVG(reviews.rating) DESC').limit(3)

How to get value from select field in controller?

I have a problem with get value from :days new form.
new.html.haml
= f.select :days, [['1 month', 30], ['2 months', 60], ['3 months', 90]]
controller
def create
#bought_detail = BoughtDetail.new(bought_detail_params)
#bought_detail.entry_type = #entry_type
#bought_detail.person = current_person
if #bought_detail.entry_type.kind == 'Karnet'
#bought_detail.cost = #bought_detail.entry_type.price * (days / 30).floor
else
#bought_detail.cost = #bought_detail.entry_type.price
end
end
private
def bought_detail_params
params.require(:bought_detail).permit(:bought_data, :start_on, :end_on, :entry_type_id, :days, :person_id, :credit_card, :card_code)
end
Model
belongs_to :entry_type
belongs_to :person
before_save :set_bought_data, :set_end_on, :set_start_on
attr_accessor :credit_card, :card_code, :days
def set_bought_data
self.bought_data = Date.today
end
def set_start_on
self.start_on = bought_data if start_on.nil?
end
def set_end_on
days = "7" if days.nil?
self.end_on = Date.today + days.to_i
end
When I fill new form and click submit I get this error:
undefined local variable or method `days'
I want to deliver value from select field e.g. when I choose 2 months, value of the day will be 60 and I will can get calculate #bought_detail.cost. How to do this?
Thanks in advance!
In controller you're trying to access undefined variable days, change to accessing the model:
#bought_detail.cost = #bought_detail.entry_type.price * (#bought_detail.days.last / 30).floor
In model change to:
self.days = "7" if days.nil?
because otherwise you'll be assigning to local variable, not the attribute.
In your controller's create method. Where are you getting the days from?
#bought_detail.cost = #bought_detail.entry_type.price * (days.last / 30).floor
This bit specifically: days.last / 30. Ruby can't find days method or local variable. I guess you want to access it from params[:your_model][:days]?

Including validation method in another 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

Calculate days until next birthday in Rails

I have a model with a date column called birthday.
How would I calculate the number of days until the user's next birthday?
Here's a simple way. You'll want to make sure to catch the case where it's already passed this year (and also the one where it hasn't passed yet)
class User < ActiveRecord::Base
attr_accessible :birthday
def days_until_birthday
bday = Date.new(Date.today.year, birthday.month, birthday.day)
bday += 1.year if Date.today >= bday
(bday - Date.today).to_i
end
end
And to prove it! (all I've added is the timecop gem to keep the calculations accurate as of today (2012-10-16)
require 'test_helper'
class UserTest < ActiveSupport::TestCase
setup do
Timecop.travel("2012-10-16".to_date)
end
teardown do
Timecop.return
end
test "already passed" do
user = User.new birthday: "1978-08-24"
assert_equal 313, user.days_until_birthday
end
test "coming soon" do
user = User.new birthday: "1978-10-31"
assert_equal 16, user.days_until_birthday
end
end
Try this
require 'date'
def days_to_next_bday(bday)
d = Date.parse(bday)
next_year = Date.today.year + 1
next_bday = "#{d.day}-#{d.month}-#{next_year}"
(Date.parse(next_bday) - Date.today).to_i
end
puts days_to_next_bday("26-3-1985")
Having a swipe at this:
require 'date'
bday = Date.new(1973,10,8) // substitute your records date here.
this_year = Date.new(Date.today.year, bday.month, bday.day )
if this_year > Date.today
puts this_year - Date.today
else
puts Date.new(Date.today.year + 1, bday.month, bday.day ) - Date.today
end
I'm not sure if Rails gives you anything that makes that much easier.
Here's another way to approach this with lesser-known methods, but they make the code more self-explanatory.
Also, this works with birth dates on a February 29th.
class User < ActiveRecord::Base
attr_accessible :birthday
def next_birthday
options = { year: Date.today.year }
if birthday.month == 2 && birthday.day == 29 && !Date.leap?(Date.today.year)
options[:day] = 28
end
birthday.change(options).tap do |next_birthday|
next_birthday.advance(years: 1) if next_birthday.past?
end
end
end
And of course, the number of days until the next birthday is:
(user.next_birthday - Date.today).to_i

Resources