Date range validations wont allow update - ruby-on-rails

I have some custom validations on my reservation_start and reservation_end attributes on my model
class DateRangeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.date_ranges.each do |range|
if range.include? value
record.errors.add(attribute, "#{options[:name]} not available")
end
end
end
end
The Reservation model:
class Reservation < ActiveRecord::Base
belongs_to :user
belongs_to :transport
validates_presence_of :user, :transport, :reservation_start, :reservation_end
validates :reservation_start,
date_range: {name: "Start date"},
date_past: true
validates :reservation_end,
date_range: {name: "End date"},
date_subset: true,
start_bigger: true
def date_ranges
reservations = Reservation.where(["transport_id =?", transport_id])
reservations.map { |e| e.reservation_start..e.reservation_end }
end
end
The problem with this is that if I want to for example, edit a record to decrease the reservation_end by one day the validations kicks in. I tried setting the validation to on: :create but that is not a good idea.
What is the best way to proceed?

My initial thought is to change your query in date_ranges to exclude the current record, should it already be persisted (ie. the id field has been populated). It would look something like:
def date_ranges
reservations = Reservation.where({transport_id: transport_id})
reservations = reservations.where(['id <> ?', id]) if !id.nil?
reservations.map { |e| e.reservation_start..e.reservation_end }
end
The line reservations = reservations.where(['id <> ?', id]) if !id.nil? should exclude the current record iff (if and only if) it has already been persisted, in which case the validation should pass when editing a previously created Reservation so long as it does not overlap any other Reservation.

Related

Handling join table entries based on association attributes

TL;DR
What is the best way to create join table entries based on a form with the attributes of a association, like a bar code or a plate number?
Detailed explanation
In this system that records movements of items between storage places, there is a has_many_and_belongs_to_many relationship between storage_movements and storage_items because items can be moved multiple times and multiple items can be moved at once.
These items are previously created and are identified by a plate number that is physically attached to the item and recorded on its creation on the application.
The problem is that I need to create storage_movements with a form where the user inputs only the plate number of the storage_item that is being moved but I cant figure it out a way to easily do this.
I have been hitting my head against this wall for some time and the only solution that I can think of is creating nested fields on the new storage_movements form for the storage_items and use specific code on the model to create, update and delete these storage_movements by explicitly querying these plate numbers and manipulating the join table entries for these actions.
Is this the correct way of handling the problem? The main issue with this solution is that I can't seem to display validation errors on the specific plates number that are wrong (I'm using simple_forms) because I don't have storage_item objects to add errors.
Below there is a snipped of the code for the form that I'm currently using. Any help is welcome :D
# views/storage_movements/_form.html.erb
<%= simple_form_for #storage_movement do |movement_form| %>
#Other form inputs
<%= movement_form.simple_fields_for :storage_items do |item_form| %>
<%= item_form.input :plate, label: "Plate number" %>
<% end %>
<% end %>
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many_and_belongs_to_many :storage_items, inverse_of: :storage_movements, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
... several callbacks and validations ...
end
# models/storage_item.rb
class StorageItem < ActiveRecord::Base
has_many_and_belongs_to_many :storage_movements, inverse_of: :storage_items
... more callbacks and validations ...
end
The controllers were the default generated ones.
This was my solution, it really "feels" wrong and the validations also are not shown like I want it to... But it was what I could come up with... Hopefully it helps someone.
I created the create_from_plates and update_from_plates methods on the model to handle the create and update and updated the actions of the controller to use them.
Note: had to switch to a has_many through association due to callback necessities.
# models/storage_movement.rb
class StorageMovement < ActiveRecord::Base
has_many :movements_items, dependent: :destroy, inverse_of: :storage_movement
has_many :storage_items, through: :movements_items, inverse_of: :allocations, validate: true
accepts_nested_attributes_for :storage_items, allow_destroy: true
validate :check_plates
def StorageMovement::create_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
unless items_attributes.nil?
item_plates = items_attributes.collect {|k, h| h["plate"]}
items = StorageItem.where, plate: item_plates
end
if not items_attributes.nil? and item_plates.length == items.count
new_allocation = Allocation.new attributes
movements_items.each {|i| new_allocation.items << i}
return new_allocation
else
Allocation.new mov_attributes
end
end
def update_from_plates mov_attributes
attributes = mov_attributes.to_h
items_attributes = attributes.delete "items_attributes"
if items_attributes.nil?
self.update mov_attributes
else
transaction do
unless items_attributes.nil?
items_attributes.each do |k, item_attributes|
item = StorageItem.find_by_plate(item_attributes["plate"])
if item.nil?
self.errors.add :base, "The plate #{item_attributes["plate"]} was not found"
raise ActiveRecord::Rollback
elsif item_attributes["_destroy"] == "1" or item_attributes["_destroy"] == "true"
self.movements_items.destroy item
elsif not self.items.include? item
self.movements_items << item
end
end
end
self.update attributes
end
end
end
def check_plates
movements_items.each do |i|
i.errors.add :plate, "Plate not found" if StorageItem.find_by_plate(i.plate).nil?
end
end
... other validations and callbacks ...
end
With this, the create works as I wanted, because, in case of a error, the validation adds the error to the specific item attribute. But the update does not because it has to add the error to the base of the movement, since there is no item.

rails serialized array validation

I need to validate email saving in email_list. For validation I have EmailValidator. But I cannot figure out how to use it in pair of validates_each. Are there any other ways to make validations?
class A < ActiveRecord::Base
serialize :email_list
validates_each :email_list do |r, a, v|
# what should it be here?
# EmailValidator.new(options).validate_each r, a, v
end
end
validates_each is for validating multiple attributes. In your case you have one attribute, and you need to validate it in a custom way.
Do it like this:
class A < ActiveRecord::Base
validate :all_emails_are_valid
...
private
def all_emails_are_valid
unless self.email_list.nil?
self.email_list.each do |email|
if # email is valid -- however you want to do that
errors.add(:email_list, "#{email} is not valid")
end
end
end
end
end
Note that you could also make a custom validator for this or put the validation in a proc on the validate call. See here.
Here's an example with a custom validator.
class A < ActiveRecord::Base
class ArrayOfEmailsValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
return if value.nil?
value.each do |email|
if # email is valid -- however you want to do that
record.errors.add(attribute, "#{email} is not valid")
end
end
end
end
validates :email_list, :array_of_emails => true
...
end
Of course you can put the ArrayOfEmailsValidator class in, i.e., lib/array_of_emails_validator.rb and load it where you need it. This way you can share the validator across models or even projects.
I ended up with this:
https://gist.github.com/amenzhinsky/c961f889a78f4557ae0b
You can write your own EmailValidator according to rails guide and use the ArrayValidator like:
validates :emails, array: { email: true }

Rails validations failing due to other side of association not being set

Problem
class Match
has_many :items
end
class Item
belongs_to :match
validates :match, presence: true
end
class ItemBuilder
def self.build
5.times.map { |_| Item.new }
end
end
class MatchBuilder
def self.build
Match.new(items: ItemBuilder.build)
end
end
match = MatchBuilder.build
match.save # Validation fails because Item#match isn't set!
It seems to me that rails should set the Item#match association when assigning Match#items, but this is not the case.
In reality, the ItemBuilder builds Items from information from an API. I'd prefer it not to have knowledge of where those Items are going to be put, so I can't pass the match into ItemBuilder.
I'd also prefer to not have the Matchbuilder aware of the internals of the Items returned from ItemBuilder, so I can't do
class MatchBuilder
def self.build
items = ItemBuilder.build
match = Match.new(items: items)
items.each do |item|
item.match = match
end
end
end
Is there any way of getting around this, without explicitly setting Item#match?
Possible Solutions
Remove the validation, and leave the database to do the not null validation
Assign Item#match in a before_validation filter
I'd do something like this:
match = Match.new
5.times { match.items.build }
match.save
Not sure if autosave: true is needed for items association in this case.

How can I validate that two associated objects have the same parent object?

I have two different objects which can belong to one parent object. These child objects can both also belong to each other (many to many). What's the best way to ensure that child objects which belong to each other also belong to the same parent object.
As an example of what I'm trying to do I have a Kingdom which has both many People and Land. The People model would have a custom validate which checks each related Land and error.adds if one has a mismatched kingdom_id. The Land model would have a similar validate.
This seems to work, but when updating it allows the record to save the 'THIS IS AN ERROR' error is in people.errors, however the Land which raised the error has been added to the People collection.
kingdom = Kingdom.create
people = People.create(:kingdom => kingdom)
land = Land.create(:kingdom_id => 999)
people.lands << land
people.save
puts people.errors.inspect # #messages={:base=>["THIS IS AN ERROR"]
puts people.lands.inspect # [#<Land id: 1...
Ideally I'd want the error to cancel the record update. Is there another way I should be going about this, or am I going in the wrong direction entirely?
# Models:
class Kingdom < ActiveRecord::Base
has_many :people
has_many :lands
end
class People < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :lands
validates :kingdom_id, :presence => true
validates :kingdom, :associated => true
validate :same_kingdom?
private
def same_kingdom?
if self.lands.any?
errors.add(:base, 'THIS IS AN ERROR') unless kingdom_match
end
end
def kingdom_match
self.lands.each do |l|
if l.kingdom_id != self.kingdom_id
return false
end
end
end
end
class Land < ActiveRecord::Base
belongs_to :kingdom
has_and_belongs_to_many :people
end
Firstly, the validation won't prevent the record from being added to the model's unpersisted collection. It will prevent the revised collection from being persisted to the database. So the model will be in an invalid state, and flagged as such with the appropriate errors. To see this, you can simply reload the people object.
You also have an error in your logic - the kingdom_match method will never return true even if no invalid kingdom_id's are found. You should add a line to fix this:
def kingdom_match
self.lands.each do |l|
return false if l.kingdom_id != self.kingdom_id
end
true
end
And you can make this validation a bit more concise and skip the kingdom_match method entirely:
def same_kingdom?
if self.lands.any?{|l| l.kingdom_id != self.kingdom_id }
errors.add(:base, 'THIS IS AN ERROR')
end
end

How can I make Rails ActiveRecord automatically truncate values set to attributes with maximum length?

Assuming that I have a class such as the following:
class Book < ActiveRecord::Base
validates :title, :length => {:maximum => 10}
end
Is there a way (gem to install?) that I can have ActiveRecord automatically truncate values according to maximum length?
For instance, when I write:
b = Book.new
b.title = "123456789012345" # this is longer than maximum length of title 10
b.save
should save and return true?
If there is not such a way, how would you suggest that I proceed facing such a problem more generally?
Well, if you want the value truncated if its too long, you don't really need a validation, because it will always pass. I'd handle that like this:
class Book < ActiveRecord::Base
before_save :truncate_values
def truncate_values
self.title = self.title[0..9] if self.title.length > 10
end
end
I have come up with a new validator that does truncation. Here is how I did that:
I created the "validators" folder inside "app" folder and then created the file "length_truncate_validator.rb" with the following content:
class LengthTruncateValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
ml = options[:maximum]
record.send("#{attribute}=", value.mb_chars.slice(0,ml)) if value.mb_chars.length > ml unless value.nil? or ml.nil?
end
class << self
def maximum(record_class, attribute)
ltv = record_class.validators_on(attribute).detect { |v| v.is_a?(LengthTruncateValidator) }
ltv.options[:maximum] unless ltv.nil?
end
end
end
And inside my model class I have something like:
class Book < ActiveRecord::Base
validates :title, :length_truncate => {:maximum => 10}
end
which is quite handy and works the way I require.
But still, if you think that this one can be improved or done in another way, you are welcome.
This may not have been an option in 2011, but now there's a before_validation callback that will work.
class Book < ApplicationRecord
before_validation do
if self.params && self.params.length > 1000
self.params = self.title[0...10]
end
end
validate :title, length: { maximum: 10 }, allow_nil: true
end
I like the idea of using the before_validation callback. Here's my stab that automatically truncates all strings to within the database's limit
before_validation :truncate_strings
def truncate_strings
self.class.columns.each do |column|
next if column.type != :string
if self[column.name].length > column.limit
self[column.name] = self[column.name][0...column.limit]
end
end
end

Resources