radiant-taggable-extension 2.0.0 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/app/models/library_page.rb +1 -57
- data/lib/radiant-taggable-extension.rb +1 -1
- data/lib/radius/asset_tags.rb +14 -0
- data/lib/radius/library_tags.rb +57 -26
- data/lib/taggable/faceted_page.rb +98 -0
- data/lib/taggable/model.rb +1 -1
- data/lib/taggable/page.rb +1 -1
- data/lib/taggable/site_controller.rb +20 -9
- data/spec/controllers/site_controller_spec.rb +9 -3
- data/spec/models/library_page_spec.rb +8 -4
- data/taggable_extension.rb +1 -1
- metadata +7 -8
data/app/models/library_page.rb
CHANGED
@@ -1,60 +1,4 @@
|
|
1
1
|
class LibraryPage < Page
|
2
|
-
include
|
3
|
-
|
4
|
-
class RedirectRequired < StandardError
|
5
|
-
def initialize(message = nil); super end
|
6
|
-
end
|
7
|
-
|
2
|
+
include Taggable::FacetedPage
|
8
3
|
description %{ Takes tag names in child position or as paramaters so that tagged items can be listed. }
|
9
|
-
|
10
|
-
attr_accessor :requested_tags, :strict_match
|
11
|
-
|
12
|
-
def cache?
|
13
|
-
true
|
14
|
-
end
|
15
|
-
|
16
|
-
def find_by_url(url, live = true, clean = false)
|
17
|
-
url = clean_url(url) if clean
|
18
|
-
my_url = self.url
|
19
|
-
return false unless url =~ /^#{Regexp.quote(my_url)}(.*)/
|
20
|
-
tags = $1.split('/')
|
21
|
-
if slug_child = children.find_by_slug(tags[0])
|
22
|
-
found = slug_child.find_by_url(url, live, clean)
|
23
|
-
return found if found
|
24
|
-
end
|
25
|
-
remove_tags, add_tags = tags.partition{|t| t.first == '-'}
|
26
|
-
add_request_tags(add_tags)
|
27
|
-
remove_request_tags(remove_tags)
|
28
|
-
self
|
29
|
-
end
|
30
|
-
|
31
|
-
def add_request_tags(tags=[])
|
32
|
-
if tags.any?
|
33
|
-
tags.collect! { |tag| Tag.find_by_title(Rack::Utils::unescape(tag)) }
|
34
|
-
self.requested_tags = (self.requested_tags + tags.select{|t| !t.nil?}).uniq
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def remove_request_tags(tags=[])
|
39
|
-
if tags.any?
|
40
|
-
tags.collect! { |tag|
|
41
|
-
tag.slice!(0) if tag.first == '-'
|
42
|
-
Tag.find_by_title(Rack::Utils::unescape(tag))
|
43
|
-
}
|
44
|
-
self.requested_tags = (self.requested_tags - tags.select{|t| !t.nil?}).uniq
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def requested_tags
|
49
|
-
@requested_tags ||= []
|
50
|
-
end
|
51
|
-
|
52
|
-
# this isn't very pleasing but it's the best way to let the controller know
|
53
|
-
# of our real address once tags have been added and removed.
|
54
|
-
|
55
|
-
def url_with_tags(tags = requested_tags)
|
56
|
-
clean_url( url_without_tags + '/' + tags.uniq.map(&:clean_title).to_param )
|
57
|
-
end
|
58
|
-
alias_method_chain :url, :tags
|
59
|
-
|
60
4
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module RadiantTaggableExtension
|
2
|
-
VERSION = '2.0.
|
2
|
+
VERSION = '2.0.1'
|
3
3
|
SUMMARY = %q{Tagging, clouding and faceting extension for Radiant CMS}
|
4
4
|
DESCRIPTION = %q{General purpose tagging and retrieval extension: more versatile but less focused than the tags extension. A good way to support faceted search.}
|
5
5
|
URL = "http://spanner.org/radiant/taggable"
|
data/lib/radius/asset_tags.rb
CHANGED
@@ -26,6 +26,20 @@ module Radius
|
|
26
26
|
tag 'asset:tags:each' do |tag|
|
27
27
|
tag.render('tags:each', tag.attr.dup, &tag.block)
|
28
28
|
end
|
29
|
+
|
30
|
+
desc %{
|
31
|
+
Expands if the present asset has any tags.
|
32
|
+
}
|
33
|
+
tag 'asset:if_tags' do |tag|
|
34
|
+
tag.expand if tag.locals.asset.attached_tags.any?
|
35
|
+
end
|
36
|
+
|
37
|
+
desc %{
|
38
|
+
Expands if the present asset has no tags.
|
39
|
+
}
|
40
|
+
tag 'asset:unless_tags' do |tag|
|
41
|
+
tag.expand unless tag.locals.asset.attached_tags.any?
|
42
|
+
end
|
29
43
|
|
30
44
|
desc %{
|
31
45
|
Lists all the assets similar to this asset (based on its tagging), in descending order of relatedness.
|
data/lib/radius/library_tags.rb
CHANGED
@@ -227,47 +227,78 @@ module Radius
|
|
227
227
|
end
|
228
228
|
end
|
229
229
|
|
230
|
-
############### extra tags:* tags that only make sense on library pages
|
230
|
+
############### extra tags:* tags that only make sense on library or other faceting pages
|
231
231
|
|
232
232
|
desc %{
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
233
|
+
Makes a link that adds the current tag to the active set. Other options are passed through as usual.
|
234
|
+
If the present page is not a library page, or similar, the link will be directed to the first available
|
235
|
+
library page. You can override this behavior by specifying a 'base' parameter, which will force that
|
236
|
+
path prefix regardless of what page it designates, if any. Remote urls are also possible.
|
237
|
+
|
238
|
+
*Usage:*
|
239
|
+
<pre><code><r:tag:link base='/library' /></code></pre>
|
240
|
+
}
|
241
|
+
tag 'tag:link' do |tag|
|
242
|
+
options = tag.attr.dup
|
243
|
+
options['class'] ||= 'facet'
|
244
|
+
anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
|
245
|
+
attributes = options.inject(' ') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
|
246
|
+
text = tag.double? ? tag.expand : tag.render('tag:name')
|
247
|
+
if tag.locals.page.respond_to?(:requested_tags)
|
248
|
+
href = tag.locals.page.path(tag.locals.page.requested_tags + tag.locals.tag)
|
249
|
+
elsif base_path = options.delete('base')
|
250
|
+
href = clean_path(base_path + '/-' + tag.locals.tag.clean_title)
|
251
|
+
elsif library = LibraryPage.first
|
252
|
+
href = library.path([tag.locals.tag])
|
253
|
+
else
|
254
|
+
raise TagError "cannot find a LibraryPage to link to."
|
244
255
|
end
|
256
|
+
|
257
|
+
%{<a href="#{href}#{anchor}"#{attributes}>#{text}</a>}
|
258
|
+
end
|
259
|
+
|
260
|
+
desc %{
|
261
|
+
Summarises in a sentence the list of attached, with each one presented as a faceting link.
|
262
|
+
}
|
263
|
+
tag 'tags:link_list' do |tag|
|
264
|
+
_get_requested_tags(tag).map { |t|
|
265
|
+
tag.locals.tag = t
|
266
|
+
tag.render('tag:link', tag.attr.dup)
|
267
|
+
}.join(' | ')
|
245
268
|
end
|
246
269
|
|
247
270
|
desc %{
|
248
|
-
Makes a link that removes the current tag from the active set. Other options as for tag:link.
|
271
|
+
Makes a link that removes the current tag from the active set. Other options as for tag:link.
|
272
|
+
This only really makes sense in the context of a requested_tags list, since no other tag can be unlinked.
|
249
273
|
|
250
274
|
*Usage:*
|
251
|
-
<pre><code><r:tag:unlink
|
275
|
+
<pre><code><r:requested_tags:each><r:tag:unlink base='/library' /></r:requested_tags:each></code></pre>
|
252
276
|
}
|
253
277
|
tag 'tag:unlink' do |tag|
|
254
|
-
raise TagError
|
278
|
+
raise TagError "unlinking a tag requires a library page or similar" unless tag.locals.page.respond_to?(:requested_tags)
|
279
|
+
|
255
280
|
options = tag.attr.dup
|
256
|
-
options['class'] ||= '
|
281
|
+
options['class'] ||= 'defacet'
|
257
282
|
anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
|
258
|
-
attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
|
259
|
-
attributes = " #{attributes}" unless attributes.empty?
|
283
|
+
attributes = options.inject(' ') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
|
260
284
|
text = tag.double? ? tag.expand : tag.render('tag:name')
|
285
|
+
href = tag.locals.page.path(tag.locals.page.requested_tags - [tag.locals.tag])
|
286
|
+
%{<a href="#{href}#{anchor}"#{attributes}>#{text}</a>}
|
287
|
+
end
|
261
288
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
289
|
+
desc %{
|
290
|
+
Summarises in a sentence the list of tags currently active, with each one presented as a defaceting link.
|
291
|
+
}
|
292
|
+
tag 'tags:unlink_list' do |tag|
|
293
|
+
requested = _get_requested_tags(tag)
|
294
|
+
if requested.any?
|
295
|
+
requested.map { |t|
|
296
|
+
tag.locals.tag = t
|
297
|
+
tag.render('tag:unlink', tag.attr.dup)
|
298
|
+
}.to_sentence
|
299
|
+
else
|
300
|
+
""
|
268
301
|
end
|
269
|
-
|
270
|
-
%{<a href="#{href}#{anchor}"#{attributes}>#{text}</a>}
|
271
302
|
end
|
272
303
|
|
273
304
|
private
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Taggable
|
2
|
+
class RedirectRequired < StandardError
|
3
|
+
def initialize(message = nil); super end
|
4
|
+
end
|
5
|
+
|
6
|
+
module FacetedPage
|
7
|
+
|
8
|
+
# This module can be mixed into any Page subclass to give it mystical web 2.0 faceting powers.
|
9
|
+
# Any nonexistent child path is assumed to be a tag set, and methods are provided for reading
|
10
|
+
# that set, and adding and removing tags.
|
11
|
+
#
|
12
|
+
def self.included(base)
|
13
|
+
base.extend ClassMethods
|
14
|
+
base.class_eval {
|
15
|
+
include InstanceMethods
|
16
|
+
alias_method_chain :path, :tags
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
end
|
22
|
+
|
23
|
+
module InstanceMethods
|
24
|
+
|
25
|
+
# Faceted pages map nicely onto urls and are cacheable.
|
26
|
+
# There can be redundancy if tags are specified in varying order,
|
27
|
+
# but the site controller tries to normalize everything.
|
28
|
+
#
|
29
|
+
def cache?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
# We override the normal Page#find_by_path mechanism and treat nonexistent child paths
|
34
|
+
# are understood as tag sets. Note that the extended site_controller will add to this set
|
35
|
+
# any tags that have been supplied in query string parameters. This may trigger a redirect
|
36
|
+
# to bring the request path into line with the consolidated tag set.
|
37
|
+
#
|
38
|
+
def find_by_path(path, live = true, clean = false)
|
39
|
+
path = clean_path(path) if clean
|
40
|
+
return false unless path =~ /^#{Regexp.quote(self.path)}(.*)/
|
41
|
+
tags = $1.split('/')
|
42
|
+
if slug_child = children.find_by_slug(tags[0])
|
43
|
+
found = slug_child.find_by_url(path, live, clean)
|
44
|
+
return found if found
|
45
|
+
end
|
46
|
+
remove_tags, add_tags = tags.partition{|t| t.first == '-'}
|
47
|
+
add_request_tags(add_tags)
|
48
|
+
remove_request_tags(remove_tags)
|
49
|
+
self
|
50
|
+
end
|
51
|
+
alias_method :find_by_url, :find_by_path
|
52
|
+
|
53
|
+
# The set of tags attached to the page request.
|
54
|
+
#
|
55
|
+
def requested_tags
|
56
|
+
@requested_tags ||= []
|
57
|
+
end
|
58
|
+
|
59
|
+
# The normal `path` method is extended to append the (sorted and deduped) tags attached to the page request
|
60
|
+
#
|
61
|
+
def path_with_tags(tags = requested_tags)
|
62
|
+
clean_path( path_without_tags + '/' + tags.uniq.sort.map(&:clean_title).to_param )
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# @requested_tags is the set of Tag objects attached to the page request.
|
68
|
+
#
|
69
|
+
def requested_tags=(tags)
|
70
|
+
@requested_tags = tags
|
71
|
+
end
|
72
|
+
|
73
|
+
# We hold in memory a list of the tags that were appended to the path when this page was selected.
|
74
|
+
# This method adds tags to that list. It is normally called only once, to populate the list.
|
75
|
+
#
|
76
|
+
def add_request_tags(tags=[])
|
77
|
+
if tags.any?
|
78
|
+
tags.collect! { |tag| Tag.find_by_title(Rack::Utils::unescape(tag)) }
|
79
|
+
self.requested_tags = (self.requested_tags + tags.select{|t| !t.nil?}).uniq
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# This method removes tags from the appended list. Normally only used to handle defaceting links
|
84
|
+
# (in the form /path/to/page/tag1/tag2/tag3/-tag2).
|
85
|
+
#
|
86
|
+
def remove_request_tags(tags=[])
|
87
|
+
if tags.any?
|
88
|
+
tags.collect! { |tag|
|
89
|
+
tag.slice!(0) if tag.first == '-'
|
90
|
+
Tag.find_by_title(Rack::Utils::unescape(tag))
|
91
|
+
}
|
92
|
+
self.requested_tags = (self.requested_tags - tags.compact).uniq
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
data/lib/taggable/model.rb
CHANGED
data/lib/taggable/page.rb
CHANGED
@@ -5,26 +5,37 @@ module Taggable
|
|
5
5
|
|
6
6
|
base.class_eval {
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
# We override find_page to handle the combination of url tags and parameter tags.
|
9
|
+
# If this results in a change to the compound path for this page/tag combination,
|
10
|
+
# we raise a RedirectRequired error.
|
11
|
+
#
|
12
|
+
def find_page_with_tags(path)
|
13
|
+
path = clean_path(path)
|
14
|
+
page = find_page_without_tags(path)
|
15
|
+
if page.respond_to?(:requested_tags)
|
16
|
+
page.add_request_tags(Tag.in_this_list(params[:tag])) if params[:tag]
|
17
|
+
raise Taggable::RedirectRequired, page.path unless page.path == path
|
18
|
+
end
|
14
19
|
page
|
15
20
|
end
|
16
21
|
alias_method_chain :find_page, :tags
|
17
22
|
|
23
|
+
# Extends show_page to catch any redirect error raised by find_page, and redirect to the specified path.
|
24
|
+
# The effect of this is to standardise all tag-appended requests, which improves cache performance and
|
25
|
+
# allows us to defacet by adding -tag parameters.
|
26
|
+
#
|
27
|
+
# (I can't remember why that was necessary: it may no longer be, so don't rely on it!).
|
28
|
+
#
|
18
29
|
def show_page_with_tags
|
19
30
|
show_page_without_tags
|
20
|
-
rescue
|
31
|
+
rescue Taggable::RedirectRequired => e
|
21
32
|
redirect_to e.message
|
22
33
|
end
|
23
34
|
alias_method_chain :show_page, :tags
|
24
35
|
|
25
36
|
protected
|
26
|
-
def
|
27
|
-
"/#{
|
37
|
+
def clean_path(path)
|
38
|
+
"/#{ path.strip }/".gsub(%r{//+}, '/')
|
28
39
|
end
|
29
40
|
|
30
41
|
}
|
@@ -70,21 +70,27 @@ describe SiteController do
|
|
70
70
|
end
|
71
71
|
|
72
72
|
describe "with tags requested" do
|
73
|
-
it "should
|
73
|
+
it "should redirect to a normalised form of the requested address" do
|
74
74
|
get :show_page, :url => '/library/green/furiously'
|
75
|
+
response.should be_redirect
|
76
|
+
response.should redirect_to 'http://test.host/library/furiously/green'
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should add a default Cache-Control header with public and max-age of 5 minutes" do
|
80
|
+
get :show_page, :url => '/library/furiously/green'
|
75
81
|
response.headers['Cache-Control'].should =~ /public/
|
76
82
|
response.headers['Cache-Control'].should =~ /max-age=300/
|
77
83
|
end
|
78
84
|
|
79
85
|
it "should pass along the etag set by the page" do
|
80
|
-
get :show_page, :url => '/library/green
|
86
|
+
get :show_page, :url => '/library/furiously/green'
|
81
87
|
response.headers['ETag'].should be
|
82
88
|
end
|
83
89
|
|
84
90
|
it "should return a not-modified response when the sent etag matches" do
|
85
91
|
response.stub!(:etag).and_return("foobar")
|
86
92
|
request.if_none_match = 'foobar'
|
87
|
-
get :show_page, :url => '/library/green
|
93
|
+
get :show_page, :url => '/library/furiously/green'
|
88
94
|
response.response_code.should == 304
|
89
95
|
response.body.should be_blank
|
90
96
|
end
|
@@ -11,12 +11,14 @@ describe LibraryPage do
|
|
11
11
|
describe "on request" do
|
12
12
|
describe "with one tag" do
|
13
13
|
before do
|
14
|
-
@page = Page.
|
14
|
+
@page = Page.find_by_path('/library/colourless')
|
15
15
|
end
|
16
16
|
|
17
|
-
it "should interrupt
|
17
|
+
it "should interrupt find_by_path" do
|
18
|
+
@page.should_not be_nil
|
18
19
|
@page.should == pages(:library)
|
19
20
|
@page.is_a?(LibraryPage).should be_true
|
21
|
+
@page.respond_to?(:requested_tags).should be_true
|
20
22
|
end
|
21
23
|
|
22
24
|
it "should set tag context correctly" do
|
@@ -26,18 +28,20 @@ describe LibraryPage do
|
|
26
28
|
|
27
29
|
describe "with several tags" do
|
28
30
|
before do
|
29
|
-
@page = Page.
|
31
|
+
@page = Page.find_by_path('/library/colourless/green/ideas')
|
30
32
|
end
|
31
33
|
it "should set tag context correctly" do
|
34
|
+
@page.should_not be_nil
|
32
35
|
@page.requested_tags.should == [tags(:colourless), tags(:green), tags(:ideas)]
|
33
36
|
end
|
34
37
|
end
|
35
38
|
|
36
39
|
describe "with several tags and one tag negation" do
|
37
40
|
before do
|
38
|
-
@page = Page.
|
41
|
+
@page = Page.find_by_path('/library/colourless/green/ideas/-green')
|
39
42
|
end
|
40
43
|
it "should set tag context correctly" do
|
44
|
+
@page.should_not be_nil
|
41
45
|
@page.requested_tags.should == [tags(:colourless), tags(:ideas)]
|
42
46
|
end
|
43
47
|
end
|
data/taggable_extension.rb
CHANGED
@@ -13,7 +13,7 @@ class TaggableExtension < Radiant::Extension
|
|
13
13
|
Asset.send :include, Taggable::Asset # assets are taggable (and a fake keywords column is provided)
|
14
14
|
Page.send :include, Radius::TaggableTags # adds the basic radius tags for showing page tags and tag pages
|
15
15
|
Page.send :include, Radius::AssetTags # adds some asset:* tags
|
16
|
-
|
16
|
+
Page.send :include, Radius::LibraryTags
|
17
17
|
SiteController.send :include, Taggable::SiteController # some path and parameter handling in support of library pages
|
18
18
|
Admin::PagesController.send :include, Taggable::AdminPagesController # tweaks the admin interface to make page tags more prominent
|
19
19
|
UserActionObserver.instance.send :add_observer!, Tag # tags get creator-stamped
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: radiant-taggable-extension
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 13
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 2
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 2.0.
|
9
|
+
- 1
|
10
|
+
version: 2.0.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- William Ross
|
@@ -15,8 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
19
|
-
default_executable:
|
18
|
+
date: 2011-10-05 00:00:00 Z
|
20
19
|
dependencies:
|
21
20
|
- !ruby/object:Gem::Dependency
|
22
21
|
name: sanitize
|
@@ -76,6 +75,7 @@ files:
|
|
76
75
|
- lib/taggable/admin_pages_controller.rb
|
77
76
|
- lib/taggable/admin_ui.rb
|
78
77
|
- lib/taggable/asset.rb
|
78
|
+
- lib/taggable/faceted_page.rb
|
79
79
|
- lib/taggable/model.rb
|
80
80
|
- lib/taggable/page.rb
|
81
81
|
- lib/taggable/site_controller.rb
|
@@ -100,11 +100,10 @@ files:
|
|
100
100
|
- spec/spec.opts
|
101
101
|
- spec/spec_helper.rb
|
102
102
|
- taggable_extension.rb
|
103
|
-
has_rdoc: true
|
104
103
|
homepage: http://spanner.org/radiant/taggable
|
105
104
|
licenses: []
|
106
105
|
|
107
|
-
post_install_message: "\n Add this to your Gemfile with:\n\n gem 'radiant-taggable-extension', '~> 2.0.
|
106
|
+
post_install_message: "\n Add this to your Gemfile with:\n\n gem 'radiant-taggable-extension', '~> 2.0.1'\n\n "
|
108
107
|
rdoc_options: []
|
109
108
|
|
110
109
|
require_paths:
|
@@ -130,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
129
|
requirements: []
|
131
130
|
|
132
131
|
rubyforge_project:
|
133
|
-
rubygems_version: 1.
|
132
|
+
rubygems_version: 1.8.10
|
134
133
|
signing_key:
|
135
134
|
specification_version: 3
|
136
135
|
summary: Tagging, clouding and faceting extension for Radiant CMS
|