Mongoid paper clip photo is removed when parent.update_attributes!(attr) called - ruby-on-rails

I have 2 models
The User model embeds many pictures as follows
class User
include Mongoid::Document
field :user_name, type: String
embeds_many :pictures, cascade_callbacks: true
...
end
The Picture model is implemented as follows
class Picture
include Mongoid::Document
include Mongoid::Paperclip
field :description, type: String
has_mongoid_attached_file :photo,
default_url: '',
default_style: :thumb,
url: "/:attachment/:id/:style.:extension",
styles: {
original: ['2000x1200>', :jpg],
thumb: ['600x400#', :jpg]
},
convert_options: {
all: '-quality 90 -strip -background black -flatten +matte'
}
...
end
The problem is in the user_controller#update
def update
...
#user.update_attributes!(user_params)
...
end
def user_params
params.require(:user).permit(:user_name, pictures:[:id, :description])
end
I expected this #update to update the description of the picture with the id passed... and it does.
but the uploaded picture is removed after this update ?
how do i update the description of the uploaded photo without it getting removed after using #user.update_attributes!(user_params) ?

Not tested!
pictures_params = params[:user].delete(:pictures)
#user.pictures.where(:id.in => pictures_params.map(&:id)).each do |pic|
pic.set(:description, pictures_params.find{|p| p[:id] == pic.id}[:description])
end

I used the following code to solve the problem but i think there is a better approach for solving this issue
# get the user hash to update both pictures and user
user_params_hash = user_params
# remove the picture from the hash to separate the updating of pictures from users
picture_params_hash = user_params_hash.delete 'pictures'
# update each picture description to avoid removing the photo
unless picture_params_hash.nil?
picture_params_hash.each do |picture_hash|
picture = #user.pictures.find_or_create_by(id: picture_hash[:id])
picture.description = picture_hash[:description]
picture.save!
end
end
# update the user attributes only without touching the embedded pictures
#user.update_attributes!(user_params_hash)
end

Related

ActiveStorage File Attachment Validation

Is there a way to validate attachments with ActiveStorage? For example, if I want to validate the content type or the file size?
Something like Paperclip's approach would be great!
validates_attachment_content_type :logo, content_type: /\Aimage\/.*\Z/
validates_attachment_size :logo, less_than: 1.megabytes
Well, it ain't pretty, but this may be necessary until they bake in some validation:
validate :logo_validation
def logo_validation
if logo.attached?
if logo.blob.byte_size > 1000000
logo.purge
errors[:base] << 'Too big'
elsif !logo.blob.content_type.starts_with?('image/')
logo.purge
errors[:base] << 'Wrong format'
end
end
end
ActiveStorage doesn't support validations right now. According to https://github.com/rails/rails/issues/31656.
Update:
Rails 6 will support ActiveStorage validations.
https://github.com/rails/rails/commit/e8682c5bf051517b0b265e446aa1a7eccfd47bf7
Uploaded files assigned to a record are persisted to storage when the record
is saved instead of immediately.
In Rails 5.2, the following causes an uploaded file in `params[:avatar]` to
be stored:
```ruby
#user.avatar = params[:avatar]
```
In Rails 6, the uploaded file is stored when `#user` is successfully saved.
Came across this gem: https://github.com/igorkasyanchuk/active_storage_validations
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
validates :name, presence: true
validates :avatar, attached: true, content_type: 'image/png',
dimension: { width: 200, height: 200 }
validates :photos, attached: true, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
dimension: { width: { min: 800, max: 2400 },
height: { min: 600, max: 1800 }, message: 'is not given between dimension' }
end
You can use awesome https://github.com/musaffa/file_validators gem
class Profile < ActiveRecord::Base
has_one_attached :avatar
validates :avatar, file_size: { less_than_or_equal_to: 100.kilobytes },
file_content_type: { allow: ['image/jpeg', 'image/png'] }
end
I'm using it with form object so I'm not 100% sure it is working directly with AR but it should...
Here is my solution to validate content types in Rails 5.2, that as you may know it has the pitfall that attachments are saved as soon as they are assigned to a model. It may also work for Rails 6. What I did is monkey-patch ActiveStorage::Attachment to include validations:
config/initializers/active_storage_attachment_validations.rb:
Rails.configuration.to_prepare do
ActiveStorage::Attachment.class_eval do
ALLOWED_CONTENT_TYPES = %w[image/png image/jpg image/jpeg].freeze
validates :content_type, content_type: { in: ALLOWED_CONTENT_TYPES, message: 'of attached files is not valid' }
end
end
app/validators/content_type_validator.rb:
class ContentTypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, _value)
return true if types.empty?
return true if content_type_valid?(record)
errors_options = { authorized_types: types.join(', ') }
errors_options[:message] = options[:message] if options[:message].present?
errors_options[:content_type] = record.blob&.content_type
record.errors.add(attribute, :content_type_invalid, errors_options)
end
private
def content_type_valid?(record)
record.blob&.content_type.in?(types)
end
def types
Array.wrap(options[:with]) + Array.wrap(options[:in])
end
end
Due to the implementation of the attach method in Rails 5:
def attach(*attachables)
attachables.flatten.collect do |attachable|
if record.new_record?
attachments.build(record: record, blob: create_blob_from(attachable))
else
attachments.create!(record: record, blob: create_blob_from(attachable))
end
end
end
The create! method raises an ActiveRecord::RecordInvalid exception when validations fail, but it just needs to be rescued and that's all.
Copy contents of the ActiveStorage's DirectUploadsController in the app/controllers/active_storage/direct_uploads_controller.rb file and modify the create method. You can add authentication to this controller, add general validations on the file size or mime type, because create method of this controller creates the url for the file to be uploaded. So you can prevent any file upload by controlling size and mime type in this controller.
A simple validation could be:
# ...
def create
raise SomeError if blob_args[:byte_size] > 10240 # 10 megabytes
blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args)
render json: direct_upload_json(blob)
end
# ...
I found a way to validate and delete attachments with callback before_save.
This is a useful approach because if you validate file during the transaction (and you want to purge it), after adding error and it will rollback deleting the attachment.
before_save :check_logo_file, on: %i[create update]
def check_favicon_content_type
PartnerValidators::CustomPartnerFaviconValidator.new.validate(self)
end
module PartnerValidators
class CustomPartnerFaviconValidator < ActiveModel::Validator
ALLOWED_MIME_TYPES = %w(image/vnd.microsoft.icon image/x-icon image/png).freeze
private_constant :ALLOWED_MIME_TYPES
def validate(partner)
if partner.favicon.attached? && invalid_content_type?(partner)
partner.errors.add(:favicon, I18n.t("active_admin.errors.favicon"))
partner.favicon.purge
end
end
private
def invalid_content_type?(partner)
!partner.favicon.blob.content_type.in?(ALLOWED_MIME_TYPES)
end
end
end

Storing an image based upon nothing but a URL in Paperclip

I am using paperclip for a profile picture upload feature in my rails app. This works nicely for the default case of uploading images to a profile, but I want to allow users without a picture to pick from one of a selection of precanned 'stock' images.
These images are hosted locally, within my assets images folder. Therefore on these occasions I want to be able to add an image to my EventImage object without actually uploading an image, more just referencing a URL at a local path.
I have tried pretty much every answer from this post : Save image from URL by paperclip but none of them seem to work. I am using paperclip version paperclip (4.3.1 37589f9)
When I try the solution of :
def photo_from_url(url)
puts "we got:"+url
Thread.new do
self.photo = URI.parse(url)
end
end
It results in no image reference being stored, and regardless of the URL to an image I pass into that method, it never displays my image when I do : <%= image_tag #event.event_images.first.photo.url %> - instead it shows the default image for when an image has not been located or stored.
I also have to put it in a new thread otherwise it gets tied up and blocks / resulting in a timeout which seems to be a problem with URI.parse, also the image ends up failing validation as photo is 'empty' which is not allowed in my validation, so I end up removing the validation presence line on :photo, which still does not solve the problem. I really just want the models paperclip method :photo - to point to a local url sometimes, and my correctly normally uploaded files other times.
See the whole class here:
# == Schema Information
#
# Table name: event_images
#
# id :integer not null, primary key
# caption :string
# event_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# photo_file_name :string
# photo_content_type :string
# photo_file_size :integer
# photo_updated_at :datetime
#
class EventImage < ActiveRecord::Base
attr_accessor :PAPERCLIP_STORAGE_OPTS
has_attached_file :photo , PAPERCLIP_STORAGE_OPTS
validates_attachment_presence :photo
validates_attachment_content_type :photo, :content_type => ["image/jpg", "image/jpeg", "image/gif", "image/png"]
validates_with AttachmentSizeValidator, :attributes => :photo, :less_than => 3.megabytes
belongs_to :event
def photo_from_url(url)
Thread.new do
self.photo = URI.parse(url)
end
end
end
Instead, I have decided that it would be best to add a 'canned_image_id' to the EventImage model, then use a photo_url method in the model which can choose to return either the paperclip url or the canned image url depending on whether or not the image is a canned image. It also hides this complexity behind a helper method :)

Mongoid: add some dynamic (non-DB) fields in Model before passing it to Controller

Let's say I have a Model Item which uses Mongoid
class Item
include Mongoid::Document
field :title, type: String
...
...
end
I want to add some dynamic fields to Item right in Model before passing data to a Controller - because Item is being used by several Controllers.
For example I want to add thumb field which I will generate by adding /path/to + filename.
I tried some solutions with attr_accessor:
class Item
include Mongoid::Document
field :title, type: String
...
...
attr_accessor :thumb
def prepare_data
#thumb = "/path/to/thumb"
end
end
...And later in some Controller:
#items_all = Item.all
#thumbs = []
#items_all.each do |i]
i.prepare_data
#thumbs.push(i[:thumb])
end
# #thumbs >>> (empty)
So it seems that I'm missing some point here because it doesn't work.
Also can I avoid calling prepare_data each time manually? May be with help of after_initialize? (which didn't work for me also).
I found my mistake. First I forgot to add after_initialize :do_something and then I found that I can user #attributes.merge!
after_initialize :do_after_initialize
def do_after_initialize
#attributes.merge!({
:title => self.get_title,
:type => self.get_type,
:thumb => ImageUploader::thumb(self[:pictures][0]["filename"])
:price_f => ActionController::Base.helpers.number_to_currency(self[:price], {:precision=>0})
})
end

Auto orienting image when using delayed job

I have a photo sharing application that allows the user to drag and drop images which are then processed in delayed job and displayed in a gallery. I'm having some problems solving the problem with orientation on iPhone pictures. I have the following code:
initializers/auto_orient.rb
module Paperclip
class AutoOrient < Paperclip::Processor
def initialize(file, options = {}, *args)
#file = file
end
def make( *args )
dst = Tempfile.new([#basename, #format].compact.join("."))
dst.binmode
Paperclip.run('convert',"#{File.expand_path(#file.path)} -auto-orient #{File.expand_path(dst.path)}")
return dst
end
end
end
models/picture.rb
class Picture < ActiveRecord::Base
belongs_to :gallery
before_create :generate_slug
after_create :send_to_delayed_job
validates :slug, :uniqueness => true
scope :processing, where(:processing => true)
attr_accessible :image
has_attached_file :image,
:styles => {
:huge => "2048x1536>",
:small => "800x600>",
:thumb => "320x240>"
},
:processors => [:auto_orient, :thumbnail]
before_post_process :continue_processing
...
def process
self.image.reprocess!
self.processing = false
self.save(:validations => false)
end
private
def continue_processing
if self.new_record?
!self.processing
end
end
def send_to_delayed_job
Delayed::Job.enqueue ImageProcess.new(self.id), :queue => 'paperclip'
end
end
models/image_process.rb
class ImageProcess < Struct.new(:picture_id)
def perform
picture = Picture.find(self.picture_id)
picture.process
end
end
If I comment out the lines after_create :send_to_delayed_job and before_post_process i.e. the processing is done on the spot, the auto-orientation process works. But, when I put it through delayed job, no auto-orientation happens, just the resizing.
Does anyone have any ideas?
EDIT
It get's stranger. I moved to Carrierwave and the carrierwave_backgrounder gem. Ignoring background tasks for now, I have the following in my image_uploader.rb:
def auto_orient
manipulate! do |img|
img.auto_orient!
img
end
end
version :huge do
process :auto_orient
process resize_to_fit: [2048,1536]
end
This works. The images are in the correct orientation.
Now, if I add process_in_background :image to my picture.rb file in accordance with the instructions for carrier wave_backgrounder, auto_orient doesn't work.
I'm now going to try the store_in_background method to see if that makes a difference.
I noticed you have attr_accessible in your Picture model. I have been working with Delayed_Job recently and after many hours of struggling with it I figured out that it has serious issues with attr_accessible. There are workarounds, though.
More info is here (although you can Google for more on the topic)

Custom Paperclip processor not being called

I have a user model that is generated using Devise. I am extending this model using paperclip to enable file upload and also the processing of a file using a custom paperclip processor.
My paperclip field is declared in the user model as follows. PaperClipStorage is a hash that I create with the paperclip variables. Also, the being stored on AWS S3.
has_attached_file :rb_resume, PaperclipStorageHash.merge(:style => { :contents => 'resume_contents'}, :processors => [:resume_builder])
validates_attachment_content_type :rb_resume, :if => lambda { |x| x.rb_resume? }, :content_type => ['application/pdf', 'application/x-pdf', 'application/msword', 'application/x-doc']
The validates_attachment_content_type check is being done to make sure that it only processes pdf and MS word files.
My processor looks as follows
module Paperclip
class ResumeBuilder < Processor
def initialize(file,options = {}, attachment = nil)
#file = file
#attachment = attachment
puts "Attachment is not null " if !attachment.nil?
end
def make
rb = MyModule::MyClass.new(#file.path) ### Do something with the file
section_layout = rb.parse_html
#attachment.instance_write(:whiny, section_layout)
#file
end
end
end
In my user model I also have an after_save callback that is supposed to take the section_layout generated in the processors make method. Code is as follows
after_save :save_sections
def save_sections
section_layout = rb_resume.instance_read(:whiny)
# Do something with section_layout...
end
Now my problem is that the processor code is never being called, and I can't figure out why.
Because of that the section_layout variable is always nil.
Another point to note is that the same model also has two other has_attached_file attributes. None of the other two use a custom processor.
I've been struggling with this for last 3 hours. Any help would be greatly appreciate.
Thanks
Paul
Error in my has_attached_file declaration
has_attached_file :rb_resume, PaperclipStorageHash.merge(:style => { :contents => 'resume_contents'}, :processors => [:resume_builder])
should actually be
has_attached_file :rb_resume, PaperclipStorageHash.merge(:styles => { :contents => 'resume_contents'}, :processors => [:resume_builder])
Notice the plural styles as opposed to singular style

Resources