Attach Cloudinary image to Rails model using ActiveStorage - ruby-on-rails

I have a user model
class User < ApplicationRecord
has_one_attached :photo
end
I'm trying to:
upload an image to Cloudinary via URL (this works)
attach it to a user instance which uses ActiveStorage (this does not)
Here is what I think should work
user_img_response = Cloudinary::Uploader.upload("https://www.formula1.com/content/dam/fom-website/manual/Misc/2019-Races/Monaco2019/Monaco%20chicane%20HAM%20VER%20sized.jpg.transform/9col/image.jpg")
img_id = user_img_response["url"].match(/image\/upload.*/)[0]
signature = "#{img_id}##{user_img_response["signature"]}"
preloaded_file = Cloudinary::PreloadedFile.new(signature)
user = User.new(title: "Chris")
user.photo = preloaded_file
user.save
=> true
However, the photo is not being attached to the user instance
user.photo.attached?
=> false

Assuming your app/models/photo.rb looks similar to this:
class Photo < ActiveRecord::Base
attr_accessible :title, :bytes, :image, :image_cache
belongs_to :album
mount_uploader :image, ImageUploader
validates_presence_of :title, :image
end
What happens if you try:
...
user = User.new(title: "Chris")
user.photo.image = preloaded_file # <---- assign file to image attribute
user.save
You can also try to emulate this sample app for your case: https://github.com/cloudinary/cloudinary_gem/tree/master/samples/photo_album
EDIT: you can try something like this:
require 'uri'
file = URI.open(user_img_response["url"]) # use cloudinary url
photo.image.attach(io: file, filename: 'image.jpg')
See: https://blog.eq8.eu/til/upload-remote-file-from-url-with-activestorage-rails.html

Related

Rails 6: How can I run validations on an ActiveStorage item outside of the model?

I have a model Profile which has an attachment image.
class Profile < ApplicationRecord
belongs_to :user, dependent: :destroy
has_one_attached :image
validates :image,
content_type: [:gif, :png, :jpg, :jpeg],
size: { less_than: 2.megabytes , message: 'must be less than 2MB in size' }
after_initialize :set_default_image
has_many :gallery_items, dependent: :destroy
accepts_nested_attributes_for :gallery_items, allow_destroy: true
validates :name, :short_description, :slug, presence: true
#...
In addition to image validation (I'm using the active storage validation gem), Profile validates the presence of name, short_description and slug.
Since implementing this, I have a new requirement in a view. I now want to allow the user to submit an image separately from the other attributes.
I don't want to change the underlying model. As such, I want to introduce a separate controller and class to handle a form for the submission of the image alone.
I've tried two ways.
First, I had a simple class to handle this:
class ProfileImageFormSubmission
def initialize(record, params)
#params = params
#record = record
end
def save
record.update_attribute(:image, image_param)
end
private
attr_accessor :params, :record
def image_param
params.fetch(record_param).require(:image)
end
def record_param
record.class.name.underscore.to_sym
end
end
It would be called in my controller like so:
class ProfileImagesController < ApplicationController
before_action :require_login
def update
#profile = Profile.find_by(user_id: current_user.id)
if ProfileImageFormSubmission.new(#profile, params).save
#...
The problem is, that the image isn't validated and so is attached to the the profile no matter what. (I used #update_attribute because I wanted to skip the validations on the other attributes -- it wouldn't make sense to display an error for the name column when the field isn't presented to the user.)
I have also tried to solve the problem by running the validations outside of the model. But here, I'm struggling to understand how to integrate ActiveStorage with a plain old Ruby object.
class ProfileImageFormSubmission
include ActiveModel::Model
include ActiveStorage
include ActiveStorageValidations
attr_accessor :record, :params
has_one_attached :image
validates :image,
content_type: [:gif, :png, :jpg, :jpeg],
size: { less_than: 2.megabytes , message: 'must be less than 2MB in size' }
def initialize(record, params)
#params = params
#record = record
end
def save
binding.pry
record.update_attribute(:image, image_param)
if record.invalid?
record.restore_attributes
return false
end
end
# This model is not backed by a table
def persisted?
false
end
private
def image_param
params.fetch(record_name).require(:image)
end
def object_name
record.class.name.underscore.to_sym
end
end
I can't even instantiate the above class as it fails with the following error:
NoMethodError (undefined method `has_one_attached' for ProfileImageFormSubmission:Class):
What's the best way to validate an active storage item seperately?
Is it possible to run validations on a single column without triggering other validation errors?
Is it at all possible to use active storage items outside of an ApplicationRecord model?
You can't really do it with just one model. You'll need to create a second "Image" or "Attachment" class with the active storage validation and then you'll be able to create the image first.
Even if it's probably possible to keep everything just in one model, skipping the validation will lead to data inconsistencies. Keeping it separate will ensure that every record in your DB is valid and you won't run into unexpected states (like profiles without a name, even though you're "validating" its presence).
So it would look like this:
class Image < ApplicationRecord
(...)
has_one_attached :file
validates :file,
content_type: [:gif, :png, :jpg, :jpeg],
size: { less_than: 2.megabytes , message: 'must be less than 2MB in size' }
(...)
end
class Profile < ApplicationRecord
(...)
has_one :image # or belongs to
(...)
end
try this
class ProfileImageFormSubmission
def initialize(record, params)
#params = params
#record = record
end
def save
record.assign_attributes(image: image_param)
record.valid?
record.errors.to_hash.except(:image).each { |k,_| record.errors.delete(k) } # removing errors for other attributes
return false if record.errors.any?
record.save(validate: false)
end
...
If a Profile is not mandatory for a User. Then you may want to move the image to User class instead.

Serializing a custom attribute

I am using the Active Model Serializer gem for my application. Now I have this situation where a user can have an avatar which is just the ID of a medium.
I have the avatar info saved into Redis. So currently what I do to show the avatar at all in the serialized JSON, is this:
class UserSerializer < ActiveModel::Serializer
include Avatar
attributes :id,
:name,
:email,
:role,
:avatar
def avatar
Medium.find(self.current_avatar)[0]
end
#has_one :avatar, serializer: AvatarSerializer
has_many :media, :comments
url :user
end
I query Redis to know what medium to look for in the database, and then use the result in the :avatar key.
Down in the code there is also a line commented out, that was the only way I found on the https://github.com/rails-api/active_model_serializers/ page about using a custom serializer for something inside of serializer.
So to get to my problem. Right now the :avatar comes just like it is in the database but I want it to be serialized before I serve it as JSON.
How can I do that in this case?
You need to serialize Avatar Class:
class Avatar
def active_model_serializer
AvatarSerializer
end
end
Then you just use this way:
class UserSerializer < ActiveModel::Serializer
include Avatar
attributes :id,
:name,
:email,
:role,
:avatar
def avatar
# Strange you query another object
avatar = Medium.find(object.current_avatar).first
avatar.active_model_serializer.new(avatar, {}).to_json
end
has_many :media, :comments
url :user
end
According to the docs if you want a custom serializer you can just add:
render json: avatar, serializer: AvatarSerializer
or whatever your serializer name could be, here are the docs:
https://github.com/rails-api/active_model_serializers/blob/v0.10.6/docs/general/serializers.md#scope

Fallback Image with Carrierwave

My uploader is working well apart from one small thing. The setting of default images. I'm using carrierwave for users to upload profile images of themselves:
user model
class User < ActiveRecord::Base
has_one :avatar, class_name: 'Image', foreign_key: :user_id
before_create :create_fallback_image
def create_fallback_image
self.create_avatar
end
end
image model
class Image < ActiveRecord::Base
mount_uploader :file_name, AvatarUploader, auto_validate: false
belongs_to :user
end
avatar uploader
class AvatarUploader < BaseUploader
include CarrierWave::RMagick
storage :file
process resize_to_fit: [75, 75]
process convert: 'gif'
def default_url
'foobar'
end
def filename
random_string + '.gif'
end
end
def random_string
#random_string ||= User.random_string
end
end
When a user signs up without uploading an optional profile image, they are assigned an association to their profile image, but instead of the default_url working, they get a random string from the filename method.
I thought I could get around it like this:
user model
class User < ActiveRecord::Base
has_one :avatar, class_name: 'Image', foreign_key: :user_id
before_create :create_fallback_image
def create_fallback_image
# look here:
self.create_avatar.create_fallback
end
end
image model
class Image < ActiveRecord::Base
mount_uploader :file_name, AvatarUploader, auto_validate: false
belongs_to :user
def create_fallback
self.update_attributes(file_name: 'my_fallback.jpg')
end
end
and while it nearly, nearly works, when I update the attributes of the file_name column, the uploader kicks in and my_fallback.jpg is overridden by a random string from my random_string method!
Carrierwave has a built-in fallback mechanism for default image
Update your default_url method in AvatarUploader as below:
def default_url
ActionController::Base.helpers.asset_path("fallback/" + [version_name, "my_fallback.jpg"].compact.join('_'))
end
where change fallback/ to your desired folder path.
This way, when an avatar is not uploaded for a particular user then my_fallback.jpg would be used as fallback image.
Refer to section Providing a default URL in Carrierwave Documentation.
when I update the attributes of the file_name column, the uploader
kicks in and my_fallback.jpg is overridden by a random string from my
random_string method!
This happens because you have overridden filename method in AvatarUploader which gets called every time an image is uploaded. If you notice, its calling random_string method in it. Hence, you get a random string as your filename.
UPDATE
As per the chat session with OP, if an avatar is not uploaded for a user then a default image should be shown. I suggested the following helper :
module ApplicationHelper
def display_avatar(user)
unless user.avatar.nil?
image_tag(user.avatar.file_name)
else
image_tag("/path/to/fallback.jpg")
end
end
## ...
end
Use this helper method in views to display avatar image appropriately.
Also, you can do it within model:
class User < ApplicationRecord
has_one :avatar, class_name: 'User::Avatar', as: :parent, dependent: :destroy
accepts_nested_attributes_for :avatar, allow_destroy: true #, ...
def avatar
super || build_avatar
end
end

Dynamic uploader with Carrierwave

I am using a single Image model to store information about images used by different other models (via a polymorphic association).
I'd like to change the uploader on this model depending on the model associated to have different versions for different models.
For example, if the imageable is a Place, the mounted uploader would be a PlaceUploader. And if there is no PlaceUploader, it would be the default ImageUploader.
At the moment I have:
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
mount_uploader :image, ImageUploader
end
Ideally I'd like to have:
# This is not supported by CarrierWave, just a proof of concept
mount_uploader :image, -> { |model| "#{model.imageable.class.to_s}Uploader".constantize || ImageUploader }
Is there a way to achieve that? Or a better way to have different versions depending on the associated model?
Edit
I found another solution using one single ImageUploader:
class ImageUploader < BaseUploader
version :thumb_place, if: :attached_to_place? do
process resize_to_fill: [200, 200]
end
version :thumb_user, if: :attached_to_user? do
process :bnw
process resize_to_fill: [100, 100]
end
def method_missing(method, *args)
# Define attached_to_#{model}?
if m = method.to_s.match(/attached_to_(.*)\?/)
model.imageable_type.underscore.downcase.to_sym == m[1].to_sym
else
super
end
end
end
As you can see my 2 versions are named thumb_place and thumb_user because if I name them both thumb only the first one will be considered (even if it doesn't fill the condition).
I need to implement same logic that i have a single Image model and base on polymorphic association to mount different uploaders.
finally i come up below solution in Rails 5:
class Image < ApplicationRecord
belongs_to :imageable, polymorphic: true
before_save :mount_uploader_base_on_imageable
def mount_uploader_base_on_imageable
if imageable.class == ImageableA
self.class.mount_uploader :file, ImageableAUploader
else
self.class.mount_uploader :file, ImageableBUploader
end
end
end

Carrierwave: Duplicating File In a Second Model

I have two models, each with their own Carrierwave uploaders:
class User < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
end
and:
class Bookshelf < ActiveRecord::Base
mount_uploader :image, ImageUploader
end
I want the user's avatar to be the latest bookshelf image he's uploaded. I try to achieve this like so:
class BookcasesController < ApplicationController
def create
#bookcase = current_user.bookcases.build(params[:bookcase])
if #bookcase.save
current_user.avatar = #bookcase.image
current_user.avatar.recreate_versions!
end
end
end
Unfortunately, this has no effect on the avatar at all. How else might I achieve this?
current_user.avatar = #bookcase.image
current_user.avatar.recreate_versions!
Doesn't actually save --- you can either:
current_user.avatar.save
or as you put:
current_user.update_attribute(:avatar, #bookcase.image)
If your image file is stored locally and you don't mind opening a file descriptor, you could also do this:
current_user.avatar = File.open(#bookcase.image.path)
current_user.save

Resources