I am overriding an attribute accessor in ActiveRecord to convert a string in the format "hh:mm:ss" into seconds. Here is my code:
class Call < ActiveRecord::Base
attr_accessible :duration
def duration=(val)
begin
result = val.to_s.split(/:/)
.map { |t| Integer(t) }
.reverse
.zip([60**0, 60**1, 60**2])
.map { |i,j| i*j }
.inject(:+)
rescue ArgumentError
#TODO: How can I correctly report this error?
errors.add(:duration, "Duration #{val} is not valid.")
end
write_attribute(:duration, result)
end
validates :duration, :presence => true,
:numericality => { :greater_than_or_equal_to => 0 }
validate :duration_string_valid
def duration_string_valid
if !duration.is_valid? and duration_before_type_cast
errors.add(:duration, "Duration #{duration_before_type_cast} is not valid.")
end
end
end
I am trying to meaningfully report on this error during validation. The first two ideas that I have had are included in the code sample.
Adding to errors inside of the accessor override - works but I am not certain if it is a nice solution.
Using the validation method duration_string_valid. Check if the other validations failed and report on duration_before_type_cast. In this scenario duration.is_valid? is not a valid method and I am not certain how I can check that duration has passed the other validations.
I could set a instance variable inside of duration=(val) and report on it inside duration_string_valid.
I would love some feedback as to whether this is a good way to go about this operation, and how I could improve the error reporting.
First, clean up your code. Move string to duration converter to the service layer. Inside the lib/ directory create StringToDurationConverter:
# lib/string_to_duration_converter.rb
class StringToDurationConverter
class << self
def convert(value)
value.to_s.split(/:/)
.map { |t| Integer(t) }
.reverse
.zip([60**0, 60**1, 60**2])
.map { |i,j| i*j }
.inject(:+)
end
end
end
Second, add custom DurationValidator validator
# lib/duration_validator.rb
class DurationValidator < ActiveModel::EachValidator
# implement the method called during validation
def validate_each(record, attribute, value)
begin
StringToDurationConverter.convert(value)
resque ArgumentError
record.errors[attribute] << 'is not valid.'
end
end
end
And your model will be looking something like this:
class Call < ActiveRecord::Base
attr_accessible :duration
validates :duration, :presence => true,
:numericality => { :greater_than_or_equal_to => 0 },
:duration => true
def duration=(value)
result = StringToDurationConverter.convert(value)
write_attribute(:duration, result)
end
end
Related
I get this error on my posts index page :
This the model :
class Post < ApplicationRecord
include Filterable
belongs_to :region
belongs_to :category
belongs_to :topic
validates :title, presence: true, length: { maximum: 500 }
validates :content, presence: true
validates :published_at, presence: true
translates :title, :content, :slug, touch: true, fallbacks_for_empty_translations: true
has_attached_file :image, styles: { thumb: "100x70#", featured: "1560x868#", small: "760x868#", big: ">1600x1600" }
validates_attachment :image, content_type: { content_type: ["image/jpeg", "image/gif", "image/png"] }
validates_attachment_presence :image
scope :published, -> (published) { where(published: (['true', true].include? published)).order(featured: :desc, published_at: :desc) }
scope :published_until_now, -> { where("published_at < ?", Time.now).merge(Post.published(true)) }
scope :topic, -> (topic_id) {
joins(:topic).where('topic_id = ?', topic_id) }
scope :category, -> (post_category) {
joins(:category).where('category_id = ?', post_category) }
scope :match, -> (search_term) {
with_translations(I18n.locale).where('content like ? or title like ?', "%#{search_term}%", "%#{search_term}%") }
self.per_page = 10
after_save :unfeature_older_posts, if: Proc.new { |post| post.featured? }
extend FriendlyId
friendly_id :title, use: :globalize
def unfeature_older_posts
featured_posts = Post.where(featured: true).where.not(id: id).order(published_at: :desc)
if featured_posts.size == 1
featured_posts.last.update(featured: false)
end
end
end
This the controller :
class PostsController < ApplicationController
before_action :get_pages_tree, :get_privacy_policy, only: [:index, :show]
def index
#filters = params.slice(:topic, :category)
#posts = Post.published_until_now
.filter(#filters)
.paginate(:page => params[:page], per_page: 11)
end
def show
#post = Post.friendly.find(params[:id])
end
end
and filter is defined here :
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
results = results.public_send(key, value) if value.present?
end
results
end
end
end
I'm not sure where to go from here. I recently upgraded to Ruby on Rails 5 and Ruby 2.7.0, I don't know if it's related.
Try replacing module ClassMethods with class_methods do.
If it works, then please keep in mind:
filter method comes from Ruby. It's defined in Array. As you can see in the doc, filter method on Array takes no argument. That's the direct cause of the error you see.
In Rails, when methods on Array are called on ActiveRecord object (in your case, Post.published_until_now) and when methods cannot be found on a model, it automatically converts itself into an Array. So, it calls filter method on Array. Generally, you don't want to define methods such as filter which is confusing.
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
How can I skip validation for nested_attribute if condition is true
aquarium.rb
has_many :fishes
accepts_nested_attributes_for :fishes,
fish.rb
belongs_to :aquarium
validates :ratio, :numericality => { :greater_than => 0 }, if: :skip_this_validation
then in aquariums_controller.rb
def some_action
#aquarium = Aquarium.new(aqua_params)
#aquarium.skip_this_validation = true # i know that this is not valid
#must skip validation for ratio and then save to DB
end
aquarium.rb
has_many :fishes
accepts_nested_attributes_for :fishes,
attr_accessor :skip_fishes_ratio_validation
fish.rb
belongs_to :aquarium
validates :ratio, :numericality => { :greater_than => 0 }, unless: proc { |f| f.aquarium&.skip_fishes_ratio_validation }
then in aquariums_controller.rb
def some_action
#aquarium = Aquarium.new(aqua_params)
#aquarium.skip_fishes_ratio_validation = true
#aquarium.save
end
You can just add the condition in method and check for the conditional validation
class Fish < ActiveRecord::Base
validates :ratio, :numericality => { :greater_than => 0 }, if: :custom_validation
private
def custom_validation
# some_condition_here
true
end
end
#aquarium.save(validate: false)
I believe skips validations on the model.
In Rails 5 you can simply pass optional: true to the belongs_to association.
So in your fish.rb, update association with the following code:
belongs_to :aquarium, optional: true
It will skip association validation if the fish object has no aquarium.
I think it is really basic question, but I cannot find solution.
I have simple help method:
def getProperNutrientValue(meal)
result = 0
if meal.ingredients.empty?
result
else
meal.ingredients.each do |i|
result += (i.quantity * #meal.products.find(i.product_id).calorific) / 100
end
result
end
end
Where "calorific" is an attribute in Product model.
class Product < ActiveRecord::Base
has_many :ingredients
has_many :meals, :through => :ingredients
validates :calorific, :numericality => true, :allow_nil => true
validates :water, :numericality => true, :allow_nil => true
I want to DRY this function, and set attribute as a variable. Then I will be can use this function for example for water attribute.
So I want to achieve something like that:
def getProperNutrientValue(meal, attribute)
result = 0
if meal.ingredients.empty?
result
else
meal.ingredients.each do |i|
result += (i.quantity * #meal.products.find(i.product_id).attribute) / 100
end
result
end
end
But of course it doesn't work... How can I fix it?
You can use send(method_name) method. I don't understand the logic behind using #meal variable. Either way there are some options to improve your code, for example:
def getProperNutrientValue(meal, attribute)
result = 0
meal.ingredients.each do |i|
result += (i.quantity * #meal.products.find(i.product_id).send(attribute).to_i) / 100
end
result
end
getProperNutrientValue(meal, :calorific)
theres an excerpt of my code:
module Configuracao
extend self
class Key
include ActiveModel::Validations
attr_accessor :name, :type, :default, :validations, :group, :available_values
def initialize(params)
params.symbolize_keys!.assert_valid_keys(:name, :type, :default, :validations, :group, :available_values)
#group = params[:group]
#name = params[:name]
#type = params[:type]
#available_values = params[:available_values]
#default = params[:default]
#validations = params[:validations]
#in this way each validation is being added for all keys
Configuracao::Key.class_eval do
validates :value, params[:validations]
end
end
end
end
so for every instance key i will have a diferent validation passed in a hash, example:
Key.new( validations: { presence: true, numericality: true } )
Key.new( validations: { length: { maximum: 30 } } )
There's a way to do it?
well i found a solution, maybe not so elegant or best way to do, but it works
def initialize(params)
params.symbolize_keys!.assert_valid_keys(:name, :type, :default, :validations, :group, :available_values)
#group = params[:group]
#name = params[:name]
#type = params[:type]
#available_values = params[:available_values]
#default = params[:default]
##current_validations = nil
##current_validations = #validations = params[:validations]
class << self
validates :value, ##current_validations unless ##current_validations.blank?
end
end
now each time i instantiate a Key, the class will be modified only for that instance
Will this work?
...
validates :all_hash_validations_pass
...
def all_hash_validations_pass
...iterate through the hash here, and validate each of them
end
If not, you should be able to use a custom validator for more control.