require 'active_record'
gem 'uuid'
module Compu # :nodoc:
module Acts # :nodoc:
module MbsFiles # :nodoc:
def self.included(mod)
mod.extend(ClassMethods)
end
# The MbsFiles module allows you to easily handle multiple file uploads. You can designate
# one or more columns of your model's table as "file columns" like this:
#
# class SomeItem < ActiveRecord::Base
# acts_as_mbs_files :mbs_files, :files
# # or, for more than one column
# # acts_as_mbs_files :mbs_files, [:files,:more_files]
# end
#
# In this example, the :mbs_files argument tells the plugin that the files are stored with the MbsFile class. The second argument, files,
# can be either a single symol, or an array of symbols, and tells the plugins which properties of the SomeItem class are used to
# store the GUID that correlates to the files represented in the MbsFile class.
#
# Validations can then be individually included as needed. Alternatively, it is possible to automatically include the appropriate
# validations and the acts_as_mbs_files plugin by including the automatic_mbs_validation in both the main model, and the model that
# represents the files, like this:
#
# class SomeItem < ActiveRecord::Base
# automatic_mbs_validation MbsValidationSettings.new("files")
# end
#
# class MbsFile < ActiveRecord::Base
# file_column :file_name
# automatic_mbs_validation MbsValidationSettings.new("files")
# end
#
# This makes it much easier to modify the validation settings, as they only have to be changed in one place. The MbsValidationSettings class
# itself must be generated and modified to suit your needs. It is essentially a static object, and can be generated using the command
# ruby script\generate multi_bit_shift MbsFile validation_settings. An example file is installed with that command, and can be customized.
#
# The methods of this module are automatically included into ActiveRecord::Base
# as class methods, so that you can use them in your models.
#
# == Generated Methods
#
# After calling "acts_as_mbs_files :mbs_files, :files" as in the example above, a number of instance methods
# will automatically be generated:
#
# * SomeItem#files: this will return an array of all MbsFiles associated with
# the current option.
# * SomeItem#files_guid: this will return the GUID associated with the column of this object. If none exists will create one
# and store it in the "files" column.
#
# You can access the raw value of the "files" column (which will contain the guid, or nil if "files_guid" has never been called) via the
# ActiveRecord::Base#attributes or ActiveRecord::Base#[] methods like this:
#
# some_item['files'] # e.g."200f3a6f-8977-012a-5cc8-00044b05bd87"
#
# This could potentially be useful in determining if the "files_guid" function has ever been called.
#
# The guid can be modified with the standard SomeItem#files= method, although we don't provide a mechanism to do so,
# and we generally treat it as immutable.
#
# == Dependencies
#
# The uuid gem must be installed to use this plugin. It can be installed using gem install uuid.
#
# == Notes
#
# This documentation is based on the open source file_column documentation.
module ClassMethods
# default options that will be used for the validation
DEFAULT_VALIDATION_OPTIONS = { :on => :save, :allow_nil => false, :message => nil }.freeze
# possible options for the range of the validates_number_of_mbs_files_in validation
ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
# This method should be included in both the MBS Files as described above, and automatically includes the appropriate validations. This is mainly
# a convenience method, so that validation settings are editable from one file, that file being the validation_object.
def automatic_mbs_validation(validation_object, options = {})
if self.class_name.to_s == validation_object.main_class
acts_as_mbs_files(validation_object.file_class.tableize.intern, validation_object.main_column)
validates_number_of_mbs_files_in(validation_object.main_column,options.merge({:in => validation_object.number_of_files_range}))
elsif self.class_name.to_s == validation_object.file_class
validates_total_maximum_filecount_of(validation_object.file_column, options.merge({:maximum => validation_object.maximum_files}))
validates_total_filesize_of(validation_object.file_column, options.merge({:in => Range.new(0, validation_object.maximum_total_file_size)}))
end
end
# handle the symbols in the guid_fields array as "mbs_files" columns, addtional method as decribed above are generated. The first attribute,
# association_id is the model that is associated with the files. Example:
#
# class SomeItem < ActiveRecord::Base
# acts_as_mbs_files :mbs_files, :files
# # or, for more than one column
# # acts_as_mbs_files :mbs_files, [:files,:more_files]
# end
def acts_as_mbs_files(association_id, guid_fields = [])
if guid_fields.class.to_s != "Array"
guid_fields = [guid_fields]
end
for field in guid_fields
define_method "#{field.to_s}" do
Object.const_get(association_id.to_s.camelize.singularize).find_all_by_associated_with(self.method("#{field.to_s}_guid").call)
end
define_method "#{field.to_s}_guid" do
if read_attribute(field).blank?
write_attribute(field, UUID.new.to_s)
end
read_attribute(field)
end
end
end
# Validates that the specified attribute has the number of uploaded files that match the length restrictions supplied.
# Only one option can be used at a time:
#
# class Person < ActiveRecord::Base
# # an exception will almost certainly been throws if acts_as_mbs_files isn't declared for the attribute that is being validated.
# acts_as_mbs_files :multibit_files, :files
# # Only ONE of these can be used at a time.
# validates_number_of_mbs_files_in :files, :maximum=>30
# validates_number_of_mbs_files_in :files, :maximum=>30, :message=>"upload less than %d if you don't mind"
# validates_number_of_mbs_files_in :files, :in => 7..32, :allow_nil => true
# validates_number_of_mbs_files_in :files, :within => 6..20, :too_long => "upload fewer files", :too_short => "upload more files"
# validates_number_of_mbs_files_in :files, :minimum=>1, :too_short=>"please upload at least %d file"
# validates_number_of_mbs_files_in :files, :is=>4, :message=>"%d files must be uploaded."
# end
#
# Configuration options:
# * minimum - The minimum size of the attribute
# * maximum - The maximum size of the attribute
# * is - The exact size of the attribute
# * within - A range specifying the minimum and maximum size of the attribute
# * in - A synonym(or alias) for :within
# * allow_nil - Attribute may be nil; skip validation.
#
# * too_long - The error message if the attribute goes over the maximum (default is: "has too many files (maximum is %d files)")
# * too_short - The error message if the attribute goes under the minimum (default is: "has too few files (minimum is %d files)")
# * wrong_length - The error message if using the :is method and the attribute is the wrong size (default is: "has the wrong number of files (should be %d files)")
# * message - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
# * on - Specifies when this validation is active (default is :save, other options :create, :update)
# * if - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
#
# Note:
# This function and the supporting documentation is derived in nearly its entirety from the validates_length_of function built into rails.
def validates_number_of_mbs_files_in(*attrs)
# Merge given options with defaults.
options = {
:too_long => "has too many files (maximum is %d files)",
:too_short => "has too few files (minimum is %d files)",
:wrong_length => "has the wrong number of files (should be %d files)"
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)
# Ensure that one and only one range option is specified.
range_options = ALL_RANGE_OPTIONS & options.keys
case range_options.size
when 0
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
when 1
# Valid number of options; do nothing.
else
raise ArgumentError, 'Too many range options specified. Choose only one.'
end
# Get range option and value.
option = range_options.first
option_value = options[range_options.first]
case option
when :within, :in
raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
too_short = options[:too_short] % option_value.begin
too_long = options[:too_long] % option_value.end
validates_each(attrs, options) do |record, attr, value|
# Get number of files
length = record.method("#{attr.to_s}").call.length
if value.nil? or length < option_value.begin
record.errors.add(attr, too_short)
elsif length > option_value.end
record.errors.add(attr, too_long)
end
end
when :is, :minimum, :maximum
raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
message = (options[:message] || options[message_options[option]]) % option_value
validates_each(attrs, options) do |record, attr, value|
# Get number of files
length = record.method("#{attr.to_s}").call.length
record.errors.add(attr, message) unless !value.nil? and length.method(validity_checks[option])[option_value]
end
end
end
# This validates the total file size of all the files sharing the same associated_with value in one or more columns.
# It is assumed that the columns that are passed in contain the path to the file.
# A list of columns should be given followed by an options hash. This validation is designed to be used in the model that represents the
# uploaded files.
#
# Required options:
# * :in => A size range. Note that you can use ActiveSupport's
# numeric extensions for kilobytes, etc.
#
# Examples:
# validates_total_filesize_of :field, :in => 0..100.megabytes
# validates_total_filesize_of :field, :in => 15.kilobytes..1.megabyte
#
# Note:
# This function and the supporting documentation is derived in nearly its entirety from the validates_filesize_of function from the open source
# FileColumn plugin.
def validates_total_filesize_of(*attrs)
options = attrs.pop if attrs.last.is_a?Hash
raise ArgumentError, "Please include the :in option." if !options || !options[:in]
raise ArgumentError, "Invalid value for option :in" unless options[:in].is_a?Range
validates_each(attrs, options) do |record, attr, value|
unless value.blank?
size = File.size(value)
for file in record.class.find_all_by_associated_with(record.associated_with)
size = size + File.size(file.method(attr).call) unless record == file
end
record.errors.add attr, "is smaller than the allowed size range." if size < options[:in].first
record.errors.add attr, "is larger than the allowed size range." if size > options[:in].last
end
end
end
# Validates that the specified attribute has the number of uploaded files that match the length restrictions supplied.
# THIS PLUGIN IS DESIGNED TO BE USED IN THE MODEL THAT REPRESENTS THE UPLOADED FILE. It is assumed that the columns that
# are passed in contain the path to the file. Only one option can be used at a time:
#
# class MbsFile < ActiveRecord::Base
# # this method should be used in the model representing the uploaded file.
# validates_total_filecount_of :files, :maximum=>30
# validates_total_filecount_of :files, :maximum=>30, :message=>"upload less than %d if you don't mind"
# end
#
# Configuration options:
# * maximum - The maximum size of the attribute
# * allow_nil - Attribute may be nil; skip validation.
#
# * too_long - The error message if the attribute goes over the maximum (default is: "has too many files (maximum is %d files)")
# * message - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
# * on - Specifies when this validation is active (default is :save, other options :create, :update)
# * if - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
#
# Note:
# This function and the supporting documentation is derived in nearly its entirety from the validates_length_of function built into rails.
def validates_total_maximum_filecount_of(*attrs)
# Merge given options with defaults.
options = {
:too_long => "has too many files (maximum is %d files)",
}.merge(DEFAULT_VALIDATION_OPTIONS)
options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)
# Ensure that one and only one range option is specified.
range_options = [:maximum] & options.keys
case range_options.size
when 0
raise ArgumentError, 'Range unspecified. Specify the :maximum option.'
when 1
# Valid number of options; do nothing.
else
raise ArgumentError, 'Too many range options specified. Choose only one.'
end
# Get range option and value.
option = range_options.first
option_value = options[range_options.first]
case option
when :maximum
raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
# Declare different validations per option.
validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }
message = (options[:message] || options[message_options[option]]) % option_value
validates_each(attrs, options) do |record, attr, value|
# Get number of files
length = 1
for file in record.class.find_all_by_associated_with(record.associated_with)
length = length + 1 unless record == file
end
record.errors.add(attr, message) unless !value.nil? and length.method(validity_checks[option])[option_value]
end
end
end
end
end
end
end