radiant-taggable-extension 1.2.5 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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