radiant-taggable-extension 1.2.5 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,387 @@
1
+ module Radius
2
+ module LibraryTags
3
+ include Radiant::Taggable
4
+ include TaggableHelper
5
+
6
+ class TagError < StandardError; end
7
+
8
+ ############### tags for use on library pages
9
+ # usually to build a faceted browser
10
+
11
+ tag "library" do |tag|
12
+ raise TagError, "library:* tags can only be used on a LibraryPage" unless tag.locals.page && tag.locals.page.is_a?(LibraryPage)
13
+ tag.expand
14
+ end
15
+
16
+ desc %{
17
+ Displays a list of the tags that can be used to narrow the set of results displayed. This begins as a list of
18
+ all available tags, and as they are chosen it shrinks to show only the coincident tags that will further reduce the set.
19
+
20
+ This is normally used to display a list or cloud of facets that can be added to a search.
21
+
22
+ <pre><code>
23
+ <r:library:tags:each><li><r:tag:link /></li></r:library:tags:each>
24
+ <r:library:tags:list />
25
+ <r:library:tags:cloud />
26
+ </code></pre>
27
+
28
+ To show only those tags attached to a particular kind of object, supply a 'for' parameter.
29
+ The parameter can be 'pages', 'assets' or the plural of any asset type. If you're displaying an image gallery,
30
+ you may want to start with a cloud of all the tags that have been applied to images:
31
+
32
+ <pre><code>
33
+ <r:library:tags for="images" />
34
+ </code></pre>
35
+
36
+ You can still display pages associated with those tags, but the list will not include tags that only have pages.
37
+ }
38
+ tag "library:tags" do |tag|
39
+ tag.locals.tags = _get_coincident_tags(tag).sort
40
+ tag.expand
41
+ end
42
+ tag "library:tags:each" do |tag|
43
+ tag.render('each_tag', tag.attr.dup, &tag.block)
44
+ end
45
+ tag "library:tags:list" do |tag|
46
+ tag.render('tag_list', tag.attr.dup)
47
+ end
48
+ tag "library:tags:cloud" do |tag|
49
+ tag.render('tag_cloud', tag.attr.dup)
50
+ end
51
+
52
+ desc %{
53
+ Expands if there are is more than one tag to show.
54
+
55
+ *Usage:*
56
+ <pre><code>
57
+ <r:library:if_tags>
58
+ Displaying items tagged with all of <r:requested_tags />
59
+ <r:library:if_tags>
60
+ </code></pre>
61
+ }
62
+ tag "library:if_tags" do |tag|
63
+ tag.locals.tags = _get_coincident_tags(tag).sort
64
+ tag.expand if tag.locals.tags.length > 1
65
+ end
66
+ desc %{
67
+ Expands if are is one or no tag to show.
68
+
69
+ *Usage:*
70
+ <pre><code>
71
+ <r:library:unless_tags>That's your lot.</r:library:unless_tags>
72
+ </code></pre>
73
+ }
74
+ tag "library:unless_tags" do |tag|
75
+ tag.locals.tags = _get_coincident_tags(tag).sort
76
+ tag.expand if tag.locals.tags.length > 1
77
+ end
78
+
79
+ desc %{
80
+ Displays a list of the tags requested by the user.
81
+ To offer links that remove the tag from the current set, these will both work:
82
+
83
+ *Usage:*
84
+ <pre><code>
85
+ <r:library:requested_tags:each><li><r:tag:unlink /></li></r:library:requested_tags:each>
86
+ <r:library:requested_tags />
87
+ </code></pre>
88
+ }
89
+ tag "library:requested_tags" do |tag|
90
+ tag.locals.tags = _get_requested_tags(tag).sort
91
+ if tag.double?
92
+ tag.expand
93
+ else
94
+ tag.render('tags:unlink_list', tag.attr.dup)
95
+ end
96
+ end
97
+ tag "library:requested_tags:each" do |tag|
98
+ tag.render('each_tag', tag.attr.dup, &tag.block)
99
+ end
100
+
101
+ desc %{
102
+ Expands if any tags have been specified:
103
+
104
+ *Usage:*
105
+ <pre><code>
106
+ <r:library:if_requested_tags>
107
+ Displaying items tagged with all of <r:requested_tags />
108
+ <r:library:if_requested_tags>
109
+ </code></pre>
110
+ }
111
+ tag "library:if_requested_tags" do |tag|
112
+ tag.locals.tags = _get_requested_tags(tag).sort
113
+ tag.expand if tag.locals.tags.any?
114
+ end
115
+
116
+ desc %{
117
+ Expands if no tags have been specified:
118
+
119
+ *Usage:*
120
+ <pre><code>
121
+ <r:library:unless_requested_tags>
122
+ Showing everything. Choose a tag to start narrowing down the list.
123
+ <r:library:unless_requested_tags>
124
+ </code></pre>
125
+ }
126
+ tag "library:unless_requested_tags" do |tag|
127
+ tag.expand unless _get_requested_tags(tag).any?
128
+ end
129
+
130
+ desc %{
131
+ Displays a list of the pages associated with the current tag set. If no tags are specified, this will show all pages.
132
+ You can use all the usual r:page tags within the list.
133
+
134
+ :by, :order, :limit, :offset, :status and all the usual list-control attributes are obeyed.
135
+
136
+ To paginate the list, set paginated="true" and, optionally, per_page="xx".
137
+
138
+ *Usage:*
139
+ <pre><code>
140
+ <r:library:pages:each><li><r:link /><br /><r:content part="description" /></li></r:library:pages:each>
141
+ </code></pre>
142
+ }
143
+ tag "library:pages" do |tag|
144
+ tag.locals.pages = _get_pages(tag)
145
+ tag.expand
146
+ end
147
+ tag "library:pages:each" do |tag|
148
+ tag.render('page_list', tag.attr.dup, &tag.block) # r:page_list is defined in taggable
149
+ end
150
+
151
+ desc %{
152
+ Expands if there are any pages associated with all of the current tag set.
153
+
154
+ *Usage:*
155
+ <pre><code>
156
+ <r:library:if_pages><h2>Pages</h2>...</r:library:if_pages>
157
+ </code></pre>
158
+ }
159
+ tag "library:if_pages" do |tag|
160
+ tag.expand if _get_pages(tag).any?
161
+ end
162
+
163
+ desc %{
164
+ Displays a list of the assets associated with the current tag set. If no tags are specified, this will show all assets.
165
+ You can use all the usual r:assets tags within the list.
166
+
167
+ By, order, limit and offset attributes are obeyed. The default is to sort by creation date, descending.
168
+
169
+ To paginate the list, set paginated="true" and, optionally, per_page="xx".
170
+
171
+ *Usage:*
172
+ <pre><code>
173
+ <r:library:assets:each><li><r:assets:thumbnail /></li></r:library:assets:each>
174
+ </code></pre>
175
+ }
176
+ tag "library:assets" do |tag|
177
+ tag.locals.assets = _get_assets(tag)
178
+ tag.expand
179
+ end
180
+ tag "library:assets:each" do |tag|
181
+ tag.render('asset_list', tag.attr.dup, &tag.block) # r:page_list is defined in spanner's paperclipped
182
+ end
183
+
184
+ desc %{
185
+ Expands if there are any assets associated with all of the current tag set.
186
+
187
+ *Usage:*
188
+ <pre><code>
189
+ <r:library:if_assets><h2>Assets</h2>...</r:library:if_assets>
190
+ </code></pre>
191
+ }
192
+ tag "library:if_assets" do |tag|
193
+ tag.expand if _get_assets(tag).any?
194
+ end
195
+
196
+ Asset.known_types.each do |type|
197
+ these = type.to_s.pluralize
198
+
199
+ desc %{
200
+ Displays a list of the all the #{these} associated with the current tag set. If no tags are specified, this will show all such assets.
201
+ You can use all the usual r:assets tags within the list.
202
+
203
+ *Usage:*
204
+ <pre><code>
205
+ <r:library:#{these}:each><li><r:assets:link /></li></r:library:#{these}:each>
206
+ </code></pre>
207
+ }
208
+ tag "library:#{these}" do |tag|
209
+ tag.locals.assets = _get_assets(tag).send(these.intern)
210
+ tag.expand
211
+ end
212
+ tag "library:#{these}:each" do |tag|
213
+ tag.render('asset_list', tag.attr.dup, &tag.block)
214
+ end
215
+
216
+ desc %{
217
+ Expands if there are any #{these} associated with all of the current tag set.
218
+
219
+ *Usage:*
220
+ <pre><code>
221
+ <r:library:if_#{these}><h2>#{these.titlecase}</h2>...</r:library:if_#{these}>
222
+ </code></pre>
223
+ }
224
+ tag "library:if_#{these}" do |tag|
225
+ tag.locals.assets = _get_assets(tag).send(these.intern)
226
+ tag.expand if tag.locals.assets.any?
227
+ end
228
+
229
+ ############### tags:* tags that only make sense on library pages
230
+
231
+ desc %{
232
+ Summarises in a sentence the list of tags currently active, with each one presented as a defaceting link.
233
+ }
234
+ tag 'tags:unlink_list' do |tag|
235
+ requested = _get_requested_tags(tag)
236
+ if requested.any?
237
+ requested.map { |t|
238
+ tag.locals.tag = t
239
+ tag.render('tag:unlink', tag.attr.dup)
240
+ }.to_sentence
241
+ else
242
+ ""
243
+ end
244
+ end
245
+
246
+ desc %{
247
+ Makes a link that removes the current tag from the active set. Other options as for tag:link.
248
+
249
+ *Usage:*
250
+ <pre><code><r:tag:unlink linkto='/library' /></code></pre>
251
+ }
252
+ tag 'tag:unlink' do |tag|
253
+ raise TagError, "tag must be defined for tag:unlink tag" unless tag.locals.tag
254
+ options = tag.attr.dup
255
+ options['class'] ||= 'detag'
256
+ anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
257
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
258
+ attributes = " #{attributes}" unless attributes.empty?
259
+ text = tag.double? ? tag.expand : tag.render('tag:name')
260
+
261
+ if tag.locals.page.is_a?(LibraryPage)
262
+ href = tag.locals.page.url(tag.locals.page.requested_tags - [tag.locals.tag])
263
+ elsif page_url = (options.delete('tagpage') || Radiant::Config['tags.page'])
264
+ href = clean_url(page_url + '/-' + tag.locals.tag.clean_title)
265
+ else
266
+ href ||= Rack::Utils.escape("-#{tag.locals.tag.title}") + '/'
267
+ end
268
+
269
+ %{<a href="#{href}#{anchor}"#{attributes}>#{text}</a>}
270
+ end
271
+
272
+
273
+ ############### libraryish utility tags that don't really belong here
274
+
275
+ desc %{
276
+ Truncates the contained text to the specified number of words. Attributes:
277
+ * `limit` sets the number of words shown. Default is 64.
278
+ * `ellipsis` is the suffix used to indicate truncation. Default is '&hellip;'
279
+ * `strip="true"` will cause all html tags to be stripped from the contained text before it is truncated. Default is false.
280
+ }
281
+ tag "truncate" do |tag|
282
+ # truncate_words is in LibraryHelper
283
+ truncate_words tag.expand, :limit => tag.attr['limit'], :ellipsis => tag.attr['ellipsis'], :strip => tag.attr['strip'] == 'true'
284
+ end
285
+
286
+ desc %{
287
+ Strips all html tags from the contained text, leaving the text itself unchanged.
288
+ Useful when, for example, using a page part to populate a meta tag.
289
+ }
290
+ tag "strip" do |tag|
291
+ # strip_html is in LibraryHelper
292
+ strip_html tag.expand
293
+ end
294
+
295
+ desc %{
296
+ Removes all unsafe html tags and attributes from the enclosed text, protecting from cross-site scripting attacks while leaving the text intact.
297
+ }
298
+ tag "clean" do |tag|
299
+ # clean_html is in LibraryHelper
300
+ clean_html tag.expand
301
+ end
302
+
303
+
304
+
305
+ private
306
+
307
+ def _get_requested_tags(tag)
308
+ tag.locals.page.requested_tags
309
+ end
310
+
311
+ def _get_coincident_tags(tag)
312
+ requested = _get_requested_tags(tag)
313
+ limit = tag.attr['limit'] || 50
314
+ if requested.any?
315
+ Tag.coincident_with(requested)
316
+ else
317
+ Tag.most_popular(limit)
318
+ end
319
+ end
320
+ end
321
+
322
+ # a bit of extra logic so that in the absence of any requested tags we default to all, not none
323
+
324
+ def _default_library_find_options
325
+ {
326
+ :by => 'created_at',
327
+ :order => 'desc'
328
+ }
329
+ end
330
+
331
+ def _get_pages(tag)
332
+ options = children_find_options(tag)
333
+ requested = _get_requested_tags(tag)
334
+ pages = Page.scoped(options)
335
+ pages = pages.tagged_with(requested) if requested.any?
336
+ pages
337
+ end
338
+
339
+ def _get_assets(tag)
340
+ options = asset_find_options(tag)
341
+ requested = _get_requested_tags(tag)
342
+ assets = Asset.scoped(options)
343
+ assets = assets.tagged_with(requested) if requested.any?
344
+ assets
345
+ end
346
+
347
+ # duplicate of children_find_options except:
348
+ # no virtual or status options
349
+ # defaults to chronological descending
350
+
351
+ def asset_find_options(tag)
352
+ attr = tag.attr.symbolize_keys
353
+
354
+ options = {}
355
+
356
+ [:limit, :offset].each do |symbol|
357
+ if number = attr[symbol]
358
+ if number =~ /^\d{1,4}$/
359
+ options[symbol] = number.to_i
360
+ else
361
+ raise TagError.new("`#{symbol}' attribute of `each' tag must be a positive number between 1 and 4 digits")
362
+ end
363
+ end
364
+ end
365
+
366
+ by = (attr[:by] || 'created_at').strip
367
+ order = (attr[:order] || 'desc').strip
368
+ order_string = ''
369
+ if self.attributes.keys.include?(by)
370
+ order_string << by
371
+ else
372
+ raise TagError.new("`by' attribute of `each' tag must be set to a valid field name")
373
+ end
374
+ if order =~ /^(asc|desc)$/i
375
+ order_string << " #{$1.upcase}"
376
+ else
377
+ raise TagError.new(%{`order' attribute of `each' tag must be set to either "asc" or "desc"})
378
+ end
379
+ options[:order] = order_string
380
+ options
381
+ end
382
+
383
+ end
384
+
385
+ end
386
+
387
+