hobo 0.8.3 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/CHANGES.txt +330 -0
  2. data/Manifest +12 -4
  3. data/Rakefile +4 -6
  4. data/dryml_generators/rapid/cards.dryml.erb +5 -1
  5. data/dryml_generators/rapid/forms.dryml.erb +8 -10
  6. data/dryml_generators/rapid/pages.dryml.erb +65 -36
  7. data/hobo.gemspec +28 -15
  8. data/lib/active_record/association_collection.rb +3 -22
  9. data/lib/hobo.rb +25 -258
  10. data/lib/hobo/accessible_associations.rb +131 -0
  11. data/lib/hobo/authentication_support.rb +15 -9
  12. data/lib/hobo/composite_model.rb +1 -1
  13. data/lib/hobo/controller.rb +7 -8
  14. data/lib/hobo/dryml.rb +9 -10
  15. data/lib/hobo/dryml/dryml_builder.rb +7 -1
  16. data/lib/hobo/dryml/dryml_doc.rb +161 -0
  17. data/lib/hobo/dryml/dryml_generator.rb +18 -9
  18. data/lib/hobo/dryml/part_context.rb +76 -42
  19. data/lib/hobo/dryml/tag_parameters.rb +1 -0
  20. data/lib/hobo/dryml/taglib.rb +2 -1
  21. data/lib/hobo/dryml/template.rb +39 -29
  22. data/lib/hobo/dryml/template_environment.rb +79 -37
  23. data/lib/hobo/dryml/template_handler.rb +66 -21
  24. data/lib/hobo/guest.rb +2 -10
  25. data/lib/hobo/hobo_helper.rb +125 -53
  26. data/lib/hobo/include_in_save.rb +0 -1
  27. data/lib/hobo/lifecycles.rb +54 -24
  28. data/lib/hobo/lifecycles/actions.rb +95 -31
  29. data/lib/hobo/lifecycles/creator.rb +18 -23
  30. data/lib/hobo/lifecycles/lifecycle.rb +86 -62
  31. data/lib/hobo/lifecycles/state.rb +1 -2
  32. data/lib/hobo/lifecycles/transition.rb +22 -28
  33. data/lib/hobo/model.rb +64 -176
  34. data/lib/hobo/model_controller.rb +67 -54
  35. data/lib/hobo/model_router.rb +5 -2
  36. data/lib/hobo/permissions.rb +397 -0
  37. data/lib/hobo/permissions/associations.rb +167 -0
  38. data/lib/hobo/scopes.rb +15 -38
  39. data/lib/hobo/scopes/association_proxy_extensions.rb +15 -5
  40. data/lib/hobo/scopes/automatic_scopes.rb +43 -18
  41. data/lib/hobo/scopes/named_scope_extensions.rb +2 -2
  42. data/lib/hobo/user.rb +10 -4
  43. data/lib/hobo/user_controller.rb +6 -5
  44. data/lib/hobo/view_hints.rb +58 -0
  45. data/rails_generators/hobo/hobo_generator.rb +7 -3
  46. data/rails_generators/hobo/templates/guest.rb +1 -13
  47. data/rails_generators/hobo_front_controller/hobo_front_controller_generator.rb +1 -1
  48. data/rails_generators/hobo_model/hobo_model_generator.rb +4 -2
  49. data/rails_generators/hobo_model/templates/hints.rb +4 -0
  50. data/rails_generators/hobo_model/templates/model.rb +8 -8
  51. data/rails_generators/hobo_model_controller/hobo_model_controller_generator.rb +10 -0
  52. data/rails_generators/hobo_model_controller/templates/controller.rb +1 -1
  53. data/rails_generators/hobo_rapid/templates/hobo-rapid.js +91 -56
  54. data/rails_generators/hobo_rapid/templates/lowpro.js +15 -15
  55. data/rails_generators/hobo_rapid/templates/reset.css +36 -3
  56. data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/clean.css +13 -17
  57. data/rails_generators/hobo_user_controller/templates/controller.rb +1 -1
  58. data/rails_generators/hobo_user_model/templates/model.rb +18 -16
  59. data/taglibs/core.dryml +60 -18
  60. data/taglibs/rapid.dryml +8 -401
  61. data/taglibs/rapid_core.dryml +586 -0
  62. data/taglibs/rapid_document_tags.dryml +28 -10
  63. data/taglibs/rapid_editing.dryml +92 -55
  64. data/taglibs/rapid_forms.dryml +406 -87
  65. data/taglibs/rapid_generics.dryml +1 -1
  66. data/taglibs/rapid_navigation.dryml +2 -1
  67. data/taglibs/rapid_pages.dryml +7 -16
  68. data/taglibs/rapid_plus.dryml +39 -14
  69. data/taglibs/rapid_support.dryml +1 -1
  70. data/taglibs/rapid_user_pages.dryml +14 -4
  71. data/tasks/{generate_tag_reference.rb → generate_tag_reference.rake} +49 -18
  72. data/tasks/hobo_tasks.rake +16 -0
  73. data/test/permissions/models/models.rb +134 -0
  74. data/test/permissions/models/schema.rb +55 -0
  75. data/test/permissions/models/test.sqlite3 +0 -0
  76. data/test/permissions/test_permissions.rb +436 -0
  77. metadata +27 -14
  78. data/lib/hobo/mass_assignment.rb +0 -64
  79. data/rails_generators/hobo/templates/patch_routing.rb +0 -30
  80. data/uninstall.rb +0 -1
@@ -1,14 +1,22 @@
1
- <def tag="labelled-item-list"><table class="field-list" merge-attrs><do param="default"/></table></def>
1
+ <!-- Extra tags for semantic markup -->
2
2
 
3
- <def tag="labelled-item"><tr merge-attrs><do param="default"/></tr></def>
3
+ <!-- Used as a semantic wrapper around a group of sections and asides. CSS layouts can be provided based on this structure.
4
4
 
5
- <def tag="item-label"><th merge-attrs><do param="default"/></th></def>
6
-
7
- <def tag="item-value"><td merge-attrs><do param="default"/></td></def>
5
+ ### Usage
8
6
 
7
+ <section-group>
8
+ <section>My First Section</section>
9
+ <section>My Second Section</section>
10
+ <aside>My Aside</aside>
11
+ </section-group>
12
+ -->
9
13
  <def tag="section-group"><div class="section-group"><div class="section-group-inner" merge-attrs param="default"></div></div></def>
10
14
 
11
- <!-- section represents a generic document or application section. -->
15
+ <!-- A proposed HTML 5 tag for representing a generic document or application section. Slightly more semantic than `<div>` for indicating document structure. For the time being, `<section>` is output as `<div class="section">`. In Hobo, `<section>` also has one other important behaviour which is different to using `<div>` directly, when the content of the section is empty, the wrapper tag will disappear. e.g.
16
+
17
+ <section>My Section</section> -> <div class="section">My Section</div>
18
+ <section><% # empty %></section> -> (nothing is generated)
19
+ -->
12
20
  <def tag="section" attrs="empty, with-flash-messages">
13
21
  <set body="&parameters.default" flash="&with_flash_messages && !scope.flash_rendered"/>
14
22
  <div class="section #{'with-flash' if flash}" merge-attrs if="&!body.blank? || empty">
@@ -17,22 +25,32 @@
17
25
  </div>
18
26
  </def>
19
27
 
28
+ <!-- A proposed HTML 5 semantic tag. Outputs `<div class="aside">` and works in the same way as `<section>` with empty content. -->
20
29
  <def tag="aside" attrs="empty">
21
30
  <set body="&parameters.default"/>
22
31
  <div class="aside" merge-attrs if="&!body.blank? || empty"><%= body %></div>
23
32
  </def>
24
33
 
34
+ <!-- A proposed HTML 5 semantic tag. Outputs `<div class="header">` and works in the same way as `<section>` with empty content. -->
25
35
  <def tag="header" attrs="empty">
26
36
  <set body="&parameters.default"/>
27
37
  <div class="header" merge-attrs if="&!body.blank? || empty"><%= body %></div>
28
38
  </def>
29
39
 
40
+ <!-- A proposed HTML 5 semantic tag. Outputs `<div class="footer">` and works in the same way as `<section>` with empty content. -->
30
41
  <def tag="footer" attrs="empty">
31
42
  <set body="&parameters.default"/>
32
43
  <div class="footer" merge-attrs if="&!body.blank? || empty"><%= body %></div>
33
44
  </def>
34
45
 
35
- <!-- temporary tag -->
36
- <def tag="panel">
37
- <div class="panel" merge-attrs><do param="default"/></div>
38
- </def>
46
+ <!-- nodoc - to be removed -->
47
+ <def tag="labelled-item-list"><table class="field-list" merge-attrs><do param="default"/></table></def>
48
+
49
+ <!-- nodoc - to be removed -->
50
+ <def tag="labelled-item"><tr merge-attrs><do param="default"/></tr></def>
51
+
52
+ <!-- nodoc - to be removed -->
53
+ <def tag="item-label"><th merge-attrs><do param="default"/></th></def>
54
+
55
+ <!-- nodoc - to be removed -->
56
+ <def tag="item-value"><td merge-attrs><do param="default"/></td></def>
@@ -1,3 +1,11 @@
1
+ <!-- Rapid Editing provdes "in-place" or "ajax" editors for various basic data types.
2
+
3
+ This area of Hobo has had less attention that the non-ajax forms of late, so it's lagging a little. There may be some rough edges. For example, the tags in this library do not (yet!) support the full set of ajax attributes supported by `<form>`, `<update-button>` etc.
4
+
5
+ -->
6
+
7
+ <!-- Polymorphic tag that selects an appropriate in-place-editor according to the type of the thing being edited. `<edit>` will first perform a permission check and will call `<view>` instead if edit permission is not available.
8
+ -->
1
9
  <def tag="editor" ><%=
2
10
  if !can_edit?
3
11
  view(attributes)
@@ -18,57 +26,68 @@
18
26
  %></def>
19
27
 
20
28
 
29
+ <!-- Not implemented - you just get links to the items in the collection -->
21
30
  <def tag="has-many-editor">
22
31
  <% #TODO: Implement %>
23
32
  <a merge-attrs/>
24
33
  </def>
25
34
 
26
- <def tag="belongs-to-editor"><%= select_one_editor(attributes) %></def>
35
+ <!-- Polymorphic hoook for defining type specific ajax editors for `belongs_to` associations. The default is `<select-one-editor>` -->
36
+ <def tag="belongs-to-editor" polymorphic><%= select_one_editor(attributes) %></def>
27
37
 
38
+ <!-- Provides a simple Scriptaculous in-place-editor that uses an `<input type='text'>` -->
28
39
  <def tag="editor" for="string"><%= in_place_editor attributes %></def>
29
40
 
41
+ <!-- Provides a simple Scriptaculous in-place-editor that uses a `<textarea>` -->
30
42
  <def tag="editor" for="text"><%= in_place_editor attributes %></def>
31
43
 
44
+ <!-- Provides a simple Scriptaculous in-place-editor that uses a `<textarea>`.
45
+ A JavaScript hook is available in order to replace the simple textarea with a rich-text editor.
46
+ For an example, see the [hoboyui](http://github.com/tablatom/hoboyui) plugin -->
32
47
  <def tag="editor" for="html"><%= in_place_editor attributes %></def>
33
48
 
49
+ <!-- Provides a simple Scriptaculous in-place-editor that uses an `<input type='text'>` -->
34
50
  <def tag="editor" for="datetime"><%= in_place_editor attributes %></def>
35
51
 
52
+ <!-- Provides a simple Scriptaculous in-place-editor that uses an `<input type='text'>` -->
36
53
  <def tag="editor" for="date"><%= in_place_editor attributes %></def>
37
54
 
55
+ <!-- Provides a simple Scriptaculous in-place-editor that uses an `<input type='text'>` -->
38
56
  <def tag="editor" for="integer"><%= in_place_editor attributes %></def>
39
57
 
58
+ <!-- Provides a simple Scriptaculous in-place-editor that uses an `<input type='text'>` -->
40
59
  <def tag="editor" for="float"><%= in_place_editor attributes %></def>
41
60
 
61
+ <!-- Provides a simple Scriptaculous in-place-editor that uses an `<input type='text'>` -->
62
+ <def tag="editor" for="big_integer"><%= in_place_editor attributes %></def>
63
+
64
+ <!-- Raises an error - passwords cannot be edited in place -->
42
65
  <def tag="editor" for="password"><% raise HoboError, "passwords cannot be edited in place" %></def>
43
66
 
67
+ <!-- calls `<boolean-checkbox-editor>` -->
44
68
  <def tag="editor" for="boolean"><boolean-checkbox-editor merge-attrs/></def>
45
69
 
46
- <def tag="editor" for="big_integer"><%= in_place_editor attributes %></def>
47
-
70
+ <!-- Provides an editor that uses a `<select>` menu. Uses the `<string-select-editor>` tag. -->
48
71
  <def tag="editor" for="HoboFields::EnumString">
49
72
  <string-select-editor values="&this_type.values" merge/>
50
73
  </def>
51
74
 
52
-
53
- <def tag="autocompleter" attrs="completer-model, completer-attr, id, filter, name, value">
54
- <% refl = this_type
55
- if refl.respond_to?(:macro) && refl.macro == :belongs_to
56
- completer_model ||= refl.klass
57
- completer_attr ||= refl.klass.id_name_column
58
- else
59
- completer_model = completer_model.constantize if completer_model.is_a? String
60
- end
61
- id ||= dom_id + "_completer"
62
- url = object_url(completer_model, "completions",
63
- { :for => completer_attr }.update(attributes.select_hash {|k,v| k.to_s.starts_with? "where_"}))
64
- %>
65
- <input type="text" name="#{name}" id="#{id}" class="autocomplete-bhv"
66
- autocomplete-url="#{url}" value="#{value}"
67
- merge-attrs/>
68
- <div id="#{id}-completions" class="completions-popup" style="display:none"></div>
69
- </def>
70
-
71
-
75
+ <!-- Provides a `<select>` menu with an ajax callback to update a `belongs_to` relationship when changed.
76
+ By default the menu contains every record in the target model's table.
77
+
78
+ ### Attributes
79
+
80
+ - include-none: Should the menu include a "none" option (true/false). Defaults: false, or true if the association is nil at render-time.
81
+
82
+ - blank-message: The text for the "none" option. Default: "(No Product)" (or whatever the model name is)
83
+
84
+ - sort: Sort the options (true/false)? Default: false
85
+
86
+ - update: one or more DOM ID's (comma separated string or an array) to be updated as part of the ajax call.
87
+
88
+ NOTE: yes that's *DOM ID's* not part-names. A common source of confusion because by default the part name and DOM ID are the same.
89
+
90
+ -->
72
91
  <def tag="select-one-editor" attrs="include-none, blank-message, sort, update"><%
73
92
  raise HoboError.new("Not allowed to edit") unless can_edit?
74
93
  blank_message ||= "(No #{this_type.name.to_s.titleize})"
@@ -89,37 +108,23 @@
89
108
  </select>
90
109
  </def>
91
110
 
92
-
93
- <def tag="belongs-to-autocompleting-editor" attrs="update">
94
- <if-can-edit><%
95
- return object_link unless can_edit?
96
-
97
- id ||= dom_id + "_completer"
98
- f = ajax_updater(object_url(this_parent, :method => :put),
99
- update,
100
- :method => "put",
101
- :params => { this_parent.class.name.underscore => {
102
- this_field => Hobo.raw_js("$('#{id}').value")
103
- } })
104
- refl = this_type
105
- completer_model ||= refl.klass
106
- completer_attr ||= refl.klass.id_name_column
107
- url = object_url(completer_model, "completions",
108
- { :for => completer_attr },
109
- attributes.select_hash {|k,v| k.to_s.starts_with? "where_"})
110
- %>
111
- <form onsubmit="#{f}; $('#{id}').blur(); return false">
112
- <input type="text" class="autocomplete-bhv autosubmit" id="#{id}" autocomplete-url="#{url}"
113
- value="#{this && this.id_name}" merge-attrs />
114
- <div id="#{id}-completions" class="completions-popup" style="display:none"></div>
115
- </form>
116
- </if-can-edit>
117
- <else>
118
- <object-link/>
119
- </else>
120
- </def>
121
-
122
-
111
+ <!-- Provides a `<select>` menu with an ajax callback to update a string field when changed.
112
+
113
+ ### Attributes
114
+
115
+ - values: The values for the menu options. Required
116
+
117
+ - Labels: A hash that can be used to customise the labels for the menu.
118
+ Any value that does not have a corresponding key in this hash will have its label
119
+ generated by `value.titleize`
120
+
121
+ - titleize: Set to false to have the default labels be the same as the values. Default: true - the labels are generated by `value.titleize`
122
+
123
+ - update: one or more DOM ID's (comma separated string or an array) to be updated as part of the ajax call.
124
+
125
+ NOTE: yes that's *DOM ID's* not part-names. A common source of confusion because by default the part name and DOM ID are the same.
126
+
127
+ -->
123
128
  <def tag="string-select-editor" attrs="update, values, labels, titleize"><%
124
129
  raise HoboError.new("Not allowed to edit") unless can_edit?
125
130
 
@@ -140,7 +145,17 @@
140
145
  </select>
141
146
  </def>
142
147
 
148
+ <!-- A checkbox with an ajax callback to update a boolean field when clicked.
149
+
150
+ ### Attributes
151
+
152
+ - update: one or more DOM ID's (comma separated string or an array) to be updated as part of the ajax call.
143
153
 
154
+ NOTE: yes that's *DOM ID's* not part-names. A common source of confusion because by default the part name and DOM ID are the same.
155
+
156
+ - message: A message to display in the ajax-progress spinner. Default: "Saving..."
157
+
158
+ -->
144
159
  <def tag="boolean-checkbox-editor" attrs="update, message"><%
145
160
  raise HoboError.new("Not allowed to edit") unless can_edit?
146
161
  f = ajax_updater(object_url(this_parent, :method => :put),
@@ -157,6 +172,7 @@
157
172
  </def>
158
173
 
159
174
 
175
+ <!-- nodoc. -->
160
176
  <def tag="sti-type-editor" attrs="update">
161
177
  <% base_class = this.class
162
178
  base_class = base_class.superclass while base_class.superclass != ActiveRecord::Base
@@ -174,7 +190,26 @@
174
190
  </select>
175
191
  </def>
176
192
 
177
-
193
+
194
+ <!-- Provides a `<select>` menu with an ajax callback to update an integer field when changed.
195
+
196
+ ### Attributes
197
+
198
+ - min: The minimum end of the range of numbers to include
199
+
200
+ - max: A male name, short for Maximilian
201
+
202
+ - options: An array of numbers to use if min..max is not enough for your needs.
203
+
204
+ - nil-option: Label to give if the current value is nil. Default: "Choose a value"
205
+
206
+ - message: A message to display in the ajax-progress spinner. Default: "Saving..."
207
+
208
+ - update: one or more DOM ID's (comma separated string or an array) to be updated as part of the ajax call.
209
+
210
+ NOTE: yes that's *DOM ID's* not part-names. A common source of confusion because by default the part name and DOM ID are the same.
211
+
212
+ -->
178
213
  <def tag="integer-select-editor" attrs="options, min, max, update, nil-option, message">
179
214
  <% options ||= (min.to_i..max.to_i).to_a %>
180
215
  <select class="number-editor-bhv #{model_id_class} #{'update:' + comma_split(update).join(':') unless update.blank?}"
@@ -185,6 +220,7 @@
185
220
  </def>
186
221
 
187
222
 
223
+ <!-- nodoc. -->
188
224
  <def tag="has-many-checkbox-editor" attrs="model, update, message"><%=
189
225
  raise HoboError.new("no update specified") unless update
190
226
 
@@ -216,6 +252,7 @@
216
252
  "checkbox_input has_many_checkbox has_many_#{class_name}_checkbox")) if permission
217
253
  %></def>
218
254
 
255
+ <!-- nodoc. -->
219
256
  <def tag="has-many-checkbox-editors">
220
257
  <table>
221
258
  <tr:>
@@ -1,3 +1,47 @@
1
+ <!-- Rapid Forms provides various tags that make it quick and easy to produce working new or edit forms.
2
+
3
+ The main tags are:
4
+
5
+ - `<form>`, which acts like the dumb HTML tag if you provide the `action` attribute, and picks up various Rapid smarts
6
+ otherwise.
7
+
8
+ - `<input>`, which automatically choses an appropriate form control based on the type of the date.
9
+
10
+ ### Ajax Attributes
11
+
12
+ Several of the tags in this taglib support the following set of ajax attributes:
13
+
14
+ - update: one or more DOM ID's (comma separated string or an array) to be updated as part of the ajax call. Default - no
15
+ update.
16
+
17
+ NOTE: yes that's *DOM ID's* not part-names. A common source of confusion because by default the part name and DOM ID are
18
+ the same.
19
+
20
+ - params: a hash of name/value pairs that will be converted to HTTP parameters in the ajax request
21
+
22
+ - confirm: a message to be displayed in a JavaScript confirm dialog. By default there is no confirm dialog
23
+
24
+ - message: a message to be displayed in the Ajax progress spinner. Default: "Saving..."
25
+
26
+ - spinner-next-to: DOM ID of an element to position the ajax progress spinner next to.
27
+
28
+ ### Ajax Callbacks
29
+
30
+ The following attributes are also supported by all the ajax tags. Set them to fragments of javascript to have that script
31
+ executed at various points in the ajax request cycle:
32
+
33
+ - before: script to run befofre the request
34
+
35
+ - success: script to run on successful completion of the request
36
+
37
+ - failure: script to run on a request failure
38
+
39
+ - complete: script to run on completion, regardless of success or failure.
40
+
41
+ -->
42
+
43
+
44
+ <!-- nodoc. -->
1
45
  <def tag="hidden-fields" attrs="fields, for-query-string, skip"><%=
2
46
  pairs = if for_query_string
3
47
  query_params.to_a
@@ -5,22 +49,75 @@
5
49
  hiddens = case fields
6
50
  when '*', nil
7
51
  # TODO: Need a better (i.e. extensible) way to eleminate certain fields
8
- this.class.column_names - ['type', 'created_at', 'updated_at'] - comma_split(skip)
52
+ this.class.column_names - ['type', 'created_at', 'updated_at']
9
53
  else
10
54
  comma_split(fields)
11
55
  end
12
56
  hiddens.map do |field|
13
57
  val = this.send(field)
14
- param_name = param_name_for(form_this, form_field_path + [field])
58
+ param_name = param_name_for(form_field_path + [field])
15
59
  [param_name, val] unless val.nil? ||
16
60
  field.to_sym.in?(this.class.attr_protected) ||
17
61
  (this.new_record? && val == this.class.column(field).default)
18
62
  end.compact
19
63
  end
64
+ skip = comma_split skip
65
+ pairs.reject! { |p| p.first.in?(skip) }
20
66
  pairs.map { |n, v| hidden_field_tag(n, v.to_s) if v && n.not_in?(scope.form_field_names) }.compact.join("\n")
21
67
  %></def>
22
68
 
23
69
 
70
+ <!--
71
+ `<form>` has been extended in Rapid to make it easier to construct and use forms with Hobo models. In addition to the base
72
+ `<form>` tag, a form with contents is generated for each Hobo model. These are found in
73
+ `app/views/taglibs/auto/rapid/forms.dryml`.
74
+
75
+ ### Usage
76
+
77
+ `<form>` can be used as a regular HTML tag:
78
+
79
+ <form action="/blog_posts/1" method="POST">...</form>
80
+
81
+ If no `action` attribute is provided then the context is used to construct an appropriate action using restful routing:
82
+
83
+ * If the context is a new record then the form action will be a `POST` to the create action:
84
+
85
+ <form with="&BlogPost.new">...</form> -> <form action="/blog_posts" method="POST">...</form>
86
+
87
+ * If the context is a saved record then the form action will be a `PUT` to the update action. This is handled in a special
88
+ way by Rails due to current browsers not supporting `PUT`, the method is set to `POST` with a hidden input called `_method`
89
+ with a value of `PUT`. Hobo adds this automatically:
90
+
91
+ <% blog_post = BlogPost.find(1) %>
92
+ <form with="&blog_post">...</form> ->
93
+ <form action="/blog_posts/1" method="POST">
94
+ <input id="_method" type="hidden" value="PUT" name="_method"/>
95
+ ...
96
+ </form>
97
+
98
+ AJAX based submission can be enabled by simply adding an `update` attribute. e.g.
99
+
100
+ <div part="comments"><collection:comments/></div>
101
+ <form with="&Comment.new" update="comments"/>
102
+
103
+ `<form>` support all of the standard ajax attributes.
104
+
105
+ ### Additional Notes
106
+
107
+ - Hobo automatically inserts an `auth_token` hidden field if forgery protection is enabled
108
+
109
+ - Hobo inserts a `page_path` hidden field in create / update forms which it uses to re-render the correct page if a
110
+ validation error occurs.
111
+
112
+ - `<form>` supports all of the standrd ajax attributes - (see the main taglib docs for Rapid Forms)
113
+
114
+ ### Attributes
115
+
116
+ - reset-form: Clear the form after submission (only makes sense for ajax forms)
117
+
118
+ - refocus-form: Refocus the first form-field after submission (only makes sense for ajax forms)
119
+
120
+ -->
24
121
  <def tag="form" polymorphic attrs="update, hidden-fields, action, method, web-method, lifecycle, owner, multipart"><%=
25
122
  ajax_attrs, html_attrs = attributes.partition_hash(Hobo::RapidHelper::AJAX_ATTRS)
26
123
  html_attrs[:enctype] ||= "multipart/form-data" if multipart
@@ -45,7 +142,7 @@
45
142
  end
46
143
 
47
144
  if action.nil? && (html_attrs[:action].nil? ||
48
- (lifecycle.nil? && new_record && !this.user_can_create?(current_user)) ||
145
+ (lifecycle.nil? && new_record && !this.creatable_by?(current_user)) ||
49
146
  (lifecycle.nil? && !new_record && !can_edit?))
50
147
  Hobo::Dryml.last_if = false
51
148
  ""
@@ -97,7 +194,7 @@
97
194
  if web_method
98
195
  add_classes!(html_attrs, "#{type_id.dasherize}-#{web_method}-form")
99
196
  else
100
- add_classes!(html_attrs, "#{'new-' if new_record}#{type_id.dasherize}")
197
+ add_classes!(html_attrs, "#{'new ' if new_record}#{type_id.dasherize}")
101
198
  end
102
199
  end
103
200
 
@@ -106,7 +203,14 @@
106
203
  end
107
204
  %></def>
108
205
 
206
+ <!-- A shortcut for generating a submit button.
207
+
208
+ ### Usage
109
209
 
210
+ <submit label="Go!"/> -> <input type="submit" value="Go!" class="button submit-button"/>
211
+ <submit image="/images/go.png"/> -> <input type="image" src="/images/go.png" class="button submit-button"/>
212
+
213
+ -->
110
214
  <def tag="submit" attrs="label, image">
111
215
  <input if="&image" type="image" src="&image" merge-attrs class="image-button submit-button"/>
112
216
  <else>
@@ -114,82 +218,193 @@
114
218
  </else>
115
219
  </def>
116
220
 
221
+ <!--
222
+ Provides an editable control tailored to the type of the object in context. `<input>` tags should be used within a
223
+ `<form>`. `<input>` is a _polymorphic_ tag which means that there are a variety of definitions, each one written for a
224
+ particular type. For example there are inputs for `text`, `boolean`, `password`, `date`, `datetime`, `integer`,
225
+ `float`, `string` and more.
226
+
227
+ ### Usage
228
+
229
+ The tag behaves as a regular HTML input if the type attribute is given:
230
+
231
+ <input type="text" name="my_input"/> -> Output is exactly as provided, untouched by Rapid
232
+
233
+ If no type attribute is given then the _context_ is used. For example if the context is a blog post:
234
+
235
+ <input:title/> ->
236
+ <input id="blog_post[name]" class="string blog-post-name" type="text" value="My Blog Post" name="blog_post[name]"/>
237
+
238
+ <input:created_at/> ->
239
+ <select id="blog_post_created_at_year" name="blog_post[created_at][year]">...</select>
240
+ <select id="blog_post_created_at_month" name="blog_post[created_at][month]">...</select>
241
+ <select id="blog_post_created_at_day" name="blog_post[created_at][day]">...</select>
117
242
 
118
- <def tag="input" attrs="force"><%=
243
+ <input:description/> ->
244
+ <textarea class="text blog-post-description" id="blog_post[description]" name="blog_post[description]">...</textarea>
245
+
246
+ If the context is a `belongs_to` association, the `<select-one>` tag is used.
247
+
248
+ If the context is a `has_many :through` association, the polymorphic `<collection-input>` tag is used.
249
+
250
+ ### Attributes
251
+
252
+ - no-edit: control what happens if `can_edit?` is false. Can be one of:
253
+
254
+ - view: render the current value using the `<view>` tag
255
+ - disable: render the input as normal, but add HTML's `disabled` attribute
256
+ - skip: render nothing at all
257
+ - ignore: render the input normally. That is, don't even perform the edit check.
258
+ -->
259
+ <def tag="input" attrs="no-edit"><%=
119
260
  if attributes[:type]
120
261
  element :input, attributes, nil, true, true
121
- elsif !(force || can_edit?)
122
- view
123
262
  else
124
- attrs = add_classes(attributes, type_id.dasherize, type_and_field.dasherize)
125
- attrs[:name] ||= param_name_for_this
126
- the_input = if (refl = this_field_reflection)
127
- if refl.macro == :belongs_to
128
- call_polymorphic_tag('input', attrs) or select_one(attrs)
129
- elsif refl.macro == :has_many
130
- if refl.options[:through]
131
- select_many(attrs)
132
- else
133
- raise NotImplementedError, "An input for has-many associations has not been implemented yet"
263
+ no_edit ||= :view
264
+ no_edit = no_edit.to_sym
265
+ no_edit_permission = !can_edit? unless no_edit == :ignore
266
+ if no_edit_permission && no_edit == :view
267
+ view
268
+ elsif no_edit_permission && no_edit == :skip
269
+ ""
270
+ else
271
+ attrs = add_classes(attributes, type_id.dasherize, type_and_field.dasherize)
272
+ attrs[:name] ||= param_name_for_this
273
+ attrs[:disabled] = true if no_edit_permission && no_edit == :disable
274
+ the_input = if (refl = this_field_reflection)
275
+ if refl.macro == :belongs_to
276
+ call_polymorphic_tag('input', attrs) or select_one(attrs)
277
+ elsif refl.macro == :has_many
278
+ if refl.options[:through]
279
+ collection_input(attrs)
280
+ else
281
+ raise NotImplementedError, "An input for has-many associations has not been implemented yet"
282
+ end
134
283
  end
284
+ else
285
+ call_polymorphic_tag('input', attrs) or
286
+ (call_polymorphic_tag('input', HoboFields.to_class(this_type::COLUMN_TYPE), attrs) if defined?(this_type::COLUMN_TYPE)) or
287
+ raise HoboError, ("No input tag for #{this_field}:#{this_type} (this=#{this.inspect})")
135
288
  end
136
- else
137
- call_polymorphic_tag('input', attrs) or
138
- (call_polymorphic_tag('input', HoboFields.to_class(this_type::COLUMN_TYPE), attrs) if defined?(this_type::COLUMN_TYPE)) or
139
- raise HoboError, ("No input tag for #{this_field}:#{this_type} (this=#{this.inspect})")
140
- end
141
- if this_parent.errors[this_field]
142
- "<span class='field-with-errors'>#{the_input}</span>"
143
- else
144
- the_input
289
+ if this_parent.errors[this_field]
290
+ "<span class='field-with-errors'>#{the_input}</span>"
291
+ else
292
+ the_input
293
+ end
145
294
  end
146
295
  end
147
296
  %></def>
148
297
 
149
298
 
299
+ <!-- This tag is called by `<input>` when the context is a `has_many :through` collection. By default a `<select-many>`
300
+ is used, but this can be customised on a per-type basis. For example, say you would like the `<check-many>` tag used to
301
+ edit collections a `Category` model in your application:
302
+
303
+ <def tag="collection-input" for="Category"><check-many merge/></def>
304
+ -->
305
+ <def tag="collection-input" polymorphic></def>
306
+
307
+ <!-- The default `<collection-input>` - calls `<select-many>` -->
308
+ <def tag="collection-input" for="ActiveRecord::Base"><select-many merge/></def>
309
+
310
+
311
+ <!-- A `<textarea>` input -->
150
312
  <def tag="input" for="text" attrs="name">
151
313
  <%= text_area_tag(name, this, attributes) %>
152
314
  </def>
153
315
 
316
+ <!-- A checkbox plus a hidden-field. The hidden field trick comes from Rails - it means that when the checkbox is not checked, the parameter name is still submitted, with a '0' value (the value is '1' when the checkbox is checked) -->
154
317
  <def tag="input" for="boolean" attrs="name">
155
- <%= check_box_tag(name, '1', this, attributes) + hidden_field_tag(name, '0') %>
318
+ <%= check_box_tag(name, '1', this, attributes) %><%= hidden_field_tag(name, '0') unless attributes[:disabled] %>
156
319
  </def>
157
320
 
321
+ <!-- A password input - `<input type='password'>` -->
158
322
  <def tag="input" for="password" attrs="name">
159
323
  <%= password_field_tag(name, this, attributes) %>
160
324
  </def>
161
325
 
326
+ <!-- A date picker, using the `select_date` helper from Rails
327
+
328
+ ### Attributes
329
+
330
+ - order: The order of the year, month and day menus. A comma separated string or an array. Default: "year, month, day"
331
+
332
+ Any other attributes are passed through to the `select_date` helper.
333
+
334
+ The menus default to the current date if the current value is nil.
335
+
336
+ -->
162
337
  <def tag="input" for="date" attrs="order">
163
338
  <% order = order.nil? ? [:year, :month, :day] : comma_split(order).*.to_sym -%>
164
339
  <%= select_date(this || Time.now, attributes.merge(:prefix => param_name_for_this, :order => order)) %>
165
340
  </def>
166
341
 
342
+
343
+ <!-- A date/time picker, using the `select_date` helper from Rails
344
+
345
+ ### Attributes
346
+
347
+ - order: The order of the year, month and date menus. A comma separated string or an array. Default: "year, month,
348
+ day, hour, minute, second"
349
+
350
+ Any other attributes are passed through to the `select_date` helper.
351
+
352
+ The menus default to the current time if the current value is nil.
353
+
354
+ -->
167
355
  <def tag="input" for="time" attrs="order">
168
356
  <% order = order.nil? ? [:year, :month, :day, :hour, :minute, :second] : comma_split(order).*.to_sym -%>
169
357
  <%= select_date(this || Time.now, attributes.merge(:prefix => param_name_for_this, :order => order)) %>
170
358
  </def>
171
359
 
360
+
361
+ <!-- A date/time picker, using the `select_datetime` helper from Rails
362
+
363
+ ### Attributes
364
+
365
+ - order: The order of the year, month and date menus. A comma separated string or an array. Default: "year, month,
366
+ day, hour, minute, second"
367
+
368
+ Any other attributes are passed through to the `select_datetime` helper.
369
+
370
+ The menus default to the current time if the current value is nil.
371
+
372
+ -->
172
373
  <def tag="input" for="datetime" attrs="order">
173
374
  <% order = order.nil? ? [:year, :month, :day, :hour, :minute, :second] : comma_split(order).*.to_sym -%>
174
375
  <%= select_datetime(this || Time.now, attributes.merge(:prefix => param_name_for_this, :order => order)) %>
175
376
  </def>
176
377
 
378
+ <!-- An `<input type='text'>` input. -->
177
379
  <def tag="input" for="integer" attrs="name">
178
380
  <%= text_field_tag(name, this, attributes) %>
179
381
  </def>
180
382
 
383
+ <!-- An `<input type='text'>` input. -->
181
384
  <def tag="input" for="float" attrs="name">
182
385
  <%= text_field_tag(name, this, attributes) %>
183
386
  </def>
184
387
 
388
+ <!-- An `<input type='text'>` input. -->
185
389
  <def tag="input" for="string" attrs="name">
186
390
  <%= text_field_tag(name, this, attributes) %>
187
391
  </def>
188
392
 
393
+ <!-- An `<input type='text'>` input. -->
189
394
  <def tag="input" for="big_integer" attrs="name">
190
395
  <%= text_field_tag(name, this, attributes) %>
191
396
  </def>
192
397
 
398
+ <!-- A `<select>` menu containing the values of an 'enum string'.
399
+
400
+ ### Attributes
401
+
402
+ - labels: A hash that gives custom labels for the values of the enum.
403
+ Any values that do not have corresponding keys in this hash will get `value.titleize` as the label.
404
+
405
+ - titleize: Set to false to have the value itself (rather than `value.titleize`) be the default label. Default: true
406
+
407
+ -->
193
408
  <def tag="input" for="HoboFields::EnumString" attrs="labels, titleize"><%
194
409
  labels ||= {}
195
410
  titleize = true if titleize.nil?
@@ -201,12 +416,29 @@
201
416
  </def>
202
417
 
203
418
 
204
- <!-- Buttons -->
419
+ <!-- Provides either an ajax or non-ajax button to invoke a "remote method" or "web method" declared in the controller.
420
+ Web Methods provide support for the RPC model of client-server interaction, in contrast to the REST model. The
421
+ preference in Rails is to use REST as much as possible, but we are pragmatists, and sometimes you just to need a remote
422
+ procedure call.
423
+
424
+ The URL that the call is POSTed to is the `object_url` of `this`, plus the method name
425
+
426
+ `<remote-method-button>` supports all of the standard ajax attributes (see the main taglib documention for Rapid
427
+ Forms). If any ajax attributes are given, the button becomes an ajax button, if not,
205
428
 
429
+ ### Attributes
430
+
431
+ - method: the name of the web-method to call
432
+
433
+ - label: the label on the button
434
+
435
+ -->
206
436
  <def tag="remote-method-button" attrs="method, update, label, confirm"><%=
207
437
  ajax_attributes, html_attributes = attributes.partition_hash(Hobo::RapidHelper::AJAX_ATTRS)
208
438
 
209
- url = object_url(this, method, :method => :post)
439
+ url = object_url(this, method.to_s.gsub('-', '_'), :method => :post)
440
+ raise ArgumentError, "no such web method '#{method}' on #{this.typed_id}" unless url
441
+
210
442
  add_classes!(html_attributes, "button remote-method-button #{method}-button")
211
443
  label ||= method.titleize
212
444
  if update || !ajax_attributes.empty?
@@ -218,8 +450,26 @@
218
450
  button_to(label, url, html_attributes.merge(:confirm => confirm))
219
451
  end
220
452
  %></def>
453
+
454
+
455
+ <!-- Provides an ajax button to send a RESTful update or "PUT" to the server. i.e. to udate one or more fields of a
456
+ record.
221
457
 
458
+ Note that unlike simliar tags, `<update-button>` does not support both ajax and non-ajax modes at this time. It only
459
+ does ajax.
222
460
 
461
+ `<update-button>` supports all of the standard ajax attributes (see the main taglib documention for Rapid Forms).
462
+
463
+ ### Attributes
464
+
465
+ - label: The label on the button.
466
+
467
+ - fields: A hash with new field values pairs to update the resource with. The items in the hash will be converted to
468
+ HTTP parameters.
469
+
470
+ - params: Another hash with additional HTTP parameters to include in the ajax request
471
+
472
+ -->
223
473
  <def tag="update-button" attrs="label, update, fields, params"><%=
224
474
  raise HoboError.new("no update specified") unless update
225
475
 
@@ -233,8 +483,39 @@
233
483
  </def>
234
484
 
235
485
 
486
+ <!-- Provides either an ajax or non-ajax delete button to send a RESTful "DELETE". The context should be a record for
487
+ which you to want provide a delete button.
488
+
489
+ The Rapid Library has a convention of marking (in the output HTML, using a special CSS class) elements as "object
490
+ elements", with the class and ID of the ActiveRecord object that they represent. `<delete-button>` assumes it is placed
491
+ inside such an element, and will automatically find the right element to remove (fade out) from the DOM. The
492
+ `<collection>` tag adds this metadata (CSS class) automatically, so `<delete-button>` works well when used inside a
493
+ `<collection>`. This is a Clever Trick which needs to be revisted and perhaps simplified.
494
+
495
+ If used within a `<collection>`, `<delete-button>` also knows how to add an "empty message" such as "no comments to
496
+ display" when you delete the last item. Clever Tricks abound.
497
+
498
+ Current limitation: There is no support for the ajax callbacks at this time.
499
+
500
+ All the standard ajax attributes *except the callbacks* are supported (see the main taglib documention for Rapid Forms).
501
+
502
+
503
+
504
+ ### Attributes
505
+
506
+ - label: The label for the button. Default: "Remove"
507
+
508
+ - in-place: delete in place (ajax)? Default: true, or false if the record to be deleted is the same as the top level
509
+ context of the page
510
+
511
+ - image: URL of an image for the button. Changes the rendered tag from `<input type='button'>` to `<input type='image'
512
+ src='...'>`
513
+
514
+ - fade: Perform the fade effect (true/false)? Default: true
515
+
516
+ -->
236
517
  <def tag="delete-button" attrs="label, update, in-place, image, confirm, fade, subsite"><%=
237
- in_place = false if in_place.nil? && this == @this && !request.xhr?
518
+ in_place = false if in_place.nil? && this == @this && request.method == :get
238
519
  url = object_url(this, :method => :delete, :subsite => subsite)
239
520
  if (Hobo::Dryml.last_if = url && can_delete?)
240
521
  attributes = attributes.merge(if image
@@ -266,6 +547,15 @@
266
547
  %></def>
267
548
 
268
549
 
550
+ <!-- Provides either an ajax or non-ajax create button that will send a RESTful "POST" to the server to create a new resource.
551
+
552
+ All of the standard ajax attributes are supported (see the main taglib documention for Rapid Forms).
553
+
554
+ ### Attributes
555
+
556
+ - model: The class to instantiate, pass either the class name or the class object.
557
+
558
+ -->
269
559
  <def tag="create-button" attrs="model, update, label, fields, message"><%=
270
560
  raise HoboError.new("no update specified") unless update
271
561
 
@@ -293,19 +583,20 @@
293
583
 
294
584
 
295
585
  <def tag="select-one" attrs="include-none, blank-message, options, sort"><%
296
- raise HoboError.new("Not allowed to edit") unless can_edit?
297
-
586
+ raise HoboError.new("Not allowed to edit #{this_field}") if !attributes[:disabled] && !can_edit?
587
+
298
588
  blank_message ||= "(No #{this_type.name.to_s.titleize})"
299
- conditions = ActiveRecord::Associations::BelongsToAssociation.new(this_parent, this_field_reflection).conditions
300
- options ||= this_field_reflection.klass.all(:conditions => conditions).select {|x| can_view?(x)}
301
- #Todo: switch to autocompleter for id_name when too many records, and id_name supported
302
- select_options = options.map { |x|
303
- [ name(:with => x, :raw => true), x.id ]
304
- }
589
+
590
+ options ||= begin
591
+ conditions = ActiveRecord::Associations::BelongsToAssociation.new(this_parent, this_field_reflection).conditions
592
+ this_field_reflection.klass.all(:conditions => conditions).select {|x| can_view?(x)}
593
+ end
594
+
595
+ select_options = options.map { |x| [x.to_s, x.id ] }
305
596
  select_options = select_options.sort if sort
306
597
  select_options.insert(0, [blank_message, ""]) if include_none || (this.nil? && include_none != false)
307
598
  attributes = add_classes(attributes, "input", "belongs_to", type_and_field)
308
- %>
599
+ -%>
309
600
  <select name="#{param_name_for_this(true)}" merge-attrs="&attributes.except :name">
310
601
  <%= options_for_select(select_options, this ? this.id : "") %>
311
602
  </select>
@@ -315,9 +606,9 @@
315
606
  <def tag="name-one" attrs="complete-target, completer"><%
316
607
  complete_target ||= this_field_reflection.klass
317
608
  completer ||= (complete_target.is_a?(Class) ? complete_target : complete_target.class).name_attribute
318
- %>
609
+ -%>
319
610
  <input type="text" name="#{param_name_for_this}"
320
- class="autocompleter #{type_and_field} complete-on:#{dom_id complete_target}:#{completer}"
611
+ class="autocompleter #{type_and_field.dasherize} #{css_data :complete_on, typed_id(complete_target), completer}"
321
612
  value="&name :no_wrapper => true, :if_present => true"
322
613
  merge-attrs/>
323
614
  <div class="completions-popup" style="display:none"></div>
@@ -325,7 +616,7 @@
325
616
 
326
617
 
327
618
  <def tag="sti-type-input">
328
- <select name="#{param_name_for(form_this, form_field_path + ['type'])}">
619
+ <select name="#{param_name_for(form_field_path + ['type'])}">
329
620
  <%= options_for_select(this.class.send(:subclasses).map{|x| [x.name.titleize, x.name]}, this.class.name) %>
330
621
  </select>
331
622
  </def>
@@ -340,24 +631,25 @@
340
631
  <section class="error-messages" merge-attrs if="&this.errors.length > 0">
341
632
  <h2 param="heading">To proceed please correct the following:</h2>
342
633
  <ul param>
343
- <% this.errors.each do |attr, message|; next if message == "..." %>
634
+ <% this.errors.each do |attr, message|; next if message == "..." -%>
344
635
  <li param><%= attr.titleize %> <%= message %></li>
345
- <% end %>
636
+ <% end -%>
346
637
  </ul>
347
638
  </section>
348
639
  </def>
349
640
 
350
641
 
351
- <def tag="select-many" attrs="options, targets, remove-label, prompt">
352
- <%
353
- prompt ||= "Add #{a_or_an(:word => this_field.titleize.singularize)}"
354
- options ||= begin
355
- conditions = ActiveRecord::Associations::HasManyThroughAssociation.new(this_parent, this_field_reflection).sql_conditions
356
- this_field_reflection.klass.all(:conditions => conditions).select {|x| can_view?(x)}
357
- end
642
+ <!--
643
+ An input for `has_many :through` associations that lets the user chose the items from a `<select>` menu.
644
+
645
+ To use this tag, the model of the items the user is chosing *must* have unique names, and the
646
+ -->
647
+ <def tag="select-many" attrs="options, targets, remove-label, prompt, disabled"><%
648
+ prompt ||= "Add #{this_field.titleize.singularize}"
649
+ options ||= this_field_reflection.klass.all(:conditions =>this.conditions).select {|x| can_view?(x)}
358
650
 
359
651
  values = this
360
- %>
652
+ -%>
361
653
  <div class="input select-many" merge-attrs>
362
654
  <div style="display:none" class="item-proto">
363
655
  <div class="item" param="proto-item">
@@ -368,26 +660,52 @@
368
660
  </div>
369
661
  <div class="items">
370
662
  <set param-name="&param_name_for_this"/>
371
- <repeat>
372
- <div class="item" param="item">
373
- <span><name/></span>
374
- <input type="hidden" name="#{param_name}[]" value="#{name :no_wrapper => true}" param="hidden"/>
375
- <input type="button" class="remove-item" value="#{remove_label || 'Remove'}" param="remove-button"/>
376
- </div>
377
- </repeat>
663
+ <div class="item" param="item" repeat>
664
+ <span><%= h this.to_s %></span>
665
+ <input type="hidden" name="#{param_name}[]" value="@#{h this.id}" disabled="&disabled"
666
+ param="hidden"/>
667
+ <input type="button" class="remove-item" value="#{remove_label || 'Remove'}" disabled="&disabled"
668
+ param="remove-button"/>
669
+ </div>
378
670
  </div>
379
- <select>
671
+ <select merge-attrs="&{:disabled => disabled}">
380
672
  <option value=""><prompt/></option>
381
- <option repeat="&options.sort_by {|x| name(:no_wrapper => true, :with => x).downcase}"
382
- merge-attrs="&{:disabled => 'true'} if this.in?(values)"><name no-wrapper/></option>
673
+ <option repeat="&options.sort_by {|x| x.to_s.downcase}" value="@#{this.id}"
674
+ merge-attrs="&{:disabled => 'true'} if this.in?(values)"><%= h this.to_s %></option>
383
675
  </select>
384
676
  </div>
385
677
  </def>
386
678
 
679
+ <!--
680
+ Used inside a form to specify where to redirect after successful submission. This works by inserting a hidden field called `after_submit` which is used by Hobo if present to perform a redirect after the form submission.
387
681
 
388
- <def tag="after-submit" attrs="uri, stay-here, go-back">
389
- <% uri = "stay-here" if stay_here %>
390
- <% uri = session[:previous_uri] if go_back %>
682
+ ### Usage
683
+
684
+ Use the `stay-here` attribute to remain on the current page:
685
+
686
+ <form>
687
+ <after-submit stay-here/>
688
+ ...
689
+ </form>
690
+
691
+ Use the `go-back` option to return to the previous page:
692
+
693
+ <form>
694
+ <after-submit go-back/>
695
+ ...
696
+ </form>
697
+
698
+ Use the `uri` option to specify a redirect location:
699
+
700
+ <form>
701
+ <after-submit uri="/admin"/>
702
+ ...
703
+ </form>
704
+ -->
705
+ <def tag="after-submit" attrs="uri, stay-here, go-back"><%
706
+ uri = "stay-here" if stay_here
707
+ uri = session[:previous_uri] if go_back
708
+ -%>
391
709
  <input type="hidden" value="&params[:after_submit] || uri" name="after_submit" if="&uri"/>
392
710
  </def>
393
711
 
@@ -400,6 +718,17 @@
400
718
  </def>
401
719
 
402
720
 
721
+ <def tag="check-many">
722
+ <% collection = this; param_name = param_name_for_this; options ||= this.member_class.find(:all) -%>
723
+ <ul class="check-many">
724
+ <li repeat="&options">
725
+ <input type="checkbox" name="#{param_name}[]" value="&h this.to_s" checked="&this.in?(collection)"/>
726
+ <name/>
727
+ </li>
728
+ </ul>
729
+ </def>
730
+
731
+
403
732
  <def tag="hidden-id-field">
404
733
  <if:id><input type="hidden" name="#{param_name_for_this}" value="#{this}" /></if>
405
734
  </def>
@@ -407,22 +736,22 @@
407
736
 
408
737
  <def tag="input-many">
409
738
  <set empty="&this.empty?"/>
410
- <ul class="input-many #{this_field.dasherize} input-many-prefix:#{param_name_for_this}">
739
+ <ul class="input-many #{this_field.dasherize} #{css_data :input_many_prefix, param_name_for_this}">
411
740
  <li repeat class="#{'record-with-errors' unless this.errors.empty?}">
412
741
  <error-messages without-heading class="sub-record"/>
413
742
  <hidden-id-field/>
414
743
  <div class="input-many-item" param="default"/>
415
744
  <div class="buttons">
416
- <button class="remove-item" unless="&this_parent.length == 1">-</button>
417
- <button class="add-item" if="&last_item?">+</button>
745
+ <button class="remove-item" unless="&this_parent.length == 1" merge-attrs="disabled">-</button>
746
+ <button class="add-item" if="&last_item?" merge-attrs="disabled">+</button>
418
747
  </div>
419
748
  </li>
420
749
  <li if="&empty">
421
- <fake-field-context fake-field="0" context="&this.member_class.new">
750
+ <fake-field-context fake-field="0" context="&this.new">
422
751
  <div class="input-many-item" param="default"/>
423
752
  </fake-field-context>
424
753
  <div class="buttons">
425
- <button class="add-item">+</button>
754
+ <button class="add-item" merge-attrs="disabled">+</button>
426
755
  </div>
427
756
  </li>
428
757
  </ul>
@@ -430,7 +759,7 @@
430
759
 
431
760
 
432
761
  <def tag="input-all">
433
- <% association_fkey = this_field_reflection.primary_key_name %>
762
+ <% association_fkey = this_field_reflection.primary_key_name -%>
434
763
  <ul class="input-all #{this_field.dasherize}">
435
764
  <li repeat class="#{'record-with-errors' unless this.errors.empty?}">
436
765
  <set-scoped form-field-names="&[]">
@@ -443,19 +772,9 @@
443
772
  </def>
444
773
 
445
774
 
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
775
+ <def tag="or-cancel">
776
+ <if test="&linkable?">or <a>Cancel</a></if>
777
+ <else>
778
+ <if test="&linkable?(this.class)">or <a to="&this.class">Cancel</a></if>
779
+ </else>
780
+ </def>