I have User model with the following association:
class User < ActiveRecord
has_many :pictures, :as => :imageable
accepts_nested_attributes :pictures
end
My Picture model looks like this:
class Picture < ActiveRecord
belongs_to :imageable, :polymorphic => true
has_attached_file :image
end
Now, I want to user to be able to upload maximum 5 images. And he will select 1 image as his avatar. Now, user can upload images but I don't know how to limit the maximum number of pictures. One more thing, user needs to be able to change his avatar image. How can I achieve this?
In my view, I use input file with name user[picture_attributes][0][image] in order to allow user to change the first picture but it keeps inserting new pictures into database instead of replacing the first picture.
Please help me on this.
Thanks in advance
For the first part of the problem you have, i would suggest you use rails built-in counter_cache method.
Your picture model would thus become:
class Picture < ActiveRecord::Base
belongs_to :imageable, :polymorphic => true, counter_cache: true
has_attached_file :image
end
Also you would need to add a column called pictures_count to the User model.
This way in your controller you could check if the count is upto 5 records and therefore inform them that they have uploaded the maximum allowed.
if #user.pictures.size == 5 #sorry no more uploads
For the second part of the problem. Is the form action pointed to the new/create action or to your update action. If pointed to the new action a new record would be created but if pointed to the update action then it should change the first image record as you expect.
#charinten For the second part of the problem, some suggestions:
You could try making the id of the pictures accessible in the user model. This way when you try to point to the image to use as an avatar, rails uses that id to update the record. If you try to update the record without pointing rails to that id, it would assume you are trying to create a new record.
Also rather than using user[picture_attributes][0][image] you could in your user profile. Find a specific image and point to that image on the edit action.
Hope this helps
Related
I'm using Rails 6.
How to get the total size of user attachments in active storage?
======
Update:
If I have User A and User B
How to get the total size of User A attachments?
If you want to get size of all attachments associated with some record (e.g. user = User.first) you can use this:
ActiveStorage::Attachment.where(record: user).map(&:byte_size).sum
or more efficient with such query
ActiveStorage::Attachment.joins(:blob).where(record: user).sum(:byte_size)
or like this
ActiveStorage::Blob.
joins(:attachments).
where(active_storage_attachments: { record_id: user.id, record_type: user.class.name }).
sum(:byte_size)
For example this user has_many_attached :images
In this case you can get size only of all images of this user as:
user.images.map(&:byte_size).sum
or more efficient with such query
user.images.joins(:blob).sum(:byte_size)
or like this
user.images_blobs.sum(:byte_size)
Using include ActionView::Helpers::NumberHelper you can convert byte size (integer) to human format:
number_to_human_size(size_of_all_attachments)
Here is how I do it based in #Yshmarov and #mechnicov answers
In model
after_create_commit :add_user
private
def add_user
files.attachments.update(user_id: self.user.id)
end
In controller
#checksize = ActiveStorage::Blob.where(id: current_user.id).map(&:byte_size).sum
In view
<%= number_to_human_size(#checksize) %>
This is not a solution for the question; but this helped me solve my own case. So I'm posting it to possibly help others, as I think my case is quite common.
If you have this nested structure:
class Album < ApplicationRecord
has_many :album_items, dependent: :destroy
end
class AlbumItem < ApplicationRecord
belongs_to :album, counter_cache: :album_items_count
before_destroy :purge_attachment
has_one_attached :photo # automatically adds a `with_attached_photo` scope to prevent n+1 queries
private
def purge_attachment
photo.purge_later
end
end
To count total album size:
Album.first.album_items.with_attached_photo.map { |album_item| album_item.photo.byte_size }.sum
Note the added with_attached_photo scope, to prevent n+1 queries.
You can do it directly on the database, which is much more performant than the other answers posted here.
Let's say your User has_many_attached :images.
Finding the size of one user's images
You can find the total size of one user's images with:
size_of_all_attachments = user.images_blobs.sum("active_storage_blobs.byte_size")
And then, as mechnicov pointed out, you can then include ActionView::Helpers::NumberHelper to convert byte size (integer) in human format:
number_to_human_size(size_of_all_attachments)
Finding the size of all user's images
Also very easily:
User.joins(:images_blobs).sum("active_storage_blobs.byte_size")
The way this works is, as per the documentation, the has_many_attached macro actually creates two associations in your User model, just like if you had declared them yourself:
has_many :images_attachments, ...
has_many :images_blobs
And these :images_blobs are regular ActiveRecord models named ActiveStorage::Blob, which the table name is active_storage_blob and they contain a column named byte_size, with type :bigint, containing the byte size.
The other answers here suggested using ActiveStorage::Attachment.where(record: User.first).map(&:byte_size).sum. However, this is not efficient for two reasons:
It will first return all ActiveStorage::Attachment models; these are a join model used by Rails to connect your User model to the ActiveStorage::Blob model, the latter actually containing the byte_size column we're after. Calling ActiveStorage::Attachement#byte_size will work, but it does so due to the delegate_missing_to :blob declaration on ActiveStorage::Attachment, so it uses method_missing which is much slower.
Due to #1, since ActiveStorage::Attachment doesn't have the byte_size column, it's calling .map, creating a needless array and loading and instanciating all these models needlessly.
So, this answer goes straight to the database and asks for the sum that's already waiting for you there.
I am trying to use a WYSIWYG editor like Froala to upload images to my server. The issue is I need to store multiple images into one attribute of the model. This is because I wont know ahead of time how many images will to be stored when my users upload images.
Would be great if anybody can give me some pointers. Or examples.
Have a look at http://guides.rubyonrails.org/association_basics.html and search for has_many. You need an extra model which stores the images and which has an other_model_id attribute which points to the other model.
If you have never worked with has_many you'll find an example application at http://www.xyzpub.com/en/ruby-on-rails/4.0/activerecord_has_many.html
If you have to save several images related to a model, you'll need an 1 - N relation.
So you have two choices:
Create a model like album and make an 1 - 1 relationship
Use an has_many (1-N) relationship.
To learn more about associations I really recommend you to read the official guide.
class User < ActiveRecord::Base
has_many :images
accepts_nested_attributes_for :images
end
class Image < ActiveRecord::Base
belongs_to :user
#use paper clip for the following method
has_attached_file :attachment
end
Now upload the images using your WYSIWYG editor, and append nested images form to the user form, submit the form and you are good to go. Hope this helps.
I have a model Company which has_many :company_pictures but at most 5 photos. I have to show user all 5 file field for image uploading.
class Company < ActiveRecord::Base
has_many :company_pictures
accepts_nested_attributes_for :company_pictures
end
class CompanyPicture < ActiveRecord::Base
belongs_to :company
has_attached_file :picture, :styles => { :medium => "300x300>"}
end
controller code
def new
#company = Company.new
5.times { #company.company_pictures.build }
end
Now through this in new form I am able to show 5 file field. Now on submit . let say now my form did not get saved because of any other error. now I find only those file field that hold some value with that. i want to make sure that user will always able to attach at most 5 photos whether he is creating a new company or updating a company.
What I usually do in this cases is that before presenting the user with the "new" layout, I create and save the object first in the database with a "draft" status with the objective of getting a primary key and be able to save the pictures (the pictures need a primary key in order to be saved). Then, when the user submits the form, no matter if there are errors or not I always save the pictures and then present the user with the form, the errors (in case there are) and since the images got saved to the database you can display them to the user. If there are no errors just remove the "draft" status from the user and continue your workflow.
Now the question is, what do we do with all those drafts ? Create a rake task that run's every hour and delete all the records in this state.
I have two models: Image and Lightbox. To relate the two models (has_many) I have a third model LightboxImages.
One Lightbox can have several Images and a Image can be in various Lightboxes.
A Lightbox is created dynamically (with remote js) and the user can add Images to this Lightbox dynamically too.
My first idea was to simply create a url to the create method of the LightboxImages Controller passing the Lightbox Id and Image Id as parameter.
However, this approach seemed fragile and insecure, since a user could easily simulate such behavior.
What is the best design for this situation, create relationship records dynamically?
You actually have a fourth model -- User. I'm assuming you do anyway.
User has_many :lightboxes
Lightbox has_many :lightbox_items
Image has_many :lightbox_items
LightboxItem belongs_to :lightbox
LightboxItem belongs_to :image
Lightbox has_many :images, :through => :lightbox_item
Image has_many :lightboxes, :through => :lightbox_item
LightboxItem is where you might want to store additional data like the position of the image in that specific Lightbox.
The key thing is that every Lightbox belongs to a User. With this you can then scope all of the AR calls in your controller to the current user -- thus preventing them from modifying other light boxes.
So, user carlos has lightbox id 1. User phallstrom has lightbox id 2.
In your controller, you somehow set current_user to the current user in a before filter. Then, for example in the action to get the specified light box you would do something like this:
#lightbox = current_user.lightboxes.find_by_id(params[:id].to_i)
In doing it this way if current_user is carlos, and params[:id] is 2, nothing will be returned.
Similarly, for all the other CRUD actions you'd scope it to current_user as well.
I am trying to associate a set of uploaded pictures with a post. In my controller I am using the following code:
image_uploads = params[:image_uploads]
iu = ImageUpload.find(image_uploads)
#post.image_uploads = iu
While this does make the uploaded images accessible from #post.image_uploads, I think it doesn't associate these images with the post because when the post is deleted, the image uploads are not deleted -- even though I have used :dependent=>:destroy for their relationships.
> Post.first.delete
=>[]
> ImageUpload.all
=> [#<ImageUpload id: 3 ...>]
And this is the model:
class Post < ActiveRecord::Base
has_many :image_uploads, :dependent => :destroy
end
class ImageUploads < ActiveRecord::Base
belongs_to :post
end
What can I do to make sure that cascading deletes work?
Try instead:
Post.first.destroy
destroy invokes the callbacks defined, whereas delete deletes the record directly using SQL without creating an AR object.
What you do seems correct to me. You have a post with image uploads dependent on the post and you should get them deleted when a post is deleted. I could be wrong but i do not see anything.
However, i see something that could cause heavy problems and it could also be the source of your problem(it's a source of many).
You have a model that ends with an S. Not a good idea. Do yourself a favor and change to ImageUpload please :)