mirror of
https://github.com/kemko/paperclip.git
synced 2026-01-01 16:05:40 +03:00
Extract attachment options to class level
This commit is contained in:
@@ -216,7 +216,8 @@ module Paperclip
|
|||||||
include InstanceMethods
|
include InstanceMethods
|
||||||
|
|
||||||
self.attachment_definitions = self.attachment_definitions&.dup || {}
|
self.attachment_definitions = self.attachment_definitions&.dup || {}
|
||||||
attachment_definitions[name] = {:validations => []}.merge(options)
|
attachment_definitions[name] = Attachment.build_class(name, options)
|
||||||
|
const_set("#{name}_attachment".camelize, attachment_definitions[name])
|
||||||
|
|
||||||
after_save :save_attached_files
|
after_save :save_attached_files
|
||||||
after_commit :destroy_attached_files, on: :destroy
|
after_commit :destroy_attached_files, on: :destroy
|
||||||
@@ -291,7 +292,7 @@ module Paperclip
|
|||||||
end
|
end
|
||||||
|
|
||||||
def _add_attachment_validation(name, type, default_options, options)
|
def _add_attachment_validation(name, type, default_options, options)
|
||||||
attachment_definitions[name][:validations] << [
|
attachment_definitions[name].validations << [
|
||||||
type,
|
type,
|
||||||
**options,
|
**options,
|
||||||
**default_options.slice(:message, :if, :unless)
|
**default_options.slice(:message, :if, :unless)
|
||||||
@@ -302,7 +303,7 @@ module Paperclip
|
|||||||
module InstanceMethods #:nodoc:
|
module InstanceMethods #:nodoc:
|
||||||
def attachment_for name
|
def attachment_for name
|
||||||
@_paperclip_attachments ||= {}
|
@_paperclip_attachments ||= {}
|
||||||
@_paperclip_attachments[name] ||= Attachment.build(name, self, self.class.attachment_definitions[name])
|
@_paperclip_attachments[name] ||= self.class.attachment_definitions[name].new(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
def each_attachment
|
def each_attachment
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ module Paperclip
|
|||||||
:styles => {},
|
:styles => {},
|
||||||
:default_url => "/:attachment/:style/missing.png",
|
:default_url => "/:attachment/:style/missing.png",
|
||||||
:default_style => :original,
|
:default_style => :original,
|
||||||
:validations => [],
|
|
||||||
:storage => :filesystem,
|
:storage => :filesystem,
|
||||||
:whiny => true,
|
:whiny => true,
|
||||||
:restricted_characters => /[^\w\p{Word}\d\.\-]|(^\.{0,2}$)+/,
|
:restricted_characters => /[^\w\p{Word}\d\.\-]|(^\.{0,2}$)+/,
|
||||||
@@ -27,53 +26,57 @@ module Paperclip
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.attachment_class_cache
|
class << self
|
||||||
@attachment_class_cache ||= Hash.new do |hash, storage|
|
# Every attachment definition creates separate class which stores configuration.
|
||||||
storage_name = storage.to_s.downcase.camelize
|
# This class is instantiated later with model instance.
|
||||||
unless Storage.const_defined?(storage_name, false)
|
def build_class(name, options)
|
||||||
raise "Cannot load storage module '#{storage_name}'"
|
options = default_options.merge(options)
|
||||||
|
storage_name = options.fetch(:storage).to_s.downcase.camelize
|
||||||
|
storage_module = Storage.const_get(storage_name, false)
|
||||||
|
Class.new(self) do
|
||||||
|
include storage_module
|
||||||
|
setup(name, options)
|
||||||
end
|
end
|
||||||
hash[storage] =
|
|
||||||
if storage_name == storage
|
|
||||||
storage_module = Storage.const_get(storage_name)
|
|
||||||
Class.new(self) { include(storage_module) }.tap { |x| const_set(storage_name, x) }
|
|
||||||
else
|
|
||||||
hash[storage_name]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Basic attrs
|
||||||
|
attr_reader :attachment_name, :options, :validations
|
||||||
|
|
||||||
|
# Extracted from options
|
||||||
|
attr_reader :url_template, :path_template, :default_url, :processing_url, :styles, :default_style, :whiny
|
||||||
|
|
||||||
|
def setup(name, options)
|
||||||
|
@attachment_name = name
|
||||||
|
@options = options
|
||||||
|
@validations = []
|
||||||
|
@url_template = options[:url]
|
||||||
|
@path_template = options[:path]
|
||||||
|
@default_url = options[:default_url]
|
||||||
|
@processing_url = options[:processing_url] || default_url
|
||||||
|
@styles = StylesParser.new(options).styles
|
||||||
|
@default_style = options[:default_style]
|
||||||
|
@whiny = options[:whiny_thumbnails] || options[:whiny]
|
||||||
|
end
|
||||||
|
|
||||||
|
# For delayed_paperclip
|
||||||
|
delegate :[], :[]=, to: :options
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.build(name, instance, options = {})
|
delegate :options, :styles, :default_style, to: :class
|
||||||
storage = options[:storage] || default_options[:storage]
|
|
||||||
attachment_class_cache[storage].new(name, instance, options)
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :name, :instance, :options, :url_template, :path_template,
|
attr_reader :instance
|
||||||
:styles, :default_style, :default_url, :validations, :whiny
|
|
||||||
|
|
||||||
attr_accessor :post_processing
|
attr_accessor :post_processing
|
||||||
|
|
||||||
# Creates an Attachment object. +name+ is the name of the attachment,
|
# Creates an Attachment object. +name+ is the name of the attachment,
|
||||||
# +instance+ is the ActiveRecord object instance it's attached to, and
|
# +instance+ is the ActiveRecord object instance it's attached to.
|
||||||
# +options+ is the same as the hash passed to +has_attached_file+.
|
def initialize(instance)
|
||||||
def initialize name, instance, options = {}
|
@instance = instance
|
||||||
@name = name
|
@post_processing = true
|
||||||
@instance = instance
|
end
|
||||||
|
|
||||||
options = Attachment.default_options.merge(options)
|
def name
|
||||||
|
self.class.attachment_name
|
||||||
@url_template = options[:url]
|
|
||||||
@path_template = options[:path]
|
|
||||||
@styles = StylesParser.new(options).styles
|
|
||||||
@default_url = options[:default_url]
|
|
||||||
@validations = options[:validations]
|
|
||||||
@default_style = options[:default_style]
|
|
||||||
@storage = options[:storage]
|
|
||||||
@whiny = options[:whiny_thumbnails] || options[:whiny]
|
|
||||||
@options = options
|
|
||||||
|
|
||||||
@post_processing = true
|
|
||||||
@processing_url = options[:processing_url] || default_url
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def queued_for_delete
|
def queued_for_delete
|
||||||
@@ -120,11 +123,9 @@ module Paperclip
|
|||||||
|
|
||||||
queued_for_write[:original] = to_tempfile(uploaded_file)
|
queued_for_write[:original] = to_tempfile(uploaded_file)
|
||||||
|
|
||||||
file_name = if options[:filename_sanitizer]
|
file_name = uploaded_file.original_filename
|
||||||
options[:filename_sanitizer].call uploaded_file.original_filename, self
|
sanitizer = self.class.options[:filename_sanitizer]
|
||||||
else
|
file_name = sanitizer ? sanitizer.call(file_name, self) : sanitize_filename(file_name)
|
||||||
sanitize_filename uploaded_file.original_filename
|
|
||||||
end
|
|
||||||
|
|
||||||
instance_write(:file_name, file_name)
|
instance_write(:file_name, file_name)
|
||||||
instance_write(:content_type, uploaded_file.content_type.to_s.strip)
|
instance_write(:content_type, uploaded_file.content_type.to_s.strip)
|
||||||
@@ -164,17 +165,17 @@ module Paperclip
|
|||||||
# update time appended to the url
|
# update time appended to the url
|
||||||
def url style = default_style, include_updated_timestamp = true
|
def url style = default_style, include_updated_timestamp = true
|
||||||
# for delayed_paperclip
|
# for delayed_paperclip
|
||||||
return interpolate(processing_url, style) if instance.try("#{name}_processing?")
|
return interpolate(self.class.processing_url, style) if instance.try("#{name}_processing?")
|
||||||
interpolate_url(url_template, style, include_updated_timestamp)
|
interpolate_url(self.class.url_template, style, include_updated_timestamp)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Метод необходим в ассетах
|
# Метод необходим в ассетах
|
||||||
def filesystem_url style = default_style, include_updated_timestamp = true
|
def filesystem_url style = default_style, include_updated_timestamp = true
|
||||||
interpolate_url(url_template, style, include_updated_timestamp)
|
interpolate_url(self.class.url_template, style, include_updated_timestamp)
|
||||||
end
|
end
|
||||||
|
|
||||||
def interpolate_url(template, style, include_updated_timestamp)
|
def interpolate_url(template, style, include_updated_timestamp)
|
||||||
url = original_filename.nil? ? interpolate(default_url, style) : interpolate(template, style)
|
url = original_filename.nil? ? interpolate(self.class.default_url, style) : interpolate(template, style)
|
||||||
include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
|
include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -184,7 +185,7 @@ module Paperclip
|
|||||||
# URL, and the :bucket option refers to the S3 bucket.
|
# URL, and the :bucket option refers to the S3 bucket.
|
||||||
def path style = default_style
|
def path style = default_style
|
||||||
return if original_filename.nil?
|
return if original_filename.nil?
|
||||||
interpolate(path_template, style)
|
interpolate(self.class.path_template, style)
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_method :filesystem_path, :path
|
alias_method :filesystem_path, :path
|
||||||
@@ -268,7 +269,9 @@ module Paperclip
|
|||||||
|
|
||||||
def sanitize_filename(file_name)
|
def sanitize_filename(file_name)
|
||||||
file_name = file_name.strip
|
file_name = file_name.strip
|
||||||
file_name.gsub!(options[:restricted_characters], '_') if options[:restricted_characters]
|
|
||||||
|
restricted_characters = self.class.options[:restricted_characters]
|
||||||
|
file_name.gsub!(restricted_characters, '_') if restricted_characters
|
||||||
|
|
||||||
# Укорачиваем слишком длинные имена файлов.
|
# Укорачиваем слишком длинные имена файлов.
|
||||||
if file_name.length > MAX_FILE_NAME_LENGTH
|
if file_name.length > MAX_FILE_NAME_LENGTH
|
||||||
@@ -342,7 +345,7 @@ module Paperclip
|
|||||||
|
|
||||||
def validate #:nodoc:
|
def validate #:nodoc:
|
||||||
return if @validated
|
return if @validated
|
||||||
validations.each do |validation|
|
self.class.validations.each do |validation|
|
||||||
name, options = validation
|
name, options = validation
|
||||||
error = send(:"validate_#{name}", options) if allow_validation?(options)
|
error = send(:"validate_#{name}", options) if allow_validation?(options)
|
||||||
errors[name] = error if error
|
errors[name] = error if error
|
||||||
@@ -404,7 +407,7 @@ module Paperclip
|
|||||||
end
|
end
|
||||||
rescue PaperclipError => e
|
rescue PaperclipError => e
|
||||||
log("An error was received while processing: #{e.inspect}")
|
log("An error was received while processing: #{e.inspect}")
|
||||||
(errors[:processing] ||= []) << e.message if whiny
|
(errors[:processing] ||= []) << e.message if self.class.whiny
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ module Paperclip
|
|||||||
# So it just interpolates :url template without checking if preocessing and
|
# So it just interpolates :url template without checking if preocessing and
|
||||||
# file existence.
|
# file existence.
|
||||||
def url attachment, style_name
|
def url attachment, style_name
|
||||||
interpolate(attachment.options[:url], attachment, style_name)
|
interpolate(attachment.class.url_template, attachment, style_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the timestamp as defined by the <attachment>_updated_at field
|
# Returns the timestamp as defined by the <attachment>_updated_at field
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
require "sidekiq"
|
||||||
|
begin
|
||||||
|
require "aws-sdk-s3"
|
||||||
|
rescue LoadError => e
|
||||||
|
e.message << " (You may need to install the aws-sdk-s3 gem)"
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
|
||||||
module Paperclip
|
module Paperclip
|
||||||
module Storage
|
module Storage
|
||||||
# Need to create boolean field synced_to_s3
|
# Need to create boolean field synced_to_s3
|
||||||
module Delayeds3
|
module Delayeds3
|
||||||
class << self
|
class << self
|
||||||
def included(*)
|
def included(base)
|
||||||
require "sidekiq"
|
base.extend(ClassMethods)
|
||||||
begin
|
|
||||||
require "aws-sdk-s3"
|
|
||||||
rescue LoadError => e
|
|
||||||
e.message << " (You may need to install the aws-sdk-s3 gem)"
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_credentials creds
|
def parse_credentials creds
|
||||||
@@ -32,6 +34,47 @@ module Paperclip
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
attr_reader :s3_url_template, :s3_path_template,
|
||||||
|
:filesystem_url_template, :filesystem_path_template,
|
||||||
|
:s3_credentials, :s3_bucket,
|
||||||
|
:fog_provider, :fog_credentials, :fog_directory,
|
||||||
|
:synced_to_s3_field, :synced_to_fog_field
|
||||||
|
|
||||||
|
def setup(*)
|
||||||
|
super
|
||||||
|
|
||||||
|
@s3_url_template = options[:s3_url]
|
||||||
|
@s3_path_template = options[:s3_path]
|
||||||
|
@filesystem_url_template = options[:filesystem_url]
|
||||||
|
@filesystem_path_template = options[:filesystem_path]
|
||||||
|
|
||||||
|
@s3_credentials = Delayeds3.parse_credentials(options[:s3_credentials])
|
||||||
|
@s3_bucket = options[:bucket] || @s3_credentials[:bucket]
|
||||||
|
|
||||||
|
@fog_provider = options[:fog_provider]
|
||||||
|
@fog_directory = options[:fog_directory]
|
||||||
|
@fog_credentials = options[:fog_credentials].symbolize_keys
|
||||||
|
|
||||||
|
@synced_to_s3_field ||= :"#{attachment_name}_synced_to_s3"
|
||||||
|
@synced_to_fog_field ||= :"#{attachment_name}_synced_to_fog"
|
||||||
|
end
|
||||||
|
|
||||||
|
def fog_storage
|
||||||
|
@fog_storage ||= Fog::Storage.new(fog_credentials.merge(provider: fog_provider))
|
||||||
|
end
|
||||||
|
|
||||||
|
def aws_bucket
|
||||||
|
@aws_bucket ||= begin
|
||||||
|
params = s3_credentials.reject { |_k, v| v.blank? }
|
||||||
|
params[:region] ||= 'us-east-1'
|
||||||
|
s3_client = Aws::S3::Client.new(params)
|
||||||
|
s3_resource = Aws::S3::Resource.new(client: s3_client)
|
||||||
|
s3_resource.bucket(s3_bucket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class UploadWorker
|
class UploadWorker
|
||||||
include ::Sidekiq::Worker
|
include ::Sidekiq::Worker
|
||||||
sidekiq_options queue: :paperclip
|
sidekiq_options queue: :paperclip
|
||||||
@@ -69,40 +112,34 @@ module Paperclip
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
delegate :synced_to_s3_field, :synced_to_fog_field, to: :class
|
||||||
|
|
||||||
def initialize(*)
|
def initialize(*)
|
||||||
super
|
super
|
||||||
|
@queued_jobs = []
|
||||||
@s3_credentials = Delayeds3.parse_credentials(options[:s3_credentials])
|
|
||||||
@bucket = options[:bucket] || @s3_credentials[:bucket]
|
|
||||||
|
|
||||||
@fog_provider = options[:fog_provider]
|
|
||||||
@fog_directory = options[:fog_directory]
|
|
||||||
@fog_credentials = options[:fog_credentials]
|
|
||||||
|
|
||||||
@queued_jobs = []
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def url(style = default_style, include_updated_timestamp = true)
|
def url(style = default_style, include_updated_timestamp = true)
|
||||||
# for delayed_paperclip
|
# for delayed_paperclip
|
||||||
return interpolate(processing_url, style) if instance.try("#{name}_processing?")
|
return interpolate(self.class.processing_url, style) if instance.try("#{name}_processing?")
|
||||||
template = instance_read(:synced_to_s3) ? options[:s3_url] : options[:filesystem_url]
|
template = instance_read(:synced_to_s3) ? self.class.s3_url_template : self.class.filesystem_url_template
|
||||||
interpolate_url(template, style, include_updated_timestamp)
|
interpolate_url(template, style, include_updated_timestamp)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Метод необходим в ассетах
|
# Метод необходим в ассетах
|
||||||
def filesystem_url(style = default_style, include_updated_timestamp = true)
|
def filesystem_url(style = default_style, include_updated_timestamp = true)
|
||||||
interpolate_url(options[:filesystem_url], style, include_updated_timestamp)
|
interpolate_url(self.class.filesystem_url_template, style, include_updated_timestamp)
|
||||||
end
|
end
|
||||||
|
|
||||||
def path(style = default_style)
|
def path(style = default_style)
|
||||||
return if original_filename.nil?
|
return if original_filename.nil?
|
||||||
path = instance_read(:synced_to_s3) ? options[:s3_path] : options[:filesystem_path]
|
path = instance_read(:synced_to_s3) ? self.class.s3_path_template : self.class.filesystem_path_template
|
||||||
interpolate(path, style)
|
interpolate(path, style)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filesystem_path(style = default_style)
|
def filesystem_path(style = default_style)
|
||||||
return if original_filename.nil?
|
return if original_filename.nil?
|
||||||
interpolate(options[:filesystem_path], style)
|
interpolate(self.class.filesystem_path_template, style)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reprocess!
|
def reprocess!
|
||||||
@@ -110,34 +147,6 @@ module Paperclip
|
|||||||
flush_jobs
|
flush_jobs
|
||||||
end
|
end
|
||||||
|
|
||||||
def aws_bucket
|
|
||||||
return @aws_bucket if @aws_bucket
|
|
||||||
|
|
||||||
params = { region: @s3_credentials[:region] || 'us-east-1',
|
|
||||||
access_key_id: @s3_credentials[:access_key_id],
|
|
||||||
secret_access_key: @s3_credentials[:secret_access_key] }
|
|
||||||
|
|
||||||
params[:endpoint] = @s3_credentials[:endpoint] if @s3_credentials[:endpoint].present?
|
|
||||||
params[:http_proxy] = @s3_credentials[:http_proxy] if @s3_credentials[:http_proxy].present?
|
|
||||||
|
|
||||||
s3_client = Aws::S3::Client.new(params)
|
|
||||||
|
|
||||||
s3_resource = Aws::S3::Resource.new(client: s3_client)
|
|
||||||
@aws_bucket = s3_resource.bucket(bucket_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def bucket_name
|
|
||||||
@bucket
|
|
||||||
end
|
|
||||||
|
|
||||||
def synced_to_s3_field
|
|
||||||
@synced_to_s3_field ||= "#{name}_synced_to_s3".freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
def synced_to_fog_field
|
|
||||||
@synced_to_fog_field ||= "#{name}_synced_to_fog".freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_file style = default_style
|
def to_file style = default_style
|
||||||
queued_for_write[style] || (File.new(filesystem_path(style), 'rb') if exists?(style)) || download_file(style)
|
queued_for_write[style] || (File.new(filesystem_path(style), 'rb') if exists?(style)) || download_file(style)
|
||||||
end
|
end
|
||||||
@@ -156,7 +165,7 @@ module Paperclip
|
|||||||
end
|
end
|
||||||
|
|
||||||
def s3_path style
|
def s3_path style
|
||||||
interpolate(options[:s3_path], style)
|
interpolate(self.class.s3_path_template, style)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filesystem_paths
|
def filesystem_paths
|
||||||
@@ -176,7 +185,7 @@ module Paperclip
|
|||||||
end
|
end
|
||||||
paths.each do |style, file|
|
paths.each do |style, file|
|
||||||
log("saving to s3 #{file}")
|
log("saving to s3 #{file}")
|
||||||
s3_object = aws_bucket.object(s3_path(style).gsub(/^\/+/,''))
|
s3_object = self.class.aws_bucket.object(s3_path(style).gsub(/^\/+/,''))
|
||||||
s3_object.upload_file(file,
|
s3_object.upload_file(file,
|
||||||
cache_control: "max-age=#{10.year.to_i}",
|
cache_control: "max-age=#{10.year.to_i}",
|
||||||
content_type: instance_read(:content_type),
|
content_type: instance_read(:content_type),
|
||||||
@@ -188,10 +197,6 @@ module Paperclip
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fog_storage
|
|
||||||
@fog_storage ||= Fog::Storage.new(@fog_credentials.merge(provider: @fog_provider).symbolize_keys)
|
|
||||||
end
|
|
||||||
|
|
||||||
def write_to_fog
|
def write_to_fog
|
||||||
return unless instance.respond_to? synced_to_fog_field
|
return unless instance.respond_to? synced_to_fog_field
|
||||||
return true if instance_read(:synced_to_fog)
|
return true if instance_read(:synced_to_fog)
|
||||||
@@ -210,7 +215,7 @@ module Paperclip
|
|||||||
}
|
}
|
||||||
|
|
||||||
File.open(file, 'r') do |f|
|
File.open(file, 'r') do |f|
|
||||||
fog_storage.put_object @fog_directory, path, f, options
|
self.class.fog_storage.put_object self.class.fog_directory, path, f, options
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# не вызываем колбеки и спокойно себя ведем если объект удален
|
# не вызываем колбеки и спокойно себя ведем если объект удален
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class InterpolationsTest < Test::Unit::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
should "reinterpolate :url" do
|
should "reinterpolate :url" do
|
||||||
attachment = stub(options: {url: "/:id/"}, instance: stub(id: "1234"))
|
attachment = stub(class: stub(url_template: "/:id/"), instance: stub(id: "1234"))
|
||||||
assert_equal "/1234/", Paperclip::Interpolations.url(attachment, :style)
|
assert_equal "/1234/", Paperclip::Interpolations.url(attachment, :style)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -83,5 +83,5 @@ class FakeModel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def attachment options
|
def attachment options
|
||||||
Paperclip::Attachment.new(:avatar, FakeModel.new, options)
|
Paperclip::Attachment.build_class(:avatar, options).new(FakeModel.new)
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user