I am new to Rails and in this project I am suppose to build a program that allows you to create user accounts and upload profile pictures to it. Then it suppose to send you an email to notify that your upload was succesfull.
- User Accounts are handled with Devise
- Image uploads are handled with Active Storage avatar attachments
On Devise, these kind of attachment uploads are handled in Edit Page, which is connected to Devise::RegistrationsController :
# frozen_string_literal: true
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
# def new
# super
# end
# POST /resource
# def create
# super
# end
# GET /resource/edit
# def edit
# super
# end
# PUT /resource
def update
self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key)
prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email)
resource_updated = update_resource(resource, account_update_params)
yield resource if block_given?
if resource_updated
set_flash_message_for_update(resource, prev_unconfirmed_email)
bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?
respond_with resource, location: after_update_path_for(resource)
else
clean_up_passwords resource
set_minimum_password_length
respond_with resource
end
binding.irb
# PAY ATTENTION TO HERE
if @user.avatar.attached?
UserMailer.upload_email(@user).deliver
end
end
# DELETE /resource
# def destroy
# @user = User.find(params[:user_id])
# puts @user
# if @user.destroy
# redirect_to root_path
# end
# end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
# def cancel
# super
# end
# protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [])
end
# If you have extra params to permit, append them to the sanitizer.
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:avatar])
end
# The path used after sign up.
# def after_sign_up_path_for(resource)
# super(resource)
# end
# The path used after sign up for inactive accounts.
# def after_inactive_sign_up_path_for(resource)
# super(resource)
# end
end
edit.html.erb :
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
# PAY ATTENTION TO HERE
<div >
<%= f.label :avatar %><br />
<%= f.file_field :avatar %>
</div>
<div >
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password, autocomplete: "current-password" %>
</div>
<div >
<%= f.submit "Update"%>
</div>
<% end %>
I want you to pay attention to the update action in registrations_controller, especially to the line that goes like:
if @user.avatar.attached?
UserMailer.upload_email(@user).deliver
end
Now this is completely wrong, even though it sends email the first time you upload an image, after that it still sends emails once you click "update" without uploading any attachments, because the only condition is "avatar.attached?", which returns true since there is already an image attached.
Now, thanks to the binding.irb, I can check what is passed to update as params when there is a file added and when there is not.
- When there is a file added:
irb(#<Users::RegistrationsController:0x0000023b767c6790>):001:0> params
=> #<ActionController::Parameters {"_method"=>"put", "authenticity_token"=>"6O0jz5U2XWfb4pu_An3pMh1BUq5o71n0U2_E3zWy0-efqm0orl_Vi5o_jo1B2o4Oc11AyRt-Zd6XBPJnmXpr8w", "user"=>#<ActionController::Parameters {"email"=>"***@hotmail.com", "avatar"=>#<ActionDispatch::Http::UploadedFile:0x0000023b767c5660 @tempfile=#<Tempfile:C:/Users/pc/AppData/Local/Temp/RackMultipart20220903-13632-upo8fa.png>, @original_filename="WhatsApp Image 2022-08-06 at 23.03.10.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"user[avatar]\"; filename=\"WhatsApp Image 2022-08-06 at 23.03.10.png\"\r\nContent-Type: image/png\r\n">, "password"=>"", "password_confirmation"=>"", "current_password"=>"password"} permitted: false>, "commit"=>"Update", "controller"=>"users/registrations", "action"=>"update"} permitted: false>
- When there is not:
irb(#<Users::RegistrationsController:0x0000023b763ea950>):001:0> params
=> #<ActionController::Parameters {"_method"=>"put", "authenticity_token"=>"NCJA7QSTCuAVdogoAN9Z96B4EsJIRoGQyvwmNPVqvu1DZQ4KP_qCDFSrnRpDeD7LzmQApTvXvboOlxCMWaIG-Q", "user"=>#<ActionController::Parameters {"email"=>"***@hotmail.com", "password"=>"", "password_confirmation"=>"", "current_password"=>"123456"} permitted: false>, "commit"=>"Update", "controller"=>"users/registrations", "action"=>"update"} permitted: false>
As you can see, when there is a file added to f.file_field, you can see things like "avatar" "@tempfile" "original_filename" in the params.
So my logic is that before update action, I should check if there is a file added to f.file_field or not. If there is a file added to it, once the User is updated THEN I should send an email to notify it is succesfully uploaded but I dont know how to do that. Any ideas?
- Rest of the relevant parts of my code is like :
UsersController:
class UsersController < ApplicationController
before_action :set_user
def profile;end
private
def set_user
@user = User.find(params[:id])
end
end
User Model (user.rb):
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one_attached :avatar
end
My Tables (schema.rb) :
ActiveRecord::Schema[7.0].define(version: 2022_08_28_175416) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
end
CodePudding user response:
There are several ways to do this. But my suggestion is to create an after_save callback on the User
model, something like this:
after_save :notify_new_avatar
def notify_new_avatar
notify if new_avatar?
end
def notify
UserMailer.notify_new_avatar
end
def new_avatar?
# how do we determine this?
end
To determine if a new avatar was uploaded, I suggest using the features of Active Model Dirty.
I'm not clear what the attribute is that connotes an avatar change in your app, but User.avatar_id_previously_changed?
will probably give you the detection you need.