Rails Dynamic Range Validations - ruby-on-rails

How can I validate a number within a range dynamically using existing data?
For example - I have certain discounts on bulk ordering of products. If a customer buys 10-50 units they get X off and if they order 51-200 units Y off.
How can I validate this so that users can't put in quantity discounts over the same range?

I don't quite understand your question but I'm sure a custom validation would be one way to solve whatever you are trying to achieve. Simply add a validate method in your model like so:
def validate
self.errors.add(:amount, "is out of range") unless self.amount_in_allowed_range
end
private
def amount_in_allowed_range
# logic to return true or false
end

If I understand your question correctly then you are trying to avoid the creation of a discount range that overlaps an already existing one. The following code should do this for you
class QtyDiscount < ActiveRecord::Base
def validate
self.errors.add(:amount, "overlaps an existing range")
unless self.amount_in_allowed_range
end
def amount_in_allowed_range
# Check for overlapping ranges where our record either
# - overlaps the start of another
# - or overlaps the end of another
conditions = "
id != :id AND (
( min_value BETWEEN :min_value AND :max_value) OR
( max_value BETWEEN :min_value AND :max_value))"
puts "Conditions #{conditions}"
overlaps = QtyDiscount.find(:all, :conditions =>
[ conditions, { :id => self.id.nil? ? 0 : self.id,
:min_value => self.min_value,
:max_value => self.max_value} ])
overlaps.size == 0
end
end
EDITED
Removed an extraneous condition and added some checking for self.id to ensure we are not getting a false negative from our own record

Related

What is the best way to reuse a scope in Rails?

I'm confused to reuse or writing a new scope.
for example,
one of my methods will return future subscription or current subscription or sidekiq created subscriptions.
as scopes will look like:
scope :current_subscription, lambda {
where('(? between from_date and to_date) and (? between from_time and to_time)', Time.now, Time.now)
}
scope :sidekiq_created_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is not null")
}
scope :future_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is null")
}
so these were used for separate purposes in different methods, so for me what I tried is to check whether a particular account record will come under which of three subscriptions.
so I tried like:
def find_account_status
accounts = User.accounts
name = 'future' if accounts.future_subscription.where(id: #account.id).any?
name = 'ongoing' if accounts.current_subscription.where(id: #account.id).any?
name = 'sidekiq' if accounts.sidekiq_enqued_subscription.where(id: #account.id).any?
return name
end
so here what my doubt is, whether using like this is a good way, as here we will be fetching the records based on the particular subscriptions and then we are checking whether ours is there or not.
can anyone suggest any better way to achieve this?
Firstly, you are over using the scopes here.
The method #find_account_status will execute around 4 Queries as below:
Q1 => accounts = User.accounts
Q2 => accounts.future_subscription
Q3 => accounts.current_subscription
Q4 => accounts.sidekiq_enqued_subscription
Your functionality can be achived by simply using the #account object which is already present in memory as below:
Add below instance methods in the model:
def current_subscription?
# Here I think just from_time and to_time will do the work
# but I've added from_date and to_date as well based on the logic in the question
Time.now.between?(from_date, to_date) && Time.now.between?(from_time, to_time)
end
def future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].blank?
end
def sidekiq_future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].present?
end
#find_account_status can be refactored as below:
def find_account_status
if #account.current_subscription?
'ongoing'
elsif #account.future_subscription?
'future'
elsif #account.sidekiq_future_subscription?
'sidekiq'
end
end
Additionally, as far as I've understood the code, I think you should also handle a case wherein the from_date and to_date are past dates because if that is not handled, the status can be set based on the field meta["special_sub_enqueued_at"] which can provide incorrect status.
e.g. Let's say that the from_date in the account is set as 31st Dec 2021 and meta["special_sub_enqueued_at"] is false or nil.
In this case, #current_subscription? will return false but #future_subscription? will return true which is incorrect, and hence the case for past dates should be handled.

building a simple search form in Rails?

I'm trying to build a simple search form in Ruby on Rails, my form is simple enough basically you select fields from a series of options and then all the events matching the fields are shown. The problem comes when I leave any field blank.
Here is the code responsible for filtering the parameters
Event.joins(:eventdates).joins(:categories).where
("eventdates.start_date = ? AND city = ? AND categories.name = ?",
params[:event][:date], params[:event][:city], params[:event][:category]).all
From what I get it's that it looks for events with any empty field, but since all of them have them not empty, it wont match unless all 3 are filled, another problem arises when I try to say, look events inside a range or array of dates, I'm clueless on how to pass multiple days into the search.
I'm pretty new to making search forms in general, so I don't even know if this is the best approach, also I'm trying to keep the searches without the need of a secialized model.
Below is probably what you are looking for. (Note: If all fields all blank, it shows all data in the events table linkable with eventdates and categories.)
events = Event.joins(:eventdates).joins(:categories)
if params[:event]
# includes below where condition to query only if params[:event][:date] has a value
events = events.where("eventdates.start_date = ?", params[:event][:date]) if params[:event][:date].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("city = ?", params[:event][:city]) if params[:event][:city].present?
# includes below where condition to query only if params[:event][:city] has a value
events = events.where("categories.name = ?", params[:event][:category]) if params[:event][:category].present?
end
To search using multiple days:
# params[:event][:dates] is expected to be array of dates.
# Below query gets converted into an 'IN' operation in SQL, something like "where eventdates.start_date IN ['date1', 'date2']"
events = events.where("eventdates.start_date = ?", params[:event][:dates]) if params[:event][:dates].present?
It will be more easy and optimised . If you use concern for filter data.
Make one concern in Model.
filterable.rb
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
if column_type(key) == :date || column_type(key) ==
:datetime
results = results.where("DATE(#{column(key)}) = ?",
Date.strptime(value, "%m/%d/%Y")) if
value.present?
else
results = results.where("#{column(key)} Like ? ", "%#{value}%") if
value.present?
end
end
results
end
def resource_name
self.table_name
end
def column(key)
return key if key.split(".").count > 1
return "#{resource_name}.#{key}"
end
def column_type(key)
self.columns_hash[key].type
end
end
end
Include this concern in model file that you want to filter.
Model.rb
include Filterable
In your controller Add this methods
def search
#resources = Model.filter(class_search_params)
render 'index'
end
def class_search_params
params.slice(:id,:name) #Your field names
end
So, It is global solution. You dont need to use query for filter. just add this concern in your model file.
That's it.

Rails 4 Create Related Object After Save

I have two models with the [fields]:
Order [:date]
Delivery Slot [:day]
Order belongs_to :delivery_slot
When an order is created, I want a delivery slot to be created with the :day set to the order :date.
So far I have created a new method create_delivery_slots in the Order controller that creates a Delivery Slot when the Order is created, but where I am stumped is, how do I get the Order :date in the Delivery Slot :day field?
#Create delivery slots if they dont already exist
def create_delivery_slots
existingslots = []
existingslots = DeliverySlot.all.select {|slot| slot.day == #order.date}
if existingslots.empty?
slot = DeliverySlot.new(:day => #order.date)
slot.save!
end
I have tried multiple approaches, but no luck. My gut tells me its something to do with strong parameters but I can't figure it out...
I'm not sure exactly of how you're set up but you'll probably want something like this:
class Order < ActiveRecord::Base
has_a :delivery_slot
after_create => :create_delivery_slots
.
#other code stuffs
.
.
private
def create_delivery_slots
existingslots = []
existingslots = DeliverySlot.all.select {|slot| slot.day == self.date}
if existingslots.empty?
slot = DeliverySlot.new(:day => self.date)
slot.save!
end
end
end
That's untested but it should be basically what you need.

Tracking object changes rails (Active Model Dirty)

I'm trying to track changes on a method just like we are tracking changes on record attributes using the Active Model Dirty.
At the moment I'm using this method to check changes to a set of attributes.
def check_updated_attributes
watch_attrs = ["brand_id", "name", "price", "default_price_tag_id", "terminal_usp", "primary_offer_id", "secondary_offer_id"]
if (self.changed & watch_attrs).any?
self.tag_updated_at = Time.now
end
end
However the attribute price is not a normal attribute with a column but a method defined in the model that combines two different attributes.
This is the method:
def price
if manual_price
price = manual_price
else
price = round_99(base_price)
end
price.to_f
end
Is there a way to track changes on this price method? or does this only work on normal attributes?
Edit: Base price method:
def base_price(rate = Setting.base_margin, mva = Setting.mva)
(cost_price_map / (1 - rate.to_f)) * ( 1 + mva.to_f)
end
cost_price and manual_price are attributes with columns it the terminal table.
Ok, solved it.
I had to create a custom method named price_changed? to check if the price had changed.
def price_changed?
if manual_price
manual_price_changed?
elsif cost_price_map_changed?
round_99(base_price) != round_99(base_price(cost_price = read_attribute(:cost_price_map)))
else
false
end
end
This solved the problem, although not a pretty solution if you have many custom attributes.

How to format values before saving to database in rails 3

I have a User model with Profit field. Profit field is a DECIMAL (11,0) type. I have a masked input on the form which allows user to input something like $1,000. I want to format that value and remove everything except numbers from it so i will have 1000 saved. Here is what i have so far:
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
end
But it keeps saving 0 in database. Looks like it is converting it to decimal before my formatting function.
Try this:
def profit=(new_profit)
self[:profit] = new_profit.gsub(/[^0-9]/, '')
end
First of all, this:
def format_values
self.profit.to_s.delete!('^0-9') unless self.profit.nil?
end
is pretty much the same as this:
def format_values
return if(self.profit.nil?)
p = self.profit
s = p.to_s
s.delete!('^0-9')
end
So there's no reason to expect your format_values method to have any effect whatsoever on self.profit.
You could of course change format_values to assign the processed string to self.profit but that won't help because your cleansing logic is in the wrong place and it will be executed after '$1,000' has been turned into a zero.
When you assign a value to a property, ActiveRecord will apply some type conversions along the way. What happens when you try to convert '$1,000' to a number? You get zero of course. You can watch this happening if you play around in the console:
> a = M.find(id)
> puts a.some_number
11
> a.some_number = 'pancakes'
=> "pancakes"
> puts a.some_number
0
> a.some_number = '$1,000'
=> "1,000"
> puts a.some_number
0
> a.some_number = '1000'
=> "1000"
> puts a.some_number
1000
So, your data cleanup has to take place before the data goes into the model instance because as soon as AR gets its hands on the value, your '$1,000' will become 0 and all is lost. I'd put the logic in the controller, the controller's job is to mediate between the outside world and the models and data formatting and mangling certainly counts as mediation. So you could have something like this in your controller:
def some_controller
fix_numbers_in(:profit)
# assign from params as usual...
end
private
def fix_numbers_in(*which)
which.select { |p| params.has_key?(p) }.each do |p|
params[p] = params[p].gsub(/\D/, '') # Or whatever works for you
end
end
Then everything would be clean before ActiveRecord gets its grubby little hands on your data and makes a mess of things.
You could do similar things by overriding the profit= method in your model but that's really not the model's job.
class User < ActiveRecord::Base
before_save :format_values
private
def format_values
self.profit = profit.to_s.gsub(/\D/,'') if profit
end
end
def format_values
self.profit.to_d!
end
I recommend you to write custom setter for this particular instance variable #profit:
class User
attr_accessor :profit
def profit= value
#profit = value.gsub(/\D/,'')
end
end
u = User.new
u.profit = "$1,000"
p u.profit # => "1000"
I would suggest using the rails helper of number with precision. Below is some code.
Generic Example:
number_with_precision(111.2345, :precision => 1, :significant => true) # => 100
Rails code Example:
def profit=(new_profit)
number_with_precision(self[:profit], :precision => 1, :significant => true)
end

Resources