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
this is the schema and my model for Visit (visit's status can be: Confirmed, Current, Expired and To be approved)
schema.rb
create_table "visits", force: true do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.date "start"
t.date "end"
t.integer "idVisit"
t.integer "employee_id"
t.integer "visitor_id"
t.string "status", default: "Confirmed"
end
Visit.rb
class Visit < ActiveRecord::Base
belongs_to :employee
belongs_to :visitor
default_scope -> { order(:created_at) }
validates :start, presence: true, uniqueness: {scope: [:end, :visitor_id]}
validates :end, presence: true
validates :visitor_id, presence: true
validates :employee_id, presence: true
validate :valid_date_range_required
def valid_date_range_required
if (start && end) && (end < start)
errors.add(:end, "must be after start")
end
end
end
Now my problem is that I need to compare for each visit, after each time I do show action in employees_controller.rb, the start and end date to Date.today (except for To be approved status); according to it I will change the status of visits in the database.
Here is what I did but probably there will be some mistakes since for now an error occurs at least, so I hope you could help me to fix it.
In Visit.rb I created this:
def check_visit_status(visit)
if visit.status != 'To be confirmed'
if visit.start <= Date.today && visit.end >= Date.today
visit.status = 'Current'
end
if visit.end < Date.today
visit.status = 'Expired'
end
end
end
Now in employees_controller.rb I have (I won't post it all):
class EmployeesController < ApplicationController
after_action :update_status, only: :show
def show
if logged_in?
#employee = Employee.find(params[:id])
#indirizzimac = current_employee.indirizzimacs.new
#visitor = current_employee.visitors.new
#visit = current_employee.visits.new
#visits = current_employee.visits.all
if params[:act]=='myData'
render 'myData'
elsif params[:act]=='myNetwork'
render 'myNetwork'
elsif params[:act]=='temporaryUsers'
render 'temporaryUsers'
elsif params[:act]=='guestsVisits'
render 'guestsVisits'
elsif params[:act]=='myAccount'
render 'myAccount'
else
render 'show'
end
else
render 'static_pages/errorPage'
end
end
def update_status
if #visits.any?
#visits.each do |visit|
check_visit_status(visit)
end
end
end
end
Thank you a lot in advance
I really have to thank eeeeeean for his immense help.
I figured out my problem so I want to post here my solution in order to help someone looking for the same thing I was asking for.
employees_controller.rb
class EmployeesController < ApplicationController
after_action :update_status, only: :show
def show
[...]
end
def update_status
if #visits.any?
#visits.each do |visit|
visit.check_visit_status
end
end
end
end
Visit.rb
def check_visit_status
if self.status != 'To be confirmed'
if self.start <= Date.today && self.end >= Date.today
self.update_attribute :status, 'Current'
end
if self.end < Date.today
self.update_attribute :status, 'Expired'
end
end
end
You need to call check_visit_status on an instance of Visit, but right now it's being called on self, which in this scope refers to the employees controller. Try this:
visit.rb
def check_visit_status
if self.status != 'To be confirmed'
if self.start <= Date.today && end >= Date.today
self.status = 'Current'
end
if self.end < Date.today
self.status = 'Expired'
end
end
end
Then call it like this:
employees_controller.rb
#visits.each do |visit|
visit.check_visit_status
end
That should get you out of that particular error.
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...
class Reservation < ActiveRecord::Base
validates :table, presence: true
validates :start, presence: true
validates :finish, presence: true
validate :checking_the_time, :uniqueness_reservation
scope :tables, ->(table) { where("'reservations'.'table' = ? ", table) }
def checking_the_time
if start >= finish
errors.add(:finish, "Invalid time range")
end
end
def uniqueness_reservation
unless Reservation.diapazone(start, finish).tables(table).where.not(id: id).empty?
errors.add(:booked_from, 'Invalid period.')
end
end
private
def self.diapazone(start, finish)
where("(start >= :s AND finish <= :f) OR (start <= :s AND finish >= :s)
OR (start <= :f AND finish >= :f)",
{s: start, f: finish})
end
end
How to refactor the validation checks on entering the order in the order that has already been created?
I will be grateful for the advice, if you can with an example.
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