Permalink
Browse files

Add attribute_fu

  • Loading branch information...
1 parent ade4d53 commit 57a938322896793cbe8f87da1d220735afc7a7e8 GIT_AUTHOR_NAME committed with veezus Feb 18, 2009
View
4 vendor/plugins/attribute_fu/.gitignore
@@ -0,0 +1,4 @@
+.DS_Store
+test/debug.log
+test/db/*.sqlite3
+test/log/*
View
20 vendor/plugins/attribute_fu/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008 James Golick
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
121 vendor/plugins/attribute_fu/README.rdoc
@@ -0,0 +1,121 @@
+= AttributeFu
+
+
+Creating multi-model forms is amazingly easy with AttributeFu.
+
+= Get It!
+
+ $ script/plugin install git://github.com/giraffesoft/attribute_fu.git
+
+= Conventions
+
+attribute_fu requires the fewest keystrokes if you follow certain conventions.
+
+* The partial that contains your associated model's form is expected to be called _class_name.template_ext
+ (e.g. the partial for your Task model would be called _task.html.erb)
+* The DOM element that contains the form for your model should have the CSS class .class_name
+ (e.g. the CSS class for your Task would be .task)
+* The DOM element that contains all of the rendered forms should have the DOM ID #class_name
+ (e.g. the DOM ID of the container of your Task forms would be #tasks)
+ <i>Note: This is only relevant if using the add_associated_link method.</i>
+
+= Example
+
+In this example, you'll build a form for a Project model, in which a list of associated (has_many) tasks can be edited.
+
+The first thing you need to do is enable attributes on the association.
+
+ class Project < ActiveRecord::Base
+ has_many :tasks, :attributes => true
+ end
+
+Instances of Project will now respond to task_attributes, whose format is as follows:
+
+ @project.task_attributes = {
+ @project.tasks.first.id => {:title => "A new title for an existing task"},
+ :new => {
+ "0" => {:title => "A new task"}
+ }
+ }
+
+Any tasks that already exist in that collection, and are not included in the hash, as supplied to task_attributes, will be removed from the association when saved. Most of the time, the form helpers should take care of building that hash for you, though.
+
+== Form Helpers
+
+If you follow certain conventions, rendering your associated model's form elements is incredibly simple. The partial should have the name of the associated element's type, and look like a regular old form partial (no messy fields_for calls, or any nonsense like that).
+
+ ## _task.html.erb
+ <div class="task">
+ <label>Title</label>
+ <%= f.text_field :title %>
+ </div>
+
+Then, in your parent element's form, call the render_associated_form method on the form builder, with the collection of elements you'd like to render as the only argument.
+
+ ## _form.html.erb
+ <%= f.render_associated_form(@project.tasks) %>
+
+That call will render the partial named _task.html.erb with each element in the supplied collection of tasks, wrapping the partial in a form builder (fields_for) with all the necessary arguments to produce a hash that will satisfy the task_attributes method.
+
+You may want to add a few blank tasks to the bottom of your form; no need to do that in the controller anymore.
+
+ <%= f.render_associated_form(@project.tasks, :new => 3) %>
+
+Since this is Web2.0, no form would be complete without some DHTML add and remove buttons. Fortunately, there are some nifty helpers to create them for us. Simply calling remove_link on the form builder in your _task partial will do the trick.
+
+ ## _task.html.erb
+ <div class="task">
+ <label>Title</label>
+ <%= f.text_field :title %>
+ <%= f.remove_link "remove" %>
+ </div>
+
+Creating the add button is equally simple. The add_associated_link helper will do all of the heavy lifting for you.
+
+ ## _form.html.erb
+ <%= f.add_associated_link "Add New Task", @project.tasks.build %>
+
+That's all you have to do to create a multi-model form with attribute_fu!
+
+== Discarding Blank Child Models
+
+If you want to show a bunch of blank child model forms at the bottom of your form, but you only want to save the ones that are filled out, you can use the discard_if option. It accepts either a proc:
+
+ class Project < ActiveRecord::Base
+ has_many :tasks, :attributes => true, :discard_if => proc { |task| task.title.blank? }
+ end
+
+...or a symbol...
+
+ class Project < ActiveRecord::Base
+ has_many :tasks, :attributes => true, :discard_if => :blank?
+ end
+
+ class Task < ActiveRecord::Base
+ def blank?
+ title.blank?
+ end
+ end
+
+Using a symbol allows you to keep code DRYer if you are using that routine in more than one place. Both of those examples, however, would have the same effect.
+
+= Updates
+
+Come join the discussion on the {mailing list}[link:http://groups.google.com/group/attribute_fu]
+
+Updates will be available {here}[http://jamesgolick.com/attribute_fu]
+
+= Running the tests
+
+To run the tests, you need Shoulda, mocha and multi-rails:
+
+ $ sudo gem install thoughtbot-shoulda --source http://gems.github.com/
+ $ sudo gem install mocha multi_rails
+
+== Credits
+
+attribute_fu was created, and is maintained by {James Golick}[http://jamesgolick.com].
+
+
+
+Copyright (c) 2007 James Golick, GiraffeSoft Inc., released under the MIT license
View
23 vendor/plugins/attribute_fu/Rakefile
@@ -0,0 +1,23 @@
+require 'rake'
+require "load_multi_rails_rake_tasks"
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the attribute_fu plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the attribute_fu plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'AttributeFu'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
View
13 vendor/plugins/attribute_fu/attribute_fu.gemspec
@@ -0,0 +1,13 @@
+Gem::Specification.new do |s|
+ s.name = "attribute_fu"
+ s.version = "0.2.1"
+ s.date = "2008-08-04"
+ s.summary = "rails multi-model forms made easy!"
+ s.email = "james@giraffesoft.ca"
+ s.homepage = "http://jamesgolick.com/attribute_fu"
+ s.description = "Creating multi-model forms is amazingly easy with AttributeFu."
+ s.has_rdoc = true
+ s.authors = ["James Golick"]
+ s.files = ["init.rb", "lib", "lib/attribute_fu", "lib/attribute_fu/associated_form_helper.rb", "lib/attribute_fu/associations.rb", "lib/attribute_fu.rb", "MIT-LICENSE", "Rakefile", "README", "tasks", "tasks/attribute_fu_tasks.rake", "test", "test/app", "test/app/controllers", "test/app/controllers/application.rb", "test/app/helpers", "test/app/helpers/application_helper.rb", "test/app/models", "test/app/models/comment.rb", "test/app/models/photo.rb", "test/config", "test/config/boot.rb", "test/config/database.yml", "test/config/environment.rb", "test/config/environments", "test/config/environments/development.rb", "test/config/environments/test.rb", "test/config/routes.rb", "test/db", "test/db/migrate", "test/db/migrate/001_create_photos.rb", "test/db/migrate/002_create_comments.rb", "test/db/schema.rb", "test/Rakefile", "test/script", "test/script/console", "test/script/destroy", "test/script/generate", "test/script/server", "test/test", "test/test/test_helper.rb", "test/test/unit", "test/test/unit/associated_form_helper_test.rb", "test/test/unit/comment_test.rb", "test/test/unit/photo_test.rb", "test/vendor", "test/vendor/plugins", "test/vendor/plugins/shoulda", "test/vendor/plugins/shoulda/init.rb", "test/vendor/plugins/shoulda/lib", "test/vendor/plugins/shoulda/lib/shoulda", "test/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb", "test/vendor/plugins/shoulda/lib/shoulda/context.rb", "test/vendor/plugins/shoulda/lib/shoulda/general.rb", "test/vendor/plugins/shoulda/lib/shoulda/private_helpers.rb", "test/vendor/plugins/shoulda/lib/shoulda.rb", "uninstall.rb"]
+ s.rdoc_options = ["--main", "README"]
+end
View
2 vendor/plugins/attribute_fu/init.rb
@@ -0,0 +1,2 @@
+ActiveRecord::Base.class_eval { include AttributeFu::Associations }
+ActionView::Helpers::FormBuilder.class_eval { include AttributeFu::AssociatedFormHelper }
View
2 vendor/plugins/attribute_fu/lib/attribute_fu.rb
@@ -0,0 +1,2 @@
+module AttributeFu #:nodoc:
+end
View
139 vendor/plugins/attribute_fu/lib/attribute_fu/associated_form_helper.rb
@@ -0,0 +1,139 @@
+module AttributeFu
+ # Methods for building forms that contain fields for associated models.
+ #
+ # Refer to the Conventions section in the README for the various expected defaults.
+ #
+ module AssociatedFormHelper
+ # Works similarly to fields_for, but used for building forms for associated objects.
+ #
+ # Automatically names fields to be compatible with the association_attributes= created by attribute_fu.
+ #
+ # An options hash can be specified to override the default behaviors.
+ #
+ # Options are:
+ # <tt>:javascript</tt> - Generate id placeholders for use with Prototype's Template class (this is how attribute_fu's add_associated_link works).
+ # <tt>:name</tt> - Specify the singular name of the association (in singular form), if it differs from the class name of the object.
+ #
+ # Any other supplied parameters are passed along to fields_for.
+ #
+ # Note: It is preferable to call render_associated_form, which will automatically wrap your form partial in a fields_for_associated call.
+ #
+ def fields_for_associated(associated, *args, &block)
+ conf = args.last.is_a?(Hash) ? args.last : {}
+ associated_name = extract_option_or_class_name(conf, :name, associated)
+ name = associated_base_name associated_name
+
+ unless associated.new_record?
+ name << "[#{associated.new_record? ? 'new' : associated.id}]"
+ else
+ @new_objects ||= {}
+ @new_objects[associated_name] ||= -1 # we want naming to start at 0
+ identifier = !conf.nil? && conf[:javascript] ? '#{number}' : @new_objects[associated_name]+=1
+
+ name << "[new][#{identifier}]"
+ end
+
+ @template.fields_for(name, *args.unshift(associated), &block)
+ end
+
+ # Creates a link for removing an associated element from the form, by removing its containing element from the DOM.
+ #
+ # Must be called from within an associated form.
+ #
+ # An options hash can be specified to override the default behaviors.
+ #
+ # Options are:
+ # * <tt>:selector</tt> - The CSS selector with which to find the element to remove.
+ # * <tt>:function</tt> - Additional javascript to be executed before the element is removed.
+ #
+ # Any remaining options are passed along to link_to_function
+ #
+ def remove_link(name, *args)
+ options = args.extract_options!
+
+ css_selector = options.delete(:selector) || ".#{@object.class.name.split("::").last.underscore}"
+ function = options.delete(:function) || ""
+
+ function << "$(this).up('#{css_selector}').remove()"
+
+ @template.link_to_function(name, function, *args.push(options))
+ end
+
+ # Creates a link that adds a new associated form to the page using Javascript.
+ #
+ # Must be called from within an associated form.
+ #
+ # Must be provided with a new instance of the associated object.
+ #
+ # e.g. f.add_associated_link 'Add Task', @project.tasks.build
+ #
+ # An options hash can be specified to override the default behaviors.
+ #
+ # Options are:
+ # * <tt>:partial</tt> - specify the name of the partial in which the form is located.
+ # * <tt>:container</tt> - specify the DOM id of the container in which to insert the new element.
+ # * <tt>:expression</tt> - specify a javascript expression with which to select the container to insert the new form in to (i.e. $(this).up('.tasks'))
+ # * <tt>:name</tt> - specify an alternate class name for the associated model (underscored)
+ #
+ # Any additional options are forwarded to link_to_function. See its documentation for available options.
+ #
+ def add_associated_link(name, object, opts = {})
+ associated_name = extract_option_or_class_name(opts, :name, object)
+ variable = "attribute_fu_#{associated_name}_count"
+
+ opts.symbolize_keys!
+ partial = opts.delete(:partial) || associated_name
+ container = opts.delete(:expression) || "'#{opts.delete(:container) || associated_name.pluralize}'"
+
+ form_builder = self # because the value of self changes in the block
+
+ @template.link_to_function(name, opts) do |page|
+ page << "if (typeof #{variable} == 'undefined') #{variable} = 0;"
+ page << "new Insertion.Bottom(#{container}, new Template("+form_builder.render_associated_form(object, :fields_for => { :javascript => true }, :partial => partial).to_json+").evaluate({'number': --#{variable}}).gsub(/__number_/, #{variable}))"
+ end
+ end
+
+ # Renders the form of an associated object, wrapping it in a fields_for_associated call.
+ #
+ # The associated argument can be either an object, or a collection of objects to be rendered.
+ #
+ # An options hash can be specified to override the default behaviors.
+ #
+ # Options are:
+ # * <tt>:new</tt> - specify a certain number of new elements to be added to the form. Useful for displaying a
+ # few blank elements at the bottom.
+ # * <tt>:name</tt> - override the name of the association, both for the field names, and the name of the partial
+ # * <tt>:partial</tt> - specify the name of the partial in which the form is located.
+ # * <tt>:fields_for</tt> - specify additional options for the fields_for_associated call
+ # * <tt>:locals</tt> - specify additional variables to be passed along to the partial
+ # * <tt>:render</tt> - specify additional options to be passed along to the render :partial call
+ #
+ def render_associated_form(associated, opts = {})
+ associated = associated.is_a?(Array) ? associated : [associated] # preserve association proxy if this is one
+
+ opts.symbolize_keys!
+ (opts[:new] - associated.select(&:new_record?).length).times { associated.build } if opts[:new]
+
+ unless associated.empty?
+ name = extract_option_or_class_name(opts, :name, associated.first)
+ partial = opts[:partial] || name
+ local_assign_name = partial.split('/').last.split('.').first
+
+ associated.map do |element|
+ fields_for_associated(element, (opts[:fields_for] || {}).merge(:name => name)) do |f|
+ @template.render({:partial => "#{partial}", :locals => {local_assign_name.to_sym => element, :f => f}.merge(opts[:locals] || {})}.merge(opts[:render] || {}))
+ end
+ end
+ end
+ end
+
+ private
+ def associated_base_name(associated_name)
+ "#{@object_name}[#{associated_name}_attributes]"
+ end
+
+ def extract_option_or_class_name(hash, option, object)
+ (hash.delete(option) || object.class.name.split('::').last.underscore).to_s
+ end
+ end
+end
View
136 vendor/plugins/attribute_fu/lib/attribute_fu/associations.rb
@@ -0,0 +1,136 @@
+module AttributeFu
+ module Associations #:nodoc:
+
+ def self.included(base) #:nodoc:
+ base.class_eval do
+ extend ClassMethods
+ class << self; alias_method_chain :has_many, :association_option; end
+
+ class_inheritable_accessor :managed_association_attributes
+ write_inheritable_attribute :managed_association_attributes, {}
+
+ after_update :save_managed_associations
+ end
+ end
+
+ def method_missing(method_name, *args) #:nodoc:
+ if method_name.to_s =~ /.+?\_attributes=/
+ association_name = method_name.to_s.gsub '_attributes=', ''
+ association = managed_association_attributes.keys.detect { |element| element == association_name.to_sym } || managed_association_attributes.keys.detect { |element| element == association_name.pluralize.to_sym }
+
+ unless association.nil?
+ has_many_attributes association, args.first
+
+ return
+ end
+ end
+
+ super
+ end
+
+ private
+ def has_many_attributes(association_id, attributes) #:nodoc:
+ association = send(association_id)
+ attributes = {} unless attributes.is_a? Hash
+
+ attributes.symbolize_keys!
+
+ if attributes.has_key?(:new)
+ new_attrs = attributes.delete(:new)
+ new_attrs = new_attrs.sort do |a,b|
+ value = lambda { |i| i < 0 ? i.abs + new_attrs.length : i }
+
+ value.call(a.first.to_i) <=> value.call(b.first.to_i)
+ end
+ new_attrs.each { |i, new_attrs| association.build new_attrs }
+ end
+
+ attributes.stringify_keys!
+ instance_variable_set removal_variable_name(association_id), association.reject { |object| object.new_record? || attributes.has_key?(object.id.to_s) }.map(&:id)
+ attributes.each do |id, object_attrs|
+ object = association.detect { |associated| associated.id.to_s == id }
+ object.attributes = object_attrs unless object.nil?
+ end
+
+ # discard blank attributes if discard_if proc exists
+ unless (discard = managed_association_attributes[association_id][:discard_if]).nil?
+ association.reject! { |object| object.new_record? && discard.call(object) }
+ association.delete(*association.select { |object| discard.call(object) })
+ end
+ end
+
+ def save_managed_associations #:nodoc:
+ if managed_association_attributes != nil
+ managed_association_attributes.keys.each do |association_id|
+ if send(association_id).loaded? # don't save what we haven't even loaded
+ association = send(association_id)
+ association.each(&:save)
+
+ unless (objects_to_remove = instance_variable_get removal_variable_name(association_id)).nil?
+ objects_to_remove.each { |remove_id| association.delete association.detect { |obj| obj.id.to_s == remove_id.to_s } }
+ instance_variable_set removal_variable_name(association_id), nil
+ end
+ end
+ end
+ end
+ end
+
+ def removal_variable_name(association_id) #:nodoc:
+ "@#{association_id.to_s.pluralize}_to_remove"
+ end
+
+ module ClassMethods
+
+ # Behaves identically to the regular has_many, except adds the option <tt>:attributes</tt>, which, if true, creates
+ # a method called association_id_attributes (i.e. task_attributes, or comment_attributes) for setting the attributes
+ # of a collection of associated models.
+ #
+ # It also adds the option <tt>:discard_if</tt>, which accepts a proc or a symbol. If the proc evaluates to true, the
+ # child model will be discarded. The symbol is sent as a message to the child model instance; if it returns true,
+ # the child model will be discarded.
+ #
+ # e.g.
+ #
+ # :discard_if => proc { |comment| comment.title.blank? }
+ # or
+ # :discard_if => :blank? # where blank is defined in Comment
+ #
+ #
+ # The format is as follows:
+ #
+ # @project.task_attributes = {
+ # @project.tasks.first.id => {:title => "A new title for an existing task"},
+ # :new => {
+ # "0" => {:title => "A new task"}
+ # }
+ # }
+ #
+ # Any existing tasks that are not present in the attributes hash will be removed from the association when the (parent) model
+ # is saved.
+ #
+ def has_many_with_association_option(association_id, options = {}, &extension)
+ unless (config = options.delete(:attributes)).nil?
+ managed_association_attributes[association_id] = {}
+ if options.has_key?(:discard_if)
+ discard_if = options.delete(:discard_if)
+ discard_if = discard_if.to_proc if discard_if.is_a?(Symbol)
+ managed_association_attributes[association_id][:discard_if] = discard_if
+ end
+ collection_with_attributes_writer(association_id)
+ end
+
+ has_many_without_association_option(association_id, options, &extension)
+ end
+
+ private
+
+ def collection_with_attributes_writer(association_name)
+ define_method("#{association_name.to_s.singularize}_attributes=") do |attributes|
+ has_many_attributes association_name, attributes
+ end
+ end
+
+ end
+
+ end # Associations
+end # AttributeFu
View
4 vendor/plugins/attribute_fu/tasks/attribute_fu_tasks.rake
@@ -0,0 +1,4 @@
+# desc "Explaining what the task does"
+# task :attribute_fu do
+# # Task goes here
+# end
View
376 vendor/plugins/attribute_fu/test/associated_form_helper_test.rb
@@ -0,0 +1,376 @@
+require File.dirname(__FILE__)+'/test_helper'
+
+class AssociatedFormHelperTest < Test::Unit::TestCase
+ include ActionView::Helpers::FormHelper
+ include ActionView::Helpers::FormTagHelper
+ include ActionView::Helpers::UrlHelper
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::TextHelper
+ include AttributeFu::AssociatedFormHelper
+
+ def setup
+ @photo = Photo.create
+ @controller = mock()
+ @controller.stubs(:url_for).returns 'asdf'
+ @controller.stubs(:protect_against_forgery?).returns false
+ stubs(:protect_against_forgery?).returns false
+ end
+
+ context "fields for associated" do
+ context "with existing object" do
+ setup do
+ @photo.comments.create :author => "Barry", :body => "Oooh I did good today..."
+
+ @erbout = assoc_output @photo.comments.first
+ end
+
+ should "name field with attribute_fu naming conventions" do
+ assert_match "photo[comment_attributes][#{@photo.comments.first.id}]", @erbout
+ end
+ end
+
+ context "with non-existent object" do
+ setup do
+ @erbout = assoc_output(@photo.comments.build) do |f|
+ f.fields_for_associated(@photo.comments.build) do |comment|
+ comment.text_field(:author)
+ end
+ end
+ end
+
+ should "name field with attribute_fu naming conventions" do
+ assert_match "photo[comment_attributes][new][0]", @erbout
+ end
+
+ should "maintain the numbering of the new object if called again" do
+ assert_match "photo[comment_attributes][new][1]", @erbout
+ end
+ end
+
+ context "with overridden name" do
+ setup do
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.fields_for_associated(@photo.comments.build, :name => :something_else) do |comment|
+ _erbout.concat comment.text_field(:author)
+ end
+ end
+
+ @erbout = _erbout
+ end
+
+ should "use override name" do
+ assert_dom_equal "<input name='photo[something_else_attributes][new][0][author]' size='30' type='text' id='photo_something_else_attributes__new__0_author' />", @erbout
+ end
+ end
+ end
+
+ context "remove link" do
+ context "with just a name" do
+ setup do
+ remove_link "remove"
+ end
+
+ should "create a link" do
+ assert_match ">remove</a>", @erbout
+ end
+
+ should "infer the name of the current @object in fields_for" do
+ assert_match "$(this).up('.comment').remove()", @erbout
+ end
+ end
+
+ context "with an alternate CSS selector" do
+ setup do
+ remove_link "remove", :selector => '.blah'
+ end
+
+ should "use the alternate selector" do
+ assert_match "$(this).up('.blah').remove()", @erbout
+ end
+ end
+
+ context "with an extra function" do
+ setup do
+ @other_function = "$('asdf').blah();"
+ remove_link "remove", :function => @other_function
+ end
+
+ should "still infer the name of the current @object in fields_for, and create the function as usual" do
+ assert_match "$(this).up('.comment').remove()", @erbout
+ end
+
+ should "append the secondary function" do
+ assert_match @other_function, @erbout
+ end
+ end
+ end
+
+ context "with javascript flag" do
+ setup do
+ _erbout = ''
+ fields_for(:photo) do |f|
+ _erbout.concat(f.fields_for_associated(@photo.comments.build, :javascript => true) do |comment|
+ comment.text_field(:author)
+ end)
+ end
+
+ @erbout = _erbout
+ end
+
+ should "use placeholders instead of numbers" do
+ assert_match 'photo[comment_attributes][new][#{number}]', @erbout
+ end
+ end
+
+ context "add_associated_link " do
+ setup do
+ comment = @photo.comments.build
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:render_associated_form).with(comment, :fields_for => {:javascript => true}, :partial => 'comment')
+ _erbout.concat f.add_associated_link("Add Comment", comment, :class => 'something')
+ end
+
+ @erbout = _erbout
+ end
+
+ should "create link" do
+ assert_match ">Add Comment</a>", @erbout
+ end
+
+ should "insert into the bottom of the parent container by default" do
+ assert_match "Insertion.Bottom('comments'", @erbout
+ end
+
+ should "wrap the partial in a prototype template" do
+ assert_match "new Template", @erbout
+ assert_match "evaluate", @erbout
+ end
+
+ should "name the variable correctly" do
+ assert_match "attribute_fu_comment_count", @erbout
+ end
+
+ should "pass along the additional options to the link_to_function call" do
+ assert_match 'class="something"', @erbout
+ end
+
+ should "produce the following link" do
+ # this is a way of testing the whole link
+ assert_equal %{
+ <a class=\"something\" href=\"#\" onclick=\"if (typeof attribute_fu_comment_count == 'undefined') attribute_fu_comment_count = 0;\nnew Insertion.Bottom('comments', new Template(null).evaluate({'number': --attribute_fu_comment_count}).gsub(/__number_/, attribute_fu_comment_count)); return false;\">Add Comment</a>
+ }.strip, @erbout
+ end
+ end
+
+ context "add_associated_link with parameters" do
+ setup do
+ comment = @photo.comments.build
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:render_associated_form).with(comment, :fields_for => {:javascript => true}, :partial => 'some_other_partial')
+ _erbout.concat f.add_associated_link("Add Comment", comment, :container => 'something_comments', :partial => 'some_other_partial')
+ end
+
+ @erbout = _erbout
+ end
+
+ should "create link" do
+ assert_match ">Add Comment</a>", @erbout
+ end
+
+ should "insert into the bottom of the container specified" do
+ assert_match "Insertion.Bottom('something_comments'", @erbout
+ end
+
+ should "wrap the partial in a prototype template" do
+ assert_match "new Template", @erbout
+ assert_match "evaluate", @erbout
+ end
+
+ should "name the variable correctly" do
+ assert_match "attribute_fu_comment_count", @erbout
+ end
+
+ should "produce the following link" do
+ # this is a way of testing the whole link
+ assert_equal %{
+ <a href=\"#\" onclick=\"if (typeof attribute_fu_comment_count == 'undefined') attribute_fu_comment_count = 0;\nnew Insertion.Bottom('something_comments', new Template(null).evaluate({'number': --attribute_fu_comment_count}).gsub(/__number_/, attribute_fu_comment_count)); return false;\">Add Comment</a>
+ }.strip, @erbout
+ end
+ end
+
+ context "add associated link with expression parameter" do
+ setup do
+ comment = @photo.comments.build
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:render_associated_form).with(comment, :fields_for => {:javascript => true}, :partial => 'some_other_partial')
+ _erbout.concat f.add_associated_link("Add Comment", comment, :expression => '$(this).up(".something_comments")', :partial => 'some_other_partial')
+ end
+
+ @erbout = _erbout
+ end
+
+ should "create link" do
+ assert_match ">Add Comment</a>", @erbout
+ end
+
+ should "use the javascript expression provided instead of passing the ID in" do
+ assert_match "Insertion.Bottom($(this).up(&quot;.something_comments&quot;)", @erbout
+ end
+
+ should "wrap the partial in a prototype template" do
+ assert_match "new Template", @erbout
+ assert_match "evaluate", @erbout
+ end
+
+ should "name the variable correctly" do
+ assert_match "attribute_fu_comment_count", @erbout
+ end
+
+ should "produce the following link" do
+ # this is a way of testing the whole link
+ assert_equal %{
+ <a href=\"#\" onclick=\"if (typeof attribute_fu_comment_count == 'undefined') attribute_fu_comment_count = 0;\nnew Insertion.Bottom($(this).up(&quot;.something_comments&quot;), new Template(null).evaluate({'number': --attribute_fu_comment_count}).gsub(/__number_/, attribute_fu_comment_count)); return false;\">Add Comment</a>
+ }.strip, @erbout
+ end
+ end
+
+ context "render_associated_form" do
+ setup do
+ comment = @photo.comments.build
+
+ associated_form_builder = mock()
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:fields_for_associated).yields(associated_form_builder)
+ expects(:render).with(:partial => "comment", :locals => { :comment => comment, :f => associated_form_builder })
+ _erbout.concat f.render_associated_form(comment).to_s
+ end
+
+ @erbout = _erbout
+ end
+
+ should "extract the correct parameters for render" do
+ # assertions in mock
+ end
+ end
+
+ context "render_associated_form with specified partial name" do
+ setup do
+ comment = @photo.comments.build
+
+ associated_form_builder = mock()
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:fields_for_associated).yields(associated_form_builder)
+ expects(:render).with(:partial => "somewhere/something.html.erb", :locals => { :something => comment, :f => associated_form_builder })
+ _erbout.concat f.render_associated_form(comment, :partial => "somewhere/something.html.erb").to_s
+ end
+
+ @erbout = _erbout
+ end
+
+ should "extract the correct parameters for render" do
+ # assertions in mock
+ end
+ end
+
+ context "render_associated_form with collection" do
+ setup do
+ associated_form_builder = mock()
+ new_comment = Comment.new
+ @photo.comments.expects(:build).returns(new_comment).times(3)
+ @photo.comments.stubs(:empty?).returns(false)
+ @photo.comments.stubs(:first).returns(new_comment)
+ @photo.comments.stubs(:map).yields(new_comment)
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:fields_for_associated).yields(associated_form_builder)
+ expects(:render).with(:partial => "comment", :locals => { :comment => new_comment, :f => associated_form_builder })
+ _erbout.concat f.render_associated_form(@photo.comments, :new => 3).to_s
+ end
+
+ @erbout = _erbout
+ end
+
+ should "extract the correct parameters for render" do
+ # assertions in mock
+ end
+ end
+
+ context "render_associated_form with collection that already has a couple of new objects in it" do
+ setup do
+ associated_form_builder = mock()
+ 2.times { @photo.comments.build }
+ new_comment = Comment.new
+ @photo.comments.expects(:build).returns(new_comment)
+ @photo.comments.stubs(:empty?).returns(false)
+ @photo.comments.stubs(:first).returns(new_comment)
+ @photo.comments.stubs(:map).yields(new_comment)
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:fields_for_associated).yields(associated_form_builder)
+ expects(:render).with(:partial => "comment", :locals => { :comment => new_comment, :f => associated_form_builder })
+ _erbout.concat f.render_associated_form(@photo.comments, :new => 3).to_s
+ end
+
+ @erbout = _erbout
+ end
+
+ should "extract the correct parameters for render" do
+ # assertions in mock
+ end
+ end
+
+ context "render_associated_form with overridden name" do
+ setup do
+ associated_form_builder = mock()
+ comment = @photo.comments.build
+
+ _erbout = ''
+ fields_for(:photo) do |f|
+ f.stubs(:fields_for_associated).with(comment, :name => 'something_else').yields(associated_form_builder)
+ expects(:render).with(:partial => "something_else", :locals => { :something_else => comment, :f => associated_form_builder })
+ _erbout.concat f.render_associated_form(@photo.comments, :name => :something_else).to_s
+ end
+
+ @erbout = _erbout
+ end
+
+ should "render with correct parameters" do
+ # assertions in mock
+ end
+ end
+
+ private
+ def assoc_output(comment, &block)
+ _erbout = ''
+ fields_for(:photo) do |f|
+ _erbout.concat(f.fields_for_associated(comment) do |comment|
+ comment.text_field(:author)
+ end)
+
+ _erbout.concat yield(f) if block_given?
+ end
+
+ _erbout
+ end
+
+ def remove_link(*args)
+ @erbout = assoc_output(@photo.comments.build) do |f|
+ f.fields_for_associated(@photo.comments.build) do |comment|
+ comment.remove_link *args
+ end
+ end
+ end
+end
View
6 vendor/plugins/attribute_fu/test/comment_test.rb
@@ -0,0 +1,6 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+class CommentTest < ActiveSupport::TestCase
+ should_belong_to :photo
+ should_require_attributes :author, :body
+end
View
1 vendor/plugins/attribute_fu/test/db/.gitignore
@@ -0,0 +1 @@
+*.sqlite3
View
18 vendor/plugins/attribute_fu/test/db/database.yml
@@ -0,0 +1,18 @@
+sqlite3:
+ adapter: sqlite3
+ dbfile: attribute_fu.sqlite3.db
+sqlite3mem:
+ :adapter: sqlite3
+ :dbfile: ":memory:"
+postgresql:
+ :adapter: postgresql
+ :username: postgres
+ :password: postgres
+ :database: attribute_fu_plugin_test
+ :min_messages: ERROR
+mysql:
+ :adapter: mysql
+ :host: localhost
+ :username: root
+ :password:
+ :database: attribute_fu_plugin_test
View
16 vendor/plugins/attribute_fu/test/db/schema.rb
@@ -0,0 +1,16 @@
+ActiveRecord::Schema.define(:version => 2) do
+
+ create_table :comments, :force => true do |t|
+ t.integer :photo_id
+ t.string :author
+ t.text :body
+ t.timestamps
+ end
+
+ create_table :photos, :force => true do |t|
+ t.string :title
+ t.text :description
+ t.timestamps
+ end
+
+end
View
8 vendor/plugins/attribute_fu/test/models/comment.rb
@@ -0,0 +1,8 @@
+class Comment < ActiveRecord::Base
+ belongs_to :photo
+ validates_presence_of :author, :body
+
+ def blank?
+ author.blank? && body.blank?
+ end
+end
View
3 vendor/plugins/attribute_fu/test/models/photo.rb
@@ -0,0 +1,3 @@
+class Photo < ActiveRecord::Base
+ has_many :comments, :attributes => true
+end
View
160 vendor/plugins/attribute_fu/test/photo_test.rb
@@ -0,0 +1,160 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+class PhotoTest < ActiveSupport::TestCase
+ should_have_many :comments
+
+ context "comment_attributes" do
+ context "with valid children" do
+ setup do
+ create_photo_and_children
+
+ @photo.comment_attributes = { @gob.id.to_s => { :author => "Buster Bluth", :body => "I said it was _our_ nausia..." },
+ :new => { "0" => { :author => "George-Michael", :body => "I was going to smoke the marijuana like a ciggarette." },
+ "-1" => { :author => "Tobias Funke", :body => "I am an actor! An actor for crying out loud!" }}}
+
+ end
+
+ context "before save" do
+ should "not have deleted anything in the remove array" do
+ assert @photo.comments.any? { |comment| comment.author == "Bob Loblaw" }, "Comment in remove array was removed."
+ end
+
+ should "not have saved any new objects" do
+ assert @photo.comments.any? { |comment| comment.new_record? }
+ end
+ end
+
+ context "after save" do
+ setup do
+ @photo.save
+ end
+
+ context "with existing child" do
+ setup do
+ @gob.reload
+ end
+
+ should "update attributes" do
+ assert_equal "Buster Bluth", @gob.author, "Author attribute of child model was not updated."
+ assert_equal "I said it was _our_ nausia...", @gob.body, "Body attribute of child model was not updated."
+ end
+ end
+
+ context "with new hash" do
+ should "create new comment" do
+ assert @photo.comments.any? { |comment| comment.author == "George-Michael" && comment.body =~ /was going to smoke/i }, "New comment was not created."
+ end
+
+ should "order the negatives after the positives" do
+ assert_equal "Tobias Funke", @photo.comments.last.author, "Tobias is not the last comment: #{@photo.comments.inspect}"
+ end
+ end
+
+ context "with missing associated" do
+ should "remove those children from the parent" do
+ assert !@photo.comments.any? { |comment| comment.author == "Bob Loblaw" }, "Comment not included was not removed."
+ end
+ end
+ end
+
+ context "with comment_attributes = nil" do
+ setup do
+ @photo.save
+ @photo.comment_attributes = nil
+ @photo.save
+ end
+
+ should "remove all comments" do
+ assert @photo.comments.empty?, "one or more comments not removed: #{@photo.comments.inspect}"
+ end
+ end
+
+ context "with comment_attributes unset" do
+ setup do
+ @photo.reload
+ @photo.save
+ end
+
+ should "not load the comments" do
+ assert !@photo.comments.loaded?, "comments were loaded unnecessarily: #{@photo.comments.inspect}"
+ end
+ end
+
+ context "with discard_if => proc { }" do
+ setup do
+ create_photo_with_discard(proc { |comment| comment.author.blank? && comment.body.blank? })
+ end
+
+ teardown do
+ Photo.class_eval do
+ managed_association_attributes[:comments].delete(:discard_if)
+ end
+ end
+
+ should "discard any child objects for which discard_if evaluates to true" do
+ assert !@photo.comments.any? { |comment| comment.author.blank? && comment.body.blank? }, @photo.comments.inspect
+ end
+
+ should "not discard other objects" do
+ assert_equal 1, @photo.comments.length
+ end
+ end
+
+ context "with discard_if => :symbol" do
+ setup do
+ create_photo_with_discard(:blank?)
+ end
+
+ teardown do
+ Photo.class_eval do
+ managed_association_attributes[:comments].delete(:discard_if)
+ end
+ end
+
+ should "discard any child objects for which discard_if evaluates to true" do
+ assert !@photo.comments.any? { |comment| comment.author.blank? && comment.body.blank? }, @photo.comments.inspect
+ end
+
+ should "not discard other objects" do
+ assert_equal 1, @photo.comments.length
+ end
+ end
+ end
+
+ context "updating with invalid children" do
+ setup do
+ @photo = Photo.create
+ @saved = @photo.update_attributes :comment_attributes => {:new => {"0" => {:author => "Tobias"}}}
+ end
+
+ should "not save" do
+ assert !@saved
+ end
+
+ should "have errors on child" do
+ assert @photo.comments.first.errors.on(:body)
+ end
+ end
+ end
+
+ private
+ def create_photo_and_children
+ @photo = Photo.create
+ @gob = @photo.comments.create :author => "Gob Bluth", :body => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed..."
+ @bob = @photo.comments.create :author => "Bob Loblaw", :body => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed..."
+ end
+
+ def create_photo_with_discard(discard_if)
+ Photo.class_eval do
+ has_many :comments, :attributes => true, :discard_if => discard_if
+ end
+
+ create_photo_and_children
+
+
+ @photo.comment_attributes = { @gob.id.to_s => { :author => "Buster Bluth", :body => "I said it was _our_ nausia..." },
+ @bob.id.to_s => { :author => '', :body => '' },
+ :new => { "0" => { :author => "", :body => "" }}}
+ @photo.save
+ end
+end
View
24 vendor/plugins/attribute_fu/test/test_helper.rb
@@ -0,0 +1,24 @@
+$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
+plugin_test_dir = File.dirname(__FILE__)
+
+require 'rubygems'
+require 'multi_rails_init'
+require 'active_record'
+require 'action_view'
+require 'test/unit'
+require 'mocha'
+require 'shoulda/rails'
+
+require 'attribute_fu'
+require 'attribute_fu/associations'
+require 'attribute_fu/associated_form_helper'
+require plugin_test_dir + '/../init.rb'
+
+ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
+
+ActiveRecord::Base.configurations = YAML::load(IO.read(plugin_test_dir + "/db/database.yml"))
+ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
+ActiveRecord::Migration.verbose = false
+load(File.join(plugin_test_dir, "db", "schema.rb"))
+
+Dir["#{plugin_test_dir}/models/*.rb"].each {|file| require file }
View
1 vendor/plugins/attribute_fu/uninstall.rb
@@ -0,0 +1 @@
+# Uninstall hook code here

0 comments on commit 57a9383

Please sign in to comment.