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
@@ -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>