giraffesoft-attribute_fu 0.2

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.
Files changed (38) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README +115 -0
  3. data/Rakefile +22 -0
  4. data/init.rb +2 -0
  5. data/lib/attribute_fu.rb +2 -0
  6. data/lib/attribute_fu/associated_form_helper.rb +139 -0
  7. data/lib/attribute_fu/associations.rb +124 -0
  8. data/tasks/attribute_fu_tasks.rake +4 -0
  9. data/test/Rakefile +10 -0
  10. data/test/app/controllers/application.rb +10 -0
  11. data/test/app/helpers/application_helper.rb +3 -0
  12. data/test/app/models/comment.rb +8 -0
  13. data/test/app/models/photo.rb +3 -0
  14. data/test/config/boot.rb +97 -0
  15. data/test/config/database.yml +15 -0
  16. data/test/config/environment.rb +15 -0
  17. data/test/config/environments/development.rb +18 -0
  18. data/test/config/environments/test.rb +22 -0
  19. data/test/config/routes.rb +35 -0
  20. data/test/db/migrate/001_create_photos.rb +14 -0
  21. data/test/db/migrate/002_create_comments.rb +15 -0
  22. data/test/db/schema.rb +29 -0
  23. data/test/script/console +3 -0
  24. data/test/script/destroy +3 -0
  25. data/test/script/generate +3 -0
  26. data/test/script/server +3 -0
  27. data/test/test/test_helper.rb +6 -0
  28. data/test/test/unit/associated_form_helper_test.rb +376 -0
  29. data/test/test/unit/comment_test.rb +6 -0
  30. data/test/test/unit/photo_test.rb +149 -0
  31. data/test/vendor/plugins/shoulda/init.rb +3 -0
  32. data/test/vendor/plugins/shoulda/lib/shoulda.rb +20 -0
  33. data/test/vendor/plugins/shoulda/lib/shoulda/active_record_helpers.rb +338 -0
  34. data/test/vendor/plugins/shoulda/lib/shoulda/context.rb +143 -0
  35. data/test/vendor/plugins/shoulda/lib/shoulda/general.rb +119 -0
  36. data/test/vendor/plugins/shoulda/lib/shoulda/private_helpers.rb +17 -0
  37. data/uninstall.rb +1 -0
  38. metadata +110 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 James Golick
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,115 @@
1
+ = AttributeFu
2
+
3
+
4
+ Creating multi-model forms is amazingly easy with AttributeFu.
5
+
6
+ = Get It!
7
+
8
+ $ piston import http://svn.jamesgolick.com/attribute_fu/tags/stable vendor/plugins/attribute_fu
9
+
10
+ = Conventions
11
+
12
+ attribute_fu requires the fewest keystrokes if you follow certain conventions.
13
+
14
+ * The partial that contains your associated model's form is expected to be called _class_name.template_ext
15
+ (e.g. the partial for your Task model would be called _task.html.erb)
16
+ * The DOM element that contains the form for your model should have the CSS class .class_name
17
+ (e.g. the CSS class for your Task would be .task)
18
+ * The DOM element that contains all of the rendered forms should have the DOM ID #class_name
19
+ (e.g. the DOM ID of the container of your Task forms would be #tasks)
20
+ <i>Note: This is only relevant if using the add_associated_link method.</i>
21
+
22
+ = Example
23
+
24
+ In this example, you'll build a form for a Project model, in which a list of associated (has_many) tasks can be edited.
25
+
26
+ The first thing you need to do is enable attributes on the association.
27
+
28
+ class Project < ActiveRecord::Base
29
+ has_many :tasks, :attributes => true
30
+ end
31
+
32
+ Instances of Project will now respond to task_attributes, whose format is as follows:
33
+
34
+ @project.task_attributes = {
35
+ @project.tasks.first.id => {:title => "A new title for an existing task"},
36
+ :new => {
37
+ "0" => {:title => "A new task"}
38
+ }
39
+ }
40
+
41
+ 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.
42
+
43
+ == Form Helpers
44
+
45
+ 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).
46
+
47
+ ## _task.html.erb
48
+ <div class="task">
49
+ <label>Title</label>
50
+ <%= f.text_field :title %>
51
+ </div>
52
+
53
+ 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.
54
+
55
+ ## _form.html.erb
56
+ <%= f.render_associated_form(@project.tasks) %>
57
+
58
+ 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.
59
+
60
+ You may want to add a few blank tasks to the bottom of your form; no need to do that in the controller anymore.
61
+
62
+ <%= f.render_associated_form(@project.tasks, :new => 3) %>
63
+
64
+ 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.
65
+
66
+ ## _task.html.erb
67
+ <div class="task">
68
+ <label>Title</label>
69
+ <%= f.text_field :title %>
70
+ <%= f.remove_link "remove" %>
71
+ </div>
72
+
73
+ Creating the add button is equally simple. The add_associated_link helper will do all of the heavy lifting for you.
74
+
75
+ ## _form.html.erb
76
+ <%= f.add_associated_link "Add New Task", @project.tasks.build %>
77
+
78
+ That's all you have to do to create a multi-model form with attribute_fu!
79
+
80
+ == Discarding Blank Child Models
81
+
82
+ 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:
83
+
84
+ class Project < ActiveRecord::Base
85
+ has_many :tasks, :attributes => true, :discard_if => proc { |task| task.title.blank? }
86
+ end
87
+
88
+ ...or a symbol...
89
+
90
+ class Project < ActiveRecord::Base
91
+ has_many :tasks, :attributes => true, :discard_if => :blank?
92
+ end
93
+
94
+ class Task < ActiveRecord::Base
95
+ def blank?
96
+ title.blank?
97
+ end
98
+ end
99
+
100
+ 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.
101
+
102
+ = Updates
103
+
104
+ Come join the discussion on the {mailing list}[link:http://groups.google.com/group/attribute_fu]
105
+
106
+ Updates will be available {here}[http://jamesgolick.com/attribute_fu]
107
+
108
+
109
+ == Credits
110
+
111
+ attribute_fu was created, and is maintained by {James Golick}[http://jamesgolick.com].
112
+
113
+
114
+
115
+ Copyright (c) 2007 James Golick, GiraffeSoft Inc., released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the attribute_fu plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the attribute_fu plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'AttributeFu'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ ActiveRecord::Base.class_eval { include AttributeFu::Associations }
2
+ ActionView::Helpers::FormBuilder.class_eval { include AttributeFu::AssociatedFormHelper }
@@ -0,0 +1,2 @@
1
+ module AttributeFu #:nodoc:
2
+ end
@@ -0,0 +1,139 @@
1
+ module AttributeFu
2
+ # Methods for building forms that contain fields for associated models.
3
+ #
4
+ # Refer to the Conventions section in the README for the various expected defaults.
5
+ #
6
+ module AssociatedFormHelper
7
+ # Works similarly to fields_for, but used for building forms for associated objects.
8
+ #
9
+ # Automatically names fields to be compatible with the association_attributes= created by attribute_fu.
10
+ #
11
+ # An options hash can be specified to override the default behaviors.
12
+ #
13
+ # Options are:
14
+ # <tt>:javascript</tt> - Generate id placeholders for use with Prototype's Template class (this is how attribute_fu's add_associated_link works).
15
+ # <tt>:name</tt> - Specify the singular name of the association (in singular form), if it differs from the class name of the object.
16
+ #
17
+ # Any other supplied parameters are passed along to fields_for.
18
+ #
19
+ # Note: It is preferable to call render_associated_form, which will automatically wrap your form partial in a fields_for_associated call.
20
+ #
21
+ def fields_for_associated(associated, *args, &block)
22
+ conf = args.last.is_a?(Hash) ? args.last : {}
23
+ associated_name = extract_option_or_class_name(conf, :name, associated)
24
+ name = associated_base_name associated_name
25
+
26
+ unless associated.new_record?
27
+ name << "[#{associated.new_record? ? 'new' : associated.id}]"
28
+ else
29
+ @new_objects ||= {}
30
+ @new_objects[associated_name] ||= -1 # we want naming to start at 0
31
+ identifier = !conf.nil? && conf[:javascript] ? '#{number}' : @new_objects[associated_name]+=1
32
+
33
+ name << "[new][#{identifier}]"
34
+ end
35
+
36
+ @template.fields_for(name, *args.unshift(associated), &block)
37
+ end
38
+
39
+ # Creates a link for removing an associated element from the form, by removing its containing element from the DOM.
40
+ #
41
+ # Must be called from within an associated form.
42
+ #
43
+ # An options hash can be specified to override the default behaviors.
44
+ #
45
+ # Options are:
46
+ # * <tt>:selector</tt> - The CSS selector with which to find the element to remove.
47
+ # * <tt>:function</tt> - Additional javascript to be executed before the element is removed.
48
+ #
49
+ # Any remaining options are passed along to link_to_function
50
+ #
51
+ def remove_link(name, *args)
52
+ options = args.extract_options!
53
+
54
+ css_selector = options.delete(:selector) || ".#{@object.class.name.split("::").last.underscore}"
55
+ function = options.delete(:function) || ""
56
+
57
+ function << "$(this).up('#{css_selector}').remove()"
58
+
59
+ @template.link_to_function(name, function, *args.push(options))
60
+ end
61
+
62
+ # Creates a link that adds a new associated form to the page using Javascript.
63
+ #
64
+ # Must be called from within an associated form.
65
+ #
66
+ # Must be provided with a new instance of the associated object.
67
+ #
68
+ # e.g. f.add_associated_link 'Add Task', @project.tasks.build
69
+ #
70
+ # An options hash can be specified to override the default behaviors.
71
+ #
72
+ # Options are:
73
+ # * <tt>:partial</tt> - specify the name of the partial in which the form is located.
74
+ # * <tt>:container</tt> - specify the DOM id of the container in which to insert the new element.
75
+ # * <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'))
76
+ # * <tt>:name</tt> - specify an alternate class name for the associated model (underscored)
77
+ #
78
+ # Any additional options are forwarded to link_to_function. See its documentation for available options.
79
+ #
80
+ def add_associated_link(name, object, opts = {})
81
+ associated_name = extract_option_or_class_name(opts, :name, object)
82
+ variable = "attribute_fu_#{associated_name}_count"
83
+
84
+ opts.symbolize_keys!
85
+ partial = opts.delete(:partial) || associated_name
86
+ container = opts.delete(:expression) || "'#{opts.delete(:container) || associated_name.pluralize}'"
87
+
88
+ form_builder = self # because the value of self changes in the block
89
+
90
+ @template.link_to_function(name, opts) do |page|
91
+ page << "if (typeof #{variable} == 'undefined') #{variable} = 0;"
92
+ page << "new Insertion.Bottom(#{container}, new Template("+form_builder.render_associated_form(object, :fields_for => { :javascript => true }, :partial => partial).to_json+").evaluate({'number': --#{variable}}))"
93
+ end
94
+ end
95
+
96
+ # Renders the form of an associated object, wrapping it in a fields_for_associated call.
97
+ #
98
+ # The associated argument can be either an object, or a collection of objects to be rendered.
99
+ #
100
+ # An options hash can be specified to override the default behaviors.
101
+ #
102
+ # Options are:
103
+ # * <tt>:new</tt> - specify a certain number of new elements to be added to the form. Useful for displaying a
104
+ # few blank elements at the bottom.
105
+ # * <tt>:name</tt> - override the name of the association, both for the field names, and the name of the partial
106
+ # * <tt>:partial</tt> - specify the name of the partial in which the form is located.
107
+ # * <tt>:fields_for</tt> - specify additional options for the fields_for_associated call
108
+ # * <tt>:locals</tt> - specify additional variables to be passed along to the partial
109
+ # * <tt>:render</tt> - specify additional options to be passed along to the render :partial call
110
+ #
111
+ def render_associated_form(associated, opts = {})
112
+ associated = associated.is_a?(Array) ? associated : [associated] # preserve association proxy if this is one
113
+
114
+ opts.symbolize_keys!
115
+ (opts[:new] - associated.select(&:new_record?).length).times { associated.build } if opts[:new]
116
+
117
+ unless associated.empty?
118
+ name = extract_option_or_class_name(opts, :name, associated.first)
119
+ partial = opts[:partial] || name
120
+ local_assign_name = partial.split('/').last.split('.').first
121
+
122
+ associated.map do |element|
123
+ fields_for_associated(element, (opts[:fields_for] || {}).merge(:name => name)) do |f|
124
+ @template.render({:partial => "#{partial}", :locals => {local_assign_name.to_sym => element, :f => f}.merge(opts[:locals] || {})}.merge(opts[:render] || {}))
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+ def associated_base_name(associated_name)
132
+ "#{@object_name}[#{associated_name}_attributes]"
133
+ end
134
+
135
+ def extract_option_or_class_name(hash, option, object)
136
+ (hash.delete(option) || object.class.name.split('::').last.underscore).to_s
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,124 @@
1
+ module AttributeFu
2
+ module Associations #:nodoc:
3
+
4
+ def self.included(base) #:nodoc:
5
+ base.class_eval do
6
+ extend ClassMethods
7
+ class << self; alias_method_chain :has_many, :association_option; end
8
+
9
+ class_inheritable_accessor :managed_association_attributes
10
+ write_inheritable_attribute :managed_association_attributes, {}
11
+
12
+ after_update :save_managed_associations
13
+ end
14
+ end
15
+
16
+ def method_missing(method_name, *args) #:nodoc:
17
+ if method_name.to_s =~ /.+?\_attributes=/
18
+ association_name = method_name.to_s.gsub '_attributes=', ''
19
+ association = managed_association_attributes.keys.detect { |element| element == association_name.to_sym } || managed_association_attributes.keys.detect { |element| element == association_name.pluralize.to_sym }
20
+
21
+ unless association.nil?
22
+ has_many_attributes association, args.first
23
+
24
+ return
25
+ end
26
+ end
27
+
28
+ super
29
+ end
30
+
31
+ private
32
+ def has_many_attributes(association_id, attributes) #:nodoc:
33
+ association = send(association_id)
34
+ attributes = {} unless attributes.is_a? Hash
35
+
36
+ attributes.symbolize_keys!
37
+
38
+ if attributes.has_key?(:new)
39
+ new_attrs = attributes.delete(:new)
40
+ new_attrs = new_attrs.sort do |a,b|
41
+ value = lambda { |i| i < 0 ? i.abs + new_attrs.length : i }
42
+
43
+ value.call(a.first.to_i) <=> value.call(b.first.to_i)
44
+ end
45
+ new_attrs.each { |i, new_attrs| association.build new_attrs }
46
+ end
47
+
48
+ attributes.stringify_keys!
49
+ instance_variable_set removal_variable_name(association_id), association.reject { |object| object.new_record? || attributes.has_key?(object.id.to_s) }.map(&:id)
50
+ attributes.each do |id, object_attrs|
51
+ object = association.detect { |associated| associated.id.to_s == id }
52
+ object.attributes = object_attrs unless object.nil?
53
+ end
54
+
55
+ # discard blank attributes if discard_if proc exists
56
+ unless (discard = managed_association_attributes[association_id][:discard_if]).nil?
57
+ association.reject! { |object| object.new_record? && discard.call(object) }
58
+ association.delete(*association.select { |object| discard.call(object) })
59
+ end
60
+ end
61
+
62
+ def save_managed_associations #:nodoc:
63
+ if managed_association_attributes != nil
64
+ managed_association_attributes.keys.each do |association_id|
65
+ association = send(association_id)
66
+ association.each(&:save)
67
+
68
+ unless (objects_to_remove = instance_variable_get removal_variable_name(association_id)).nil?
69
+ objects_to_remove.each { |remove_id| association.delete association.detect { |obj| obj.id.to_s == remove_id.to_s } }
70
+ instance_variable_set removal_variable_name(association_id), nil
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def removal_variable_name(association_id) #:nodoc:
77
+ "@#{association_id.to_s.pluralize}_to_remove"
78
+ end
79
+
80
+ module ClassMethods
81
+
82
+ # Behaves identically to the regular has_many, except adds the option <tt>:attributes</tt>, which, if true, creates
83
+ # a method called association_id_attributes (i.e. task_attributes, or comment_attributes) for setting the attributes
84
+ # of a collection of associated models.
85
+ #
86
+ # It also adds the option <tt>:discard_if</tt>, which accepts a proc or a symbol. If the proc evaluates to true, the
87
+ # child model will be discarded. The symbol is sent as a message to the child model instance; if it returns true,
88
+ # the child model will be discarded.
89
+ #
90
+ # e.g.
91
+ #
92
+ # :discard_if => proc { |comment| comment.title.blank? }
93
+ # or
94
+ # :discard_if => :blank? # where blank is defined in Comment
95
+ #
96
+ #
97
+ # The format is as follows:
98
+ #
99
+ # @project.task_attributes = {
100
+ # @project.tasks.first.id => {:title => "A new title for an existing task"},
101
+ # :new => {
102
+ # "0" => {:title => "A new task"}
103
+ # }
104
+ # }
105
+ #
106
+ # Any existing tasks that are not present in the attributes hash will be removed from the association when the (parent) model
107
+ # is saved.
108
+ #
109
+ def has_many_with_association_option(association_id, options = {}, &extension)
110
+ unless (config = options.delete(:attributes)).nil?
111
+ managed_association_attributes[association_id] = {}
112
+ if options.has_key?(:discard_if)
113
+ discard_if = options.delete(:discard_if)
114
+ discard_if = discard_if.to_proc if discard_if.is_a?(Symbol)
115
+ managed_association_attributes[association_id][:discard_if] = discard_if
116
+ end
117
+ end
118
+
119
+ has_many_without_association_option(association_id, options, &extension)
120
+ end
121
+ end
122
+
123
+ end # Associations
124
+ end # AttributeFu
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :attribute_fu do
3
+ # # Task goes here
4
+ # end
data/test/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require(File.join(File.dirname(__FILE__), 'config', 'boot'))
5
+
6
+ require 'rake'
7
+ require 'rake/testtask'
8
+ require 'rake/rdoctask'
9
+
10
+ require 'tasks/rails'