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
Related
I have a rails application for creating volumes and have written two custom validators using ActiveModel::Validator.
volume.rb:
class Volume < ActiveRecord::Base
include UrlSafeCode
include PgSearch::Model
include ActiveModel::Validations
validates :user_id, presence: true
validates_with Validators::VolumeValidator
validates_with Validators::CreateVolumeValidator, on: :create
def self.digest text
Digest::SHA256.hexdigest(text)
end
def text=(new_text)
new_text.rstrip!
new_text.downcase!
self.text_digest = Volume.digest(new_text)
super(new_text)
end
My Problem: The CreateVolumeValidator checks if a record with the same text_digest is already in the database. I only want to run this when creating a new volume so that I can still update existing volumes. However, adding on: :create to the CustomVolumeValidator causes the validator to stop working.
I've read through a lot of the other entries about similar issues and haven't found a solution. I am pretty sure I am missing something about when different attributes are getting created, validated, and saved, but I haven't worked with custom validations much, and I'm lost.
Here is the other relevant code.
volumes_controller.rb
def new
#volume = Volume.new
end
def create
our_params = params
.permit(:text, :description)
if params[:text].nil?
render :retry
return
end
text = params[:text].read.to_s
text_digest = Volume.digest(text)
#description = our_params[:description]
begin
#volume = Volume.where(text_digest: text_digest)
.first_or_create(text: text, user: current_user, description: our_params[:description])
rescue ActiveRecord::RecordNotUnique
retry
end
if #volume.invalid?
render :retry
return
end
render :create
end
def edit
get_volume
end
def update
get_volume
unless #volume
render nothing: true, status: :not_found
return
end
#volume.update(params.require(:volume).permit(:text, :description))
if #volume.save
redirect_to volume_path(#volume.code)
else
flash[:notice] = #volume.errors.full_messages.join('\n')
render :edit
end
end
def get_volume
#volume = Volume.where(code: params.require(:code)).first
end
create_volume_validator.rb
class Validators::CreateVolumeValidator < ActiveModel::Validator
def validate(volume)
existing_volume = Volume.where(text_digest: volume.text_digest).first
if existing_volume
existing_volume_link = "<a href='#{Rails.application.routes.url_helpers.volume_path(existing_volume.code)}'>here</a>."
volume.errors.add :base, ("This volume is already part of the referral archive and is available " + existing_volume_link).html_safe
end
end
end
If your goal is for all Volume records to have unique text_digest, you are better off with a simple :uniqueness validator (and associated DB unique index).
However, the reason your existing code isn't working is:
Volume.where(text_digest: text_digest).first_or_create(...)
This returns either the first Volume with the matching text_digest or creates a new one. But that means if there is a conflict, no object is created, and therefore your (on: :create) validation doesn't run. Instead, it simply sets #volume to the existing object, which is, by definition, valid. If there is no matching record, it does call your validator, but there's nothing to validate because you've already proved there is no text_digest conflict.
You could resolve by replacing the first_or_create with create, but again, you are vastly better off with a unique index & validator (with custom message if you like).
Need help understanding this code, as what to my knowledge I know "<<" append to a collection but here it saves the record correctly, how come it does without calling .save method?
#user.rb
has_many :saved_properties, through: :property_saves, source: :property
#users_controller.rb
def update
if #user.saved_properties << Property.find(params[:saved_property_id])
render plain: "Property saved"
end
In the has_many documentation it says:
Adds one or more objects to the collection by setting their foreign
keys to the collection's primary key. Note that this operation
instantly fires update SQL without waiting for the save or update
call on the parent object, unless the parent object is a new record.
Maybe looking at the source code will help you. This is my trail of searches based on the << method in activerecord:
def <<(*records)
proxy_association.concat(records) && self
end
rails/collection_proxy.rb at 5053d5251fb8c03e666f1f8b765464ec33e3066e · rails/rails · GitHub
def concat(*records)
records = records.flatten
if owner.new_record?
load_target
concat_records(records)
else
transaction { concat_records(records) }
end
end
rails/collection_association.rb at 5053d5251fb8c03e666f1f8b765464ec33e3066e · rails/rails · GitHub
def concat_records(records, should_raise = false)
result = true
records.each do |record|
raise_on_type_mismatch!(record)
add_to_target(record) do |rec|
result &&= insert_record(rec, true, should_raise) unless owner.new_record?
end
end
result && records
end
rails/collection_association.rb at 5053d5251fb8c03e666f1f8b765464ec33e3066e · rails/rails · GitHub
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
set_inverse_instance(record)
if raise
record.save!(validate: validate)
else
record.save(validate: validate)
end
end
https://github.com/rails/rails/blob/5053d5251fb8c03e666f1f8b765464ec33e3066e/activerecord/lib/active_record/associations/has_many_association.rb#L32
def insert_record(record, validate = true, raise = false)
ensure_not_nested
if record.new_record? || record.has_changes_to_save?
if raise
record.save!(validate: validate)
else
return unless record.save(validate: validate)
end
end
save_through_record(record)
record
end
https://github.com/rails/rails/blob/5053d5251fb8c03e666f1f8b765464ec33e3066e/activerecord/lib/active_record/associations/has_many_through_association.rb#L38
As you can see, in the end, it does call the save method.
Disclaimer: I'm not that familiar with Rails souce code, but you have interesting question.
In a has_many relationship the link information is saved in the target record. This means that << would have to modify that record in order to add it to the set.
Perhaps intending convenience, ActiveRecord automatically saves these for you when making an assignment if the assignment was successful. The exception is for new records, the record they're being associated with doesn't have any identifier so that has to be delayed. They are saved when the record they're associated with is finally created.
This can be a little confusing, perhaps unexpected, but it's actually the thing you'd want to happen 99% of the time. If you don't want that to happen you should manipulate the linkage manually:
property = Property.find(params[:saved_property_id])
property.user = #user
property.save!
That's basically equivalent but a lot more verbose.
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
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
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.