radiant-taggable-extension 2.0.0 → 2.0.1

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