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