casey_jones 0.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +5 -0
- data/README +59 -0
- data/Rakefile +70 -0
- data/install.rb +1 -0
- data/lib/acts_as_linkable/acts_as_linkable.rb +34 -0
- data/lib/ajax_loading/ajax_loading.rb +2 -0
- data/lib/ajax_loading/controller_resource.rb +37 -0
- data/lib/anaf_active_record.rb +45 -0
- data/lib/casey_jones.rb +8 -0
- data/lib/casey_jones/active_record_extensions.rb +160 -0
- data/lib/casey_jones/application_helper_methods.rb +39 -0
- data/lib/casey_jones/string_reader.rb +113 -0
- data/lib/generators/anaf_habtm/anaf_habtm_generator.rb +11 -0
- data/lib/generators/anaf_habtm/assets/nested_attributes.css +3 -0
- data/lib/generators/anaf_habtm/assets/nested_attributes.js +73 -0
- data/test/anaf_habtm_test.rb +8 -0
- data/test/test_helper.rb +3 -0
- data/uninstall.rb +1 -0
- metadata +85 -0
    
        data/Gemfile
    ADDED
    
    
    
        data/README
    ADDED
    
    | @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            == HABTM Example
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Complex forms on a HABTM association.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Using code borrowed from Pat (http://patshaughnessy.net), whose view_mapper
         | 
| 6 | 
            +
            finally taught this hobbyist hacker to write forms.
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            Hopefully this will fit snugly into someone else's gem
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            == Installation
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            * Install as a gem:
         | 
| 13 | 
            +
                # Add to your Gemfile
         | 
| 14 | 
            +
                gem "anaf_habtm"
         | 
| 15 | 
            +
            * or as a plugin
         | 
| 16 | 
            +
                rails plugin install git://github.com/tylergannon/anaf_habtm.git
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            Run the generator to place the javascript in the right place:
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                rails generate anaf_habtm
         | 
| 21 | 
            +
                    
         | 
| 22 | 
            +
            Add the following to your layout:
         | 
| 23 | 
            +
                <%= javascript_include_tag 'nested_attributes.js' %>
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            == Usage
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            Inside your model, call anaf_habtm just as you would call has_and_belongs_to_many,
         | 
| 28 | 
            +
            except now you need to offer a code block telling rails what to do with each object.
         | 
| 29 | 
            +
            The plugin will handle deleting stuff.
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                # Basically, your code block needs to return an object of the correct
         | 
| 32 | 
            +
                # type for your association collection, or else nil if your code
         | 
| 33 | 
            +
                # determines that the object should be rejected.
         | 
| 34 | 
            +
                class Customer < ActiveRecord::Base
         | 
| 35 | 
            +
                  anaf_habtm :orders, :autosave => true, :uniq => false do |params, order|
         | 
| 36 | 
            +
                    order = order ||= Order.new
         | 
| 37 | 
            +
                    order.attributes = params
         | 
| 38 | 
            +
                    logger.error params.inspect
         | 
| 39 | 
            +
                    order
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                class Order < ActiveRecord::Base
         | 
| 44 | 
            +
                  has_and_belongs_to_many :customers
         | 
| 45 | 
            +
                  anaf_habtm :items, :autosave => true, :uniq => false do |params, item|
         | 
| 46 | 
            +
                    logger.error "ITEM:::  #{params.inspect}"
         | 
| 47 | 
            +
                    item ||= Item.find_or_create_by_name(params["name"])
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            == Additional info
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            See the example app at http://github.com/tylergannon/accepts-nested-attributes-habtm-example for more details.
         | 
| 54 | 
            +
             | 
| 55 | 
            +
            Also check out Pat's view_mapper... it can generate the code you need inside your views.  
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            http://github.com/patshaughnessy/view_mapper
         | 
| 58 | 
            +
             | 
| 59 | 
            +
             | 
    
        data/Rakefile
    ADDED
    
    | @@ -0,0 +1,70 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            PKG_FILES = FileList[
         | 
| 3 | 
            +
              'anaf_habtm.gemspec',
         | 
| 4 | 
            +
              'Gemfile',
         | 
| 5 | 
            +
              'install.rb',
         | 
| 6 | 
            +
              'Rakefile', 'README', 'uninstall.rb',
         | 
| 7 | 
            +
              'assets/**/*',
         | 
| 8 | 
            +
              'lib/**/*',
         | 
| 9 | 
            +
              'rails/**/*',
         | 
| 10 | 
            +
              'test/**/*'
         | 
| 11 | 
            +
            ]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            require 'rubygems'
         | 
| 14 | 
            +
            require 'rake'
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            begin
         | 
| 17 | 
            +
              require 'jeweler'
         | 
| 18 | 
            +
              Jeweler::Tasks.new do |s|
         | 
| 19 | 
            +
                s.name = "casey_jones"
         | 
| 20 | 
            +
                s.author = "Tyler Gannon"
         | 
| 21 | 
            +
                s.email = "t--g__a--nnon@gmail.com"
         | 
| 22 | 
            +
                s.homepage = "http://github.com/tylergannon/anaf_habtm"
         | 
| 23 | 
            +
                s.platform = Gem::Platform::RUBY
         | 
| 24 | 
            +
                s.description = "some extra stuff for rails apps"
         | 
| 25 | 
            +
                s.summary = "My favorite rails goodies all in one"
         | 
| 26 | 
            +
                s.files = PKG_FILES.to_a
         | 
| 27 | 
            +
                s.require_path = "lib"
         | 
| 28 | 
            +
                s.has_rdoc = false
         | 
| 29 | 
            +
                s.extra_rdoc_files = ["README"]
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              Jeweler::GemcutterTasks.new
         | 
| 33 | 
            +
            rescue LoadError
         | 
| 34 | 
            +
              puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
         | 
| 35 | 
            +
            end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            require 'rake/testtask'
         | 
| 38 | 
            +
            Rake::TestTask.new(:test) do |test|
         | 
| 39 | 
            +
              test.libs << 'lib' << 'test'
         | 
| 40 | 
            +
              test.pattern = 'test/**/test_*.rb'
         | 
| 41 | 
            +
              test.verbose = true
         | 
| 42 | 
            +
            end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            begin
         | 
| 45 | 
            +
              require 'rcov/rcovtask'
         | 
| 46 | 
            +
              Rcov::RcovTask.new do |test|
         | 
| 47 | 
            +
                test.libs << 'test'
         | 
| 48 | 
            +
                test.pattern = 'test/**/test_*.rb'
         | 
| 49 | 
            +
                test.verbose = true
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            rescue LoadError
         | 
| 52 | 
            +
              task :rcov do
         | 
| 53 | 
            +
                abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
            task :test => :check_dependencies
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            task :default => :test
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            require 'rake/rdoctask'
         | 
| 62 | 
            +
            Rake::RDocTask.new do |rdoc|
         | 
| 63 | 
            +
              version = File.exist?('VERSION') ? File.read('VERSION') : ""
         | 
| 64 | 
            +
             | 
| 65 | 
            +
              rdoc.rdoc_dir = 'rdoc'
         | 
| 66 | 
            +
              rdoc.title = "da_huangs_ruby_extensions #{version}"
         | 
| 67 | 
            +
              rdoc.rdoc_files.include('README*')
         | 
| 68 | 
            +
              rdoc.rdoc_files.include('lib/**/*.rb')
         | 
| 69 | 
            +
            end
         | 
| 70 | 
            +
             | 
    
        data/install.rb
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            # Install hook code here
         | 
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            module ActsAsLinkable
         | 
| 2 | 
            +
              module ActiveRecordExtensions
         | 
| 3 | 
            +
                def acts_as_linkable(opts={}, &block)
         | 
| 4 | 
            +
                  class_eval "def link_attr; #{opts.inspect}; end;"
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              module ActionViewExtensions
         | 
| 9 | 
            +
                def link(obj)
         | 
| 10 | 
            +
                  raise "I can't link to nil." unless obj
         | 
| 11 | 
            +
                  if (obj.class == Array) || (obj.class == ActiveRecord::Relation)
         | 
| 12 | 
            +
                    obj.map{|v| link(v)}.to_sentence
         | 
| 13 | 
            +
                  else
         | 
| 14 | 
            +
                    p = obj.link_attr
         | 
| 15 | 
            +
                    p[:partial] = "#{obj.class.name.tableize}/link" if p.empty?
         | 
| 16 | 
            +
                    unless p[:partial].nil?
         | 
| 17 | 
            +
                      obj_name = (p[:object_name] ||= obj.class.name.underscore).to_s.to_sym
         | 
| 18 | 
            +
                      (render :partial => p[:partial], :locals => {obj_name => obj}).strip
         | 
| 19 | 
            +
                    else
         | 
| 20 | 
            +
                      opts = {}
         | 
| 21 | 
            +
                      link_attr = obj.link_attr
         | 
| 22 | 
            +
                      link_attr.each do |key, val|
         | 
| 23 | 
            +
                        link_attr[key] = obj.send(val) if val.class == Symbol
         | 
| 24 | 
            +
                      end
         | 
| 25 | 
            +
                      opts[:title] = link_attr[:title] if link_attr.has_key?(:title)
         | 
| 26 | 
            +
                      link_to link_attr[:name], obj, opts
         | 
| 27 | 
            +
                    end
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| 32 | 
            +
            ActiveRecord::Base.extend(ActsAsLinkable::ActiveRecordExtensions)
         | 
| 33 | 
            +
            ActionView::Base.send :include, ActsAsLinkable::ActionViewExtensions
         | 
| 34 | 
            +
             | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            module AjaxLoading
         | 
| 2 | 
            +
              class ControllerResource < CanCan::ControllerResource
         | 
| 3 | 
            +
                def initialize(controller, name, parent = nil, options = {})
         | 
| 4 | 
            +
                  super(controller, name, parent, options)
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def load_resource
         | 
| 8 | 
            +
                  puts "AjaxLoading::load_resource"
         | 
| 9 | 
            +
                  params = @controller.params
         | 
| 10 | 
            +
                  if params.has_key?(:associated)
         | 
| 11 | 
            +
                    ass_params = params[:associated]
         | 
| 12 | 
            +
                    relation = ass_params["class_name"].constantize.find(ass_params["id"])
         | 
| 13 | 
            +
                    association = ass_params["association"]
         | 
| 14 | 
            +
                    instance_variable_set 'container',
         | 
| 15 | 
            +
                        ass_params["container"] ||= relation.element_id(:show, association)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    self.model_instance = relation.send(association)
         | 
| 18 | 
            +
                  elsif collection_action?
         | 
| 19 | 
            +
                    self.model_instance = model_class.where('1=1')
         | 
| 20 | 
            +
                  else
         | 
| 21 | 
            +
                    super
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                private
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def instance_variable_set(name, value)
         | 
| 28 | 
            +
                  name = '@'+name unless name.include?('@')
         | 
| 29 | 
            +
                  @controller.instance_variable_set(name, value)
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
                def instance_variable_get(name)
         | 
| 32 | 
            +
                  name = '@'+name unless name.include?('@')
         | 
| 33 | 
            +
                  @controller.instance_variable_get(name)
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
| 37 | 
            +
             | 
| @@ -0,0 +1,45 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module AnafHabtm
         | 
| 3 | 
            +
              module ActiveRecord
         | 
| 4 | 
            +
                def anaf_habtm(association, options={}, &block)
         | 
| 5 | 
            +
                  class_eval do
         | 
| 6 | 
            +
                    # Define a proc that will look up the (potentially) existing object
         | 
| 7 | 
            +
                    finder = proc {|id| association.to_s.singularize.camelize.constantize.where(:id=>id).first
         | 
| 8 | 
            +
                    }
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    # Define a proc that will set the association collection
         | 
| 11 | 
            +
                    set_collection = proc {|me, coll| me.send("#{association.to_s.tableize}=", coll)}
         | 
| 12 | 
            +
                    has_and_belongs_to_many association.to_sym, options
         | 
| 13 | 
            +
                    # Define the actual association setter.
         | 
| 14 | 
            +
                    define_method "#{association.to_s.tableize}_attributes=", lambda{|attributes_for_association|
         | 
| 15 | 
            +
                      coll = []
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                      attributes_for_association.each_value do |params|
         | 
| 18 | 
            +
                        next if params["_destroy"] == "1"
         | 
| 19 | 
            +
                        obj = finder.call(params["id"]) if params.has_key?("id")
         | 
| 20 | 
            +
                        params.extend(AnafHabtm::HashExtension)
         | 
| 21 | 
            +
                        # ActiveRecord::Base.attributes=() doesn't like extra parameters.
         | 
| 22 | 
            +
                        coll << block.call(params.copy_without_destroy, obj)
         | 
| 23 | 
            +
                      end
         | 
| 24 | 
            +
                      set_collection.call(self, coll)
         | 
| 25 | 
            +
                    }
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def named_association(member, attribute, opts={})
         | 
| 30 | 
            +
                  member = member.to_s
         | 
| 31 | 
            +
                  klass = opts.has_key?(:class) ? opts[:class_name] : member.to_s.singularize.camelize
         | 
| 32 | 
            +
                  attribute = attribute.to_s
         | 
| 33 | 
            +
                  if opts.has_key?(:create)
         | 
| 34 | 
            +
                    class_eval "def #{member}_#{attribute}=(#{attribute});
         | 
| 35 | 
            +
                    return if #{attribute}.blank?
         | 
| 36 | 
            +
                    self.#{member} = #{klass}.find_or_create_by_#{attribute}(#{attribute})
         | 
| 37 | 
            +
                    end;"
         | 
| 38 | 
            +
                  else
         | 
| 39 | 
            +
                    class_eval "def #{member}_#{attribute}=(#{attribute}); self.#{member} = #{klass}.find_by_#{attribute}(#{attribute}) unless #{attribute}.blank?; end;"
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                  class_eval "def #{member}_#{attribute}; #{member}.#{attribute} if #{member}; end;"
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| 45 | 
            +
             | 
    
        data/lib/casey_jones.rb
    ADDED
    
    | @@ -0,0 +1,8 @@ | |
| 1 | 
            +
            require 'casey_jones/active_record_extensions'
         | 
| 2 | 
            +
            require 'casey_jones/application_helper_methods'
         | 
| 3 | 
            +
            require 'casey_jones/string_reader'
         | 
| 4 | 
            +
            require 'ajax_loading/ajax_loading'
         | 
| 5 | 
            +
            require 'acts_as_linkable/acts_as_linkable'
         | 
| 6 | 
            +
            ActiveRecord::Base.extend(CaseyJones::ActiveRecord)
         | 
| 7 | 
            +
            ActionView::Base.send :include, CaseyJones::ApplicationHelperMethods
         | 
| 8 | 
            +
             | 
| @@ -0,0 +1,160 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module CaseyJones
         | 
| 3 | 
            +
              module ActiveRecord
         | 
| 4 | 
            +
                def autocomplete_format(&block)
         | 
| 5 | 
            +
                  class_eval do
         | 
| 6 | 
            +
                    @autocomplete_block = block
         | 
| 7 | 
            +
                    def self.to_autocomplete(items)
         | 
| 8 | 
            +
                        items.map{|t| @autocomplete_block.call(t)}.to_json
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                #  Needs to call scopes on a class and pass it values that are
         | 
| 14 | 
            +
                #   pulled from a hash called obj_attr
         | 
| 15 | 
            +
                def make_scope_string(find_opts)
         | 
| 16 | 
            +
                  _scopes = []
         | 
| 17 | 
            +
                  find_opts.each{ |scope, attr_name|
         | 
| 18 | 
            +
                    if attr_name.class == Hash
         | 
| 19 | 
            +
                      _scopes << "#{scope.to_s}(obj_attr[#{name.first[0].to_s}][#{name.first[1].to_s}])"
         | 
| 20 | 
            +
                    else
         | 
| 21 | 
            +
                      _scopes << "#{scope.to_s}(obj_attr[#{name.to_s}])"
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                  }
         | 
| 24 | 
            +
                  _scopes.join(".")
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def anaf_habtm(association, options={})
         | 
| 28 | 
            +
                  ar_opts = options[:ar_options] ||= {}
         | 
| 29 | 
            +
                  klass = ar_opts[:class_name] ||= association.to_s.singularize.camelize
         | 
| 30 | 
            +
                  class_eval do
         | 
| 31 | 
            +
                    has_and_belongs_to_many association.to_sym, ar_opts
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                  find_line = "obj = obj ||= #{klass}.#{make_scope_string(options[:find])}.first" if options.has_key?(:find)
         | 
| 34 | 
            +
                  association = association.to_s.underscore.tableize
         | 
| 35 | 
            +
                  scope_string =
         | 
| 36 | 
            +
                  class_eval <<END
         | 
| 37 | 
            +
                      def #{association}_attributes=(attr_hash)
         | 
| 38 | 
            +
                        obj_coll = []
         | 
| 39 | 
            +
                        attr_hash.each_value do |obj_attr|
         | 
| 40 | 
            +
                          next if obj_attr["_destroy"] == "1"
         | 
| 41 | 
            +
                          obj = #{klass}.find(obj_attr["id"]) if obj_attr.has_key?("id")
         | 
| 42 | 
            +
                          obj_attr = obj_attr.reject_keys ["id", "_destroy"]
         | 
| 43 | 
            +
                          #{find_line}
         | 
| 44 | 
            +
                          if obj && obj.update_attributes(obj_attr)
         | 
| 45 | 
            +
                            obj_coll << obj
         | 
| 46 | 
            +
                          elsif (obj = #{klass}.new(obj_attributes)) && obj.save
         | 
| 47 | 
            +
                            obj_coll << obj
         | 
| 48 | 
            +
                          end
         | 
| 49 | 
            +
                        end
         | 
| 50 | 
            +
                        self.#{association} = obj_coll
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
            END
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def association_text(association, opts={})
         | 
| 56 | 
            +
                  raise "Must give a name for text_association" unless opts.has_key?(:name)
         | 
| 57 | 
            +
                  class_eval do
         | 
| 58 | 
            +
                    if opts.has_key?(:class_name)
         | 
| 59 | 
            +
                      klass = opts[:class_name].constantize
         | 
| 60 | 
            +
                    else
         | 
| 61 | 
            +
                      klass = association.to_s.singularize.camelize.constantize
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                    define_method "#{association.to_s}_text=", lambda{ |text|
         | 
| 64 | 
            +
                      return if text.empty?
         | 
| 65 | 
            +
                      coll = StringReader.new.read_items(text) do |name, commentary|
         | 
| 66 | 
            +
                        a = klass.undecorate(name) if klass.methods.include?("undecorate")
         | 
| 67 | 
            +
                        if opts.has_key?(:scope)
         | 
| 68 | 
            +
                          obj = self.send(association).scopes[opts[:scope]].call(name).first
         | 
| 69 | 
            +
                        end
         | 
| 70 | 
            +
                        params = {opts[:name]=>name}
         | 
| 71 | 
            +
                        params[opts[:commentary]] = commentary if opts.has_key?(:commentary)
         | 
| 72 | 
            +
                        obj = obj ||= klass.new
         | 
| 73 | 
            +
                        obj = klass.find(obj) unless obj.new_record?
         | 
| 74 | 
            +
                        obj.decoration = a if a
         | 
| 75 | 
            +
                        obj.attributes = params
         | 
| 76 | 
            +
                        obj.save
         | 
| 77 | 
            +
                        obj
         | 
| 78 | 
            +
                      end
         | 
| 79 | 
            +
                      self.send("#{association.to_s}=", coll)
         | 
| 80 | 
            +
                    }
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    define_method "#{association.to_s}_text", lambda{
         | 
| 83 | 
            +
                      StringReader.new.write_items(self.send(association.to_s)) do |item|
         | 
| 84 | 
            +
                        name = item.send(opts[:name])
         | 
| 85 | 
            +
                        name = item.decorate(name) if item.methods.include?("decorate")
         | 
| 86 | 
            +
                        [name, opts.has_key?(:commentary) ? item.send(opts[:commentary]) : ""]
         | 
| 87 | 
            +
                      end
         | 
| 88 | 
            +
                    }
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def named_association(member, attribute, opts={})
         | 
| 93 | 
            +
                  member = member.to_s
         | 
| 94 | 
            +
                  klass = (opts.has_key?(:class_name) ? opts[:class_name] : member.to_s.singularize.camelize).constantize
         | 
| 95 | 
            +
                  attribute = attribute.to_s
         | 
| 96 | 
            +
                  if opts.has_key?(:create)
         | 
| 97 | 
            +
                    class_eval do
         | 
| 98 | 
            +
                      define_method "#{member}_#{attribute}=", lambda{|value|
         | 
| 99 | 
            +
                        return if value.blank?
         | 
| 100 | 
            +
                        obj = klass.named(value)
         | 
| 101 | 
            +
                        obj = obj ||= klass.create(attribute => value)
         | 
| 102 | 
            +
                        self.send("#{member}=", obj)
         | 
| 103 | 
            +
                      }
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
                  else
         | 
| 106 | 
            +
                    class_eval do
         | 
| 107 | 
            +
                      define_method "#{member}_#{attribute}=", lambda{|value|
         | 
| 108 | 
            +
                        self.send("#{member}=",klass.named(value)) unless value.blank?
         | 
| 109 | 
            +
                      }
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                  class_eval "def #{member}_#{attribute};
         | 
| 113 | 
            +
                     #{member}.#{attribute} if #{member};
         | 
| 114 | 
            +
                   end;"
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def search_on(*cols)
         | 
| 118 | 
            +
                  class_eval "def self.search_columns; #{cols.map{|t| t.to_s}.to_ary.inspect}; end;"
         | 
| 119 | 
            +
                  class_eval do
         | 
| 120 | 
            +
                    scope :search, lambda{|str|
         | 
| 121 | 
            +
                      items = like_condition(str.downcase)
         | 
| 122 | 
            +
                      if scopes.has_key?(:search_mod)
         | 
| 123 | 
            +
                        items = items.search_mod
         | 
| 124 | 
            +
                      end
         | 
| 125 | 
            +
                      items
         | 
| 126 | 
            +
                    }
         | 
| 127 | 
            +
                    scope :with_name, lambda{|str|
         | 
| 128 | 
            +
                      equals_condition(str.downcase).limit(1)
         | 
| 129 | 
            +
                    }
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                def lookup(params)
         | 
| 134 | 
            +
                  str = params[:id]
         | 
| 135 | 
            +
                  if str.match(/\D/)
         | 
| 136 | 
            +
                    named(str)
         | 
| 137 | 
            +
                  else
         | 
| 138 | 
            +
                    find(str)
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def like_condition(str)
         | 
| 143 | 
            +
                  where(condition("ilike '%#{str}%'"))
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                def equals_condition(str)
         | 
| 147 | 
            +
                  where(condition("= '#{str.downcase}'"))
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
             | 
| 151 | 
            +
                def named(str)
         | 
| 152 | 
            +
                  with_name(str).first
         | 
| 153 | 
            +
                end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                def condition(cond)
         | 
| 156 | 
            +
                   search_columns.map{|c| "trim(lower(#{c})) #{cond}"}.join(" or ")
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
              end
         | 
| 159 | 
            +
            end
         | 
| 160 | 
            +
             | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            module CaseyJones
         | 
| 2 | 
            +
              module ApplicationHelperMethods
         | 
| 3 | 
            +
                def remove_child_link(name, form_builder)
         | 
| 4 | 
            +
                  value = form_builder.object.new_record? ? "1" : "false"
         | 
| 5 | 
            +
                  klass = form_builder.object.class.name.underscore
         | 
| 6 | 
            +
                  form_builder.hidden_field(:_destroy,  {:value=>value, "data-remove"=>klass}) +
         | 
| 7 | 
            +
                  link_to(name, options={}, html_options={"href"=>"#", "class"=>"remove-child-link", :tabindex=> "0"})
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def add_child_link(name, child, form_builder, options={})
         | 
| 11 | 
            +
                  # puts "||#{form_builder}||"
         | 
| 12 | 
            +
                  new_form = new_child_fields(child, form_builder, options)
         | 
| 13 | 
            +
                  raw(content_tag(:div, new_form, {:id=>"#{child}_template", :class=>"form-template"}, escape=false))+
         | 
| 14 | 
            +
                  link_to(name, options={}, html_options={"href"=>"#", "class"=>"add-child-link", "data-class-name"=>child})
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def new_child_fields(child, form_builder, options={})
         | 
| 18 | 
            +
                  partial = options[:partial] ||= child.underscore
         | 
| 19 | 
            +
                  output = ""
         | 
| 20 | 
            +
                  form_builder.fields_for(child.pluralize.to_sym, child.camelize.constantize.new, :child_index => "__#{child}_id__") do |f|
         | 
| 21 | 
            +
                    output += render(:partial => partial, :locals => { :f => f })
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                  output
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def tfwac(f, field, controller, opts={})
         | 
| 27 | 
            +
                  opts["data-auto-complete"]='tf'
         | 
| 28 | 
            +
                  opts["data-auto-complete-url"]=eval("#{controller.to_s.underscore.tableize}_path(:format=>:json)")
         | 
| 29 | 
            +
                  f.text_field field, opts
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def text_area_with_auto_complete(f, field, controller, opts={})
         | 
| 33 | 
            +
                  opts["data-auto-complete"]='ta'
         | 
| 34 | 
            +
                  opts["data-auto-complete-url"]=eval("#{controller.to_s.underscore.tableize}_path(:format=>:json)")
         | 
| 35 | 
            +
                  f.text_area field, opts
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
| 39 | 
            +
             | 
| @@ -0,0 +1,113 @@ | |
| 1 | 
            +
            module AnafHabtm
         | 
| 2 | 
            +
              class StringReader
         | 
| 3 | 
            +
                KEY_SYMPTOM = "*"
         | 
| 4 | 
            +
                POSS_SYMPTOM = "-"
         | 
| 5 | 
            +
                START_COMMENT = "{"
         | 
| 6 | 
            +
                ONE_LINE_COMMENT = "/"
         | 
| 7 | 
            +
                END_COMMENT = "}"
         | 
| 8 | 
            +
                NEWLINE = "\n"
         | 
| 9 | 
            +
                
         | 
| 10 | 
            +
                DELIM = [START_COMMENT, NEWLINE, ONE_LINE_COMMENT, END_COMMENT]
         | 
| 11 | 
            +
                
         | 
| 12 | 
            +
                MULTI_LINE = :multi_line
         | 
| 13 | 
            +
                ONE_LINE = :one_line
         | 
| 14 | 
            +
                NAME = :name
         | 
| 15 | 
            +
                COMMENT = [MULTI_LINE, ONE_LINE]
         | 
| 16 | 
            +
                
         | 
| 17 | 
            +
                @item = ""
         | 
| 18 | 
            +
                @comment = nil
         | 
| 19 | 
            +
                @items = []
         | 
| 20 | 
            +
                
         | 
| 21 | 
            +
                def save(block)
         | 
| 22 | 
            +
                  unless @item.strip.empty?
         | 
| 23 | 
            +
                    @items << block.call(@item.strip, @comment.nil? ? nil : @comment.strip)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                  @item = ""
         | 
| 26 | 
            +
                  @comment = nil
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
                
         | 
| 29 | 
            +
                def write_items(coll, &block)
         | 
| 30 | 
            +
                  lines = []
         | 
| 31 | 
            +
                  coll.each do |obj|
         | 
| 32 | 
            +
                    item, comment = block.call(obj)
         | 
| 33 | 
            +
                    if comment.nil? || comment.empty?
         | 
| 34 | 
            +
                      lines << item
         | 
| 35 | 
            +
                    elsif comment.index("/") || comment.index("\n")
         | 
| 36 | 
            +
                      lines << "#{item} {#{comment}}"
         | 
| 37 | 
            +
                    else
         | 
| 38 | 
            +
                      lines << "#{item} / #{comment}"
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                  lines.join("\n")
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
                
         | 
| 44 | 
            +
                def read_items(str, &block)    
         | 
| 45 | 
            +
                  @items = []
         | 
| 46 | 
            +
                  buffer = ""
         | 
| 47 | 
            +
                  @item = ""
         | 
| 48 | 
            +
                  @comment = nil
         | 
| 49 | 
            +
                  state = NAME
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  str.chars.each do |char|
         | 
| 52 | 
            +
                    if (char == "/") && (state != MULTI_LINE)
         | 
| 53 | 
            +
                      @item = buffer.strip
         | 
| 54 | 
            +
                      buffer = ""
         | 
| 55 | 
            +
                      state = ONE_LINE
         | 
| 56 | 
            +
                    elsif char == "{"
         | 
| 57 | 
            +
                      @item = buffer.strip
         | 
| 58 | 
            +
                      buffer = ""
         | 
| 59 | 
            +
                      state = MULTI_LINE
         | 
| 60 | 
            +
                    elsif char == "}"
         | 
| 61 | 
            +
                      @comment = buffer.strip unless buffer.strip.empty?
         | 
| 62 | 
            +
                      buffer = ""
         | 
| 63 | 
            +
                      save(block)
         | 
| 64 | 
            +
                      state = NAME
         | 
| 65 | 
            +
                    elsif char == "\n"
         | 
| 66 | 
            +
                      if state == NAME
         | 
| 67 | 
            +
                        @item = buffer.strip
         | 
| 68 | 
            +
                        buffer = ""
         | 
| 69 | 
            +
                        save(block)
         | 
| 70 | 
            +
                      elsif state == ONE_LINE
         | 
| 71 | 
            +
                        @comment = buffer.strip
         | 
| 72 | 
            +
                        state = NAME
         | 
| 73 | 
            +
                        buffer = ""
         | 
| 74 | 
            +
                        save(block)
         | 
| 75 | 
            +
                      else
         | 
| 76 | 
            +
                        buffer << char
         | 
| 77 | 
            +
                      end
         | 
| 78 | 
            +
                    else
         | 
| 79 | 
            +
                      buffer << char
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
                  
         | 
| 83 | 
            +
                  if state == ONE_LINE
         | 
| 84 | 
            +
                    @comment = buffer
         | 
| 85 | 
            +
                  elsif state == MULTI_LINE
         | 
| 86 | 
            +
                    @comment = buffer
         | 
| 87 | 
            +
                  else
         | 
| 88 | 
            +
                    @item = buffer
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                  save(block)
         | 
| 91 | 
            +
                  @items
         | 
| 92 | 
            +
                end
         | 
| 93 | 
            +
                
         | 
| 94 | 
            +
                def self.parse_symptom(obj, symptom)
         | 
| 95 | 
            +
                  if symptom.index(KEY_SYMPTOM) == 0
         | 
| 96 | 
            +
                    obj.key_symptom = true
         | 
| 97 | 
            +
                    obj.symptom_name = symptom[KEY_SYMPTOM.length..symptom.length]
         | 
| 98 | 
            +
                  elsif symptom.index(POSS_SYMPTOM) == 0
         | 
| 99 | 
            +
                    obj.maybe = true
         | 
| 100 | 
            +
                    obj.symptom_name = symptom[POSS_SYMPTOM.length..symptom.length]
         | 
| 101 | 
            +
                  else
         | 
| 102 | 
            +
                    obj.symptom_name = symptom
         | 
| 103 | 
            +
                  end
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
                
         | 
| 106 | 
            +
                def self.decorate_symptom(obj)
         | 
| 107 | 
            +
                  decorator = ""
         | 
| 108 | 
            +
                  decorator = KEY_SYMPTOM if obj.key_symptom
         | 
| 109 | 
            +
                  decorator = POSS_SYMPTOM if obj.maybe
         | 
| 110 | 
            +
                  "#{decorator}#{obj.symptom_name}"
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
            end
         | 
| @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            require 'rails/generators'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class AnafHabtmGenerator < Rails::Generators::Base
         | 
| 4 | 
            +
              self.source_root File.expand_path("assets", File.dirname(__FILE__))
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def copy_initializer_file
         | 
| 7 | 
            +
                copy_file "nested_attributes.js", "public/javascripts/nested_attributes.js"
         | 
| 8 | 
            +
                copy_file "nested_attributes.css", "public/stylesheets/nested_attributes.css"
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
            end
         | 
| 11 | 
            +
             | 
| @@ -0,0 +1,73 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            function setUpDocument($jq) {
         | 
| 3 | 
            +
              $jq.find("input[data-auto-complete|=tf]").each( function(idx, el){
         | 
| 4 | 
            +
                $(el).autocomplete({
         | 
| 5 | 
            +
                  source: $(el).attr('data-auto-complete-url'),
         | 
| 6 | 
            +
                  minLength: 2
         | 
| 7 | 
            +
                });
         | 
| 8 | 
            +
              });
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              $jq.find("textarea[data-auto-complete|=ta]").each( function(idx, el){
         | 
| 11 | 
            +
                setTextAreaAutoComplete($(el));
         | 
| 12 | 
            +
              });
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              $jq.find("a.add-child-link").click( function() {
         | 
| 15 | 
            +
                klass = $(this).attr('data-class-name');
         | 
| 16 | 
            +
                $el = $(this).prev().children().first().clone();
         | 
| 17 | 
            +
                new_id=new Date().getTime()
         | 
| 18 | 
            +
                $el.html($el.html().replace(new RegExp('__'+klass+'_id__', 'g'), new_id));
         | 
| 19 | 
            +
                $el.find('input[data-remove|='+klass+']').attr('value', 'false');
         | 
| 20 | 
            +
                $(this).closest('.child').find('.'+klass+'_children').first()
         | 
| 21 | 
            +
                    .append($el).show();
         | 
| 22 | 
            +
                setUpDocument($el);
         | 
| 23 | 
            +
                return false;
         | 
| 24 | 
            +
              });
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              $jq.find('a.remove-child-link').click( function() {
         | 
| 27 | 
            +
                $(this).prev('input[type|=hidden]').val('1');
         | 
| 28 | 
            +
                $(this).closest('.child').hide();
         | 
| 29 | 
            +
                return false;
         | 
| 30 | 
            +
              });
         | 
| 31 | 
            +
            }
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            $(function () {
         | 
| 34 | 
            +
              setUpDocument($("body"));
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            });
         | 
| 37 | 
            +
              var text_area_delimiter = '\n';
         | 
| 38 | 
            +
                function split(val) {
         | 
| 39 | 
            +
                    return val.split(new RegExp(text_area_delimiter+'\s*'));
         | 
| 40 | 
            +
                }
         | 
| 41 | 
            +
                function extractLast(term) {
         | 
| 42 | 
            +
                    return split(term).pop();
         | 
| 43 | 
            +
                }
         | 
| 44 | 
            +
            function setTextAreaAutoComplete($el) {
         | 
| 45 | 
            +
                $el.autocomplete({
         | 
| 46 | 
            +
            		source: function(request, response) {
         | 
| 47 | 
            +
            			$.getJSON($el.attr('data-auto-complete-url'), {
         | 
| 48 | 
            +
            				term: extractLast(request.term)
         | 
| 49 | 
            +
            			}, response);
         | 
| 50 | 
            +
            		},
         | 
| 51 | 
            +
            		search: function() {
         | 
| 52 | 
            +
            			// custom minLength
         | 
| 53 | 
            +
            			var term = extractLast(this.value);
         | 
| 54 | 
            +
            			if (term.length < 2) {
         | 
| 55 | 
            +
            				return false;
         | 
| 56 | 
            +
            			}
         | 
| 57 | 
            +
            		},
         | 
| 58 | 
            +
            		focus: function() {
         | 
| 59 | 
            +
            			// prevent value inserted on focus
         | 
| 60 | 
            +
            			return false;
         | 
| 61 | 
            +
            		},
         | 
| 62 | 
            +
            		select: function(event, ui) {
         | 
| 63 | 
            +
            			var terms = split( this.value );
         | 
| 64 | 
            +
            			terms.pop();  // remove the current input
         | 
| 65 | 
            +
            			terms.push( ui.item.value );	// add the selected item
         | 
| 66 | 
            +
            			// add placeholder to get the comma-and-space at the end
         | 
| 67 | 
            +
            			// terms.push("");
         | 
| 68 | 
            +
            			this.value = terms.join(text_area_delimiter);
         | 
| 69 | 
            +
            			return false;
         | 
| 70 | 
            +
            		}
         | 
| 71 | 
            +
            	});
         | 
| 72 | 
            +
            }
         | 
| 73 | 
            +
             | 
    
        data/test/test_helper.rb
    ADDED
    
    
    
        data/uninstall.rb
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            # Uninstall hook code here
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,85 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification 
         | 
| 2 | 
            +
            name: casey_jones
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version 
         | 
| 4 | 
            +
              hash: 217
         | 
| 5 | 
            +
              prerelease: false
         | 
| 6 | 
            +
              segments: 
         | 
| 7 | 
            +
              - 0
         | 
| 8 | 
            +
              - 0
         | 
| 9 | 
            +
              - 99
         | 
| 10 | 
            +
              version: 0.0.99
         | 
| 11 | 
            +
            platform: ruby
         | 
| 12 | 
            +
            authors: 
         | 
| 13 | 
            +
            - Tyler Gannon
         | 
| 14 | 
            +
            autorequire: 
         | 
| 15 | 
            +
            bindir: bin
         | 
| 16 | 
            +
            cert_chain: []
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            date: 2010-07-21 00:00:00 -07:00
         | 
| 19 | 
            +
            default_executable: 
         | 
| 20 | 
            +
            dependencies: []
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            description: some extra stuff for rails apps
         | 
| 23 | 
            +
            email: t--g__a--nnon@gmail.com
         | 
| 24 | 
            +
            executables: []
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            extensions: []
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            extra_rdoc_files: 
         | 
| 29 | 
            +
            - README
         | 
| 30 | 
            +
            files: 
         | 
| 31 | 
            +
            - Gemfile
         | 
| 32 | 
            +
            - README
         | 
| 33 | 
            +
            - Rakefile
         | 
| 34 | 
            +
            - install.rb
         | 
| 35 | 
            +
            - lib/acts_as_linkable/acts_as_linkable.rb
         | 
| 36 | 
            +
            - lib/ajax_loading/ajax_loading.rb
         | 
| 37 | 
            +
            - lib/ajax_loading/controller_resource.rb
         | 
| 38 | 
            +
            - lib/anaf_active_record.rb
         | 
| 39 | 
            +
            - lib/casey_jones.rb
         | 
| 40 | 
            +
            - lib/casey_jones/active_record_extensions.rb
         | 
| 41 | 
            +
            - lib/casey_jones/application_helper_methods.rb
         | 
| 42 | 
            +
            - lib/casey_jones/string_reader.rb
         | 
| 43 | 
            +
            - lib/generators/anaf_habtm/anaf_habtm_generator.rb
         | 
| 44 | 
            +
            - lib/generators/anaf_habtm/assets/nested_attributes.css
         | 
| 45 | 
            +
            - lib/generators/anaf_habtm/assets/nested_attributes.js
         | 
| 46 | 
            +
            - test/anaf_habtm_test.rb
         | 
| 47 | 
            +
            - test/test_helper.rb
         | 
| 48 | 
            +
            - uninstall.rb
         | 
| 49 | 
            +
            has_rdoc: true
         | 
| 50 | 
            +
            homepage: http://github.com/tylergannon/anaf_habtm
         | 
| 51 | 
            +
            licenses: []
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            post_install_message: 
         | 
| 54 | 
            +
            rdoc_options: 
         | 
| 55 | 
            +
            - --charset=UTF-8
         | 
| 56 | 
            +
            require_paths: 
         | 
| 57 | 
            +
            - lib
         | 
| 58 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement 
         | 
| 59 | 
            +
              none: false
         | 
| 60 | 
            +
              requirements: 
         | 
| 61 | 
            +
              - - ">="
         | 
| 62 | 
            +
                - !ruby/object:Gem::Version 
         | 
| 63 | 
            +
                  hash: 3
         | 
| 64 | 
            +
                  segments: 
         | 
| 65 | 
            +
                  - 0
         | 
| 66 | 
            +
                  version: "0"
         | 
| 67 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement 
         | 
| 68 | 
            +
              none: false
         | 
| 69 | 
            +
              requirements: 
         | 
| 70 | 
            +
              - - ">="
         | 
| 71 | 
            +
                - !ruby/object:Gem::Version 
         | 
| 72 | 
            +
                  hash: 3
         | 
| 73 | 
            +
                  segments: 
         | 
| 74 | 
            +
                  - 0
         | 
| 75 | 
            +
                  version: "0"
         | 
| 76 | 
            +
            requirements: []
         | 
| 77 | 
            +
             | 
| 78 | 
            +
            rubyforge_project: 
         | 
| 79 | 
            +
            rubygems_version: 1.3.7
         | 
| 80 | 
            +
            signing_key: 
         | 
| 81 | 
            +
            specification_version: 3
         | 
| 82 | 
            +
            summary: My favorite rails goodies all in one
         | 
| 83 | 
            +
            test_files: 
         | 
| 84 | 
            +
            - test/anaf_habtm_test.rb
         | 
| 85 | 
            +
            - test/test_helper.rb
         |