Search code examples
ruby-on-railsrubyrails-apirails-activestorage

Updating a user record that uses active-storage doesn't work


I'm practicing the use of Rails active_storage. So I created an app that only has one model - User. The User has two main db columns - username and profile_pic. Then through active_storage it has_many_attached :avatars. The feature I am implementing is that before_validation of the user record I want to assign the first avatar attached (user can upload many images upon signup) as the profile_pic. Here's the complete model

class User < ApplicationRecord
  include Rails.application.routes.url_helpers

  has_many_attached :avatars
  validates :username, presence: true
  before_validation :assign_profile_pic

  def change_profile_pic
    self.profile_pic = rails_blob_path(avatars.last, only_path: true)

    save
  end

  private

  def assign_profile_pic
    self.profile_pic = rails_blob_path(avatars.first, only_path: true)
  end
end

Here's the users controller

class V1::UsersController < ApplicationController
  # include Rails.application.routes.url_helpers

  def show
    user = User.find_by(username: params[:username])

    if user.present?
      render json: success_json(user), status: :ok
    else
      head :not_found
    end
  end

  def create
    user = User.new(user_params)
    if user.save
      render json: success_json(user), status: :created
    else
      render json: error_json(user), status: :unprocessable_entity
    end
  end

  def update
    user = User.find_by(username: params[:username])
    if user.update(user_params)
      user.change_profile_pic

      render json: success_json(user), status: :accepted
    else
      render json: error_json(user), status: :unprocessable_entity
    end
  end

  def avatar
    user = User.find_by(username: params[:user_username])

    if user&.avatars&.attached?
      redirect_to rails_blob_url(user.avatars[params[:id].to_i])
    else
      head :not_found
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, avatars: [])
  end

  def success_json(user)
    {
      user: {
        id: user.id,
        username: user.username
      }
    }
  end

  def error_json(user)
    { errors: user.errors.full_messages }
  end
end

Creating the user isn't a problem. It works as expected, it assigns user.avatars.first automatically as the user.profile_pic upon creation. The problem happens in the update part. You see I am calling change_profile_pic user method upon successful update (in the users_controller). The problem is the user.profile_pic never gets updated. I have debugged this many times and there's nothing wrong with the user_params. What am I missing?


Solution

  • before_validation runs every time before before_save. You're setting it once, and then setting it again. See the order here: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

    Avoid validating with assign_profile_pic if you're merely changing profile pic. A clean way to do this is with ActiveModel::AttributeMethods

    class User < ApplicationRecord
      include Rails.application.routes.url_helpers
    
      has_many_attached :avatars
      validates :username, presence: true
      before_validation :assign_profile_pic, unless: :changing_profile_pic?
      attribute :changing_profile_pic, :boolean, default: false 
    
      def change_profile_pic
        self.profile_pic = rails_blob_path(avatars.last, only_path: true)
        self.changing_profile_pic = true 
        save
      end
    
      private
    
      def assign_profile_pic
        self.profile_pic = rails_blob_path(avatars.first, only_path: true)
      end
    end