I am working with Rails 3 and Paperclip to attach uploaded files to several object types using a polymorphic association. I have created an Asset model and an inherited Image model (will be adding others, like Video and Documents later) as follows:
# app/models/asset.rb
class Asset < ActiveRecord::Base
# Nothing here yet
end
# app/models/image.rb
class Image < Asset
belongs_to :assetable, :polymorphic => true
has_attached_file :file, {
:styles => {
:small => { :geometry => '23x23#', :format => 'png' },
:medium => { :geometry => '100x100#', :format => 'png' } }
}.merge(PAPERCLIP_STORAGE_OPTIONS).merge(PAPERCLIP_STORAGE_OPTIONS_ASSET_IMAGE) # Variables sent in environments to direct uploads to filesystem storage in development.rb and S3 in production.rb
validates_attachment_presence :file
validates_attachment_size :file, :less_than => 5.megabytes
end
I then have another object type, Unit, which I am attaching multiple images to as follows:
# app/models/unit.rb
class Unit < ActiveRecord::Base
# ...
has_many :images, :as => :assetable, :dependent => :destroy
accepts_nested_attributes_for :images
end
# app/controllers/units_controller.rb
class UnitsController < ApplicationController
# ...
def new
#unit = current_user.units.new
# ...
#unit.images.build
end
def create
#unit = current_user.units.new(params[:unit])
# ...
respond_to do |format|
if #unit.save
format.html { redirect_to(#unit, :notice => 'Unit creation successful!') }
else
format.html { render :action => "new" }
end
end
end
def show
#unit = current_user.units.find(params[:id])
#unit_images = #unit.images
# ...
end
def edit
#unit = current_user.units.find(params[:id])
# ...
#unit.images.build
end
def update
#unit = current_user.units.find(params[:id], :readonly => false)
respond_to do |format|
if #unit.update_attributes(params[:unit])
format.html { redirect_to(#unit, :notice => 'Unit was successfully updated.') }
else
format.html { render :action => "edit" }
end
end
end
def destroy
#unit = current_user.units.find(params[:id])
#unit.destroy
respond_to do |format|
format.html { redirect_to(units_url) }
end
end
end
# app/views/units/_form.html.haml
.field # Display already uploaded images
= f.fields_for :images do |assets|
- unless assets.object.new_record?
= link_to(image_tag(assets.object.file.url(:medium)), assets.object.file.url(:original))
.field # Display field to add new image
= f.fields_for :images do |assets|
- if assets.object.new_record?
= assets.label :images, "Image File"
= assets.file_field :file, :class => 'uploadify'
Using these settings I am able to upload images one at at time, per display of the form.
The issues start when I try to integrate Uploadify to add multi file uploading/previewing. I have satisfied all the Uploadify dependancies, but in order to save the images associated with the Unit model I need to somehow include a reverence to the unit_id so that the polymorphic association can be made properly. Below is my current Uploadify code:
%script
$(document).ready(function() {
$('.uploadify').uploadify({
uploader : '/uploadify/uploadify.swf',
cancelImg : '/uploadify/cancel.png',
auto : true,
multi : true,
script : '#{units_path}',
scriptData : {
"#{key = Rails.application.config.session_options[:key]}" : "#{cookies[key]}",
"#{request_forgery_protection_token}" : "#{form_authenticity_token}",
}
});
});
So while I can easily upload with Paperclip along, Uploadify will not work. Any help would be much appreciated. Thank you in advance.
UPDATE:
After doing more research I ran across this comment to a similar issue: Rails3, S3, Paperclip Attachment as it's own model?. Any thoughts on whether or not that would work in this situation? Is there an easy way of determining the unit.id from the /new method and passing it to the Uploadify-created Asset?
We had solved a very similar problem once by saving the model right away when loading the form in a draft state (using state machine). Like this the model is available when you're trying to attach the files you're uploading and once you're submitting the rest of the form, you're basically just updating the model which changes it's state to e.g. published. It's a little work to update the controllers etc., but it did the trick.
Related
I am using paperclip with s3 in a rails 4 app. It is working fine everywhere, but I have a specific use case that is requiring some special behavior.
I need to upload an image as an avatar, and have it resize to all the thumbnail sizes, but then I need to be able to update only the original image, while preserving all the thumbnail links.
Currently, I'm using a Proc to determine attachment sizes based on a class variable. This is causing image 1 to be uploaded and resized, then I am setting image 2 with no styles. I was hoping this would create all the thumbs, then replace the original. Unfortunately, it is updating the URLs for each size, but they are empty.
tl;dr - I need to have avatars resized, but I need to be able to update ONLY the original and leave the rest alone.
Controller
class StudentsController < ApplicationController
# POST /students/:id/avatar
def new_avatar
current_student.avatar = params[:avatar]
current_student.set_orginial_only TRUE
current_student.avatar = params[:avatar_orig]
if current_student.save
render json: current_student, serializer: StudentAvatarSerializer, status: 200
else
render json: ErrorSerializer.new(current_student), status: 400
end
end
# DELETE /students/:id/avatar
def destroy_avatar
current_student.avatar.destroy
if current_student.save
render json: {success: true}, status: 200
else
render json: ErrorSerializer.new(current_student), status: 400
end
end
private
# find student by id and cache
def current_student
#student ||= Student.find(params[:id])
end
end
Model
class Student < ActiveRecord::Base
##orginial_only = FALSE
def set_orginial_only value
##orginial_only = value
end
# Paperclip attachements
has_attached_file :avatar, :styles => Proc.new { |clip| clip.instance.attachment_sizes },
path: "/:class/:attachment/:id/:content_type_extension/:style/:filename",
:default_url => "/images/:style/missing.png"
validates_attachment_content_type :avatar, :content_type => /image/
validates_attachment_size :avatar, :in => 0..2.megabytes
def attachment_sizes
if ##orginial_only
styles = {}
else
styles = {
tiny: '50x50#',
tiny_retina: '100x100#',
small: '60x60#',
small_retina: '120x120#',
medium: '108x108#',
medium_retina: '216x216#',
large: '205x205#',
large_retina: '410x410#'
}
end
styles
end
end
Is there a way to only update the original image while keeping all my thumbs?
Solution: I ended up just using paperclip for the resized image, and aws-sdk to manually replace the original image in the paperclip assigned path.
I'm writing some image upload code for Ruby on Rails with Paperclip, and I've got a working solution but it's very hacky so I'd really appreciate advice on how to better implement it. I have an 'Asset' class containing information about the uploaded images including the Paperclip attachment, and a 'Generator' class that encapsulates sizing information. Each 'Project' has multiple assets and generators; all Assets should be resized according to the sizes specified by each generator; each Project therefore has a certain set of sizes that all of its assets should have.
Generator model:
class Generator < ActiveRecord::Base
attr_accessible :height, :width
belongs_to :project
def sym
"#{self.width}x#{self.height}".to_sym
end
end
Asset model:
class Asset < ActiveRecord::Base
attr_accessible :filename,
:image # etc.
attr_accessor :generators
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
# this is utterly horrendous
def styles
s = {}
if #generators == nil
#generators = self.project.generators
end
#generators.each do |g|
s[g.sym] = "#{g.width}x#{g.height}"
end
s
end
end
Asset controller create method:
def create
#project = Project.find(params[:project_id])
#asset = Asset.new
#asset.generators = #project.generators
#asset.update_attributes(params[:asset])
#asset.project = #project
#asset.uploaded_by = current_user
respond_to do |format|
if #asset.save_(current_user)
#project.last_asset = #asset
#project.save
format.html { redirect_to project_asset_url(#asset.project, #asset), notice: 'Asset was successfully created.' }
format.json { render json: #asset, status: :created, location: #asset }
else
format.html { render action: "new" }
format.json { render json: #asset.errors, status: :unprocessable_entity }
end
end
end
The problem I am having is a chicken-egg issue: the newly created Asset doesn't know which generators (size specifications) to use until after it's been instantiated properly. I tried using #project.assets.build, but then the Paperclip code is still executed before the Asset gets its project association set and nils out on me.
The 'if #generators == nil' hack is so the update method will work without further hacking in the controller.
All in all it feels pretty bad. Can anyone suggest how to write this in a more sensible way, or even an approach to take for this kind of thing?
Thanks in advance! :)
I ran into the same Paperclip chicken/egg issue on a project trying to use dynamic styles based on the associated model with a polymorphic relationship. I've adapted my solution to your existing code. An explanation follows:
class Asset < ActiveRecord::Base
attr_accessible :image, :deferred_image
attr_writer :deferred_image
has_attached_file :image,
:styles => lambda { |a| a.instance.styles }
belongs_to :project
after_save :assign_deferred_image
def styles
project.generators.each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
private
def assign_deferred_image
if #deferred_image
self.image = #deferred_image
#deferred_image = nil
save!
end
end
end
Basically, to get around the issue of Paperclip trying to retrieve the dynamic styles before the project relation information has been propagated, you can assign all of the image attributes to a non-Paperclip attribute (in this instance, I have name it deferred_image). The after_save hook assigns the value of #deferred_image to self.image, which kicks off all the Paperclip jazz.
Your controller becomes:
# AssetsController
def create
#project = Project.find(params[:project_id])
#asset = #project.assets.build(params[:asset])
#asset.uploaded_by = current_user
respond_to do |format|
# all this is unrelated and can stay the same
end
end
And the view:
<%= form_for #asset do |f| %>
<%# other asset attributes %>
<%= f.label :deferred_upload %>
<%= f.file_field :deferred_upload %>
<%= f.submit %>
<% end %>
This solution also allows using accepts_nested_attributes for the assets relation in the Project model (which is currently how I'm using it - to upload assets as part of creating/editing a Project).
There are some downsides to this approach (ex. validating the Paperclip image in relation to the validity of the Asset instance gets tricky), but it's the best I could come up with short of monkey patching Paperclip to somehow defer execution of the style method until after the association information had been populated.
I'll be keeping an eye on this question to see if anyone has a better solution to this problem!
At the very least, if you choose to keep using your same solution, you can make the following stylistic improvement to your Asset#styles method:
def styles
(#generators || project.generators).each_with_object({}) { |g, hsh| hsh[g.sym] = "#{g.width}x#{g.height}" }
end
Does the exact same thing as your existing method, but more succinctly.
While I really like Cade's solution, just a suggestion. It seems like the 'styles' belong to a project...so why aren't you calculating the generators there?
For example:
class Asset < ActiveRecord::Base
attr_accessible :filename,
:image # etc.
attr_accessor :generators
has_attached_file :image,
:styles => lambda { |a| a.instance.project.styles }
end
class Project < ActiveRecord::Base
....
def styles
#generators ||= self.generators.inject {} do |hash, g|
hash[g.sym] = "#{g.width}x#{g.height}"
end
end
end
EDIT: Try changing your controller to (assuming the project has many assets):
def create
#project = Project.find(params[:project_id])
#asset = #project.assets.new
#asset.generators = #project.generators
#asset.update_attributes(params[:asset])
#asset.uploaded_by = current_user
end
I've just solved a similar problem that I had.
In my "styles" lambda I am returning a different style depending on the value of a "category" attribute. The problem though is that Image.new(attrs), and image.update_attributes(attrs) doesn't set the attributes in a predictable order, and thus I can't be guaranteed that image.category will have a value before my styles lambda is called. My solution was to override attributes=() in my Image model as follows:
class Image
...
has_attached_file :image, :styles => my_lambda, ...
...
def attributes=(new_attributes, guard_protected_attributes = true)
return unless new_attributes.is_a?(Hash)
if new_attributes.key?("image")
only_attached_file = {
"image" => new_attributes["image"]
}
without_attached_file = new_attributes
without_attached_file.delete("image")
# set the non-paperclip attributes first
super(without_attached_file, guard_protected_attributes)
# set the paperclip attribute(s) after
super(only_attached_file, guard_protected_attributes)
else
super(new_attributes, guard_protected_attributes)
end
end
...
end
This ensures that the paperclip attribute is set after the other attributes and can thus use them in a :style lambda.
It clearly won't help in situations where the paperclip attribute is "manually" set. However in those circumstances you can help yourself by specifying a sensible order. In my case I could write:
image = Image.new
image.category = "some category"
image.image = File.open("/somefile") # styles lambda can use the "category" attribute
image.save!
(Paperclip 2.7.4, rails 3, ruby 1.8.7)
I'm trying to upload an attachment using REST API on my server through a PUT request. I can do this by putting the binary file in the request body but I'd also like to save this file as an attachment to a model which uses paperclip to save attachments.
Here's my current involved class definitions:
class Cl < ActiveRecord::Base
after_update :save_tses
validates_associated :tses
has_many :tses
...truncated...
def save_tses
tses.each do |ts|
ts.save(false)
end
end
end
class Ts < ActiveRecord::Base
has_attached_file :tsa, :styles => { :thumb => {:geometry => "100x141>", :format => :jpg} },
:path => ":rails_root/public/system/:attachment/:id/:style/:friendly_filename",
:url => "/system/:attachment/:id/:style/:friendly_filename"
belongs_to :cl
def friendly_filename
"#{self.tsa_file_name.gsub( /[^a-zA-Z0-9_\.]/, '_')}"
end
end
I can save the attachments just fine using the file upload on the html page. I'd like to do this on a controller that receives the file as binary data through a PUT request.
Any suggestions?
Also you can you use -
https://github.com/jwagener/httmultiparty
Got it,
# controller.rb
def add_ts
# params[:id]
# params[:tsa]
#cl = Cl.find(params[:id])
ts = #cl.tses.build(:name => "#{#cl.name}_#{Time.now.to_i}")
ts.tsa = params[:tsa]
if ts.save
render :json => {:status => "OK"}
else
render :json => {:status => "ERROR"}
end
end
# Test
curl -F "tsa=#file.pdf" "http://host/cl/474/add_ts"
=> {"status":"OK"}
I'm trying to upload to a portfolio app I've built, specifically trying to find where to hook delayed_job into the process. It all works otherwise. Right now it returns undefined method 'call' for #<Class:0xae68750> on app/controllers/portfolio_items_controller.rb:18:in 'create' so here's my model and that portion of the controller... anyone see anything that could be going wrong? The hook I'm using now I got from this blog: http://madeofcode.com/posts/42-paperclip-s3-delayed-job-in-rails
/app/controllers/portfolio_items_controller.rb
def create
#portfolio_item = PortfolioItem.new(params[:portfolio_item])
if #portfolio_item.save
flash[:notice] = "Portfolio item created. As soon as files are uploaded Portfolio item will be made live."
redirect_to #portfolio_item
else
render :action => 'new'
end
end
/app/models/asset.rb
class Asset < ActiveRecord::Base
attr_accessible :image, :image_file_name, :image_content_type, :image_file_size, :portfolio_item_id, :order
belongs_to :portfolio_item
has_attached_file :image,
:styles => {
:thumb => "20x20#",
:small => "100x100",
:large => "600x600>"
},
:storage => :s3,
:s3_credentials => {
:access_key_id => ENV["S3_KEY"],
:secret_access_key => ENV["S3_SECRET"]
},
:bucket => ENV["S3_BUCKET"],
:path => "portfolio/:attachment/:id/:style/:basename.:extension"
before_source_post_process do |image|
if source_changed?
processing = true
false
end
end
after_save do |image|
if image.source_changed?
Delayed::Job.enqueue ImageJob.new(image.id)
end
end
def regenerate_styles!
self.source.reprocess!
self.processing = false
self.save(false)
end
def source_changed?
self.source_file_size_changed? ||
self.source_file_name_changed? ||
self.source_content_type_changed? ||
self.source_update_at_changed?
end
end
class ImageJob < Struct.new(:image_id)
def perform
Image.find(self.image_id).regenerate_styles!
end
end
Edit: thanks to kind people, it's not the missing .new anymore. But now it's that the before_source_post_process is not defined? And I can't find that method in anywhere but that blog post and this SO question. Is there something more appropriate?
The before_source_post_process won't work for you. It only works for:
has_attached_file :source
In your case it should be
before_image_post_process
Similarly, the source_changed? method should be:
def source_changed?
self.image_file_size_changed? ||
self.image_file_name_changed? ||
self.image_content_type_changed? ||
self.image_update_at_changed?
end
I think this:
#portfolio_item = PortfolioItem.(params[:portfolio_item])
should most likely be this:
#portfolio_item = PortfolioItem.new(params[:portfolio_item])
I had a quick question. Is it possible to save a file without actually uploading it through a form?
For example, let's say I'm looking at attachments from emails, and I want to save them using a paperclip. How do I do this? Do I manually have to call a save_file(or something similar) somewhere?
Any help would be much appreciated!
I have a rake task that loads images (client logos) from a directory directly onto parperclip. You can probably adapt it to your needs.
This is my simplified Client model:
class Client < ActiveRecord::Base
LOGO_STYLES = {
:original => ['1024x768>', :jpg],
:medium => ['256x192#', :jpg],
:small => ['128x96#', :jpg]
}
has_attached_file :logo,
:styles => Client::LOGO_STYLES,
:url => "/clients/logo/:id.jpg?style=:style"
attr_protected :logo_file_name, :logo_content_type, :logo_size
Then on my rake task I do this:
# the logos are in a folder with path logos_dir
Dir.glob(File.join(logos_dir,'*')).each do |logo_path|
if File.basename(logo_path)[0]!= '.' and !File.directory? logo_path
client_code = File.basename(logo_path, '.*') #filename without extension
client = Client.find_by_code(client_code) #you could use the ids, too
raise "could not find client for client_code #{client_code}" if client.nil?
File.open(logo_path) do |f|
client.logo = f # just assign the logo attribute to a file
client.save
end #file gets closed automatically here
end
end
Regards!
The file saved in Paperclip doesn't have to be uploaded directly through a form.
I'm using Paperclip in a project to save files from URLs from webcrawler results. I'm not sure how you'd get email attachments (are they on the local file system of the server? Is your app an email app like GMail?) but as long as you can get a file stream (via something like open(URI.parse(crawl_result)) in my case...) you can attach that file to your model field that's marked has_attached_file.
This blog post about Easy Upload via URL with Paperclip helped me figure this out.
Since it now appears the original blog post is no longer available - here's the gist of it pulled from wayback machine:
This example shows a Photo model that has an Image attachment.
The technique we're using requires adding a *_remote_url (string) column for your attachment, which is used to store the original URL. So, in this case, we need to add a column named image_remote_url the photos table.
# db/migrate/20081210200032_add_image_remote_url_to_photos.rb
class AddImageRemoteUrlToPhotos < ActiveRecord::Migration
def self.up
add_column :photos, :image_remote_url, :string
end
def self.down
remove_column :photos, :image_remote_url
end
end
Nothing special is required for the controller...
# app/controllers/photos_controller.rb
class PhotosController < ApplicationController
def create
#photo = Photo.new(params[:photo])
if #photo.save
redirect_to photos_path
else
render :action => 'new'
end
end
end
In the form, we add a text_field called :image_url, so people can upload a file or provide a URL...
# app/views/photos/new.html.erb
<%= error_messages_for :photo %>
<% form_for :photo, :html => { :multipart => true } do |f| %>
Upload a photo: <%= f.file_field :image %><br>
...or provide a URL: <%= f.text_field :image_url %><br>
<%= f.submit 'Submit' %>
<% end %>
The meaty stuff is in the Photo model. We need to require open-uri, add an attr_accessor :image_url, and do the normal has_attached_file stuff. Then, we add a before_validation callback to download the file in the image_url attribute (if provided) and save the original URL as image_remote_url. Finally, we do a validates_presence_of :image_remote_url, which allows us to rescue from the many exceptions that can be raised when attempting to download the file.
# app/models/photo.rb
require 'open-uri'
class Photo < ActiveRecord::Base
attr_accessor :image_url
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
self.image = do_download_remote_image
self.image_remote_url = image_url
end
def do_download_remote_image
io = open(URI.parse(image_url))
def io.original_filename; base_uri.path.split('/').last; end
io.original_filename.blank? ? nil : io
rescue # catch url errors with validations instead of exceptions (Errno::ENOENT, OpenURI::HTTPError, etc...)
end
end
Everything will work as normal, including the creation of thumbnails, etc. Plus, since we're doing all of the hard stuff in the model, "uploading" a file via URL works from within script/console as well:
$ script/console
Loading development environment (Rails 2.2.2)
>> Photo.new(:image_url => 'http://www.google.com/intl/en_ALL/images/logo.gif')
=> #<Photo image_file_name: "logo.gif", image_remote_url: "http://www.google.com/intl/en_ALL/images/logo.gif">