Paperclip add image from URL in has_many association - ruby-on-rails

I've this models:
Prodcuts -> Products_Images.
I can add multiple images from a form uploading the image that it's my computer. I want to add images from URLs instead of locally images.
Paperclip has added this feature:
https://github.com/thoughtbot/paperclip/wiki/Attachment-downloaded-from-a-URL
but I don't know how to apply it in a has_many association.
I've tried adding a method in ProductImages model and call it for each URL after product is created. I don't know if I must use this method directly in Product model.
Where should I try to put the method of the wiki of Paperclip?

Here is a great gist (that I did not write). It should get you there: https://gist.github.com/jgv/1502777
require 'open-uri'
class Photo < ActiveRecord::Base
has_attached_file :image # etc...
before_validation :download_remote_image, :if => :image_url_provided?
validates_presence_of :image_remote_url, :if => :image_url_provided?, :message => 'is invalid or inaccessible'
private
def image_url_provided?
!self.image_url.blank?
end
def download_remote_image
io = open(URI.parse(image_url))
self.original_filename = io.base_uri.path.split('/').last
self.image = io
self.image_remote_url = image_url
rescue # catch url errors with validations instead of exceptions (Errno::ENOENT, OpenURI::HTTPError, etc...)
end
end
All credit to the author.
In the future, it's usually best to post the code with your attempt to solve the problem.

Related

Rails 5 + Shrine + Polymorphic model + Pretty Location

I'm using Shrine for direct uploads to S3 and I'm trying to user the pretty_location plugin to set the location in my S3 bucket.
I have a document model has the file_datatext attribute and is connected to a FileUploader:
class Document < ApplicationRecord
belongs_to :documentable, polymorphic: true
include FileUploader[:file]
validates :file, presence: true
end
Other models are associated with the document model through the following concern:
module Documentable
extend ActiveSupport::Concern
included do
has_one :document, as: :documentable, dependent: :destroy
accepts_nested_attributes_for :document, allow_destroy: true
end
end
This is my FileUploader:
class FileUploader < Shrine
Attacher.promote { |data| PromoteJob.perform_later(data) }
Attacher.delete { |data| DeleteJob.perform_later(data) }
plugin :upload_options, cache: {acl: "public-read"}
plugin :upload_options, store: {acl: "public-read"}
plugin :logging, logger: Rails.logger
plugin :pretty_location
plugin :processing
plugin :delete_promoted
plugin :recache
plugin :restore_cached_data
plugin :delete_raw
plugin :validation_helpers
def generate_location(io, context = {})
# do something depending on context[:record].documentable
end
end
When uploading files from the user's filesystem via the client browser through nested attributes all works as expected and I'm able to generate a pretty location in my S3 bucket.
However, I have another model where I am trying to upload to S3 a PDF file which is generated in the backend with the following setup.
class Invoice < ApplicationRecord
has_one :documents, as: :documentable, dependent: :destroy
end
The Invoice model doesn't use the concern as I want it to connect to the polymorphic document with a has_many association.
class Api::V1::InvoicesController < Api::V1::BaseController
def upload_pdf
pdf = InvoicePdf.new(#invoice)
attachment = pdf.render
file = StringIO.new(attachment)
file.class.class_eval { attr_accessor :original_filename, :content_type }
file.original_filename = "invoice_#{#invoice.reference}.pdf"
file.content_type = "application/pdf"
#document = #invoice.documents.new(file: file)
if #document.save
render "documents/show.json", status: :created
else
render json: { errors: #document.errors }, status: :unprocessable_entity
end
end
end
The upload works fine and I am able to upload the PDF to my S3 bucket, but I am not able to generate a pretty location because when I'm inside the generate_location method the context[:record] both the documentable_type and the documentable_id are nil.
This is a strange behaviour as in the rails console I am able to see that the association is correctly set after the upload has been done (without pretty_location) by running Invoice.last.documents.file.url.
I have tried creating the document record in different ways, have tried using the same documentable concern that works for other models but the result is alway the same and I have run out of ideas.
Does anyone have a clue why the documentable_type and documentable_id are not being passed into the context object inside the FileUploader?
The above setup actually works. I was using a breakpoint inside the generate_location FileUploader method and the api was breaking because that method was returning nil.
After fixing that, the first time it ran documentable was still nil but the method would run a second time with the documentable attributes present.
def generate_location(io, context = {})
return "" unless context[:record].documentable
path = if context[:record].documentable_type == "SomeClass"
# do something
elsif context[:record].documentable_type == "OtherClass"
# do something else
else
# do something else
end
return path
end

skip carrierwave Integirty and Processing validation

I have white listed some of the extensions in the carrierwave uploader class
def extension_white_list
%w(doc docx)
end
In some cases I would like to skip the Integrity validation while saving a record. But as per their documentation validates_integrity_of validation exist by default.
https://github.com/carrierwaveuploader/carrierwave/wiki/How-to%3A-Validate-uploads-with-Active-Record
can anyone please tell me how to skip such validation ?
in uploaders/file_uploader.rb
def extension_white_list
if model.do_i_need_validation?
%w(doc docx)
else
file.extension
end
end
and define this instance method in the model
def do_i_need_validation?
condition? ? true : false
end
Just replace the content of the method suitable to your app
I couldn't find anything about this in any of carrierwave's documentation, but reading its source code, one can pass specific uploader options in the mount_uploader call:
mount_uploader :field, MyUploader, options
Validations configuration do exist in uploader options, so you can, for example, disable all validations using:
mount_uploader :field, MyUploader, validate_download: false, validate_integrity: false, validate_processing: false
Note that when doing this the errors are silently ignored, so saves will succeed. This could be unexpected behavior. You can check if the operation actually had any errors using the model helpers <field>_processing_error, <field>_integrity_error and <field>_download_error:
class Article < ActiveRecord::Base
mount_uploader :image, ImageUploader, validate_integrity: false
end
article = Article.find(1)
article.update_attributes!(title: "New article title", image: open("/path/to/invalid_image.jpg")) # => this will actually succeed
article.image_integrity_error # => returns the error message from carrierwave

carrierwave: mount uploader on serialized dynamic attribute

first of all, i am using rails 3.1.3 and carrierwave from the master
branch of the github repo.
i use a after_init hook to determine fields based on an attribute of
the page model instance and define attribute accessors for these field
which store the values in a serialized hash (hope it's clear what i am
talking about). here is a stripped down version of what i am doing:
class Page < ActiveRecord::Base
serialize :fields, Hash
after_initialize :set_accessors
def set_accessors
case self.template
when 'standard'
class << self
define_method 'image' do
self.fields['image']
end
define_method 'image=' do |value|
self.fields['image'] = value
end
end
mount_uploader :image, PageImageUploader
end
end
end
end
leaving out the mount_uploader command gives me access to the
attribute as i want. but when i mount the uploader a get an error
message saying 'undefined method new for nil class'
i read in the source that there are the methods read_uploader and
write_uploader in the extensions module.
how do i have to override these to make the mount_uploader command
work with my 'virtual' attribute.
i hope somebody has an idea how i can solve this problem. thanks a lot
for your help.
best regard. dominik.
Same problem but solved in your model you should override read_uploader(column) and write_uploader(column, identifier) instance methods. I also have a problem with #{column}_will_change! and #{column}_changed? for a virtual column so I had to define them too:
class A < ActiveRecord::Base
serialize :meta, Hash
mount_uploader :image, ImageUploader
def image_will_change!
meta_will_change!
#image_changed = true
end
def image_changed?
#image_changed
end
def write_uploader(column, identifier)
self.meta[column.to_s] = identifier
end
def read_uploader(column)
self.meta[column.to_s]
end
end
Now there's also an add-on to carrierwave which provides the exact functionality as described by Antiarchitect:
https://github.com/timsly/carrierwave-serializable

How to bulk validate association in Rails

I have the following scenario:
One of my models, let's call it 'Post', has multiple associated models, Images.
One, and only one, of those images can be the key Image to its Post (that is represented as a boolean flag on the Image model and enforced through a validation on the Image model which uses the Post as its scope).
Now of course when I want to update the primary Image flag, it happens that an Image model's key flag is set to true and the validation fails because there's still another Image with the key flag set to true.
I know, that thing screams to be transformed into an association on the Post model, which links to the key Image, but is there a way to validate associations in bulk in Rails?
What would be your take, would you make the key Image a separate association on the Post model or would you use the boolean flag?
there is a simple solution but it needs some trust:
Remove the validation "is there only one primary image?"
Make sure there will be only one primary image by adding a filter
The big plus is that you don't have to check anything in your controller or post model. Just take an image, set is_primary to true and save it.
So the setup could look like:
class Post < ActiveRecord::Base
has_many :images
# some sugar, #mypost.primary_image gets the primary image
has_one :primary_image,
:class_name => "Image",
:conditions => {:is_primary => true }
end
class Image < ActiveRecord::Base
belongs_to :post
# Image.primary scopes on primary images only
scope :primary, where(:is_primary => true)
# we need to clear the old primary if:
# this is a new record and should be primary image
# this is an existing record and is_primary has been changed to true
before_save :clear_primary,
:if => Proc.new{|r| (r.new_record? && r.is_primary) || (r.is_primary_changed? && r.is_primary) }
def clear_primary
# remove old primary image
Image.update_all({:is_primary => false}, :post_id => self.post_id)
end
end
Edit:
This will work in any case - why?
before_save is only invoked if all validations succeed
the whole save is wrapped in a transaction, this means if clear_primary or the saving of the image itself fails, everyhing will be rolled back to it's original state.
Well you can do this within your Post model:
# Post.rb
has_many :images, :conditions => ['primary = ?', false]
has_one :primary_image, :conditions => ['primary = ?', true]
When you want to change the primary image, do something like this:
# Post.rb
def new_primary_image(image_id)
primary_image.primary = false
Image.find(image_id).primary = true
end

ActiveRecord serialize using JSON instead of YAML

I have a model that uses a serialized column:
class Form < ActiveRecord::Base
serialize :options, Hash
end
Is there a way to make this serialization use JSON instead of YAML?
In Rails 3.1 you can just
class Form < ActiveRecord::Base
serialize :column, JSON
end
In Rails 3.1 you can use custom coders with serialize.
class ColorCoder
# Called to deserialize data to ruby object.
def load(data)
end
# Called to convert from ruby object to serialized data.
def dump(obj)
end
end
class Fruits < ActiveRecord::Base
serialize :color, ColorCoder.new
end
Hope this helps.
References:
Definition of serialize:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/base.rb#L556
The default YAML coder that ships with rails:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/coders/yaml_column.rb
And this is where the call to the load happens:
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/read.rb#L132
Update
See mid's high rated answer below for a much more appropriate Rails >= 3.1 answer. This is a great answer for Rails < 3.1.
Probably this is what you're looking for.
Form.find(:first).to_json
Update
1) Install 'json' gem:
gem install json
2) Create JsonWrapper class
# lib/json_wrapper.rb
require 'json'
class JsonWrapper
def initialize(attribute)
#attribute = attribute.to_s
end
def before_save(record)
record.send("#{#attribute}=", JsonWrapper.encrypt(record.send("#{#attribute}")))
end
def after_save(record)
record.send("#{#attribute}=", JsonWrapper.decrypt(record.send("#{#attribute}")))
end
def self.encrypt(value)
value.to_json
end
def self.decrypt(value)
JSON.parse(value) rescue value
end
end
3) Add model callbacks:
#app/models/user.rb
class User < ActiveRecord::Base
before_save JsonWrapper.new( :name )
after_save JsonWrapper.new( :name )
def after_find
self.name = JsonWrapper.decrypt self.name
end
end
4) Test it!
User.create :name => {"a"=>"b", "c"=>["d", "e"]}
PS:
It's not quite DRY, but I did my best. If anyone can fix after_find in User model, it'll be great.
My requirements didn't need a lot of code re-use at this stage, so my distilled code is a variation on the above answer:
require "json/ext"
before_save :json_serialize
after_save :json_deserialize
def json_serialize
self.options = self.options.to_json
end
def json_deserialize
self.options = JSON.parse(options)
end
def after_find
json_deserialize
end
Cheers, quite easy in the end!
The serialize :attr, JSON using composed_of method works like this:
composed_of :auth,
:class_name => 'ActiveSupport::JSON',
:mapping => %w(url to_json),
:constructor => Proc.new { |url| ActiveSupport::JSON.decode(url) }
where url is the attribute to be serialized using json
and auth is the new method available on your model that saves its value in json format to the url attribute. (not fully tested yet but seems to be working)
I wrote my own YAML coder, that takes a default. Here is the class:
class JSONColumn
def initialize(default={})
#default = default
end
# this might be the database default and we should plan for empty strings or nils
def load(s)
s.present? ? JSON.load(s) : #default.clone
end
# this should only be nil or an object that serializes to JSON (like a hash or array)
def dump(o)
JSON.dump(o || #default)
end
end
Since load and dump are instance methods it requires an instance to be passed as the second argument to serialize in the model definition. Here's an example of it:
class Person < ActiveRecord::Base
validate :name, :pets, :presence => true
serialize :pets, JSONColumn.new([])
end
I tried creating a new instance, loading an instance, and dumping an instance in IRB, and it all seemed to work properly. I wrote a blog post about it, too.
A simpler solution is to use composed_of as described in this blog post by Michael Rykov. I like this solution because it requires the use of fewer callbacks.
Here is the gist of it:
composed_of :settings, :class_name => 'Settings', :mapping => %w(settings to_json),
:constructor => Settings.method(:from_json),
:converter => Settings.method(:from_json)
after_validation do |u|
u.settings = u.settings if u.settings.dirty? # Force to serialize
end
Aleran, have you used this method with Rails 3? I've somewhat got the same issue and I was heading towards serialized when I ran into this post by Michael Rykov, but commenting on his blog is not possible, or at least on that post. To my understanding he is saying that you do not need to define Settings class, however when I try this it keeps telling me that Setting is not defined. So I was just wondering if you have used it and what more should have been described? Thanks.

Resources