radiant-taggable-extension 1.2.5 → 2.0.0.rc1

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