Ruby complex validation - ruby-on-rails

Have a product that belongs to a category. Want to create a promotion for a short period of time (lets say a week or two), but their can be only one promotion per category during that time.
How can I create a custom validation for this?
product class
belongs_to :categories
name:string
desc:text
reg_price:decimal
category_id:integer
promo_active:boolean
promo_price:decimal
promo_start:datetime
promo_end:datetime
end
category class
has_many :products
name:string
end
Update to possible solution???
class Product < ActiveRecord::Base
attr_accessible :name, :desc, :reg_price, :category_id, :promo_active, :promo_price, :promo_start, :promo_end
belongs_to :category
#validate :check_unique_promo
#Tweaked original to be more exact and
#Give clue if its the start or end date with the error.
validate :check_unique_promo_start
validate :check_unique_promo_end
def check_unique_promo
errors.add_to_base("Only 1 promo allowed") unless Product.count(:conditions => ["promo_active = ? AND promo_end < ?", true, self.promo_start]) == 0
end
def check_unique_promo_start
errors.add_to_base("Start date overlaps with another promotion.") unless self.promo_active == false || Product.count(:conditions => ['promo_end BETWEEN ? AND ? AND category_id = ? AND promo_active = ? AND id != ?',self.promo_start, self.promo_end, self.category_id, true, self.id]) == 0
end
def check_unique_promo_end
errors.add_to_base("End date overlaps with another promotion.") unless self.promo_active == false || Product.count(:conditions => ['promo_start BETWEEN ? AND ? AND category_id = ? AND promo_active = ? AND id != ?',self.promo_start, self.promo_end, self.category_id, true, self.id]) == 0
end
end
I Skip self if promo_active false for performance.

I would use the validates_uniqueness_of validation so:
class Product < ActiveRecord::Base
belongs_to :categories
validates_uniqueness_of :promo_active, :scope => :category_id, :allow_nil => true
before_save :update_promos
private
def update_promos
# custom code to set :promo_active to nil if the promo is
# not active and to something else if it is active
end
end
Take 2:
validate :check_unique_promo
def check_unique_promo
errors.add_to_base("Only 1 promo allowed") unless Product.count(:conditions => ["active_promo = 1 AND promo_end < ?", self.promo_start]) == 0
end

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```

Upgrading from Rails 4 to Rails 6 and handling Nested Parameters

I am helping a client upgrade his application from Rails 4 to Rails 6, and one of his model objects isn't saving properly and I'm having trouble figuring out why. I think the main trouble is figuring out how to set up the params.permit based on a somewhat unorthodox parameter structure. The main problem at this point is that the nested parameter frequency isn't attaching to the service_agreement even though the data appears to be coming through in the parameters correctly. I would think that redefining the strong parameters would provide a solution, but I'm not sure if that's true.
From the Model
# Model object
class ServiceAgreement < ApplicationRecord
has_one :frequency, dependent: :destroy
accepts_nested_attributes_for :frequency, reject_if: :reject_frequency
end
From the Controller
def create
#service_agreement = ServiceAgreement.new(service_agreement_params)
authorize #service_agreement
if params[:service_agreement][:start_date].present?
#service_agreement.start_date = #service_agreement.set_proper_date(params[:service_agreement][:start_date])
end
if params[:first] && params[:second] && params[:third]
#service_agreement.phone_number = params[:first] + params[:second] + params[:third]
end
if params[:service_agreement][:end_date].present?
#service_agreement.end_date = #service_agreement.set_proper_date(params[:service_agreement][:end_date])
end
if params[:location_id] && params[:location_id] == "master"
#service_agreement.master = true
elsif params[:location_id]
#location = Location.find(params[:location_id])
#service_agreement.location_id = #location.id
end
if #service_agreement.save
redirect_to pick_services_service_agreement_path(#service_agreement), notice: "Service Agreement Successfully Created"
else
#account = #service_agreement.account
#chosen_invoice_frequency = #service_agreement.try(:invoice_frequency)
#default_invoice_frequency = #account.try(:invoice_frequency)
#service_agreement.build_frequency unless #service_agreement.frequency.present?
respond_with(#service_agreement)
end
end
Here are the paramters as they come through from the form:
Parameters: {"authenticity_token"=>"xxx", "service_agreement"=>{"active_status"=>"active", "service_type"=>"on_site_shredding", "job_type"=>"recurring", "sa_name"=>"", "contact_name"=>"", "location_id"=>"45729", "department"=>"", "frequency_attributes"=>{"day_count"=>"", "week_count"=>"1", "month_count"=>"", "day_on"=>"", "date_on"=>"", "week_on"=>"", "once"=>"false", "second_day_on"=>"", "second_date_on"=>"", "second_week_on"=>"", "on_demand"=>"false", "third_day_on"=>"", "fourth_day_on"=>"", "fifth_day_on"=>"", "sixth_day_on"=>""}, "start_date"=>"01/06/2021", "end_date"=>"06/23/2021", "estimated_service_time"=>"15", "notes"=>"", "standard_minimum"=>"0.0", "next_day_minimum"=>"0.0", "same_day_minimum"=>"0.0", "rush_minimum"=>"0.0", "invoice_frequency"=>"immediate", "po_number"=>"", "taxable"=>"0", "tax_rate_id"=>"33", "organization_id"=>"15", "account_id"=>"41447"}, "same_info"=>"1", "first"=>"727", "second"=>"714", "third"=>"7750", "commit"=>"Save"}
Originally, the strong parameters were defined as follows:
def service_agreement_params
params.require(:service_agreement).permit(:account_id, :location_id, :organization_id, :minimum_storage, :minimum_delivery, :standard_minimum, :rush_minimum, :next_day_minimum, :same_day_minimum, :payment_method, :service_type, :active, :contact_name, :estimated_service_time, :phone_number, :notes, :department, :po_number, :invoice_frequency, :active_status, :taxable, :tax_rate_id, :sa_name, :job_type, frequency_attributes: [:id, :day_count, :week_count, :month_count, :day_on, :second_day_on, :third_day_on, :fourth_day_on, :fifth_day_on, :sixth_day_on, :date_on, :second_date_on, :week_on, :second_week_on, :service_agreement_id, :once, :on_demand])
end
I have redefined the strong parameters to cut down on unpermitted errors as follows:
def service_agreement_params
params.permit(:start_date, :end_date, {
service_agreement: [
:account_id,
:location_id,
:organization_id,
:minimum_storage,
:minimum_delivery,
:standard_minimum,
:rush_minimum,
:next_day_minimum,
:same_day_minimum,
:payment_method,
:service_type,
:active,
:contact_name,
:estimated_service_time,
:phone_number,
:notes,
:department,
:po_number,
:invoice_frequency,
:active_status,
:taxable,
:tax_rate_id,
:sa_name,
:job_type,
{ frequency_attributes: [
:id,
:day_count,
:week_count,
:month_count,
:day_on,
:second_day_on,
:third_day_on,
:fourth_day_on,
:fifth_day_on,
:sixth_day_on,
:date_on,
:second_date_on,
:week_on,
:second_week_on,
:service_agreement_id,
:once,
:on_demand
] }
]
})
end
EDIT: Adding Frequency model based on comment
class Frequency < ApplicationRecord
belongs_to :service_agreement
validate :first_day_if_second
validate :first_date_if_second
validate :first_day_before_second
validate :second_week_after_first
validate :day_on_if_week_on
validate :second_date_on_after_first
validate :week_on_and_date_on_has_month
validate :date_between_1_to_30
validate :second_date_between_1_to_30
private
def first_day_if_second
if second_day_on.present? && !day_on.present?
errors.add(:day_on, "You must add a first day if you have a second.")
end
end
def first_day_before_second
if second_day_on.present? && day_on.present? && day_on >= second_day_on && second_week_on == nil
errors.add(:second_day_on, "Your second day must be later in the week than the first.")
end
end
def second_week_after_first
if second_week_on.present? && week_on.present? && second_week_on <= week_on
errors.add(:second_week_on, "Your second week for service for your frequency must be after the first")
end
end
def day_on_if_week_on
if week_on.present? && !day_on.present?
errors.add(:week_on, "You must select a day on if you select a week")
end
end
def second_date_on_after_first
if second_date_on.present? && date_on.present? && second_date_on <= date_on
errors.add(:second_date_on, "Your second date must be after the first.")
end
end
def first_date_if_second
if second_date_on.present? && !date_on.present?
errors.add(:second_date_on, "You must have a first date if you have a second")
end
end
def week_on_and_date_on_has_month
if (week_on.present? || date_on.present?) && month_count == nil
errors.add(:month_count, "You must have a month if you have weeks or dates selected")
end
end
def date_between_1_to_30
if date_on.present? && !date_on.between?(1, 30)
errors.add(:date_on, "Your date for your frequency must be between 1 and 30")
end
end
def second_date_between_1_to_30
if second_date_on.present? && !second_date_on.between?(1, 30)
errors.add(:second_date_on, "Your second date for your frequency must be between 1 and 30")
end
end
end

Restrict the chance to delete a booking just more than x hours before departure in Rails

I want users not to be able to cancel a booking just 2 hours before departure time.
I don't know where can I write this restriction. Should I write it in the model or in the controller application?
This is the pseudo-code I wrote so far:
class CancelValidator < ActiveMOdel::Validator
def validate(record)
if record.date_trip.to_time < Date.now + 2
record.errors[:base] << 'error'
end
end
end
EDIT: This is all the code, but it still lets me destroy the booking.. why?
class CountValidator < ActiveModel::Validator
def validate(record)
if (record.second || record.first)
record.errors[:base]<< ' error '
end
end
end
class DepartureValidator < ActiveModel::Validator
def validate(record)
if record.date_trip.to_date < Date.today
record.errors[:base]<< ' error '
end
end
end
class Reservation < ActiveRecord::Base
validates_with DepartureValidator
validates_with CountValidator
before_destroy :ensure_deletable
belongs_to :dep ,:class_name => 'Stop', :foreign_key => 'dep_id'
belongs_to :arr ,:class_name => 'Stop',:foreign_key => 'arr_id'
belongs_to :route
belongs_to :user
delegate :CountStop, :to => :route, prefix: true, :allow_nil => false
delegate :city ,:to => :arr, :allow_nil => false
delegate :city ,:to => :dep, :allow_nil => false
def division
return Reservation.select{|r| r.route_id == route_id && r.date_trip == date_trip }
end
def second
if (class_point == 2)
y=division.select{ |l| l.class_point == 2 }.count
if(y+1 > route.train.second_class_seats)
return true
end
end
return false
end
def first
if (class_point == 1)
y=division.select{ |l| l.class_point == 1 }.count
if(y+1 > route.train.prima_classe_seats)
return true
end
end
return false
end
def ensure_deletable
self.date_trip.to_time < Time.now + 2
end
end
Since you delete the value, you're going to want to add a callback instead.
The benefit of this is that, before you go and delete the entity, you can decide to stop it outright if it fails your condition.
Here's an example below. Caution: this is untested, but this should give you the gist of things.
class Booking < ActiveRecord::Base
before_destroy :ensure_deletable
private
def ensure_deletable
self.date_trip.to_time < Date.now + 2
end
end
Remember, from the documentation:
The method reference callbacks work by specifying a protected or private method available in the object...

Method to merge objects which are similar (differing by quantity)

I need help with a cart object which has_many line_items. If a line_item in the cart is created and it has the same exact attributes as a line_item which is already in the cart I just want to update the pre existing line_items quantity rather than create duplicate objects with separate quantities.
I wrote a few methods in my models to try and make this work but it isn't working. Below is my code:
models
class LineItem < ActiveRecord::Base
attr_accessible :cart_id, :product_id, :quantity, :unit_price, :product, :cart, :color_id, :size_id, :extra_id
belongs_to :cart
belongs_to :product
belongs_to :color
belongs_to :size
belongs_to :extra
validates :quantity, :presence => true
def update_quantity(qty)
quantity += qty
quantity.save
end
def exists_in_collect?(items)
if items.include?(product)
if color == items.color && size == items.sizes && extra == items.extra
return true
end
else
return false
end
end
end
class Cart < ActiveRecord::Base
attr_accessible :purchased_at
has_many :line_items
has_one :order
def where_line_item_with(prod_id)
line_items.where(:product_id => prod_id)
end
end
controller
class LineItemsController < ApplicationController
def new
#line_item = LineItem.new
end
def create
#line_item = LineItem.new(params[:line_item].merge(:cart => current_cart))
if #line_item.exists_in_collect?(current_cart.line_items)
current_cart.where_line_item_with(product.id).update_quantity(#line_item.quantity)
#line_item.destroy!
else
#line_item.save!
#line_item.update_attributes!(:unit_price => #line_item.item_price)
end
redirect_to current_cart_url
end
def update
#line_item = LineItem.find(params[:id])
#line_item.update_attributes(params[:line_item])
redirect_to current_cart_url
end
Any insight is fully appreciated.
1.You should change your where_line_item_with(prod_id) to the following:
def where_line_item_with(prod_id)
line_items.where(:product_id => prod_id).first
end
As the where returns an array and you cannot do update_quantity(#line_item.quantity) on an array.
2.In exists_in_collect?(items) - Here your aim is to find if the items of the cart include the item similar to new item. it should be updated as following:
def exists_in_collect?(items)
items.each do |item|
if color == item.color && size == item.sizes && extra == item.extra && product == item.product
return true
end
end
return false
end

Rails custom validation has errors but still saving?

I'm creating a rails app to manage events and I want to add validation to check if an event overlaps with another.
Here's my custom validation code to check for overlapping events:
class Event < ActiveRecord::Base
attr_accessible :full_date, :start_hour, :end_hour, :address
validate :no_overlapping_events
def no_overlapping_events
overlap = false
same_date = Event.where(:full_date => self.full_date).where("id != ?", self.id).where(:address => self.address)
same_date.each do |t|
if self.start_hour <= t.start_hour
if self.end_hour >= t.start_hour
overlap = true
break
end
end
end
if overlap == true
errors.add(:base, "Can't have overlapping events!")
end
end
end
I can still create events that are overlapping and they're still being saved. What am I doing wrong here?
Your custom validation method "ends" too soon. Try:
class Event < ActiveRecord::Base
attr_accessible :full_date, :start_hour, :end_hour, :address
validate :no_overlapping_events
def no_overlapping_events
overlap = false
same_date = Event.where(:full_date => self.full_date).where("id != ?", self.id).where(:address => self.address)
same_date.each do |t|
if self.start_hour <= t.start_hour
if self.end_hour >= t.start_hour
overlap = true
break
end
end
end
if overlap == true
errors.add(:base, "Can't have overlapping events!")
end
end
end
Also, as a logic error, what about the case when:
self.start_hour > t.start_hour and self.start_hour < t.end_hour

Resources