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.
@@ -1,60 +1,4 @@
1
1
  class LibraryPage < Page
2
- include WillPaginate::ViewHelpers
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.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"
@@ -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.
@@ -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
- Summarises in a sentence the list of tags currently active, with each one presented as a defaceting link.
234
- }
235
- tag 'tags:unlink_list' do |tag|
236
- requested = _get_requested_tags(tag)
237
- if requested.any?
238
- requested.map { |t|
239
- tag.locals.tag = t
240
- tag.render('tag:unlink', tag.attr.dup)
241
- }.to_sentence
242
- else
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 linkto='/library' /></code></pre>
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, "tag must be defined for tag:unlink tag" unless tag.locals.tag
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'] ||= 'detag'
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
- if tag.locals.page.is_a?(LibraryPage)
263
- href = tag.locals.page.url(tag.locals.page.requested_tags - [tag.locals.tag])
264
- elsif page_url = (options.delete('tagpage') || Radiant::Config['tags.page'])
265
- href = clean_url(page_url + '/-' + tag.locals.tag.clean_title)
266
- else
267
- href ||= Rack::Utils.escape("-#{tag.locals.tag.title}") + '/'
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
@@ -1,5 +1,5 @@
1
1
  module Taggable
2
- module Model # for inclusion into ActiveRecord::Base
2
+ module Model
3
3
 
4
4
  def self.included(base)
5
5
  base.extend ClassMethods
data/lib/taggable/page.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Taggable
2
- module Page # for inclusion into Page
2
+ module Page
3
3
 
4
4
  # here we have a few special cases for page tags.
5
5
  # because of the page tree
@@ -5,26 +5,37 @@ module Taggable
5
5
 
6
6
  base.class_eval {
7
7
 
8
- def find_page_with_tags(url)
9
- url = clean_url(url)
10
- page = find_page_without_tags(url)
11
- return page unless page.is_a?(LibraryPage)
12
- page.add_request_tags(Tag.in_this_list(params[:tag])) if params[:tag]
13
- raise LibraryPage::RedirectRequired, page.url unless page.url == url # to handle removal of tags and ensure consistent addressing. should also allow cache hit.
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 LibraryPage::RedirectRequired => e
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 clean_url(url)
27
- "/#{ url.strip }/".gsub(%r{//+}, '/')
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 add a default Cache-Control header with public and max-age of 5 minutes" do
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/furiously'
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/furiously'
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.find_by_url('/library/colourless')
14
+ @page = Page.find_by_path('/library/colourless')
15
15
  end
16
16
 
17
- it "should interrupt find_by_url" do
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.find_by_url('/library/colourless/green/ideas')
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.find_by_url('/library/colourless/green/ideas/-green')
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
@@ -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
- LibraryPage.send :include, Radius::LibraryTags #
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: 15
4
+ hash: 13
5
5
  prerelease:
6
6
  segments:
7
7
  - 2
8
8
  - 0
9
- - 0
10
- version: 2.0.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-09-06 00:00:00 +01:00
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.0'\n\n "
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.5.3
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