Fallback Image with Carrierwave - ruby-on-rails

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

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.

The proper way of creating association between Ckeditor::Picture and a model in Ruby on Rails?

I have an Article model and Ckeditor + Paperclip installed. When I upload pictures into Article body, everything works fine. However, I want to make these pictures accessible via #article.pictures without creating a separate Picture model. I've already created a regular association between Article and Ckeditor::Picture. But when I'm uploading a picture, Ckeditor not surprisingly requires an Article id. Where and how am I supposed to pass it?
class CreateCkeditorAssets < ActiveRecord::Migration[5.2]
t.references :article, foreign_key: true
end
class Article < ApplicationRecord
has_many :pictures, class_name: 'Ckeditor::Picture'
end
class Ckeditor::Picture < Ckeditor::Asset
belongs_to :article
end
You can't pass an article ID, because at the time when you upload pictures your article isn't persisted (unless you're editing an already saved article).
So what you can do is to build an article with some unique token, then after uploading pictures and saving the article, update article_id in all pictures that have the same token.
Like this: (pseudocode, not tested)
class Article < ApplicationRecord
has_many :pictures, class_name: 'Ckeditor::Picture'
after_save :assign_pictures
private
def assign_pictures
Ckeditor::Picture.where(token: picture_token).update_all(article_id: id)
end
end
-
class Ckeditor::Picture < Ckeditor::Asset
belongs_to :article, optional: true
end
-
class Ckeditor::PicturesController
def create
#picture = Ckeditor::Picture.new
#picture.token = params[:picture_token] # pass this param via javascript, see: https://github.com/galetahub/ckeditor/blob/dc2cef2c2c3358124ebd86ca2ef2335cc898b41f/app/assets/javascripts/ckeditor/filebrowser/javascripts/fileuploader.js#L251-L256
super
end
end
-
class ArticlesController < ApplicationController
def new
#article = Article.new(picture_token: SecureRandom.hex)
end
end
Obviosly you need to add picture_token field to your Article model and token field to Ckeditor::Picture. Hope that helps.

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

Rails 4 ActiveRecord Module with ActiveSupport

So I have a few different models in my Rails 4 app that have image uploads. Rather than adding identical code to each of the models I've created a module that I can include into all of them.
Here it is:
module WithImage
extend ActiveSupport::Concern
included do
attr_accessor :photo
has_one :medium, as: :imageable
after_save :find_or_create_medium, if: :photo?
def photo?
self.photo.present?
end
def find_or_create_medium
medium = Medium.find_or_initialize_by_imageable_id_and_imageable_type(self.id, self.class.to_s)
medium.attachment = photo
medium.save
end
end
def photo_url
medium.attachment if medium.present?
end
end
class ActiveRecord::Base
include WithImage
end
A Medium (singular of media) in this case is a polymorphic model that has paperclip on it. The attr_accessor is a f.file_field :photo that I have on the various forms.
Here's my PurchaseType Model (that uses this mixin):
class PurchaseType < ActiveRecord::Base
include WithImage
validates_presence_of :name, :type, :price
end
So here's the thing, the after_save works great here. However, when I go to the console and do PurchaseType.last.photo_url I get the following error:
ActiveRecord::ActiveRecordError: ActiveRecord::Base doesn't belong in a hierarchy descending from ActiveRecord
I haven't the faintest clue what this means or why it is happening. Anyone have any insight?
Thanks!
It turns out I was trying to do things I had seen in various examples of modules. It was simple to get it working:
module WithImage
extend ActiveSupport::Concern
included do
attr_accessor :photo
has_one :medium, as: :imageable
after_save :find_or_create_medium, if: :photo?
def photo?
self.photo.present?
end
def find_or_create_medium
medium = Medium.find_or_initialize_by_imageable_id_and_imageable_type(self.id, self.class.to_s)
medium.attachment = photo
medium.save
end
def photo_url
medium.attachment.url if medium.present?
end
end
end

CarrierWave and multiple uploaders to store files based on a specific model's permalink

Can someone please assist with regards to CarriveWave's store_dir.
How does one have mulitple image models that stores files based on the associated belong_to model's permalink?
# Garage model
class Garage < ActiveRecord:Base
attr_accessible: :avatar, :permalink, :car_image_attributes,
:motorcycle_image_attributes
has_many :car_image
has_many :motorcycle_image
mount_uploader :avatar, AvatarUploader
def set_permalink
self.permalink = permalink.parameterize
end
def to_param
permalink.parameterize
end
end
This is what my Image Models that links with CarrierWave
# Car Image model
CarImage < ActiveRecord:Base
belongs_to :garage
attr_accessible: :garage_id, :image
mount_uploader :car_image, CarUploader
end
# Motocycle Image model
MotocycleImage < ActiveRecord:Base
belongs_to :garage
attr_accessible: :garage_id, :image
mount_uploader :motorcycle_image, MotocycleUploader
end
This is what my CarrierWave uploaders look like.
# CarrierWave avatar uploader
avatar_uploader.rb
# This uploader directly relates to the Garage model table
# column avatar:string.
def store_dir
# This works for the avatar because it calls on the Garage permalink
# but it fails for the other image models because it's a model relation
# has_many, belongs_to and the model.permalink will be based on the
# uploader's respective model and not the Garage model
# eg. car_uploader.rb = model.permalink = CarImage.permalink
# I would like it to refer to to Garage.permalink at all times.
"garage/#{model.permalink}/#{mounted_as}/"
end
end
# CarrierWave car and motorcycle uploaders
car_uploader.rb
# Fails to upload because it doesn't know what permalink is
end
motorcycle_uploader.rb
# Fails to upload because it doesn't know what permalink is
end
Apologies if I wants so clear but a big thanks for any insight given.
probably the easiest way would be to delegate the permalink to the parent on the model
CarImage < ActiveRecord:Base
belongs_to :garage
delegate : permalink, :to => :garage
attr_accessible: :garage_id, :image
mount_uploader :car_image, CarUploader
end

Resources