Before save validation not working properly - ruby-on-rails

I have a concern that checks addresses and zip codes with the intention of returning an error if the zip code does not match the state that is inputed. I also don't want the zip code to save unless the problem gets fixed.
The problem that I am having is that it appears that the if I submit the form, the error message in create pops up and I am not able to go through to the next page, but then somehow the default zip code is still saved. This only happens on edit. The validations are working on new.
I don't know if I need to share my controller, if I do let me know and I certainly will.
In my model I just have a
include StateMatchesZipCodeConcern
before_save :verify_zip_matches_state
Here is my concern
module StateMatchesZipCodeConcern
extend ActiveSupport::Concern
def verify_zip_matches_state
return unless zip.present? && state.present?
state_search_result = query_zip_code
unless state_search_result.nil?
return if state_search_result.upcase == state.upcase
return if validate_against_multi_state_zip_codes
end
errors[:base] << "Please verify the address you've submitted. The postal code #{zip.upcase} is not valid for the state of #{state.upcase}"
false
end
private
def query_zip_code
tries ||= 3
Geocoder.search(zip).map(&:state_code).keep_if { |x| Address::STATES.values.include?(x) }.first
rescue Geocoder::OverQueryLimitError, Timeout::Error
retry unless (tries -= 1).zero?
end
def validate_against_multi_state_zip_codes
::Address::MULTI_STATE_ZIP_CODES[zip].try(:include?, state)
end
end

Related

How to trigger Rails ActiveRecord Validations manually ? (ActiveAdmin)

I'm currently working on a project using Rails 5.2.6. (that's a pretty big project, and i wont be able to update rails version)
We're using ActiveAdmin to handle the admin part, and I have a model in which i save a logo using ActiveStorage.
Recently, i needed to perform validations over that logo properties. (File format, Size and ratio). For that matter, I've been searching through multiple solutions, including the ActiveStorageValidations gem.
This one provided me half of a solution because the validators worked as expected, but the logo would be stored and associated to the model even when the validators would fail.
(I'm redirected to the edit form with the different error displayed on over the logo field, but still the logo would be updated). This is apparently a known issue, from ActiveStorage that is supposedly fixed in Rails 6, but I'm not able to update the project. (And ActiveStorageValidations does not want to do anything about it as well according to an issue i found on the GitHub)
In the end I managed to make a working solution "by hand", using some before_actions that would make the required checks on the image, and render the edit form again before anything could happen if some of the checks fail.
I'm also adding some errors to my model in the process so that when the edit view from active admin is rendered, the errors display correctly on top of the form and the logo field.
Here is the code behind (admin/mymodel.rb)
controller do
before_action :prevent_save_if_invalid_logo, only: [:create, :update]
private
# Active Storage Validations display error messages but still attaches the file and persist the model
# That's a known issue, which is solved in Rails 6
# This is a workaround to make it work for our use case
def prevent_save_if_invalid_logo
return unless params[:my_model][:logo]
file = params[:my_model][:logo]
return if valid_logo_file_format(file) && valid_logo_aspect_ratio(file) && valid_logo_file_size(file)
if #my_model.errors.any?
render 'edit'
end
end
def valid_logo_aspect_ratio(file)
width, height = IO.read(file.tempfile.path)[0x10..0x18].unpack('NN')
valid = (2.to_f / 1).round(3) == (width.to_f / height).round(3) ? true : false
#my_model.errors[:logo] << "Aspect ratio must be 2 x 1" unless valid
valid
end
def valid_logo_file_size(file)
valid = File.size(file.tempfile) < 200.kilobytes ? true : false
#my_model.errors[:logo] << "File size must be < 200 kb" unless valid
valid
end
def valid_logo_file_format(file)
content_type = file.present? && file.content_type
#my_model.errors[:logo] << "File must be a PNG" unless content_type
content_type == "image/png" ? true : content_type
end
end
This works pretty well, but now my problem is that if any other error occurs on the form at the same time than a logo error (mandatory fields or other stuff), then it's not getting validated, and the errors are not displayed as this renders the edit view before other validations can occur.
My question is, do i have any mean to manually trigger the validations on my model at this level, so that every other field is getting validated and #my_model.errors is populated with the right errors, resulting in the form being able to display every form errors wether logo is involved or not.
Like so :
...
def prevent_save_if_invalid_logo
return unless params[:my_model][:logo]
file = params[:my_model][:logo]
return if valid_logo_file_format(file) && valid_logo_aspect_ratio(file) && valid_logo_file_size(file)
if #my_model.errors.any?
# VALIDATE WHOLE FORM SO OTHER ERRORS ARE CHECKED
render 'edit'
end
end
...
If anybody has an idea about how to do this, or a clue on how to do things in a better way, any clue would be much appreciated!
Approach 1:
Instead of explicitly rendering 'edit' which halts the default ActiveAdmin flow you could simply remove the file form parameters and let things go the standard way:
before_action :prevent_logo_assignment_if_invalid, only: [:create, :update]
def prevent_logo_assignment_if_invalid
return unless params[:my_model][:logo]
file = params[:my_model][:logo]
return if valid_logo_file_format?(file) && valid_logo_aspect_ratio?(file) && valid_logo_file_size?(file)
params[:my_model][:logo] = nil
# or params[:my_model].delete(:logo)
end
Approach 2:
The idea is the same, but you can also do it on model level.
You can prevent file assignment by overriding the ActiveStorage's setter method:
class MyModel < ApplicationModel
def logo=(file)
return unless valid_logo?(file)
super
end
By the way, your valid_logo_file_format method will always return true.

Ruby on rails. Callbacks, specifying changed attribute

I'm trying to send emails when a certain attribute changes in my model.
My model has a string which I set to hired reject seen and notseen.
For example, If the attribute gets changed to reject I want to send an email, and if it's changed to hired I want to send a different one.
In my model I have:
after_update :send_email_on_reject
def send_email_on_reject
if status_changed?
UserMailer.reject_notification(self).deliver
end
end
Which sends the email when the status gets changed regardless of what the status is. I don't know how to specify this. I have tried something like:
def send_email_on_reject
if status_changed?
if :status == "reject"
UserMailer.reject_notification(self).deliver
end
end
end
which just doesn't send the email.
I have been searching but cannot find any up to date similar questions/examples.
Thanks in advance.
def send_email_on_reject
if status_changed? && status == "reject"
UserMailer.reject_notification(self).deliver
end
end

Custom validation Rails for boolean attributes

I need to add custom validation to my AR model: there is Order model with 'approved' attribute - this attribute cannot be approved twice. It's boolean attribute. I don't understand how can I check if this attribute has been approved already.
validate :cannot_be_approved_twice
def cannot_be_approved_twice
errors[:base] << ERROR_MSG if ...
end
How can I check it? Thanks!
If it is approved already then the value of that field would be something and not nil so you can do it like this:
def cannot_be_approved_twice
errors[:base] << ERROR_MSG unless approved.nil?
end
And if suppose it would have true value if it is approved then you can do:
def cannot_be_approved_twice
errors[:base] << ERROR_MSG if approved
end
Hope this helps.
I'd prefer attaching the error to the approved attribute, so the error would be something like this
validate :prevent_double_approval, if: :some_check
def prevent_double_approval
errors[:approved] << "can't be approved more than once" if approved?
end
This some_check method should be true when you detect that the user is trying to change the value, then the inner check if approved? checks if the value is already true before that second approval.
You can do it by checking previous value in your validation. Something like this:
def cannot_be_approved_twice
errors[:approved] << "can't be approved more than once" unless self.approved_was.nil?
end
The "_was" postfix gives the value before the update. So, if it was not nil then someone has set it to true or false (meaning approved or disapproved).
Update
I read through your comments and realized that what when order is created it is set to false (initially, I thought that it is set to nil, so you could approve or disapprove it). Thus, if the field initially set to false, then the method above becomes:
def cannot_be_approved_twice
errors[:approved] << "can't be approved more than once" if self.approved_was # if approved field was true (approved)
end
One more thing, with the way you are doing order update (order.update!(), with the bung), and your order has been approved then your application will fail with validation error because bung(!) explicitly tells application to fail in case of validation errors. You probably want to use just the "update" method and not "update!".

Model custom validation passing true even though it is not

I'm very confused about this. My model has the following custom validation:
def custom_validation
errors[:base] << "Please select at least one item" if #transactionparams.blank?
end
Basically it's checking to make sure that certain parameters belonging to a different model are not blank.
def request_params
#requestparams = params.require(:request).permit(:detail, :startdate, :enddate)
#transactionparams = params["transaction"]
#transactionparams = #transactionparams.first.reject { |k, v| (v == "0") || (v == "")}
end
If it's not blank, then what happens is that the record is saved, and then all kinds of other things happen.
def create
request_params
#request = #user.requests.create(#requestparams)
if #request.save
...
else
render 'new'
end
end
If the record is not saved, the re-rendered new view then shows what the errors are that stopped #request from being created. The problem is that whether or not #transactionparams.blank? is true or false, the record always fails to save, and I checked this specifically with a puts in the log.
What's happening? I read through the docs because I thought that maybe custom validators couldn't be used on other variables... but that's not the case...
Thanks!
OK actually read up on related articles. It's bad practice to ever access a variable from the controller in the model. That's why... If i put the puts inspection in the model not controller, #transactionparams is always nil.

Locking an attribute after it has a certain value

I have a model where if it is given a certain status, the status can never be changed again. I've tried to achieve this by putting in a before_save on the model to check what the status is and raise an exception if it is set to that certain status.
Problem is this -
def raise_if_exported
if self.exported?
raise Exception, "Can't change an exported invoice's status"
end
end
which works fine but when I initially set the status to exported by doing the following -
invoice.status = "Exported"
invoice.save
the exception is raised because the status is already set the exported on the model not the db (I think)
So is there a way to prevent that attribute from being changed once it has been set to "Exported"?
You can use an validator for your requirement
class Invoice < ActiveRecord::Base
validates_each :status do |record, attr, value|
if ( attr == :status and status_changed? and status_was == "Exported")
record.errors.add(:status, "you can't touch this")
end
end
end
Now
invoice.status= "Exported"
invoice.save # success
invoice.status= "New"
invoice.save # error
You can also use ActiveModel::Dirty to track the changes, instead of checking current status:
def raise_if_exported
if status_changed? && status_was == "Exported"
raise "Can't change an exported invoice's status"
end
end
Try this, only if you really want that exception to raise on save. If not, check it during the validation like #Trip suggested
See this page for more detail.
I'd go for a mix of #Trip and #Sikachu's answers:
validate :check_if_exported
def check_if_exported
if status_changed? && status_was.eql?("Exported")
errors.add(:status, " cannot be changed once exported.")
end
end
Invalidating the model is a better response than just throwing an error, unless you reeeally want to do that.
Try Rails built in validation in your model :
validate :check_if_exported
def check_if_exported
if self.exported?
errors.add(:exported_failure, "This has been exported already.")
end
end

Resources