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
Related
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.
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
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
I am putting together an app that allows users to make posts. Each post will have a type (image, video, link, text) with both similar and unique variables. I want to submit these through different forms in a "composer" that's available throughout the site.
Here is the post model, Gallery:
class Gallery < ActiveRecord::Base
attr_accessible :name, :asset
has_attached_file :asset, :styles => { :small => '160x120#', :medium => "500x", :thumb => "300x200#" }
belongs_to :user
has_many :likes
validates :user_id, presence: true
validates :type, presence: true
validates :name, presence: true, length: { maximum: 140, minimum: 1 }
end
I was thinking of using Single Table Inheritance, like this:
class Image < Gallery
end
class Link < Gallery
end
class Video < Gallery
end
class Text < Gallery
end
But I'm not achieving the result I want. For one, I'd like to use the methods in my Gallery controller, such as:
# galleries_controller.rb
def like
#gallery = Gallery.find(params[:id])
#like = #gallery.likes.build(:user_id => current_user.id)
respond_to do |format|
if #like.save
format.html { redirect_to #gallery }
format.js
end
end
end
Moreover, I want to create a "publisher" form that contains a form for each post type and lets you create any kind of post.
How would you approach this? I'm new to Rails and I want to take advantage of all the conveniences it offers. Much appreciated!
I had a similar scenario, and after looking for a nice way of implementing it, I found this gem which I'm using:
https://github.com/hzamani/acts_as_relation
It uses a polymorphic has_one association to simulate a multi-table inheritance, so in your case you could do the following:
class Gallery < ActiveRecord::Base
acts_as_superclass
...
end
class Image < ActiveRecord::Base
acts_as :gallery
...
end
class Link < ActiveRecord::Base
acts_as :gallery
...
end
I hope this helps you.
Cheers!
I would make Gallery an abstract class and each sub class have a seperate db table. This will allow the sub classes to have unique attributes as well as shared ones.
class Gallery < ActiveRecord::Base
self.abstract_class = true # very important
.
.
end
I would make the gallery controller polymorphic:
class GalleriessController < ApplicationController
before_filter :find_gallery
private
def find_gallery
#klass = params[:gallery_type].capitalize.constantize
#gallery = #klass.find(params[:gallery_id])
end
end
Views:
You can use form_for #gallery but it will need a little bit of massaging to work with the polymorphic controller above. It will need to submit a gallery_type and gallery_id parameters instead of just an id. You could modify form_for's :url param to do this or with hidden_fields.
Inside the form_for you could use partials, one for each sub class.
Routes:
You can define a regular resource like so: resources :galleries
You will need, however to make the id parameter to be gallery_id (and also always pass the galley_type param). Check this: Changing the id parameter in Rails routing
(Possible) conclusion:
Sometimes fancy polymorphic stuff will work out well but in other cases a more simple brute-force approach might be better meaning: have a controller for each sub-class. It depends on the situation.
If you end up doing too much stuff that the Rails framework does not give you for free maybe it's better to think of another approach.
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