hobo 0.8.3 → 0.8.4

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 (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
@@ -0,0 +1,586 @@
1
+ <!-- Core Rapid tags and tags that don't belong anywhere else. -->
2
+
3
+ <!-- Renders a table with one row per field, where each row contains a `<th>` with the field name, and a `<td>` with (by default)
4
+ a `<view>` of the field.
5
+
6
+ ### Attributes
7
+
8
+ - fields: Comma separated list of field names to display. Defaults to the fields returned by the `standard_fields` helper. That is, all fields apart from IDs and timestamps.
9
+
10
+ - force-all: All non-viewable fields will be skipped unless this attribute is given
11
+
12
+ - skip: Comma separated list of fields to exclude
13
+
14
+ - tag: The name of a tag to use inside the `<td>` to display the value. Defaults to `view`
15
+
16
+ - show-non-editable: By default, if `tag` is set to `input`, fields for which the current user does not have edit permission
17
+ will be skipped (the entire row is skipped). Set this attribute to keep them. (Note that `<input>` automatically degrades
18
+ to `<view>` if the user does not have edit permission.)
19
+
20
+ -->
21
+ <def tag="field-list" attrs="tag, no-edit">
22
+ <% tag ||= scope.in_form ? "input" : "view"; no_edit ||= "skip" %>
23
+ <labelled-item-list merge-attrs="&attributes - attrs_for(:with_fields)">
24
+ <with-fields merge-attrs="&attributes & attrs_for(:with_fields)">
25
+ <% field_name = this_field_name
26
+ input_attrs = {:no_edit => no_edit} if tag == "input" && no_edit == "disable"
27
+ -%>
28
+ <labelled-item unless="&tag == 'input' && no_edit == 'skip' && !can_edit?">
29
+ <item-label param="#{this_field.to_s.sub('?', '')}-label" unless="&field_name.blank?">
30
+ <do param="label"><%= field_name %></do>
31
+ </item-label>
32
+ <item-value param="#{this_field.to_s.sub('?', '')}-view" colspan="&2 if field_name.blank?">
33
+ <do param="view"><call-tag tag="&tag" param="#{this_field.to_s.sub('?', '')}-tag" merge-attrs="&input_attrs"/></do>
34
+ <div param="input-help" if="&tag.to_sym == :input && !this_field_help.blank?"><%= this_field_help %></div>
35
+ </item-value>
36
+ </labelled-item>
37
+ </with-fields>
38
+ </labelled-item-list>
39
+ </def>
40
+
41
+ <!-- Used to render nil values. By default renders "(Not Available)"
42
+
43
+ ### Usage
44
+
45
+ Redefine in your app to have nil values displayed differently, e.g.:
46
+
47
+ <def tag="nil-view">-</def>
48
+
49
+ -->
50
+ <def tag="nil-view"><%= scope.nil_view || "(Not Available)" %></def>
51
+
52
+ <!--
53
+ `<table>` is extended in Rapid to provide a shorthand way to output a set of fields for a given collection. This is enabled using the `field` attribute (without the `field` attribute this is just the regular HTML `<table>` tag)
54
+
55
+ ### Usage
56
+
57
+ If the context is an array of blog posts...
58
+
59
+ <table fields="name, created_at, description"/>
60
+
61
+ This will output a header row containing "Name", "Created At" and "Description" followed by a row for each record in the collection. By default, the `<view/>` tag is called for each field in the row. This can be altered with the `field-tag` attribute, e.g.
62
+
63
+ <table fields="name, created_at, description" field-tag="input"/>
64
+
65
+ This will use `<input/>` as the tag in each table cell instead of `<view/>`
66
+
67
+ ### Additional Notes
68
+
69
+ * `<table>` provides parameters based on the names of the fields which can be used to further customise the output. For each field a heading parameter is provided, e.g. name-heading, created-at-heading, description-heading. These can be used to customise the headings:
70
+
71
+ <table fields="name, created_at, description">
72
+ <created-at-heading:>Creation Date</created-at-heading:>
73
+ </table>
74
+ * Similarly, "view" parameters are provided as an additional way to customise the table cells of the table body, e.g. `name-view`, `created-at-view`, `description-view`:
75
+
76
+ <table fields="name, created_at, description">
77
+ <created-at-view:><view format="%d %B %Y"/></created-at-view:>
78
+ </table>
79
+ * By adding an empty `control` parameter, the default control column is enable adding an edit link and delete button for each table row:
80
+
81
+ <table fields="name, created_at, description">
82
+ <controls:/>
83
+ </table>
84
+ The controls can be further customised using the "edit-link" and "delete-button" parameters or by providing completely new content for the control column, e.g.
85
+
86
+ <table fields="name, created_at, description">
87
+ <controls:>my controls!</controls:>
88
+ </table>
89
+ -->
90
+ <def tag="table" attrs="fields, field-tag, empty">
91
+ <if test="&!(fields || all_parameters.tr?)">
92
+ <%= element("table", attributes, all_parameters.default) %>
93
+ </if>
94
+ <else>
95
+ <% field_tag ||= "view" %>
96
+ <unless test="&this.empty? && !empty">
97
+ <% element "table", attributes - attrs_for(:with_fields) do %>
98
+ <thead if="&all_parameters[:thead] || fields" param>
99
+ <tr param="field-heading-row">
100
+ <with-field-names merge-attrs="&all_attributes & attrs_for(:with_fields)">
101
+ <th param="#{scope.field_name}-heading"><%= this.member_class.view_hints.field_name(scope.field_name) %></th>
102
+ </with-field-names>
103
+ <th if="&all_parameters[:controls]" class="controls"/>
104
+ </tr>
105
+ </thead>
106
+ <tbody>
107
+ <repeat>
108
+ <tr param if="&can_view?"
109
+ class="#{scope.even_odd} #{this_type.name.underscore} #{model_id_class}">
110
+ <if test="&fields">
111
+ <with-fields merge-attrs="&all_attributes & attrs_for(:with_fields)" force-all>
112
+ <td param="#{this_field.to_s.sub('?', '').gsub('.', '-')}-view"><call-tag tag="&field_tag"/></td>
113
+ </with-fields>
114
+ <td class="controls" param="controls" if="&all_parameters[:controls]">
115
+ <a param="edit-link" action="edit">Edit</a>
116
+ <delete-button param/>
117
+ </td>
118
+ </if>
119
+ </tr>
120
+ </repeat>
121
+ </tbody>
122
+ <tfoot if="&all_parameters[:tfoot]" param/>
123
+ <% end %>
124
+ </unless>
125
+ </else>
126
+ </def>
127
+
128
+ <!--
129
+ Provides a short hand way of displaying images in public/images
130
+
131
+ ### Usage
132
+
133
+ <image src="hobo.png"/> -> <img src="/images/hobo.png"/>
134
+ <image src="blog/funny.jpg" alt="Funny Scene"/> -> <img src="/images/blog/funny.jpg" alt="Funny Scene"/>
135
+ -->
136
+ <def tag="image" attrs="src">
137
+ <img src="#{base_url}/images/#{src}" merge-attrs/>
138
+ </def>
139
+
140
+
141
+ <!-- Renders an ajax-progress 'spinner' using `spinner.gif` from the current theme, with a `class='hidden'` -->
142
+ <def tag="spinner">
143
+ <img src="#{base_url}/hobothemes/#{Hobo.current_theme}/images/spinner.gif" class="hidden" merge-attrs/>
144
+ </def>
145
+
146
+
147
+ <!-- Renders some standard JavaScript code that various features of the Rapid library rely on. This tag would typicallu be called from your `<page>` tag. The default Rapid pages include this already. -->
148
+ <def tag="hobo-rapid-javascripts"><%=
149
+ res = 'var hoboParts = {};'
150
+ # FIXME: This should interrogate the model-router - not the models
151
+ unless Hobo::Model.all_models.empty?
152
+ # Tell JS code how to pluralize names, unless they follow the simple rule
153
+ names = Hobo::Model.all_models.map do |m|
154
+ m = m.name.underscore
155
+ "#{m}: '#{m.pluralize}'" unless m.pluralize == m + 's'
156
+ end.compact
157
+ res << "var pluralisations = {#{names * ', '}}; "
158
+ end
159
+ base = [base_url, subsite].compact.join("/")
160
+ res << "urlBase = '#{base}'; hoboPagePath = '#{view_name}'"
161
+ if protect_against_forgery?
162
+ res << "; formAuthToken = { name: '#{request_forgery_protection_token}', value: '#{form_authenticity_token}' }"
163
+ end
164
+ res
165
+ %></def>
166
+
167
+ <!-- Renders the name of the current context using a variety of methods.
168
+
169
+ ### Details
170
+
171
+ - Equivalent to `<nil-view>` if `this` is nil
172
+ - Equivalent to `<count>` if `this` is an Array
173
+ - Equivalent to `<type-name>` if `this` is a class
174
+ - If the context has a `name_attribute` defined, equivalent to `<view:abc/>` (where `abc` is the name attribute)
175
+ - Finally falls back to `this.to_s` (html escaped), but only if the user has view permission for `this`
176
+
177
+ ### Attributes
178
+
179
+ - if-present: if given, nothing at all will be rendered for nil values (as opposed to rendering `<nil-view>`)
180
+
181
+ -->
182
+ <def tag="name" attrs="if-present"><%=
183
+ if this.nil?
184
+ nil_view unless if_present
185
+ else
186
+ if this.is_a?(Array)
187
+ count
188
+ elsif this.is_a?(Class)
189
+ type_name(attributes)
190
+ elsif (name_attr = this.class.try.name_attribute) && can_view?(this, name_attr)
191
+ view(merge_attrs(attributes, {:field => name_attr}))
192
+ elsif can_view?(this)
193
+ h this.to_s
194
+ end
195
+ end
196
+ %></def>
197
+
198
+
199
+ <!-- Renders a human readable version of the type of the context
200
+
201
+ ### Details
202
+
203
+ - If `this` is already a class, the name of that class is used
204
+ - Otherwise, first `this.member_class` (for collections), then `this.class` are tried
205
+ - By default the name is titleised and singular.
206
+
207
+ ### Attributes
208
+
209
+ - plural: pluralise the name
210
+ - lowercase: render the name in all lower case
211
+ - dasherize: render the name in lower case with dashes instead of spaces.
212
+
213
+ -->
214
+ <def tag="type-name" attrs="plural, lowercase, dasherize"><%=
215
+ type ||= (this if this.is_a?(Class)) || this.try.member_class || this.class
216
+
217
+ name = dasherize ? type.name.underscore.dasherize : type.name.titleize
218
+ name = name.pluralize if plural
219
+ name = name.downcase if lowercase
220
+ name
221
+ %></def>
222
+
223
+
224
+ <!-- Renders a human readable name of a collection
225
+
226
+ ### Details
227
+
228
+ - Uses `this.origin_attribute` as the name.
229
+ - Falls back to `<type-name>` otherwise.
230
+ - By default the name is titleised and plural.
231
+
232
+ ### Attributes
233
+
234
+ - singular: singularise the name
235
+ - lowercase: render the name in all lower case
236
+ - dasherize: render the name in lower case with dashes instead of spaces.
237
+
238
+ -->
239
+ <def tag="collection-name" attrs="singular, lowercase, dasherize"><%=
240
+ if (attr = this.try.origin_attribute)
241
+ name = attr.to_s
242
+ name = dasherize ? name.underscore.dasherize : name.titleize
243
+ name = name.singularize if singular
244
+ name = name.downcase if lowercase
245
+ name
246
+ else
247
+ type_name(:plural => !singular, :lowercase => lowercase, :dasherize => dasherize)
248
+ end
249
+ %></def>
250
+
251
+ <!--
252
+ `<a>` is extended in Rapid to automatically provide URLs for Hobo model routes
253
+
254
+ ### Usage
255
+
256
+ The tag behaves as a regular HTML link or anchor if either the href or name attribute is given:
257
+
258
+ <a href="/admin">Admin</a> -> Output is exactly as provided, untouched by Rapid
259
+
260
+ If no href or name is given then the _context_ is used to determine the link URL.
261
+ The helper method `object_url` is used to construct the URL using restful routing:
262
+
263
+ If the context is a class then the link will be an index page:
264
+
265
+ <a with="&BlogPost">My Blog</a> -> <a href="/blog_posts">My Blog</a>
266
+
267
+ If the context is a hobo model instance then the link will be a show page:
268
+
269
+ <% blog_post = BlogPost.find(1) %>
270
+ <a with="&blog_post">My Blog Post</a> -> <a href="/blog_posts/1">My Blog Post</a>
271
+
272
+ An action can be provided for an alternative show page:
273
+
274
+ <a with="&blog_post" action="edit">Edit Post</a> -> <a href="/blog_posts/1/edit">Edit Post</a>
275
+
276
+ Or a new page if the context is a class:
277
+
278
+ <a with="&BlogPost" action="new">New Blog Post</a> -> <a href="/blog_posts/new">New Blog Post</a>
279
+
280
+ ### Additional Features
281
+
282
+ * If the constructed route does not exist then the link will not be created, but the content of the link will still be output. E.g. when `/blog_posts` does not exist (because the hobo model controller does not exist or the index action is disabled):
283
+
284
+ <a with="&BlogPost">My Blog</a> -> My Blog
285
+
286
+ when the show action `/blog_posts/:id` does not exist:
287
+
288
+ <a with="&blog_post">My Blog Post</a> -> My Blog Post
289
+ * If no content text is provided then `<a>` will use the name method on the context to provide the text. E.g.
290
+
291
+ <a with="&blog_post"/> -> <a href="/blog_posts/1">My First Blog Post</a>`
292
+ <a with="&BlogPost"/> -> <a href="/blog_posts">Blog Posts</a>`
293
+ * If `action="new"` then `<a>` will check that the current user has permission to create the object
294
+ * Several useful classes are added automatically to the output `<a>`.
295
+ -->
296
+ <def tag="a" attrs="action, to, params, query-params, href, format, subsite"><%=
297
+ content = parameters.default
298
+
299
+ params = self.query_params.merge(params || HashWithIndifferentAccess.new) if query_params
300
+
301
+ if href || attributes[:name]
302
+ # Regular link
303
+ href += "?" + params.map { |n, v| "#{n}=#{v}" }.join('&') if !params.blank?
304
+ element(:a, attributes.update(:href => href), content)
305
+ else
306
+ target = to || this
307
+
308
+ if target.nil?
309
+ Hobo::Dryml.last_if = false
310
+ nil_view
311
+ elsif action == "new"
312
+ # Link to a new object form
313
+ new_record = target.new
314
+ new_record.set_creator(current_user)
315
+ href = object_url(target, "new", params._?.merge(:subsite => subsite))
316
+
317
+ if href && can_create?(new_record)
318
+ new_class_name = if target.respond_to?(:proxy_reflection)
319
+ target.proxy_reflection.klass.name
320
+ else
321
+ target.name
322
+ end
323
+
324
+ add_classes!(attributes, "new-#{new_class_name.underscore}-link")
325
+ content = "New #{new_class_name.titleize}" if content.blank?
326
+ Hobo::Dryml.last_if = true
327
+ element(:a, attributes.update(:href => href), content)
328
+ else
329
+ Hobo::Dryml.last_if = false
330
+ ""
331
+ end
332
+ else
333
+ # Link to an existing object
334
+
335
+ content = name if content.blank?
336
+
337
+ href = object_url(target, action, (params || {}).merge(:subsite => subsite))
338
+ if href.nil?
339
+ # This target is registered with ModelRouter as not linkable
340
+ content
341
+ else
342
+ css_class = target.try.origin_attribute || target.class.name.underscore.dasherize
343
+ add_classes!(attributes, "#{css_class}-link")
344
+
345
+ href.sub!(/\?|$/, ".#{format}\\0") unless format.blank?
346
+
347
+ # Set default link text if none given
348
+ element(:a, attributes.update(:href => href), content)
349
+ end
350
+ end
351
+ end
352
+ %></def>
353
+
354
+ <!--
355
+ Provides a read-only view tailored to the type of the object being viewed. `<view>` is a _polymorphic_ tag which means that there are a variety of definitions, each one written for a particular type. For example there are views for `Date`, `Time`, `Numeric`, `String` and `Boolean`. The type specific view is enclosed in a wrapper tag (typically a `<span>` or `<div>`) with some useful classes automatically added.
356
+
357
+ ### Usage
358
+
359
+ Assuming the context is a blog post...
360
+
361
+ * Viewing a DateTime field:
362
+
363
+ <view:created_at/> -> <span class="view blog-post-created-at">June 09, 2008 15:36</span>
364
+ * Viewing a String field:
365
+
366
+ <view:title/> -> <span class="view blog-post-title">My First Blog Post</span>
367
+ * Viewing an Integer field:
368
+
369
+ <view:comment_count/> -> <span class="view blog-post-comment-count">4</span>
370
+ * Viewing the blog post itself results in a link to the blog post (using Rapid's `<a>` tag):
371
+
372
+ <view/> -> <span class="view model:blog-post-1"><a href="/blog_posts/1">My First Blog Post</a></span>
373
+
374
+ ### Additional Notes
375
+
376
+ * The wrapper tag is `<span>` unless the field type is `Text` (different to `String`) where it is `<div>`. Use the `inline` or `block` attributes to force a `<span>` or a `<div>`, e.g.
377
+
378
+ <view:body/> -> <div class="view blog-post-body">This is my blog post body</div>
379
+
380
+ <view:body inline/> -> <span class="view blog-post-body">This is my blog post body</span>
381
+
382
+ <view:created_at block/> -> <div class="view blog-post-created-at">June 09, 2008 15:36</div>
383
+ * Use the `no-wrapper` attribute to remove the wrapper tag completely. e.g.
384
+
385
+ <view:created_at no-wrapper/> -> June 09, 2008 15:36
386
+ -->
387
+ <def tag="view" attrs="inline, block, if-blank, no-wrapper, truncate"><%=
388
+ raise HoboError, "view of non-viewable field '#{this_field}' of #{this_parent.typed_id rescue this_parent}" unless
389
+ can_view?
390
+
391
+ res = if this.nil? && if_blank.nil?
392
+ this_type.is_a?(Class) && this_type <= String ? "" : nil_view
393
+ elsif (refl = this_field_reflection) && refl.macro == :has_many
394
+ collection_view(attributes)
395
+ else
396
+ view_tag = find_polymorphic_tag("view")
397
+
398
+ if view_tag == "view" # i.e. it didn't find a type specific tag
399
+ if this.respond_to?(:to_html)
400
+ this.to_html(scope.xmldoctype)
401
+ else
402
+ this.to_s
403
+ end
404
+ else
405
+ attrs = add_classes(attributes, "view", type_and_field._?.dasherize, model_id_class)
406
+
407
+ view_attrs = attrs_for(view_tag)
408
+ the_view = send(view_tag, attrs & view_attrs)
409
+
410
+ the_view = if_blank if if_blank && the_view.blank?
411
+
412
+ truncate = 30 if truncate == true
413
+ the_view = self.truncate(the_view, truncate.to_i) if truncate
414
+ the_view = the_view.strip
415
+
416
+ if no_wrapper
417
+ the_view
418
+ else
419
+ wrapper = if inline
420
+ :span
421
+ elsif block || this.is_a?(HoboFields::Text)
422
+ :div
423
+ else
424
+ :span
425
+ end
426
+ element(wrapper, attrs - view_attrs, the_view)
427
+ end
428
+ end
429
+ end
430
+ Hobo::Dryml.last_if = !res.blank?
431
+ res
432
+ %></def>
433
+
434
+ <!-- `<view>` calls this tag when called for a `has_many` collection. By default calls `<links-for-collection/>` -->
435
+ <def tag="collection-view" polymorphic><links-for-collection mergge-attrs/></def>
436
+
437
+ <!-- Renders a comma separated list of links (`<a>`), or "(none)" if the list is empty -->
438
+ <def tag="links-for-collection"><%= this.empty? ? "(none)" : context_map { a }.join(", ") %></def>
439
+
440
+ <!-- Renders `this.to_s(:long)`, or `this.strftime(format)` if the `format` attribute is given -->
441
+ <def tag="view" for="Date" attrs="format"><%= this && (format ? this.strftime(format) : this.to_s(:long)) %></def>
442
+
443
+ <!-- Renders `this.to_s(:long)`, or `this.strftime(format)` if the `format` attribute is given -->
444
+ <def tag="view" for="Time" attrs="format"><%= this && (format ? this.strftime(format) : this.to_s(:long)) %></def>
445
+
446
+ <!-- Renders `this.to_s(:long)`, or `this.strftime(format)` if the `format` attribute is given -->
447
+ <def tag="view" for="ActiveSupport::TimeWithZone" attrs="format"><%= this && (format ? this.strftime(format) : this.to_s(:long)) %></def>
448
+
449
+ <!-- Renders `this.to_s`, or `format % this` if the `format` attribute is given -->
450
+ <def tag="view" for="Numeric" attrs="format"><%= format ? format % this : this.to_s %></def>
451
+
452
+ <!-- Renders `this` with HTML escaping and newlines replaced with `<br>` tags -->
453
+ <def tag="view" for="string"><%=
454
+ if !(this.class == String) && this.respond_to?(:to_html) # workaround for Maruku which adds String#to_html : (
455
+ this.to_html(scope.xmldoctype)
456
+ else
457
+ h(this).gsub("\n", "<br#{scope.xmldoctype ? ' /' : ''}>")
458
+ end
459
+ %></def>
460
+
461
+ <!-- Renders 'Yes' for true and 'No' for false -->
462
+ <def tag="view" for="boolean"><%= this ? 'Yes' : 'No' %></def>
463
+
464
+ <!-- Renders a link (`<a>`) to `this` -->
465
+ <def tag="view" for="ActiveRecord::Base"><a merge-attrs/></def>
466
+
467
+
468
+ <!--
469
+ A convenience tag used to output a count and a correctly pluralised label. Works with any kind of collection such as an `ActiveRecord` association or an array.
470
+
471
+ ### Usage
472
+
473
+ <count:comments/> -> <span class="count">1 Comment</span>
474
+
475
+ <count:viewings/> -> <span class="count">3 Viewings</span>
476
+
477
+ The label can be customised using the `label` attribute, e.g.
478
+
479
+ <count:comments label="blog post comment"/> -> <span class="count">12 blog post comments</span>
480
+
481
+ ### Additional Notes
482
+
483
+ * Use the `prefix` attribute to insert words before the count. If the prefix is "are" or "is" then it will be pluralised if needed:
484
+
485
+ There <count:comments prefix="are"/> -> There <span class="count">is 1 Comment</span>
486
+ There <count:viewings prefix="are"/> -> There <span class="count">are 3 Viewings</span>
487
+ * Use the `lowercase` attribute to force the generated label to be lowercase:
488
+
489
+ <count:comments lowercase/> -> <span class="count">1 comment</span>
490
+ * Use the `if-any` attribute to output nothing if the count is zero. This can be followed by an `<else>` tag to handle the empty case:
491
+
492
+ <count:comments if-any/><else>There are no comments</else>
493
+ -->
494
+ <def tag="count" attrs="label, prefix, if-any, lowercase"><span class="count"><%=
495
+ raise Exception.new("asked for count of a string") if this.is_a?(String)
496
+
497
+ c = this.try.to_int || this.try.total_entries || (this.try.loaded? && this.try.length) || this.try.count || this.try.length
498
+
499
+ label ||= if this.is_a?(Class)
500
+ this.name
501
+ elsif (attr = this.try.origin_attribute)
502
+ attr.to_s.singularize
503
+ else
504
+ this.member_class.name
505
+ end.titleize
506
+
507
+ label = label.downcase if lowercase
508
+
509
+ Hobo::Dryml.last_if = c > 0 if if_any
510
+ if if_any && c == 0
511
+ ""
512
+ else
513
+ main = label.blank? ? c : pluralize(c, label)
514
+
515
+ if prefix.in? %w(are is)
516
+ p = c == 1 ? "is" : "are"
517
+ p + ' ' + main.to_s
518
+ else
519
+ main
520
+ end
521
+ end
522
+ %></span></def>
523
+
524
+
525
+ <!-- Renders a `<link rel="Stylesheet" type="text/css">` to include the default stylesheet for the selected theme (select with `<set-theme>`). Included in the default pages.
526
+ -->
527
+ <def tag="theme-stylesheet" attrs="name">
528
+ <% name ||= Hobo.current_theme -%>
529
+ <link href="#{base_url}/hobothemes/#{Hobo.current_theme}/stylesheets/#{name}.css"
530
+ media="screen" rel="Stylesheet" type="text/css" />
531
+ </def>
532
+
533
+ <!-- Convenience tag to help with the common situation where you need to address the current user as "you", and refer to other users by name
534
+
535
+ ### Usage
536
+
537
+ The context should be a user object. If `this == current_user` the "you" form is rendered, otherwise the form with the user's name:
538
+
539
+ - `<you have/> new mail` -> "you have new mail" or "Jim has new mail"
540
+ - `<you are/> now an admin` -> "you are now an admin" or "Jim is now an admin"
541
+ - `<you do/>n't want to go there` -> "you don't want to go there" or "Jim doesn't want to go there"
542
+
543
+ ### Attributes
544
+
545
+ - titleize: render "You" instead of "you"
546
+
547
+ -->
548
+ <def tag="you" attrs="have, are, do, titleize"><if test="&this == current_user"><%= "#{titleize ? 'Y' : 'y'}ou#{' have' if have}#{' are' if are}#{' do' if do_}" %></if><else><do param="default"><name/><%= "#{' has' if have}#{' is' if are}#{' does' if do_}" %></do></else></def>
549
+
550
+ <!-- Equivalent to `<you titleize/>`. Yes it's an abuse of Ruby naming conventions, but it's so cute : ) -->
551
+ <def tag="You"><you merge titleize/></def>
552
+
553
+ <!-- Similar to `<you>`, but renders "Your" or "Fred's" -->
554
+ <def tag="your">
555
+ <if test="&this == current_user">your</if>
556
+ <else><do param="default"><%= n = name; n.ends_with?('s') ? "#{n}'" : "#{n}'s" %></do></else>
557
+ </def>
558
+
559
+ <!-- Capitalised versin of `<your>` -->
560
+ <def tag="Your">
561
+ <if test="&this == current_user">Your</if>
562
+ <else><do param="default"><%= n = name; n.ends_with?('s') ? "#{n}'" : "#{n}'s" %></do></else>
563
+ </def>
564
+
565
+ <!-- Renders "a book" or "an orange" according the the word passed in the attribute `word`
566
+
567
+ ### Usage
568
+
569
+ To render either "Please select a recipe" or "Please select an event", according to the type of the `this`:
570
+
571
+ <a-or-an word="&type_name"/>
572
+
573
+ -->
574
+ <def tag="a-or-an" attrs="word"><%=
575
+ (word =~ /^[aeiou]/i ? "an " : "a ") + word
576
+ %></def>
577
+
578
+
579
+ <!-- Capitalised version of `<a-or-an>` -->
580
+ <def tag="A-or-An" attrs="word"><%=
581
+ (word =~ /^[aeiou]/i ? "An " : "A ") + word
582
+ %></def>
583
+
584
+
585
+ <!-- Renders a collection of string joined with ", ", or some other string passed in the `join` attribute -->
586
+ <def tag="comma-list" attrs="join"><%= this.join(join || ", ") %></def>