How to model this complex validation for uniqueness on combined fields - ruby-on-rails

A link has two components: componenta_id and componentb_id. To this end, in the Link model file I have:
belongs_to :componenta, class_name: "Component"
belongs_to :componentb, class_name: "Component"
validates :componenta_id, presence: true
validates :componentb_id, presence: true
validates :componenta_id, uniqueness: { scope: :componentb_id }
validates :componentb_id, uniqueness: { scope: :componenta_id }
And in the migration file:
create_table :links do |t|
t.integer :componenta_id, null: false
t.integer :componentb_id, null: false
...
end
add_index :links, :componenta_id
add_index :links, :componentb_id
add_index :links, [:componenta_id, :componentb_id], unique: true
Question: This all works. Now I want the combination of componanta and componentb to be unique, irrespective their order. So irrespective which component is componenta and which one is componentb (after all that's the same link; a link between the two same components). So the two records below should not be allowed since they represent the same link and thus are not unique:
componenta_id = 1 ; componentb_id = 2
componenta_id = 2 ; componentb_id = 1
How can I create this uniqueness validation? I have model validation working (see below) but wonder whether and how I should also add validation at the migration/db level...?
Model validation
I have model validation working with the code below:
before_save :order_links
validates :componenta_id, uniqueness: { scope: :componentb_id }
private
def order_links
if componenta_id > componentb_id
compb = componentb_id
compa = componenta_id
self.componenta_id = compb
self.componentb_id = compa
end
end
The following test confirms the above works:
1. test "combination of two links should be unique" do
2. assert #link1.valid?
3. assert #link2.valid?
4. #link1.componenta_id = 3 ##link2 already has combination 3-4
5. #link1.componentb_id = 4
6. assert_not #link1.valid?
7. #link1.componenta_id = 4
8. #link1.componentb_id = 3
9. assert_raises ActiveRecord::RecordNotUnique do
10. #link1.save
11. end
12.end
Migration/db validation:
As an extra level of security, is there also a way to incorporate validation for this at the db level? Otherwise it is still possible to write both of the following records to the database: componenta_id = 1 ; componentb_id = 2 as well as componenta_id = 2 ; componentb_id = 1.

Perhaps it is possible to control the creation of the links with:
def create_unique_link( comp_1, comp_2 )
if comp_1.id > comp_2.id
first_component = comp_1
second_component = comp_2
end
link = Link.find_or_create_by( componenta_id: first_comp.id, componentb_id: second_comp.id )
end
If you need the validation, then you can custom validate:
def ensure_uniqueness_of_link
if comp_1.id > comp_2.id
first_component = comp_1
second_component = comp_2
end
if Link.where( componenta_id: first_component.id, componentb_id: second_component ).first
errors.add( :link, 'Links should be unique' )
end
end

validates :componenta_id, uniqueness: { scope: :componentb_id }
validates :componentb_id, uniqueness: { scope: :componenta_id }

Related

Custom Rails Dashboard, how to optimize data retrieval to display in view?

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

.save throws an error instead of execute the else part of the conditional

Validation for uniqueness applies to a combination of two fields. My problem is that patching a new record that does validate, throws the error ActiveRecord::RecordNotUnique PG::UniqueViolation: ERROR: duplicate key value violates unique constraint, rather than that it executes the else part of the method below. Why does it throw an error instead of execute the else part? How to change this?
def create
first_node = Node.find_by(id: params[:first_node_id])
second_node = Node.find_by(id: params[:second_node_id])
link = first_node.where_first_links.build(create_params)
if link.save
render json: link, status: :created
else
render json: link, message: "unable", status: :bad_request
end
end
In the migration file:
add_index :links, [:first_node_id, :second_node_id], unique: true
The model validation:
before_save :order_nodes
validates :first_node_id, presence: true
validates :second_node_id, presence: true
validates :first_node_id, uniqueness: { scope: :second_node_id }
def order_nodes
if first_node_id > second_node_id
first = first_node_id
second = second_node_id
self.first_node_id = second
self.second_node_id = first
if direction == '0'
self.direction = 1
elsif direction == '1'
self.direction = 0
end
end
end
It most probably means that:
Your object passed the validation.
THEN your before_save callback reordered the fields
Subsequent attempt to save the record to the database violated the database unique constraint
Try changing your callback from:
before_save :order_nodes
to:
before_validation :order_nodes
NOTE: In this case, you'll have to assume that your fields may be invalid and rewrite your callback accordingly.

Between SQL ActiveRecord

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 }

How can I validate an attribute to be between various noncontinuous ranges?

I'm wanting to validate that my height attribute is within a bunch of different ranges. So my attempt was something like what I did below... however this is incorrect. How should this be done? Thanks!
validates :height, :numericality => { in: { 5020..5028, 5030..5038, 5040..5048, 5050..5058, 5060..5068, 5070..5078, 5080..5088, 5090..5098, 5100..5108, 5110..5118,
6000..6008, 6010..6018, 6020..6028, 6030..6038, 6040..6048, 6050..6058, 6060..6068, 6070..6078, 6080..6088, 6090..6098, 6100..6108, 6110..6118,
7000..7008, 7010..7018, 7020..7028, 7030..7038, 7040..7048, 7050..7058, 7060..7068, 7070..7078, 7080..7088, 7090..7098, 7100..7108, 7110..7118 } }
You can put that in a custom validate method:
class YourModel < ActiveRecord::Base
VALID_HEIGHT_RANGES = [5020..5028, 5030..5038, 5040..5048, 5050..5058, 5060..5068, 5070..5078, 5080..5088, 5090..5098, 5100..5108, 5110..5118, 6000..6008, 6010..6018, 6020..6028, 6030..6038, 6040..6048, 6050..6058, 6060..6068, 6070..6078, 6080..6088, 6090..6098, 6100..6108, 6110..6118, 7000..7008, 7010..7018, 7020..7028, 7030..7038, 7040..7048, 7050..7058, 7060..7068, 7070..7078, 7080..7088, 7090..7098, 7100..7108, 7110..7118]
validate :height_in_valid_range
private
def height_in_valid_range
VALID_HEIGHT_RANGES.each do |range|
unless range.include? height
errors.add :height, "not in valid range"
break
end
end
end
end

object existence not found in console but found when called in view?

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(&macro).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.

Resources