Rails Paperclip, DRY configuration - ruby-on-rails

In order to DRY up my code for attachments pictures, I created an initializer to override the #default_options variable used by Paperclip.
This way, I don't have to specify again and again the url, path and storage I want.
I'd like to go a step further and include the validation in it but I can't make it work...
Any Idea?
EDIT 1: I want at least to validate both presence and size.
EDIT 2: Part of my current code
module Paperclip
class Attachment
def self.default_options
if Rails.env != "production"
#default_options = {
:url => "/assets/:class/:attachment/:id/:style/:normalized_name",
:path => ":rails_root/public/assets/:class/:attachment/:id/:style/:normalized_name",
:default_style => :original,
:storage => :filesystem,
:whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
}
else
...
end
end
end
normalized_name is an outside function, feat: http://blog.wyeworks.com/2009/7/13/paperclip-file-rename
EDIT 3:
This blog: http://omgsean.com/2009/02/overriding-paperclip-defaults-for-your-entire-rails-app/ presnents the default_options hash with a validations key.
So it could be possible, not found yet though.

You will not be able to move the validations into a default_options hash (as these validations are performed outside the attachment class (inside a paperclip module). My thought is that if you have the same validations across all your models, you might need to look into using inheritance to decrease code duplication. I would advise against moving validations into an initializer.

Related

Rendering Paperclip URL's using JBUILDER

I am building an API with Ruby on Rails.
I have Users that have avatars implemented using paperclip.
I am trying to access my paperclip URL's in my JSON output and it just crashed my heroku instance. This works perfectly locally.
Snippet of My User Model
class User < ActiveRecord::Base
has_attached_file :avatar, styles: { large: "600x600#", medium: "300x300#", thumb: "100x100#" }, default_url: "https://s3.amazonaws.com/whiztutor-marketing/photos/Profile300.png"
validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\Z/
end
Snippet of My Jbuilder for my user
json.cache! #user do
json.id #user.id
json.type #user.type
json.f_name #user.f_name
json.about #user.about
json.email #user.email
json.avatar_url #user.avatar.url(:medium)
json.referral_code #user.referral_code
json.education_level #user.education_level
json.photo_url #user.avatar.to_json
json.education_details #user.education_details.all
json.all_subjects #user.subjects.all
json.all_times #user.all_available_times.each do |time|
json.day time.schedule.to_s
json.time_block time.time_as_string
json.next_occurrence time.schedule.next_occurrence(Time.now)
end
end
I tried wrapping this into a method as found on this question and it breaks the server the exact same way. I can even run console and access these URL's directly with the server. Something with JBUILDER and PAPERCLIP just don't mix and I can't seem to get to the bottom of it. Any help is greatly appreciated.
Finally diagnosed this issue after several hours of research.
The issue is with the "json.cache! #user do" - Specifically the .cache! - I wanted to save a few server cycles and speed things up so this is how I implemented things at first.
Regardless, when I updated my JBUILDER code to the following I am no longer getting 500 server errors.
Snippet of My working Jbuilder for my user
json.id #user.id
json.type #user.type
json.f_name #user.f_name
json.about #user.about
json.email #user.email
json.small_photo #user.profile_url_small
json.medium_photo #user.profile_url_medium
json.referral_code #user.referral_code
json.education_level #user.education_level
json.education_details #user.education_details.all
json.all_subjects #user.subjects.all
json.all_times #user.all_available_times.each do |time|
json.day time.schedule.to_s
json.time_block time.time_as_string
json.next_occurrence time.schedule.next_occurrence(Time.now)
end
It's hard to say what the problem might be without knowing the error, but these things look suspicious or just wrong:
json.avatar_url #user.avatar.url(:medium)
json.photo_url #user.avatar.to_json
The second one is going to give you extra quotes you probably don't want. And why have both?
Also here:
json.all_times #user.all_available_times.each do |time|
json.day time.schedule.to_s
json.time_block time.time_as_string
json.next_occurrence time.schedule.next_occurrence(Time.now)
end
I think you want this:
json.all_times #user.all_available_times do |time|
json.day time.schedule.to_s
json.time_block time.time_as_string
json.next_occurrence time.schedule.next_occurrence(Time.now)
end
You can try this to get the url in json response, it works for me placed this method in your model.
def avatar_url
avatar.url(:medium)
end
And call this method from controller,by overriding to_json()
render :json => #friends.to_json(:methods => [:avatar_url])

Extract and validate data from attachment

I'm using paperclip to manage my file uploads in rails.
From the attachments users give me, I'd like to extract some data to associate with the model the attachment is associated with.
has_attached_file :resume, #...
# ...
def extract_resume_summary
path_to_resume = self.resume.queued_for_write[:original].path
extracted = parse_resume_file(path_to_resume)
self.number_of_jobs = extracted.number_of_jobs
self.highest_level_of_education = extracted.highest_level_of_education
rescue ResumeParseError => e
#problem_with_resume = e.message
end
I'm having some trouble figuring out exactly when and where to do this.
I could use a custom Paperclip::Processor:
class ::Paperclip::Summary < ::Paperclip::Processor
def make
#attachment.instance.extract_resume_summary
Tempfile.new('unused')
end
end
# ...
has_attached_file :resume,
:styles => { :summary => {} },
:processors => [ :summary ] }, #...
But the fit isn't great. I think processors are intended to create new files (which I don't need, and thus the spurious Tempfile).
Also my extraction might fail, which means my user gave me bad data. I want that to be a validation-time problem, so I can report it along with other validation errors, and post-processing happens strictly AFTER validation.
I've tried hacking it in at initialization:
validate :successfully_parses_resume
def successfully_parses_resume
errors.add(:resume, #problem_with_resume) if #problem_with_resume
end
def initialize(attributes=nil, options={})
super
extract_resume_summary
end
But I'm not quite sure that's right either, as that's not only when the file is uploaded, but also when I read the model in later. To say nothing of the havoc that could happen if I assumed #resume= or #[:resume]= was auto updating the extracted data too.
I think in an ideal world, I'd just subclass Paperclip::Attachment and make my extracted data peers of resume_file_name, resume_file_size, resume_content_type, resume_created_at, extracting that at the same time the mime-type is calculated and the file size is calculated. But looking at the source, those are pretty hard-coded throughout.
Is there another way to do this that I'm overlooking?
The solution I figured out was to wrap the attachment's setter. That's what'll be called during initialize and will give me a chance to detect problems before validation.
The only trick is that since the attachment's setter is created by has_attached_file and not inherited from ActiveRecord::Base, I can't just use super, I need to get an explicit reference to the version defined by has_attached_file (either by alias or, my preference, by instance_method):
has_attached_file :resume
old_setter = instance_method :resume=
define_method :resume= do |file|
old_setter.bind(self).call(file)
begin
extracted = parse_resume_file(resume.path)
self.number_of_jobs = extracted.number_of_jobs
self.highest_level_of_education = extracted.highest_level_of_education
rescue ResumeParseError => e
#problem_with_resume = e.message
end
end

Collecting first segments of all routes in rails 3

I'm trying to implement routes where the first segment is the profile's alias or id:
resources :profiles, :path => '' do
...
end
And I need to validate that alias is not already taken by first segments of other(higher) routes. What I have now is:
validates :alias, :exclusion => {:in => Rails.application.routes.routes.map{|r| r.path.spec.to_s.split('(').first.split('/').second}.compact.uniq },
....
In development everything is ok. In production Rails.application.routes.routes.map... returns empty array. But only inside validation in model, if I put it somewhere in view just to test it returns array of first segments of all routes as expected. What am I doing wrong or maybe there is a better solution?
I'd guess that you have a timing problem. Your routing table in Rails.application.routes probably hasn't been built when your model is loaded in production mode; but, in development mode, your model is probably being reloaded on each request so Rails.application.routes will be populated when your model is loaded and your validates call:
validates :alias, :exclusion => { :in => Rails.application.routes.routes.map { ... } }
is executed.
An easy solution would be to switch to a validation method:
class Model < ActiveRecord::Base
validate :alias_isnt_a_route, :if => :alias_changed?
private
def alias_isnt_a_route
routes = Rails.application.routes.routes.map { ... }
if(routes.include?(alias))
errors.add(:alias, "Alias #{alias} is already used for a route")
end
end
This way, you don't look at Rails.application.routes until you need to check an alias and by that time, the routes will have been loaded. You could, of course, cache the route prefix list if you wanted to.
You'll also want to add some sanity checking to your application's initialization phase. Someone in your production environment could add, say, 'pancakes' as their alias while you add a /pancakes route while developing: your validation will miss this new conflict. Something simple like this:
config.after_initialize do
Rails.application.reload_routes! # Make sure the routes have been loaded.
# Compare all the existing aliases against the route prefixes and raise an
# exception if there is a conflict.
end
in your config/application.rb would be sufficient.

Rails 3 translations within models in production

I'm trying to translate an app into Japanese and everything was going smoothly until I put it into production.
As cache_classes is now true any translation within a model reverts to the default locale.
I know I'm probably supposed to target the translations directly in the yml file but I'm not sure how I would do that for the following simplified code:
class TimeseriesForecast < ActiveRecord::Base
##field_names = {
:location_name => I18n.t('forecast_timeseries.location_name'),
:local_date_time => I18n.t('forecast_timeseries.local_date_time'),
:zulu_date_time => I18n.t('forecast_timeseries.zulu_date_time'),
:temp_mean => I18n.t('forecast_timeseries.temp_mean')
}
end
Many thanks
Your I18n.t() call is evaluated at compile time since you are defining class variables, not instance variables. You need to put your call to I18n.t where they will be evaluated at runtime.
But if you want to translate ActiveRecord field names, use human_attribute_name and provide your translations via YML. You do not need to manually provide translations, Rails handles it all for you automatically.
The respective documentation is at http://guides.rubyonrails.org/i18n.html Chapter 5.1.
Don't use I18n.t or translate method in your models. You can do this instead:
In your model
Use something like this to add internationalized errors to the name attribute of your model (Check documentation: ActiveModel/Errors/method-i-add):
self.errors.add(:name, :your_error_key)
# The error key could be something like :wrong_name
NOTE: Sometimes you won't even need to add errors with errors.add method. For example if you add validations in your model with somethind like this:
validates :name, presence: true
Rails will add an error with the key :blank (the key depens on the validation type). In other words rails internally will issue self.errors.add(:name, :blank)
In your locale
Then in your locale.jp.yml can use any of this (just one):
activerecord.errors.models.[model_name].attributes.[attribute_name]
activerecord.errors.models.[model_name]
activerecord.errors.messages
errors.attributes.[attribute_name]
errors.messages
In your case replace [model_name] with timeseries_forecast and [attribute_name] with your_error_key
For example:
en:
errors:
messages:
your_error_key: "Your error message in english"
Don't think you're improving performance by caching the names in the class. Make it a method instead.
class TimeseriesForecast < ActiveRecord::Base
def self.field_names
{ :location_name => I18n.t('forecast_timeseries.location_name'),
:local_date_time => I18n.t('forecast_timeseries.local_date_time'),
:zulu_date_time => I18n.t('forecast_timeseries.zulu_date_time'),
:temp_mean => I18n.t('forecast_timeseries.temp_mean') }
end
end
# usage
TimeseriesForecast.field_names
Better yet, return just the actual fields and do the translation in the view, if you're gonna be strict MVC about it (some Rails methods - like collection_select - make it harder to do that though, hence the suggestion above).

How can I reference images in the asset pipeline from a model?

I have a model with a method to return a url to a person's avatar that looks like this:
def avatar_url
if self.avatar?
self.avatar.url # This uses paperclip
else
"/images/avatars/none.png"
end
end
I'm in the midst of upgrading to 3.1, so now the hard-coded none image needs be referenced through the asset pipeline. In a controller or view, I would just wrap it in image_path(), but I don't have that option in the model. How can I generate the correct url to the image?
I struggled with getting this right for a while so I thought I'd post the answer here. Whilst the above works for a standard default image (i.e. same one for each paperclip style), if you need multiple default styles you need a different approach.
If you want to have the default url play nice with the asset pipeline and asset sync and want different default images per style then you need to generate the asset path without fingerprints otherwise you'll get lots of AssetNotPrecompiled errors.
Like so:
:default_url => ActionController::Base.helpers.asset_path("/missing/:style.png", :digest => false)
or in your paperclip options:
:default_url => lambda { |a| "#{a.instance.create_default_url}" }
and then an instance method in the model that has the paperclip attachment:
def create_default_url
ActionController::Base.helpers.asset_path("/missing/:style.png", :digest => false)
end
In this case you can still use the interpolation (:style) but will have to turn off the asset fingerprinting/digest.
This all seems to work fine as long as you are syncing assets without the digest as well as those with the digest.
Personally, I don't think you should really be putting this default in a model, since it's a view detail. In your (haml) view:
= image_tag(#image.avatar_url || 'none.png')
Or, create your own helper and use it like so:
= avatar_or_default(#image)
When things like this are hard in rails, it's often a sign that it's not exactly right.
We solved this problem using draper: https://github.com/jcasimir/draper. Draper let us add a wrapper around our models (for use in views) that have access to helpers.
Paperclip has an option to specify default url
has_attached_file :avatar, :default_url => '/images/.../missing_:style.png'
You can use this to serve default image' in case user has not uploaded avatar.
Using rails active storage I solved this problem by doing this:
# Post.rb
def Post < ApplicationRecord
has_one_attached :image
def thumbnail
self.image.attached? ? self.image.variant(resize: "150x150").processed.service_url : 'placeholder.png';
end
def medium
self.image.attached? ? self.image.variant(resize: "300x300").processed.service_url : 'placeholder.png';
end
def large
self.image.attached? ? self.image.variant(resize: "600x600").processed.service_url : 'placeholder.png';
end
end
Then in your views simply call:
<%= image_tag #post.thumbnail %>,

Resources