Home > Software design >  How to trigger Rails ActiveRecord Validations manually ? (ActiveAdmin)
How to trigger Rails ActiveRecord Validations manually ? (ActiveAdmin)

Time:10-12

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!

CodePudding user response:

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.

  • Related