# Adds behavior for getting, setting, and validating # images for any given model. Relies on RAILS_ROOT/config/image_definitions.rb module ImageHandling def self.included(base) base.class_eval do # Defines accessor names: if the class is Book, # names will be #book_images and #book_images= image_accessor_name = self.to_s.underscore + "_images" # Define a getter method for all images. define_method(image_accessor_name) do self.images end # Define a setter methods for images. define_method(image_accessor_name + "=") do |attrs| attrs.each do |attr_set| if attr_set['id'].blank? self.images.create(attr_set) else image = self.images.find(attr_set['id']) image.update_attributes(attr_set) end end end # Note: Uses the conditional validation plugin to determine whether # to run the validation. validation_check_name = "should_validate_#{image_accessor_name}?".to_sym validate :validate_required_images, :if => validation_check_name after_validation :filter_image_errors end base.extend(ClassMethods) base.send(:include, InstanceMethods) end module ClassMethods # Gets the image definitions from RAILS_ROOT/config/image_definitions.rb. # Expects a constant named class_name + ImageDefinition. # If you only want an array of one of the definitions' attributes, # pass in the attribute's name as a symbol (e.g., :label). def image_definitions(attr=nil) image_definition_const = const_get(self.to_s + "ImageDefinition") return image_definition_const if attr.nil? image_definition_const.map {|image| image[attr]} end end module InstanceMethods # Return available images by label. # If no label is given, return all available images. def available_images(label=nil) if label.nil? all_images = self.images self.class.image_definitions(:label).map do |label| image_with_label = all_images.detect {|image| image.label == label} image_with_label || self.images.new(:label => label) end else self.images.find(:first, :conditions => {:label => label}) end end private # Validates the presence of all images marked as # required in the class's image definition. def validate_required_images missing_images = [] self.class.image_definitions.each do |definition| if definition[:required] if (image = self.available_images(definition[:label])).nil? || image.new_record? missing_images << definition[:label] end end end return true if missing_images.blank? self.errors.add(:base, "The following required images are missing: #{missing_images.to_sentence}") end # HACK: Problem here is that ActiveRecord # automatically invokes a validates_associated callback # on new has_many relations, which produces some ugly and # unhelpful error messages. This filter removes those messages. # TODO: Remove this when a better fix is discovered. def filter_image_errors copy = self.errors.map do |attr, message| [attr, message] end self.errors.clear copy.each do |error| unless error.first =~ /image/i error.last.each do |e| self.errors.add(error.first, e) end end end end end end