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,43 @@
1
+ module Taggable
2
+ module AdminUI
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+
7
+ attr_accessor :tag
8
+ alias_method :tags, :tag
9
+
10
+ def load_default_regions_with_tags
11
+ load_default_regions_without_tags
12
+ @tag = load_default_tag_regions
13
+ end
14
+
15
+ alias_method_chain :load_default_regions, :tags
16
+
17
+ protected
18
+
19
+ def load_default_tag_regions
20
+ returning OpenStruct.new do |tag|
21
+ tag.edit = Radiant::AdminUI::RegionSet.new do |edit|
22
+ edit.main.concat %w{edit_header edit_form}
23
+ edit.form.concat %w{edit_name edit_role edit_description}
24
+ edit.form_bottom.concat %w{edit_timestamp edit_buttons}
25
+ end
26
+ tag.show = Radiant::AdminUI::RegionSet.new do |show|
27
+ show.main.concat %w{show_header show_pages show_assets}
28
+ end
29
+ tag.index = Radiant::AdminUI::RegionSet.new do |index|
30
+ index.thead.concat %w{title_header link_header description_header usage_header modify_header}
31
+ index.tbody.concat %w{title_cell link_cell description_cell usage_cell modify_cell}
32
+ index.bottom.concat %w{new_button}
33
+ end
34
+ tag.remove = tag.index
35
+ tag.new = tag.edit
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+
43
+ end
@@ -0,0 +1,37 @@
1
+ module Taggable
2
+ module Asset
3
+
4
+ def self.included(base)
5
+ base.class_eval {
6
+ has_tags
7
+ named_scope :furniture, {:conditions => 'assets.furniture = 1'}
8
+ named_scope :not_furniture, {:conditions => 'assets.furniture = 0 or assets.furniture is null'}
9
+
10
+ extend Taggable::Asset::ClassMethods
11
+ include Taggable::Asset::InstanceMethods
12
+ }
13
+ end
14
+
15
+ module ClassMethods
16
+ end
17
+
18
+ module InstanceMethods
19
+
20
+ # just keeping compatibility with page tags
21
+ # so as to present the same interface
22
+
23
+ def keywords
24
+ self.attached_tags.map {|t| t.title}.join(', ')
25
+ end
26
+
27
+ def keywords=(somewords="")
28
+ self.attached_tags = Tag.from_list(somewords)
29
+ end
30
+
31
+ def keywords_before_type_cast # called by form_helper
32
+ keywords
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,144 @@
1
+ module Taggable
2
+ module Model # for inclusion into ActiveRecord::Base
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ base.class_eval {
7
+ @@taggable_models = []
8
+ cattr_accessor :taggable_models
9
+ }
10
+ end
11
+
12
+ module ClassMethods
13
+ def has_tags?
14
+ false
15
+ end
16
+
17
+ def has_tags
18
+ return if has_tags?
19
+
20
+ has_many :taggings, :as => :tagged
21
+ has_many :attached_tags, :through => :taggings, :source => :tag # can't be just has_many :tags because that stomps on the radius tags in Page.
22
+
23
+ named_scope :from_tag, lambda { |tag|
24
+ tag = Tag.find_by_title(tag) unless tag.is_a? Tag
25
+ {
26
+ :joins => "INNER JOIN taggings as tt on tt.tagged_id = #{self.table_name}.id AND tt.tagged_type = '#{self.to_s}'",
27
+ :conditions => ["tt.tag_id = ?", tag.id],
28
+ :readonly => false
29
+ }
30
+ }
31
+
32
+ named_scope :from_tags, lambda { |tags|
33
+ {
34
+ :joins => "INNER JOIN taggings as tt on tt.tagged_id = #{self.table_name}.id AND tt.tagged_type = '#{self.to_s}'",
35
+ :conditions => ["tt.tag_id in(#{tags.map{ '?' }.join(',')})"] + tags.map(&:id),
36
+ :group => column_names.map { |n| table_name + '.' + n }.join(','), # postgres is strict and requires that we group by all selected (but not aggregated) columns
37
+ :order => "count(tt.id) DESC",
38
+ :readonly => false
39
+ }
40
+ }
41
+
42
+ named_scope :from_all_tags, lambda { |tags|
43
+ {
44
+ :joins => "INNER JOIN taggings as tt on tt.tagged_id = #{self.table_name}.id AND tt.tagged_type = '#{self.to_s}'",
45
+ :conditions => ["tt.tag_id in(#{tags.map{ '?' }.join(',')})"] + tags.map(&:id),
46
+ :group => column_names.map { |n| table_name + '.' + n }.join(','), # postgres is strict and requires that we group by all selected (but not aggregated) columns
47
+ :having => "count(tt.id) >= #{tags.length}",
48
+ :readonly => false
49
+ }
50
+ } do
51
+ # count is badly sugared here: it omits the group and having clauses.
52
+ # length performs the query and looks at the array: less neat, but more right
53
+ # this gives us back any? and empty? as well.
54
+ def count
55
+ length
56
+ end
57
+ end
58
+
59
+ # creates eg. tag.pages, tag.assets
60
+ # (returning the from_tag scope defined above)
61
+ Tag.define_retrieval_methods(self.to_s)
62
+
63
+ class_eval {
64
+ extend Taggable::Model::TaggableClassMethods
65
+ include Taggable::Model::TaggableInstanceMethods
66
+ alias_method "related_#{self.to_s.underscore.pluralize}".intern, :related
67
+ alias_method "closely_related_#{self.to_s.underscore.pluralize}".intern, :closely_related
68
+ }
69
+
70
+ ActiveRecord::Base.taggable_models.push(self.to_s.intern)
71
+ end
72
+
73
+ alias :is_taggable :has_tags
74
+ alias :is_taggable? :has_tags?
75
+ end
76
+
77
+ module TaggableClassMethods
78
+ def tagged_with(somewords=[])
79
+ if somewords.is_a?(Tag)
80
+ self.from_tag(somewords)
81
+ elsif somewords.is_a?(Array)
82
+ self.from_all_tags(somewords)
83
+ else
84
+ self.from_all_tags( Tag.from_list(somewords) )
85
+ end
86
+ end
87
+
88
+ def has_tags?
89
+ true
90
+ end
91
+
92
+ def tags_for_cloud_from(these, limit=50)
93
+ Tag.attached_to(these).most_popular(limit) # here popularity is use-count *within the group*
94
+ end
95
+ end
96
+
97
+ module TaggableInstanceMethods
98
+
99
+ def add_tag(word=nil)
100
+ self.attached_tags << Tag.for(word) if word && !word.blank?
101
+ end
102
+
103
+ def remove_tag(word=nil)
104
+ tag = Tag.find_by_title(word) if word && !word.blank?
105
+ self.attached_tags.delete(tag) if tag
106
+ end
107
+
108
+ def related
109
+ self.attached_tags.empty? ? [] : self.class.from_tags(self.attached_tags) - [self]
110
+ end
111
+
112
+ def closely_related
113
+ self.attached_tags.empty? ? [] : self.class.from_all_tags(self.attached_tags) - [self]
114
+ end
115
+
116
+ # in the case of pages and anything else that keywords in the same way this overrides the existing column
117
+ # the rest of the time it's just another way of specifying tags.
118
+
119
+ def keywords
120
+ self.attached_tags.map {|t| t.title}.join(', ')
121
+ end
122
+
123
+ def keywords=(somewords="")
124
+ if somewords.blank?
125
+ self.attached_tags.clear
126
+ else
127
+ self.attached_tags = Tag.from_list(somewords)
128
+ end
129
+ end
130
+
131
+ def keywords_before_type_cast # for form_helper
132
+ keywords
133
+ end
134
+
135
+ def tags_from_keywords
136
+ if self.class.column_names.include?('keywords') && keys = read_attribute(:keywords)
137
+ self.attached_tags = Tag.from_list(keys)
138
+ end
139
+ end
140
+
141
+ end
142
+ end
143
+
144
+ end
@@ -0,0 +1,54 @@
1
+ module Taggable
2
+ module Page # for inclusion into Page
3
+
4
+ # here we have a few special cases for page tags.
5
+ # because of the page tree
6
+
7
+ def self.included(base)
8
+ base.class_eval {
9
+ has_tags
10
+ has_one :pointer, :class_name => 'Tag'
11
+ named_scope :children_of, lambda { |these|
12
+ { :conditions => ["parent_id IN (#{these.map{'?'}.join(',')})", *these.map{|t| t.id}] }
13
+ }
14
+ extend Taggable::Page::ClassMethods
15
+ include Taggable::Page::InstanceMethods
16
+ }
17
+ end
18
+
19
+ module ClassMethods
20
+ end
21
+
22
+ module InstanceMethods
23
+
24
+ def has_pointer?
25
+ !pointer.nil?
26
+ end
27
+
28
+ # note varying logic here: tag clouds are used differently when describing a group.
29
+ # if only one object is relevant, all of its tags will be equally (locally) important.
30
+ # Presumably that cloud should show global tag importance.
31
+ # If several objects are relevant, either from a list or a tree of descendants, we
32
+ # probably want to show local tag importance, ie prominence within that list.
33
+
34
+ def tags_for_cloud(limit=50, bands=6)
35
+ tags = Tag.attached_to(self.with_children).visible.most_popular(limit)
36
+ Tag.sized(tags, bands)
37
+ end
38
+
39
+ # the family-tree builder works with generations instead of individuals to cut down the number of retrieval steps
40
+
41
+ def with_children
42
+ this_generation = [self]
43
+ return this_generation unless self.respond_to?(:children) && self.children.any?
44
+ family = [self]
45
+ while this_generation.any? && next_generation = self.class.children_of(this_generation)
46
+ family.push(*next_generation)
47
+ this_generation = next_generation
48
+ end
49
+ family
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,33 @@
1
+ module Taggable
2
+ module SiteController
3
+
4
+ def self.included(base)
5
+
6
+ base.class_eval {
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.
14
+ page
15
+ end
16
+ alias_method_chain :find_page, :tags
17
+
18
+ def show_page_with_tags
19
+ show_page_without_tags
20
+ rescue LibraryPage::RedirectRequired => e
21
+ redirect_to e.message
22
+ end
23
+ alias_method_chain :show_page, :tags
24
+
25
+ protected
26
+ def clean_url(url)
27
+ "/#{ url.strip }/".gsub(%r{//+}, '/')
28
+ end
29
+
30
+ }
31
+ end
32
+ end
33
+ end
@@ -1,93 +1,31 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "radiant-taggable-extension"
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = %q{radiant-taggable-extension}
8
- s.version = "1.2.5"
6
+ s.name = "radiant-taggable-extension"
7
+ s.version = RadiantTaggableExtension::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = RadiantTaggableExtension::AUTHORS
10
+ s.email = RadiantTaggableExtension::EMAIL
11
+ s.homepage = RadiantTaggableExtension::URL
12
+ s.summary = RadiantTaggableExtension::SUMMARY
13
+ s.description = RadiantTaggableExtension::DESCRIPTION
9
14
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["spanner"]
12
- s.date = %q{2011-04-12}
13
- s.description = %q{General purpose tagging extension: more versatile but less focused than the tags extension}
14
- s.email = %q{will@spanner.org}
15
- s.extra_rdoc_files = [
16
- "README.md"
17
- ]
18
- s.files = [
19
- ".gitignore",
20
- "README.md",
21
- "Rakefile",
22
- "VERSION",
23
- "app/controllers/admin/taggings_controller.rb",
24
- "app/controllers/admin/tags_controller.rb",
25
- "app/helpers/taggable_helper.rb",
26
- "app/models/tag.rb",
27
- "app/models/tagging.rb",
28
- "app/views/admin/pages/_edit_title.html.haml",
29
- "app/views/admin/tags/_form.html.haml",
30
- "app/views/admin/tags/_search_results.html.haml",
31
- "app/views/admin/tags/cloud.html.haml",
32
- "app/views/admin/tags/edit.html.haml",
33
- "app/views/admin/tags/index.html.haml",
34
- "app/views/admin/tags/new.html.haml",
35
- "app/views/admin/tags/show.html.haml",
36
- "config/locales/en.yml",
37
- "config/routes.rb",
38
- "db/migrate/001_create_tags.rb",
39
- "db/migrate/002_import_keywords.rb",
40
- "db/migrate/20110316210834_structural_tags.rb",
41
- "db/migrate/20110411075109_metaphones.rb",
42
- "lib/natcmp.rb",
43
- "lib/radiant-taggable-extension.rb",
44
- "lib/taggable_admin_page_controller.rb",
45
- "lib/taggable_admin_ui.rb",
46
- "lib/taggable_model.rb",
47
- "lib/taggable_page.rb",
48
- "lib/taggable_tags.rb",
49
- "lib/tasks/taggable_extension_tasks.rake",
50
- "lib/text/double_metaphone.rb",
51
- "lib/text/metaphone.rb",
52
- "public/images/admin/new-tag.png",
53
- "public/images/admin/tag.png",
54
- "public/javascripts/admin/taggable.js",
55
- "public/javascripts/autocomplete.js",
56
- "public/stylesheets/sass/admin/taggable.sass",
57
- "public/stylesheets/sass/tagcloud.sass",
58
- "radiant-taggable-extension.gemspec",
59
- "spec/datasets/tag_sites_dataset.rb",
60
- "spec/datasets/tags_dataset.rb",
61
- "spec/lib/taggable_page_spec.rb",
62
- "spec/models/tag_spec.rb",
63
- "spec/spec.opts",
64
- "spec/spec_helper.rb",
65
- "taggable_extension.rb"
66
- ]
67
- s.homepage = %q{http://github.com/spanner/radiant-taggable-extension}
68
- s.rdoc_options = ["--charset=UTF-8"]
15
+ ignores = if File.exist?('.gitignore')
16
+ File.read('.gitignore').split("\n").inject([]) {|a,p| a + Dir[p] }
17
+ else
18
+ []
19
+ end
20
+ s.files = Dir['**/*'] - ignores
21
+ s.test_files = Dir['test/**/*','spec/**/*','features/**/*'] - ignores
22
+ # s.executables = Dir['bin/*'] - ignores
69
23
  s.require_paths = ["lib"]
70
- s.rubygems_version = %q{1.3.7}
71
- s.summary = %q{Taggable Extension for Radiant CMS}
72
- s.test_files = [
73
- "spec/datasets/tag_sites_dataset.rb",
74
- "spec/datasets/tags_dataset.rb",
75
- "spec/lib/taggable_page_spec.rb",
76
- "spec/models/tag_spec.rb",
77
- "spec/spec_helper.rb"
78
- ]
79
24
 
80
- if s.respond_to? :specification_version then
81
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
82
- s.specification_version = 3
25
+ s.post_install_message = %{
26
+ Add this to your radiant project with:
83
27
 
84
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
85
- s.add_runtime_dependency(%q<radiant>, [">= 0.9.0"])
86
- else
87
- s.add_dependency(%q<radiant>, [">= 0.9.0"])
88
- end
89
- else
90
- s.add_dependency(%q<radiant>, [">= 0.9.0"])
91
- end
92
- end
28
+ config.gem 'radiant-taggable-extension', :version => '~> #{RadiantTaggableExtension::VERSION}'
93
29
 
30
+ }
31
+ end
@@ -0,0 +1,96 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe SiteController do
4
+ dataset :tags
5
+
6
+ describe "on get to a library page" do
7
+ before do
8
+ get :show_page, :url => '/library/'
9
+ end
10
+
11
+ it "should render the tag page" do
12
+ response.should be_success
13
+ response.body.should == 'Shhhhh. body.'
14
+ end
15
+
16
+ describe "with tags in child position" do
17
+ before do
18
+ get :show_page, :url => '/library/colourless/green/'
19
+ end
20
+
21
+ it "should still render the tag page" do
22
+ response.should be_success
23
+ response.body.should == 'Shhhhh. body.'
24
+ end
25
+ end
26
+
27
+ describe "with tags in child position and missing final /" do
28
+ before do
29
+ get :show_page, :url => '/library/colourless/green'
30
+ end
31
+
32
+ it "should still render the tag page" do
33
+ response.should be_success
34
+ response.body.should == 'Shhhhh. body.'
35
+ end
36
+ end
37
+
38
+ describe "with tag negation" do
39
+ before do
40
+ get :show_page, :url => '/library/colourless/green/-colourless'
41
+ end
42
+
43
+ it "should redirect to the reduced address" do
44
+ response.should be_redirect
45
+ response.should redirect_to('http://test.host/library/green/')
46
+ end
47
+ end
48
+ end
49
+
50
+ describe "caching" do
51
+ describe "without tags requested" do
52
+ it "should add a default Cache-Control header with public and max-age of 5 minutes" do
53
+ get :show_page, :url => '/library/'
54
+ response.headers['Cache-Control'].should =~ /public/
55
+ response.headers['Cache-Control'].should =~ /max-age=300/
56
+ end
57
+
58
+ it "should pass along the etag set by the page" do
59
+ get :show_page, :url => '/library/'
60
+ response.headers['ETag'].should be
61
+ end
62
+
63
+ it "should return a not-modified response when the sent etag matches" do
64
+ response.stub!(:etag).and_return("foobar")
65
+ request.if_none_match = 'foobar'
66
+ get :show_page, :url => '/library/'
67
+ response.response_code.should == 304
68
+ response.body.should be_blank
69
+ end
70
+ end
71
+
72
+ describe "with tags requested" do
73
+ it "should add a default Cache-Control header with public and max-age of 5 minutes" do
74
+ get :show_page, :url => '/library/green/furiously'
75
+ response.headers['Cache-Control'].should =~ /public/
76
+ response.headers['Cache-Control'].should =~ /max-age=300/
77
+ end
78
+
79
+ it "should pass along the etag set by the page" do
80
+ get :show_page, :url => '/library/green/furiously'
81
+ response.headers['ETag'].should be
82
+ end
83
+
84
+ it "should return a not-modified response when the sent etag matches" do
85
+ response.stub!(:etag).and_return("foobar")
86
+ request.if_none_match = 'foobar'
87
+ get :show_page, :url => '/library/green/furiously'
88
+ response.response_code.should == 304
89
+ response.body.should be_blank
90
+ end
91
+ end
92
+
93
+
94
+ end
95
+
96
+ end
@@ -16,6 +16,8 @@ class TagsDataset < Dataset::Base
16
16
  apply_tag :ideas, pages(:first), pages(:another), pages(:grandchild)
17
17
  apply_tag :sleep, pages(:first)
18
18
  apply_tag :furiously, pages(:first)
19
+
20
+ create_page "library", :slug => "library", :class_name => 'LibraryPage', :body => 'Shhhhh.'
19
21
  end
20
22
 
21
23
  helpers do
@@ -4,7 +4,7 @@ describe Page do
4
4
  dataset :tags
5
5
 
6
6
  it "should report itself taggable" do
7
- Page.is_taggable?.should be_true
7
+ Page.has_tags?.should be_true
8
8
  end
9
9
 
10
10
  it "should return a list of pages form tag list" do
@@ -0,0 +1,47 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe LibraryPage do
4
+ dataset :tags
5
+
6
+ it "should be a Page" do
7
+ page = LibraryPage.new
8
+ page.is_a?(Page).should be_true
9
+ end
10
+
11
+ describe "on request" do
12
+ describe "with one tag" do
13
+ before do
14
+ @page = Page.find_by_url('/library/colourless')
15
+ end
16
+
17
+ it "should interrupt find_by_url" do
18
+ @page.should == pages(:library)
19
+ @page.is_a?(LibraryPage).should be_true
20
+ end
21
+
22
+ it "should set tag context correctly" do
23
+ @page.requested_tags.should == [tags(:colourless)]
24
+ end
25
+ end
26
+
27
+ describe "with several tags" do
28
+ before do
29
+ @page = Page.find_by_url('/library/colourless/green/ideas')
30
+ end
31
+ it "should set tag context correctly" do
32
+ @page.requested_tags.should == [tags(:colourless), tags(:green), tags(:ideas)]
33
+ end
34
+ end
35
+
36
+ describe "with several tags and one tag negation" do
37
+ before do
38
+ @page = Page.find_by_url('/library/colourless/green/ideas/-green')
39
+ end
40
+ it "should set tag context correctly" do
41
+ @page.requested_tags.should == [tags(:colourless), tags(:ideas)]
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -1,19 +1,25 @@
1
+ require_dependency 'application_controller'
2
+ require "radiant-taggable-extension"
3
+
1
4
  class TaggableExtension < Radiant::Extension
2
- version "1.2.5"
3
- description "General purpose tagging and retrieval extension: more versatile but less focused than the tags extension"
4
- url "http://github.com/spanner/radiant-taggable-extension"
5
+ version RadiantTaggableExtension::VERSION
6
+ description RadiantTaggableExtension::DESCRIPTION
7
+ url RadiantTaggableExtension::URL
5
8
 
6
9
  def activate
7
- require 'natcmp' # a natural sort algorithm. possibly not that efficient.
8
- ActiveRecord::Base.send :include, TaggableModel # provide is_taggable for everything but don't call it for anything
9
- Page.send :is_taggable # make pages taggable
10
- Page.send :include, TaggablePage # then fake the keywords column and add some inheritance
11
- Page.send :include, TaggableTags # and the basic radius tags for showing page tags and tag pages
12
- Admin::PagesController.send :include, TaggableAdminPageController # tweak the admin interface to make page tags more prominent
13
- UserActionObserver.instance.send :add_observer!, Tag # tags get creator-stamped
10
+ require 'natcmp' # a natural sort algorithm. possibly not that efficient.
11
+ ActiveRecord::Base.send :include, Taggable::Model # provide has_tags for everything but don't call it for anything
12
+ Page.send :include, Taggable::Page # pages are taggable (and the keywords column is overridden)
13
+ Asset.send :include, Taggable::Asset # assets are taggable (and a fake keywords column is provided)
14
+ Page.send :include, Radius::TaggableTags # adds the basic radius tags for showing page tags and tag pages
15
+ Page.send :include, Radius::AssetTags # adds some asset:* tags
16
+ LibraryPage.send :include, Radius::LibraryTags #
17
+ SiteController.send :include, Taggable::SiteController # some path and parameter handling in support of library pages
18
+ Admin::PagesController.send :include, Taggable::AdminPagesController # tweaks the admin interface to make page tags more prominent
19
+ UserActionObserver.instance.send :add_observer!, Tag # tags get creator-stamped
14
20
 
15
21
  unless defined? admin.tag
16
- Radiant::AdminUI.send :include, TaggableAdminUI
22
+ Radiant::AdminUI.send :include, Taggable::AdminUI
17
23
  admin.tag = Radiant::AdminUI.load_default_tag_regions
18
24
  end
19
25