Custom validation help needed - ruby-on-rails

validate :check_product_stock
def check_product_stock
#thisproduct = product.id
#productbeingchecked = Product.find_by_id(#thisproduct)
#stocknumber = #productbeingchecked.stock_number
if producto.en_stock == 0
raise "El Producto no tiene stock suficiente para completar la venta"
#errors.add :venta, "Producto con pedos de stock"
return false
end
true
end
end
i need to be able to validate on creation of a model (sale), if it's association (product), hasn't reached zero in a product.column called stock_number.
i think i need to rewrite the entire thing but now with validate :check_product_stock then built a method from scratch that checks if product hasn't reached zero and it it has it should throw a flash notice and stay in the same place (sales/new)
class Venta < ActiveRecord::Base
hobo_model # Don't put anything above this
belongs_to :cliente, :accessible => true
belongs_to :producto, :accessible => true
belongs_to :marca, :accessible => true
belongs_to :vendedor
belongs_to :instalador
has_many :devolucions
fields do
numero_de_serie :string
precio_de_venta :integer
precio_de_instalacion :integer, :default => "0"
forma_de_pago enum_string(:contado, :tarjeta)
status enum_string(:activa, :cancelada)
timestamps
end
validates_presence_of :cliente, :precio_de_venta, :vendedor, :precio_de_instalacion
validate_on_create :check_product_stock
after_save :descontar_precio_de_instalacion_si_el_instalador_es_a_destajo
#def stock_error
#flash[:notice] = "Producto con pedos de stock"
# redirect_to :controller => :venta, :action => :stock_error
#errors.add_to_base("Producto con pedos de stock")
# end
def check_product_stock
if producto.en_stock == 0
raise "El Producto no tiene stock suficiente para completar la venta"
#errors.add :venta, "Producto con pedos de stock"
return false
end
true
end
#def check_product_stock
# if producto.en_stock == 0
# errors.add :producto, "El Producto no tiene stock suficiente para completar la venta"
# return false
# end
# true # guards against returning nil which is interpreted as false.
#end
def descontar_precio_de_instalacion_si_el_instalador_es_a_destajo
#este_instalador_id = instalador.id
#instalador = Instalador.find_by_id(#este_instalador_id)
if #instalador.a_destajo?
#last_venta = Venta.find(:last)
#vid = #last_venta.id
#precio_de_instalacion_original = precio_de_instalacion
#mitad_de_instalacion = #precio_de_instalacion_original/2
#Venta.update(#vid, :precio_de_instalacion => #mitad_de_instalacion)
ActiveRecord::Base.connection.execute "UPDATE ventas SET precio_de_instalacion = #{#mitad_de_instalacion} WHERE id = #{#vid};"
end
end
#after_save :reduce_product_stock_number
# def reduce_product_stock_number
# Producto.decrement_counter(:en_stock, producto.id)
# end
# --- Permissions --- #
def create_permitted?
true
end
def update_permitted?
false
end
def destroy_permitted?
false
end
def view_permitted?(field)
true
end
end
And this is my observer that diminishes the en_stock column from producto:
class VentaObserver < ActiveRecord::Observer
def after_save(venta)
#venta_as_array = venta
if venta.producto_id?
#pid = #venta_as_array[:producto_id]
Producto.decrement_counter(:en_stock, #pid)
end
if venta.cart_id
#cid = #venta_as_array[:cart_id]
#cart = Cart.find_by_id(#cid)
for item in #cart.cart_items do
# #pid = #cart.cart_items.producto.id
Producto.decrement_counter(:en_stock, item.producto.id)
end
#si el instalador es a destajo se debe descontar la mitad del rpecio de instalacion
end
end
end

It seems you've got a lot going wrong here.
First of all, you're doing way too much work. This is all you really need.
before_save :check_product_stock
def check_product_stock
if product.stocknumber == 0
flash[:notice] = "Producto con pedos de stock"
end
end
Secondly, the flash hash is not available in models. You can use the ActiveRecord error object to make errors available to the controller and views by replacing
flash[:notice] = with errors.add_to_base
I used errors.add_to_base because the error is not exactly a part of this model, yet is still blocking the save.
Thirdly, it seems like you're reducing product.stocknumber at some point. Probably as a before_validation so it's highly possible that product.stocknumber is less than 0 during the check if product.stocknumber was 0 before the save call.
So let's change the if condition to reflect that.
unless product.stocknumber > 0
Finally, you're using a before_save callback, so just adding an error will not cancel the transaction. You need to return false for a before/after save/create/update/valdiaiton callback to cancel the transaction.
Putting this all together gives you
before_save :check_product_stock
def check_product_stock
unless product.stocknumber > 0
errors.add_to_base "Producto con pedos de stock"
return false
end
true # guards against returning nil which is interpreted as false.
end
As for displaying these errors you can either use the nice error_messages_for helper on the object in the view. Or copy the errors to the flash hash in the controller.
In the view:
<%= error_mesages_for :order %>
Or, in the controller in the else block of if #order.save:
flash[:errors] = #order.errors.full_messages.join "<br />"
I prefer the errors over notice when it comes to passing errors in the flash hash because it makes it easy to distinguish the two via CSS if an action happens to produce both a notices or an error. Also it's more DRY to always display flash[:errors] with a class that gives red text, the write the logic into your view to determine if the contents of flash[:notice] are an error or a notice.

flash is not available in the model. You need to do something like this:
errors.add :stocknumber, "Producto con pedos de stock"
And then work with flash in the controller and views.

Related

Error handling of csv upload in rails

I have this import method in my active record which I use to import the csv file. I want to know how to do the error handling of this in the active record.
class SheetEntry < ActiveRecord::Base
unloadable
belongs_to :user
belongs_to :project
belongs_to :task
validate :project_and_task_should_be_active
def self.import(csv_file)
attributes = [:user_id, :project_id, :task_id, :date, :time_spent, :comment]
errors=[]
output = {}
i=0
CSV.foreach(csv_file, headers: true, converters: :date).with_index do |row,j|
entry_hash= row.to_hash
entry_hash['Project'] = SheetProject.where("name= ?" , entry_hash['Project']).pluck(:id)
entry_hash['Task'] = SheetTask.where("name= ?" , entry_hash['Task']).pluck(:id)
entry_hash['Date'] = Time.strptime(entry_hash['Date'], '%m/%d/%Y').strftime('%Y-%m-%d')
entry_hash['Time (Hours)'] = entry_hash['Time (Hours)'].to_f
firstname = entry_hash['User'].split(" ")[0]
lastname = entry_hash['User'].split(" ")[1]
entry_hash['User'] = User.where("firstname=? AND lastname=?",firstname,lastname).pluck(:id)
entry_hash.each do |key,value|
if value.class == Array
output[attributes[i]] = value.first.to_i
else
output[attributes[i]] = value
end
i += 1
end
entry=SheetEntry.new(output)
entry.editing_user = User.current
entry.save!
end
end
def project_and_task_should_be_active
errors.add(:sheet_project, "should be active") unless sheet_project.active?
errors.add(:sheet_task, "should be active") if sheet_task && !sheet_task.active?
end
end
I want to know how to show the error if there is a nil object returned for either entry_hash['Project'] or entry_hash['Task'] or for any of the fields in the csv.
For example: If the user had entered the wrong project or wrong task or wrong date. I want the error to be shown along with the line no and stop the uploading of the csv. Can someone help?
You can use begin and rescue statements to handle errors in any ruby classes.
You can use the rescue block to return the Exception e back to the caller.
However, you cannot call errors.add method to add error because #errors is an instance method which is not accessible inside class method self.import.
def self.import(csv_file)
begin
attributes = [:user_id, :project_id, :task_id, :date, :time_spent, :comment]
errors=[]
output = {}
i=0
CSV.foreach(csv_file, headers: true, converters: :date).with_index do |row,j|
...
end
rescue Exception => e
return "Error: #{e}"
end
end

Call a Method Model inside Controller

I have the following model;
(app/models/student_inactivation_log.rb)
class StudentInactivationLog < ActiveRecord::Base
belongs_to :student
belongs_to :institution_user
belongs_to :period
validates_presence_of :student_id, :inactivated_on, :inactivation_reason
INACTIVATION_REASONS = [{ id: 1, short_name: "HTY", name: "You didn't study enough!"},
{ id: 2, short_name: "KS", name: "Graduated!"},
{ id: 3, short_name: "SBK",name: "Other Reason"}]
Class methods
class << self
def inactivation_reason_ids
INACTIVATION_REASONS.collect{|v| v[:id]}
end
def inactivation_reason_names
INACTIVATION_REASONS.collect{|v| v[:name]}
end
def inactivation_reason_name(id)
INACTIVATION_REASONS.select{|t| t[:id] == id}.first[:name]
end
def inactivation_reason_short_name(id)
INACTIVATION_REASONS.select{|t| t[:id] == id}.first[:short_name]
end
def inactivation_reason_id(name)
INACTIVATION_REASONS.select{|t| t[:name] == name}.first[:id]
end
end
# Instance methods
def inactivation_reason_name
self.class.inactivation_reason_name(self.inactivation_reason)
end
def inactivation_reason_short_name
self.class.inactivation_reason_short_name(self.inactivation_reason)
end
def inactivation_reason_id
self.class.inactivation_reason_id(self.inactivation_reason)
end
end
I would like to call these inactivation reasons from my controller, which is app/controllers/student/session_controllers.rb file:
class Student::SessionsController < ApplicationController
layout 'session'
def create
student = Student.authenticate(params[:student_number], params[:password])
if student.active
session[:student_id] = student.id
redirect_to student_main_path, :notice => 'Welcome!'
elsif (student and student.student_status == 3) or (student and !student.active)
flash.now.alert = "You can't login because #REASON_I_AM_TRYING_TO_CALL"
render 'new'
else
....
end
end
I would like to show students their inactivation reason on the systems if they can't login.
How can I call my INACTIVATION_REASONS from this controller file? Is it possible?
Thanks in advance!
That's just a constant, so you can call it as constant anywhere.
StudentInactivationLog::INACTIVATION_REASONS
Update
I realized actually what you want is to use a reason code or short name saved in db to represent the string.
If so, I recommend you to use the short name directly as Hash. "id" looks redundant for this light case.
INACTIVATION_REASONS = {"HTY"=>"You didn't study enough!",
"KS"=>"Graduated!",
"SBK"=>"Other Reason"}
validates :inactivation_reason, inclusion: { in: INACTIVATION_REASONS.keys,
message: "%{value} is not a valid short name" }
def full_reason_message
INACTIVATION_REASONS[self.inactivation_reason]
end
Then, to show full message of a reason in controller
reason = #student.full_reason_message
This is the idea. I havn't checked your other model codes. You'll need to save reason as the short name instead of id, and need to revise/remove some code if you decide to use it in this way.

How can I lessen the verbosity of my populate method?

I wrote a form object to populate an Order, Billing, and Shipping Address objects. The populate method looks pretty verbose. Since the form fields don't correspond to Address attributes directly, I'm forced to manually assign them. For example:
shipping_address.name = params[:shipping_name]
billing_address.name = params[:billing_name]
Here's the object. Note that I snipped most address fields and validations, and some other code, for brevity. But this should give you an idea. Take note of the populate method:
class OrderForm
attr_accessor :params
delegate :email, :bill_to_shipping_address, to: :order
delegate :name, :street, to: :shipping_address, prefix: :shipping
delegate :name, :street, to: :billing_address, prefix: :billing
validates :shipping_name, presence: true
validates :billing_name, presence: true, unless: -> { bill_to_shipping_address }
def initialize(item, params = nil, customer = nil)
#item, #params, #customer = item, params, customer
end
def submit
populate
# snip
end
def order
#order ||= #item.build_order do |order|
order.customer = #customer if #customer
end
end
def shipping_address
#shipping_address ||= order.build_shipping_address
end
def billing_address
#billing_address ||= order.build_billing_address
end
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
if order.bill_to_shipping_address?
billing_address.name = params[:shipping_name]
billing_address.street = params[:shipping_street]
# Repeat for city, state, post, code, etc...
else
billing_address.name = params[:billing_name]
billing_address.street = params[:billing_street]
# Repeat for city, state, post, code, etc...
end
end
end
Here's the controller code:
def new
#order_form = OrderForm.new(#item)
end
def create
#order_form = OrderForm.new(#item, params[:order], current_user)
if #order_form.submit
# handle payment
else
render 'new'
end
end
Noe I am not interested in accepts_nested_attributes_for, which presents several problems, hence why I wrote the form object.
def populate
order.email = params[:email]
shipping_params = %i[shipping_name shipping_street]
billing_params = order.bill_to_shipping_address? ?
shipping_params : %i[billing_name billing_street]
[[shipping_address, shipping_params], [billing_address, billing_params]]
.each{|a, p|
a.name, a.street = params.at(*p)
}
end
How about
class Order < ActiveRecord::Base
has_one :shipping_address, class_name: 'Address'
has_one :billing_address, class_name: 'Address'
accepts_nested_attributes_for :shipping_address, :billing_address
before_save :clone_shipping_address_into_billing_address, if: [check if billing address is blank]
Then when you set up the form, you can have fields_for the two Address objects, and side step the populate method entirely.
A possible fix would be to use a variable for retrieving those matching params, like so:
def populate
order.email = params[:email]
shipping_address.name = params[:shipping_name]
shipping_address.street = params[:shipping_street]
# etc...
#set a default state
shipping_or_billing = "shipping_"
#or use a ternary here...
shipping_or_billing = "billing_" if order.bill_to_shipping_address?
billing_address.name = params["shipping_or_billing" + "name"]
billing_address.street = params["shipping_or_billing" + "street"]
...
end
Your address classes should probably have a method that would set the values for all the address properties from a hash that it would receive as an argument.
That way your populate method would only check for order.bill_to_shipping_address? and them pass the correct dictionary to the method I'm suggesting.
That method on the other hand, would just assign the values from the hash to the correct properties, without the need for a conditional check.

attr_accessor variable nil when doing before_validation callback

I am doing a before_validation as follows:
event.rb
attr_accessor :start_date
attr_accessible :start_time #recorded in database as a datetime
before_validation :build_start_time
...
def build_start_time
begin
self.start_time = DateTime.parse(start_date)
rescue
errors.add(:start_date, "invalid date")
return false
end
end
and the controller looks like:
def create
#event = events.build(params[:event])
if #event.save
# some other method calls
redirect_to #event
else
redirect_to :root
end
end
start_date is being set by a <%= f.text_field :start_date %> call in a form view, and when I check the params it is being passed to the 'Create' method of the model controller correctly, but in the build_start_time method it is nil, so self.start_time is not being set. Can you explain why it would be nil and what the solution would be? I also tried referring to it as self.start_date but that didn't make a difference.
Thanks
Have you tried making start_date also accessible?
Either you call attr_accessible with start_date so build() can actually set it, or you can change your controller to:
def create
#event = events.build(params[:event])
#event.start_date = params[:event][:start_date]
if #event.save
# some other method calls
redirect_to #event
else
redirect_to :root
end
end
tente assim.
#app/models/adm/video.rb
class Adm::Video < ActiveRecord::Base
validates :titulo, :url_codigo, presence: true
before_validation(on: [ :create, :update ]) do
self.url_codigo = parse_youtube(url_codigo) #url_codigo = params[:adm_video][:url_codigo]
end
private
# pega só o codigo do link youtube para inserir no banco
def parse_youtube(url)
if !url.blank?
regex = /(?:.be\/|\/watch\?v=|\/(?=p\/))([\w\/\-]+)/
return url.match(regex)[1] # https://www.youtube.com/watch?v=iX_rKHnKJSg = iX_rKHnKJSg
end
end
end
grava no banco de dados sò código do video = iX_rKHnKJSg = https://www.youtube.com/watch?v=iX_rKHnKJSg = iX_rKHnKJSg.
records in the database sò code iX_rKHnKJSg video = # = https://www.youtube.com/watch?v=iX_rKHnKJSg iX_rKHnKJSg

Bulk insert using one model

I'm trying to create a form using textarea and a submit button that will allow users to do bulk insert. For example, the input would look like this:
0001;MR A
0002;MR B
The result would look like this:
mysql> select * from members;
+------+------+------+
| id | no | name |
+------+------+------+
| 1 | 0001 | MR A |
+------+------+------+
| 2 | 0002 | MR B |
+------+------+------+
I'm very new to Rails and I'm not sure on how to proceed with this one. Should I use attr_accessor? How do I handle failed validations in the form view? Is there any example? Thanks in advance.
Update
Based on MissingHandle's comment, I created a Scaffold and replace the Model's code with this:
class MemberBulk < ActiveRecord::Base
attr_accessor :member
def self.columns
#columsn ||= []
end
def self.column(name, sql_type = nil, default = nil, null = true)
columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
column :data, :text
validates :data, :create_members, :presence => true
def create_members
rows = self.data.split("\r\n")
#member = Array.new
rows.each_with_index { |row, i|
rows[i] = row.strip
cols = row.split(";")
p = Member.new
p.no = cols[0]
p.name = cols[1]
if p.valid?
member << p
else
p.errors.map { |k, v| errors.add(:data, "\"#{row}\" #{v}") }
end
}
end
def create_or_update
member.each { |p|
p.save
}
end
end
I know the code is far from complete, but I need to know is this the correct way to do it?
class MemberBulk < ActiveRecord::Base
#Tells Rails this is not actually tied to a database table
# or is it self.abstract_class = true
# or #abstract_class = true
# ?
abstract_class = true
# members holds array of members to be saved
# submitted_text is the data submitted in the form for a bulk update
attr_accessor :members, :submitted_text
attr_accessible :submitted_text
before_validation :build_members_from_text
def build_members_from_text
self.members = []
submitted_text.each_line("\r\n") do |member_as_text|
member_as_array = member_as_text.split(";")
self.members << Member.new(:number => member_as_array[0], :name => member_as_array[1])
end
end
def valid?
self.members.all?{ |m| m.valid? }
end
def save
self.members.all?{ |m| m.save }
end
end
class Member < ActiveRecord::Base
validates :number, :presence => true, :numericality => true
validates :name, :presence => true
end
So, in this code, members is an array that is a collection of the individual Member objects. And my thinking is that as much as possible, you want to hand off work to the Member class, as it is the class that will actually be tied to a database table, and on which you can expect standard rails model behavior. In order to accomplish this, I override two methods common to all ActiveRecord models: save and valid. A MemberBulk will only be valid if all it's members are valid and it will only count as saved if all of it's members are saved. You should probably also override the errors method to return the errors of it's underlying members, possibly with an indication of which one it is in the submitted text.
In the end I had to change from using Abstract Class to Active Model (not sure why, but it stoppped working the moment I upgrade to Rails v3.1). Here's the working code:
class MemberBulk
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
attr_accessor :input, :data
validates :input, presence: true
def initialize(attributes = {})no
attributes.each do |name, value|
send("#{name}=", value) if respond_to?("#{name}=")
end
end
def persisted?
false
end
def save
unless self.valid?
return false
end
data = Array.new
# Check for spaces
input.strip.split("\r\n").each do |i|
if i.strip.empty?
errors.add(:input, "There shouldn't be any empty lines")
end
no, nama = i.strip.split(";")
if no.nil? or nama.nil?
errors.add(:input, "#{i} doesn't have no or name")
else
no.strip!
nama.strip!
if no.empty? or nama.empty?
errors.add(:input, "#{i} doesn't have no or name")
end
end
p = Member.new(no: no, nama: nama)
if p.valid?
data << p
else
p.errors.full_messages.each do |error|
errors.add(:input, "\"#{i}\": #{error}")
end
end
end # input.strip
if errors.empty?
if data.any?
begin
data.each do |d|
d.save
end
rescue Exception => e
raise ActiveRecord::Rollback
end
else
errors.add(:input, "No data to be processed")
return false
end
else
return false
end
end # def
end

Resources