Custom Validator on: :create not running on rails app - ruby-on-rails

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).

Related

Filtering at create - rails

I'm trying to check if new submissions match certain aspects of existing submissions and, if so, prevent it from being created.
if ( !Book.exists?(author: #book.author) and
!Book.exists?(publisher: #book.publisher) )
or
( !Book.exists?(name: #book.name) and
!Book.exists?(genre: #book.genre) )
...create
The problem is that if the genre and the publisher match existing records, the book is not created. That's clearly not what I intend with those operators. I tried && and || and also mixed them with and and or, knowing && and || take precedence. I also tried placing the second logic into elsif. No use. I'd appreciate any help.
UPDATE:
I have this code now in the model. I deleted if #book.save in the controller. When I save, the page does nothing, but the button remains frozen at clicked.
validate :existing_book, on: :create
# also tried before_create :existing_book
def existing_book
existing_book = Book.find_by(author: self.author, publisher: self.publisher)
existing_book ||= Book.find_by(name: self.name, genre: self.genre)
if existing_book.nil?
self.save
redirect_to book_url(self)
else
throw(:abort)
redirect_to new_book_url(self)
errors.add("matching record exists")
end
end
I added throw(:abort) after reading that in Rails 5+, returning false doesn't abort the process (it didn't when I tried it).
Error message: No template found for BooksController#create, rendering head :no_content. So I'm guessing #book.save should be in the controller? But before_create still saves
--UPDATE:
I re-added .save in the controller:
if #book.save
redirect_to book_url(#book)
else
redirect_to new_book_url(#book), alert: "Please try again."
end
The model:
before_create :existing_book
# validate :existing_book, on: :create
def existing_book
existing_book = Book.find_by(author: self.author, publisher: self.publisher)
existing_book ||= Book.find_by(name: self.name, genre: self.genre)
if existing_book != nil
return false
end
end
The record still gets created. Same when before_save instead of before_create.
Your validate method should not call save, redirect, or raise any error. Those are handled by the controller code. The model-level validate should either add errors to the instance, or do nothing at all.
For example (and simplifying the code a bit to focus on the concept):
validate :existing_book
def existing_book
existing_book = Book.find_by(name: self.name)
if existing_book != nil
errors.add(:name, "already taken")
end
end
And then testing it out:
existing_name = Book.first.name
new_book = Book.new(name: existing_name)
new_book.valid? # false
new_book.errors.full_messages # => ["name already taken"]
new_book.save # false
In the controller, for example:
book = Book.new(book_params)
if book.save
# redirect to page on success
else
#errors = book.errors.full_messages
# render the `new` page, showing the errors
end
Add a before_create filter to your model. Let's suppose you name it check_existing_records.
before_create :check_existing_records # add this line to the top of your model
You want to add code inside the body of your method which will return false in case it is detected that the new instance should not be saved. Something like:
def check_existing_records
existing_book = Book.find_by(author: self.author, publisher: self.publisher)
# in case no book with same author and publisher was found, check for book
# with same name and same genre.
existing_book ||= Book.find_by(name: self.name, genre: self.genre)
# returns false if there is an existing record which matches these conditions
# and stops persistence of record in the database
existing_book.nil?
end
You can use the find_or_create_by method for this:
book = Book.find_or_create_by(author: #book.author, publisher: #book.publisher)
This command will find any existing book with the matching author and publisher or created a new book record if there is no such entry.

Infinite Loop, what is causing this?

In my Order model I have:
validates_uniqueness_of :store_order_id, on: :create
before_validation :generate_id, on: :create if :manual_order?
def manual_order?
order_type == "manual"
end
def generate_id
begin
self.store_order_id = "M-#{(SecureRandom.random_number(9e5) + 1e5).to_i}"
end while self.class.find_by(store_order_id: store_order_id)
end
controller create method:
def create
#order = Order.new
#order.order_type = "manual"
respond_to do |format|
if #order.save!
For some reason, this is causing a loop of the :generate_id
What is causing this?
I use similar code to generate a token for each order, the only difference is I don't use a validates_uniquness_of with it like i do for :generate_id. But I don't see how this is causing this loop?
I do need the validates_uniqueness_of :store_order_id, on: :create because it's also used for webhook id's in case the webhook fires twice, which is does on occasion . I do this to avoid multiple identical records. Because of this uniqueness validaton, when I create manual orders it won't save because the store_order_id needs to be unique. So i created the generate_id method, but with issues.
Assuming it was possible the uniquness was causing this, because there is no difference from code I have used many times, i tried:
validates_uniqueness_of :store_order_id, on: :create unless :manual_order?
but this didn't solve it.

How to set a property before saving to the database that is not in the form fields

In Rails 5 I can't seem to set a field without having the validation fail and return an error.
My model has:
validates_presence_of :account_id, :guid, :name
before_save :set_guid
private
def set_buid
self.guid = SecureRandom.uuid
end
When I am creating the model, it fails with the validation error saying guid cannot be blank.
def create
#user = User.new(new_user_params)
if #user.save
..
..
private
def new_user_params
params.require(:user).permit(:name)
end
2
Another issue I found is that merging fields doesn't work now either. In rails 4 I do this:
if #user.update_attributes(new_user_params.merge(location_id: #location_id)
If I #user.inspect I can see that the location_id is not set. This worked in rails 4?
How can I work around these 2 issues? Is there a bug somewhere in my code?
You have at least two options.
Set the value in the create action of your controller
Snippet:
def create
#user = User.new(new_user_params)
#user.guid = SecureRandom.uuid
if #user.save
...
end
In your model, use before_validation and add a condition before assigning a value:
Snippet:
before_validation :set_guid
def set_guid
return if self.persisted?
self.guid = SecureRandom.uuid
end
1
Use before_validation instead:
before_validation :set_guid
Check the docs.
2
Hash#merge works fine with rails ; your problem seems to be that user is not updating at all, check that all attributes in new_user_params (including location_id) ara valid entries for User.
If update_attributes fails, it will do so silently, that is, no exception will be raised. Check here for more details.
Try using the bang method instead:
if #user.update_attributes!(new_user_params.merge(location_id: #location_id))

Upload image through Paperclip without saving object

I'm working in Rails and I have two models, a prelaunch and an initiative. Basically I want a user to be able to create an initiative using the attributes of the prelaunch. Basically what I want to have happen is when a user visit's their prelaunch and is ready to turn it into an initiative, it brings them to a form that has their prelaunch information already populated and they can just add the additional info. I've managed to do this for every attribute so far except for the attached image, called :cover_image.
I think the problem is that I'm setting the initiative's cover_image to the prelaunch's cover_image on the new action of my controller, but because this is the new action and not create, I'm not saving the initiative yet. I think this means the cover_image isn't getting reuploaded yet, so #iniative.cover_image.url doesn't point to anything. It also doesn't appear to be prepopulating the file field of my form with anything.
I'm not entirely sure how feasible all of this is, but it's what the client asked for so I'm trying to make it work for them.
Here's my controller:
def new
#initiative = Initiative.new
populate_defaults(#initiative)
#initiative.build_location
3.times{ #initiative.rewards.build }
#initiative.user = current_user
if !params[:prelaunch_id].nil? && !params[:prelaunch_id].empty?
# if user is transferring a prelaunch, assign its attributes to the intiative
#prelaunch = Prelaunch.find(params[:prelaunch_id])
#initiative.assign_attributes(title: #prelaunch.title,
teaser: #prelaunch.teaser,
category: #prelaunch.category,
funding_goal: #prelaunch.funding_goal,
term: #prelaunch.campaign.term,
story: #prelaunch.story,
location: #prelaunch.campaign.location,
video_url: #prelaunch.video_url,
EIN: #prelaunch.campaign.EIN,
nonprofit: #prelaunch.nonprofit,
organization_name: #prelaunch.campaign.organization.name)
end
end
Edit:
Thanks to peterept's answer below I've managed to get the prelaunch cover_image into the form and into the create action of the initiatives controller. The problem now is that everything seems to work perfectly in the create action: the initiative gets the prelaunch's cover image, it saves without error, and it redirects to the show action.
UNFORTUNATELY, By the time it reaches the show action of the controller, #initiative.cover_image is set to the default again. I can't figure out what could possibly be happening between the successful create action and the show action.
Here are the create and show actions of the initiatives controller:
def create
if !params[:initiative][:prelaunch_id].nil? && !params[:initiative][:prelaunch_id].empty?
#prelaunch = Prelaunch.find(params[:initiative][:prelaunch_id]) # find the prelaunch if it exists
end
#initiative = Initiative.new(initiatives_params)
#initiative.user = current_user
begin
#payment_processor.create_account(#initiative)
if #initiative.save
# #prelaunch.destroy # destroy the prelaunch now that the user has created an initiative
flash[:alert] = "Your initiative will not be submitted until you review the initiative and then press 'Go Live' on the initiative page"
redirect_to initiative_path(#initiative)
else
flash[:alert] = "Initiative could not be saved: " + #initiative.errors.messages.to_s
render :new
end
rescue Exception => e
logger.error e.message
flash[:error] = "Unable to process request - #{e.message}"
render :new
end
end
def show
#initiative = Initiative.find(params[:id])
#other_initiatives = Initiative.approved.limit(3)
end
And here is the initiatives_params method from the same controller:
def initiatives_params
initiative_params = params.require(:initiative).permit(
:terms_accepted,
:title,
:teaser,
:term,
:category,
:funding_goal,
:funding_type,
:video_url,
:story,
:cover_image,
:nonprofit,
:EIN,
:role,
:send_receipt,
:organization_name,
:crop_x, :crop_y, :crop_h, :crop_w,
location_attributes: [:address],
rewards_attributes: [:id, :name, :description, :donation, :arrival_time, :availability, :_destroy, :estimated_value])
if #prelaunch.media.cover_image
initiative_params[:cover_image] = #prelaunch.media.cover_image
end
initiative_params
end
You can pass the Image URL and display it on the page.
The user can then override this by uploading a new image (as per normal).
In you're create action, if they have not supplied a new image, then set it to the one in the assocoiated prelaunch - you'd want to copy the original so it doesn't get replaced if they upload a new one. (If you don't know which was the prelaunch, you could pass the ID down to the page).
I was able to make it work by saving the Paperclip object only. This is my model:
class Grade < ActiveRecord::Base
has_attached_file :certificate
end
If I run the following:
#grade.certificate = new_file
#grade.certificate.save
It saves/overwrite the file, but don't update the Grade object.
Versions: ruby-2.3.8, Rails 4.2.11.3 and paperclip (4.3.6)

after_commit callback is being called several times

update:
Is it the case that a call to update_attributes gets it's own transaction?
I've looked at this question and for reasons in addition to that question, i've decided to go with after_commit as the proper hook. The problem is it's being called multiple (exactly three) times. The code is a little complex to explain, but basically there is a profile model that has
include Traits::Blobs::Holder
in holder.rb I have:
module ClassMethods
def belongs_to_blob(name, options = {})
clazz = options[:class_name] ? options[:class_name].constantize : Blob
foreign_key = options[:foreign_key] || :"#{name}_id"
define_method "save_#{name}" do
blob = self.send(name)
if self.errors.any? && blob && blob.valid?
after_transaction do
blob.save!
#self[foreign_key] = blob.id
#save resume anyway
self.update_attribute(foreign_key, blob.id)
end
end
end
after_validation "save_#{name}"
belongs_to name, options
accepts_nested_attributes_for name
end
end
finally in profile.rb itself I have:
after_commit :send_messages_after_registration!
protected
def send_messages_after_registration!
Rails.logger.debug("ENTERED : send_messages_after_registration " + self.owner.email.to_s)
if self.completed?
Rails.logger.debug("completed? is true " + self.owner.email.to_s)
JobSeekerNotifier.webinar_notification(self.owner.id).deliver
Resque.enqueue_in(48.hours, TrackReminderWorker, self.owner.id)
end
end
it appears that the method is entered 3 times. I've been trying to figure this out for a few days so any guidance you can provide will be appreciated.
controller code:
def create
#user = Customer.new(params[:customer].merge(
:source => cookies[:source]
))
#user.require_password = true
respond_to do |f|
if #user.save
promote_provisional_user(#user) if cookies[:provisional_user_id]
#user.profile.update_attributes(:firsttime => true, :last_job_title => params[:job_title]) unless params[:job_title].blank?
if params[:resume]
#user.profile.firsttime = true
#user.profile.build_resume(:file => params[:resume])
#user.profile.resume.save
#user.profile.save
end
...
end
So it's happening 3 times because the profile is being saved 3 times: once when the user is saved (I assume that User accepts_nested_attributes_for :profile, once when you call update_attributes(:first_time => true,...) and once when you call save in the if params[:resume] block. Every save creates a new transaction (unless one is already in progress) you end up with multiple calls to after_commit
after_commit does take an :on option (which can take the values :create, :update, :destroy) so that you can limit it to new records. This would obviously fire on the first save so you wouldn't be able to see the profile's resumé and so on.
You could in addition wrap the entirety of those updates in a single transaction, in that case after_commit only gets called once, no matter how many saves take place inside the transaction by doing something like
User.transaction do
if #user.save
...
end
end
The transaction will get rolled back if an exception is raised (you can raise ActiveRecord::Rollback if you want to bail out)

Resources