radiant-taggable-extension 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +125 -0
- data/Rakefile +136 -0
- data/VERSION +1 -0
- data/app/controllers/admin/taggings_controller.rb +8 -0
- data/app/controllers/admin/tags_controller.rb +17 -0
- data/app/models/tag.rb +200 -0
- data/app/models/tagging.rb +50 -0
- data/app/views/admin/pages/_edit_title.html.haml +28 -0
- data/app/views/admin/tags/_form.html.haml +23 -0
- data/app/views/admin/tags/_search_results.html.haml +10 -0
- data/app/views/admin/tags/cloud.html.haml +17 -0
- data/app/views/admin/tags/edit.html.haml +14 -0
- data/app/views/admin/tags/index.html.haml +53 -0
- data/app/views/admin/tags/new.html.haml +13 -0
- data/app/views/admin/tags/show.html.haml +32 -0
- data/config/routes.rb +6 -0
- data/db/migrate/001_create_tags.rb +26 -0
- data/db/migrate/002_import_keywords.rb +11 -0
- data/lib/taggable_admin_page_controller.rb +18 -0
- data/lib/taggable_admin_ui.rb +41 -0
- data/lib/taggable_model.rb +128 -0
- data/lib/taggable_page.rb +48 -0
- data/lib/taggable_tags.rb +456 -0
- data/lib/tasks/taggable_extension_tasks.rake +55 -0
- data/pkg/radiant-taggable-extension-1.2.0.gem +0 -0
- data/public/images/admin/new-tag.png +0 -0
- data/public/images/admin/tag.png +0 -0
- data/public/images/furniture/detag.png +0 -0
- data/public/stylesheets/admin/tags.css +20 -0
- data/public/stylesheets/sass/tagcloud.sass +29 -0
- data/radiant-taggable-extension.gemspec +84 -0
- data/spec/datasets/tag_sites_dataset.rb +9 -0
- data/spec/datasets/tags_dataset.rb +43 -0
- data/spec/lib/taggable_page_spec.rb +93 -0
- data/spec/models/tag_spec.rb +39 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +36 -0
- data/taggable_extension.rb +36 -0
- metadata +123 -0
data/README.md
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# Taggable
|
2
|
+
|
3
|
+
This is another way to apply tags to objects in your radiant site and retrieve objects by tag. If you're looking at this you will also want to look at the [tags](http://github.com/jomz/radiant-tags-extension/tree) extension, which does a good job of tag-clouding and may be all you need, and at our [library](https://github.com/spanner/radiant-library-extension) which uses this functionality to make an image gallery and document library and may be a useful starting point for other extensions.
|
4
|
+
|
5
|
+
## Why?
|
6
|
+
|
7
|
+
This extension differs from `tags` in a few ways that matter to me but may not to you:
|
8
|
+
|
9
|
+
* We're not so focused on tag clouds - though you can still make them - but more on archival and linking functions.
|
10
|
+
* We replace the keywords mechanism on pages rather than adding another one.
|
11
|
+
* Anything can be tagged. By default we only do pages but other extensions can participate with a single line in a model class. See the [taggable_events](https://github.com/spanner/radiant-taggable_events-extension) extension for a minimal example or just put `is_taggable` at the top of a model class and see what happens.
|
12
|
+
* We don't use `has_many_polymorphs` (it burns!)
|
13
|
+
* Or any of the tagging libraries: it only takes a few named_scopes
|
14
|
+
* it's multi-site compatible: if our fork is installed then you get site-scoped tags and taggings.
|
15
|
+
|
16
|
+
When you first install the extension you shouldn't see much difference: all we do out of the box is to take over (and make more prominent) the keywords field in the page-edit view.
|
17
|
+
|
18
|
+
## New
|
19
|
+
|
20
|
+
I've just stripped out quite a lot of display clutter in order to focus on the basic tagging mechanism here. Retrieval and display is now handled by the [library](http://example.com/) extension. The core radius tags remain here. Anything that used to refer to a tag page is probably now handled by the library page.
|
21
|
+
|
22
|
+
## Status
|
23
|
+
|
24
|
+
The underlying code is fairly well broken-in and has been in production for a couple of years, but I've rearranged it quite drastically and the interface is all new. There are tests now: not detailed but with reasonable coverage. Silly mistakes are getting less likely.
|
25
|
+
|
26
|
+
## Efficiency
|
27
|
+
|
28
|
+
Not too bad, I think. Most of the heavy retrieval functions have been squashed down into single queries. Each of these:
|
29
|
+
|
30
|
+
Tag.most_popular(50)
|
31
|
+
Tag.coincident_with(tag1, tag2, tag3)
|
32
|
+
Page.tagged_with(tag1, tag2, tag3)
|
33
|
+
Page.related_pages # equivalent to Page.tagged_with(self.attached_tags) - [self]
|
34
|
+
|
35
|
+
is handled in a single pass.
|
36
|
+
|
37
|
+
The exception is the `r:tag_cloud` tag: there we have to gather a list of descendant pages first. It's done in a fairly frugal way (by generation rather than individual) but still likely to involve several preparatory queries as well as the cloud computation.
|
38
|
+
|
39
|
+
## Radius tags
|
40
|
+
|
41
|
+
This extension creates several radius tags. There are two kinds:
|
42
|
+
|
43
|
+
### presenting tag information
|
44
|
+
|
45
|
+
are used in the usual to display the properties and associations of a given tag (which can be supplied to a library as a query parameter or just specified in the radius tag)
|
46
|
+
|
47
|
+
<r:tag:title />
|
48
|
+
<r:tag:description />
|
49
|
+
<r:tag:pages:each>...</r:tag:pages:each>
|
50
|
+
|
51
|
+
currently only available in a tag cloud (or a `top_tags` list):
|
52
|
+
|
53
|
+
<r:tag:use_count />
|
54
|
+
|
55
|
+
### presenting page information
|
56
|
+
|
57
|
+
These display the tag-associations of a given page.
|
58
|
+
|
59
|
+
<r:if_tags>...</r:if_tags>
|
60
|
+
<r:unless_tags>...</r:unless_tags>
|
61
|
+
<r:tags:each>...</r:tags:each>
|
62
|
+
<r:related_pages:each>...</r:related_pages:each>
|
63
|
+
<r:tag_cloud [url=""] />
|
64
|
+
|
65
|
+
The library extension adds a lot more ways to retrieve lists of tags and tagged objects, and to work with assets in the same way as we do here with pages.
|
66
|
+
|
67
|
+
## Note about tag cloud prominence
|
68
|
+
|
69
|
+
The calculation of prominence here applies a logarithmic curve to create a more even distribution of weight. It's continuous rather than banded, and sets the font size and opacity for each tag in a style attribute.
|
70
|
+
|
71
|
+
## Usage examples
|
72
|
+
|
73
|
+
Add tags to your pages by putting a comma-separated list in the 'keywords' box. That's about to get more helpful.
|
74
|
+
|
75
|
+
### To show related pages:
|
76
|
+
|
77
|
+
Put this in your layout:
|
78
|
+
|
79
|
+
<r:if_tags>
|
80
|
+
<h3>See also</h3>
|
81
|
+
<ul>
|
82
|
+
<r:related_pages.each>
|
83
|
+
<li><r:link /></li>
|
84
|
+
<r:related_pages.each>
|
85
|
+
</ul>
|
86
|
+
</r:if_tags>
|
87
|
+
|
88
|
+
### To display a tag cloud on a section front page:
|
89
|
+
|
90
|
+
Include the sample tagcloud.css in your styles and put this somewhere in the page or layout:
|
91
|
+
|
92
|
+
<r:tag_cloud />
|
93
|
+
|
94
|
+
Seek venture capital immediately.
|
95
|
+
|
96
|
+
## Next steps
|
97
|
+
|
98
|
+
* auto-completer to improve tagging consistency.
|
99
|
+
|
100
|
+
## Requirements
|
101
|
+
|
102
|
+
* Radiant 0.8.1
|
103
|
+
* `will_paginate` gem
|
104
|
+
|
105
|
+
This is no longer compatible with 0.7 because we're doing a lot of :having in the scopes and you need rails 2.3 for that.
|
106
|
+
|
107
|
+
## Installation
|
108
|
+
|
109
|
+
As usual:
|
110
|
+
|
111
|
+
git clone git://github.com/spanner/radiant-taggable-extension.git vendor/extensions/taggable
|
112
|
+
rake radiant:extensions:taggable:migrate
|
113
|
+
rake radiant:extensions:taggable:update
|
114
|
+
|
115
|
+
The update task will bring over a couple of CSS files for styling tags but you'll want to improve those.
|
116
|
+
|
117
|
+
## Bugs
|
118
|
+
|
119
|
+
Very likely. [Github issues](http://github.com/spanner/radiant-taggable-extension/issues), please, or for little things an email or github message is fine.
|
120
|
+
|
121
|
+
## Author and copyright
|
122
|
+
|
123
|
+
* William Ross, for spanner. will at spanner.org
|
124
|
+
* Copyright 2009 spanner ltd
|
125
|
+
* released under the same terms as Rails and/or Radiant
|
data/Rakefile
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gem|
|
4
|
+
gem.name = "radiant-taggable-extension"
|
5
|
+
gem.summary = %Q{Taggable Extension for Radiant CMS}
|
6
|
+
gem.description = %Q{General purpose tagging extension: more versatile but less focused than the tags extension}
|
7
|
+
gem.email = "will@spanner.org"
|
8
|
+
gem.homepage = "http://github.com/spanner/radiant-taggable-extension"
|
9
|
+
gem.authors = ["spanner"]
|
10
|
+
gem.add_dependency "radiant", ">= 0.9.0"
|
11
|
+
end
|
12
|
+
rescue LoadError
|
13
|
+
puts "Jeweler (or a dependency) not available. This is only required if you plan to package taggable as a gem."
|
14
|
+
end
|
15
|
+
|
16
|
+
# In rails 1.2, plugins aren't available in the path until they're loaded.
|
17
|
+
# Check to see if the rspec plugin is installed first and require
|
18
|
+
# it if it is. If not, use the gem version.
|
19
|
+
|
20
|
+
# Determine where the RSpec plugin is by loading the boot
|
21
|
+
unless defined? RADIANT_ROOT
|
22
|
+
ENV["RAILS_ENV"] = "test"
|
23
|
+
case
|
24
|
+
when ENV["RADIANT_ENV_FILE"]
|
25
|
+
require File.dirname(ENV["RADIANT_ENV_FILE"]) + "/boot"
|
26
|
+
when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
|
27
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
|
28
|
+
else
|
29
|
+
require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'rake'
|
34
|
+
require 'rake/rdoctask'
|
35
|
+
require 'rake/testtask'
|
36
|
+
|
37
|
+
rspec_base = File.expand_path(RADIANT_ROOT + '/vendor/plugins/rspec/lib')
|
38
|
+
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
|
39
|
+
require 'spec/rake/spectask'
|
40
|
+
require 'cucumber'
|
41
|
+
require 'cucumber/rake/task'
|
42
|
+
|
43
|
+
# Cleanup the RADIANT_ROOT constant so specs will load the environment
|
44
|
+
Object.send(:remove_const, :RADIANT_ROOT)
|
45
|
+
|
46
|
+
extension_root = File.expand_path(File.dirname(__FILE__))
|
47
|
+
|
48
|
+
task :default => :spec
|
49
|
+
task :stats => "spec:statsetup"
|
50
|
+
|
51
|
+
desc "Run all specs in spec directory"
|
52
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
53
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
54
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
55
|
+
end
|
56
|
+
|
57
|
+
task :features => 'spec:integration'
|
58
|
+
|
59
|
+
namespace :spec do
|
60
|
+
desc "Run all specs in spec directory with RCov"
|
61
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
62
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
63
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
64
|
+
t.rcov = true
|
65
|
+
t.rcov_opts = ['--exclude', 'spec', '--rails']
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "Print Specdoc for all specs"
|
69
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
70
|
+
t.spec_opts = ["--format", "specdoc", "--dry-run"]
|
71
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
72
|
+
end
|
73
|
+
|
74
|
+
[:models, :controllers, :views, :helpers].each do |sub|
|
75
|
+
desc "Run the specs under spec/#{sub}"
|
76
|
+
Spec::Rake::SpecTask.new(sub) do |t|
|
77
|
+
t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
|
78
|
+
t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
desc "Run the Cucumber features"
|
83
|
+
Cucumber::Rake::Task.new(:integration) do |t|
|
84
|
+
t.fork = true
|
85
|
+
t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'pretty')]
|
86
|
+
# t.feature_pattern = "#{extension_root}/features/**/*.feature"
|
87
|
+
t.profile = "default"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Setup specs for stats
|
91
|
+
task :statsetup do
|
92
|
+
require 'code_statistics'
|
93
|
+
::STATS_DIRECTORIES << %w(Model\ specs spec/models)
|
94
|
+
::STATS_DIRECTORIES << %w(View\ specs spec/views)
|
95
|
+
::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
|
96
|
+
::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
|
97
|
+
::CodeStatistics::TEST_TYPES << "Model specs"
|
98
|
+
::CodeStatistics::TEST_TYPES << "View specs"
|
99
|
+
::CodeStatistics::TEST_TYPES << "Controller specs"
|
100
|
+
::CodeStatistics::TEST_TYPES << "Helper specs"
|
101
|
+
::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
|
102
|
+
end
|
103
|
+
|
104
|
+
namespace :db do
|
105
|
+
namespace :fixtures do
|
106
|
+
desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y"
|
107
|
+
task :load => :environment do
|
108
|
+
require 'active_record/fixtures'
|
109
|
+
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
|
110
|
+
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
|
111
|
+
Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
desc 'Generate documentation for the taggable extension.'
|
119
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
120
|
+
rdoc.rdoc_dir = 'rdoc'
|
121
|
+
rdoc.title = 'TaggableExtension'
|
122
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
123
|
+
rdoc.rdoc_files.include('README')
|
124
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
125
|
+
end
|
126
|
+
|
127
|
+
# For extensions that are in transition
|
128
|
+
desc 'Test the taggable extension.'
|
129
|
+
Rake::TestTask.new(:test) do |t|
|
130
|
+
t.libs << 'lib'
|
131
|
+
t.pattern = 'test/**/*_test.rb'
|
132
|
+
t.verbose = true
|
133
|
+
end
|
134
|
+
|
135
|
+
# Load any custom rakefiles for extension
|
136
|
+
Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.2.0
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Admin::TagsController < Admin::ResourceController
|
2
|
+
|
3
|
+
def index
|
4
|
+
@tags = Tag.with_count
|
5
|
+
response_for :plural
|
6
|
+
end
|
7
|
+
|
8
|
+
def show
|
9
|
+
@tag = load_model
|
10
|
+
end
|
11
|
+
|
12
|
+
def cloud
|
13
|
+
@tags = Tag.sized(Tag.with_count)
|
14
|
+
response_for :plural
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/app/models/tag.rb
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
class Tag < ActiveRecord::Base
|
2
|
+
|
3
|
+
attr_accessor :cloud_band, :cloud_size
|
4
|
+
|
5
|
+
belongs_to :created_by, :class_name => 'User'
|
6
|
+
belongs_to :updated_by, :class_name => 'User'
|
7
|
+
has_many :taggings, :dependent => :destroy
|
8
|
+
is_site_scoped if respond_to? :is_site_scoped
|
9
|
+
has_site if respond_to? :has_site
|
10
|
+
|
11
|
+
# this is useful when we need to go back and add popularity to an already defined list of tags
|
12
|
+
|
13
|
+
named_scope :in_this_list, lambda { |tags|
|
14
|
+
{
|
15
|
+
:conditions => ["tags.id IN (#{tags.map{'?'}.join(',')})", *tags.map{|t| t.is_a?(Tag) ? t.id : t}]
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
# this is normally used to exclude current tags from a cloud or list
|
20
|
+
|
21
|
+
named_scope :except, lambda { |tags|
|
22
|
+
if tags.any?
|
23
|
+
{ :conditions => ["tags.id NOT IN (#{tags.map{'?'}.join(',')})", *tags.map{|t| t.is_a?(Tag) ? t.id : t}] }
|
24
|
+
end
|
25
|
+
}
|
26
|
+
|
27
|
+
# NB unused tags are omitted
|
28
|
+
|
29
|
+
named_scope :with_count, {
|
30
|
+
:select => "tags.*, count(tt.id) AS use_count",
|
31
|
+
:joins => "INNER JOIN taggings as tt ON tt.tag_id = tags.id",
|
32
|
+
:group => "tags.id",
|
33
|
+
:order => 'title ASC'
|
34
|
+
}
|
35
|
+
|
36
|
+
named_scope :most_popular, lambda { |count|
|
37
|
+
{
|
38
|
+
:select => "tags.*, count(tt.id) AS use_count",
|
39
|
+
:joins => "INNER JOIN taggings as tt ON tt.tag_id = tags.id",
|
40
|
+
:group => "tags.id",
|
41
|
+
:limit => count,
|
42
|
+
:order => 'use_count DESC'
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
# this takes a list and returns all the tags attached to any item in that list
|
47
|
+
# NB. won't work with a heterogeneous group: all items must be of the same class
|
48
|
+
|
49
|
+
named_scope :attached_to, lambda { |these|
|
50
|
+
klass = these.first.is_a?(Page) ? Page : these.first.class
|
51
|
+
{
|
52
|
+
:joins => "INNER JOIN taggings as tt ON tt.tag_id = tags.id",
|
53
|
+
:conditions => ["tt.tagged_type = '#{klass}' and tt.tagged_id IN (#{these.map{'?'}.join(',')})", *these.map(&:id)],
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
# this takes a class name and returns all the tags attached to any object of that class
|
58
|
+
|
59
|
+
named_scope :attached_to_a, lambda { |klass|
|
60
|
+
klass = klass.to_s.titleize
|
61
|
+
{
|
62
|
+
:joins => "INNER JOIN taggings as tt ON tt.tag_id = tags.id",
|
63
|
+
:conditions => "tt.tagged_type = '#{klass}'",
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
title
|
69
|
+
end
|
70
|
+
|
71
|
+
# Standardises formatting of tag name in urls
|
72
|
+
|
73
|
+
def clean_title
|
74
|
+
Rack::Utils.escape(title)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns a list of all the objects tagged with this tag. We can't do this in SQL because it's polymorphic (and has_many_polymorphs makes my skin itch)
|
78
|
+
|
79
|
+
def tagged
|
80
|
+
taggings.map {|t| t.tagged}
|
81
|
+
end
|
82
|
+
|
83
|
+
def pages
|
84
|
+
Page.from_tags([self])
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns a list of all the tags that have been applied alongside this one.
|
88
|
+
|
89
|
+
def coincident_tags
|
90
|
+
tags = []
|
91
|
+
self.tagged.each do |t|
|
92
|
+
tags += t.attached_tags if t
|
93
|
+
end
|
94
|
+
tags.uniq - [self]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns a list of all the tags that have been applied alongside _all_ of the supplied tags.
|
98
|
+
# used to offer reductive facets on library pages
|
99
|
+
# not very efficient at the moment, largely thanks to polymorphic tagging relationship
|
100
|
+
# TODO: omit tags with no reductive power (ie applied to all the tagged items)
|
101
|
+
|
102
|
+
def self.coincident_with(tags)
|
103
|
+
related_tags = []
|
104
|
+
tagged = Tagging.with_all_of_these(tags).map(&:tagged)
|
105
|
+
tagged.each do |t|
|
106
|
+
related_tags += t.attached_tags if t
|
107
|
+
end
|
108
|
+
related_tags.uniq - tags
|
109
|
+
end
|
110
|
+
|
111
|
+
# returns true if tags are site-scoped
|
112
|
+
|
113
|
+
def self.sited?
|
114
|
+
!reflect_on_association(:site).nil?
|
115
|
+
end
|
116
|
+
|
117
|
+
# turns an array or comma-separated string of tag titles into a list of tag objects, creating if specified
|
118
|
+
|
119
|
+
def self.from_list(list=[], or_create=true)
|
120
|
+
list = list.split(/[,;]\s*/) if String === list
|
121
|
+
list.uniq.map {|t| self.for(t, or_create) }.select{|t| !t.nil? } if list && list.any?
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.to_list(tags=[])
|
125
|
+
tags.uniq.map(&:title).join(',')
|
126
|
+
end
|
127
|
+
|
128
|
+
# finds or creates a tag with the supplied title
|
129
|
+
|
130
|
+
def self.for(title, or_create=true)
|
131
|
+
if or_create
|
132
|
+
self.sited? ? self.find_or_create_by_title_and_site_id(title, Page.current_site.id) : self.find_or_create_by_title(title)
|
133
|
+
else
|
134
|
+
self.sited? ? self.find_by_title_and_site_id(title, Page.current_site.id) : self.find_by_title(title)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# applies the usual cloud-banding algorithm to a set of tags with use_count
|
139
|
+
|
140
|
+
def self.banded(tags=Tag.most_popular(100), bands=6)
|
141
|
+
if tags
|
142
|
+
count = tags.map{|t| t.use_count.to_i}
|
143
|
+
if count.any? # urgh. dodging named_scope count bug
|
144
|
+
max_use = count.max
|
145
|
+
min_use = count.min
|
146
|
+
divisor = ((max_use - min_use) / bands) + 1
|
147
|
+
tags.each do |tag|
|
148
|
+
tag.cloud_band = (tag.use_count.to_i - min_use) / divisor
|
149
|
+
end
|
150
|
+
tags
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# derived from here:
|
156
|
+
# http://stackoverflow.com/questions/604953/what-is-the-correct-algorthm-for-a-logarthmic-distribution-curve-between-two-poin
|
157
|
+
|
158
|
+
def self.sized(tags=Tag.most_popular(100), threshold=0, biggest=1.0, smallest=0.4)
|
159
|
+
if tags
|
160
|
+
counts = tags.map{|t| t.use_count.to_i}
|
161
|
+
if counts.any?
|
162
|
+
max = counts.max
|
163
|
+
min = counts.min
|
164
|
+
if max == min
|
165
|
+
tags.each do |tag|
|
166
|
+
tag.cloud_size = sprintf("%.2f", biggest/2 + smallest/2)
|
167
|
+
end
|
168
|
+
else
|
169
|
+
steepness = Math.log(max - (min-1))/(biggest - smallest)
|
170
|
+
tags.each do |tag|
|
171
|
+
offset = Math.log(tag.use_count.to_i - (min-1))/steepness
|
172
|
+
tag.cloud_size = sprintf("%.2f", smallest + offset)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
tags
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# takes a list of tags and reaquires it from the database, this time with incidence.
|
181
|
+
|
182
|
+
def self.for_cloud(tags)
|
183
|
+
return tags if tags.empty? || tags.first.cloud_size
|
184
|
+
sized(in_this_list(tags).with_count)
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.cloud_from(these)
|
188
|
+
for_cloud(attached_to(these))
|
189
|
+
end
|
190
|
+
|
191
|
+
# adds retrieval methods for a taggable class to this class and to Tagging.
|
192
|
+
|
193
|
+
def self.define_class_retrieval_methods(classname)
|
194
|
+
Tagging.send :named_scope, "of_#{classname.downcase.pluralize}".intern, :conditions => { :tagged_type => classname.to_s }
|
195
|
+
define_method("#{classname.downcase}_taggings") { self.taggings.send "of_#{classname.downcase.pluralize}".intern }
|
196
|
+
define_method("#{classname.downcase.pluralize}") { self.send("#{classname.to_s.downcase}_taggings".intern).map{|l| l.tagged} }
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Tagging < ActiveRecord::Base
|
2
|
+
|
3
|
+
belongs_to :tag
|
4
|
+
belongs_to :tagged, :polymorphic => true
|
5
|
+
|
6
|
+
named_scope :with, lambda { |tag|
|
7
|
+
{
|
8
|
+
:conditions => ["taggings.tag_id = ?", tag.id]
|
9
|
+
}
|
10
|
+
}
|
11
|
+
named_scope :with_any_of_these, lambda { |tags|
|
12
|
+
{
|
13
|
+
:select => "taggings.*",
|
14
|
+
:conditions => ["taggings.tag_id IN (#{tags.map{'?'}.join(',')})", *tags.map{|t| t.is_a?(Tag) ? t.id : t}],
|
15
|
+
:group => "taggings.tagged_type, taggings.tagged_id"
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
# this scope underpins a lot of the faceting:
|
20
|
+
# it returns a list of taggings of objects to whom all the supplied tags have been applied
|
21
|
+
# and is always used with map(&:tagged) to get a list of objects
|
22
|
+
|
23
|
+
named_scope :with_all_of_these, lambda { |tags|
|
24
|
+
{
|
25
|
+
:select => "taggings.*",
|
26
|
+
:conditions => ["taggings.tag_id IN (#{tags.map{'?'}.join(',')})", *tags.map{|t| t.is_a?(Tag) ? t.id : t}],
|
27
|
+
:group => "taggings.tagged_type, taggings.tagged_id",
|
28
|
+
:having => "COUNT(taggings.tag_id) >= #{tags.length}"
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
# this takes a class name and returns all the taggings of that class
|
33
|
+
# eg. @tag.taggings.of_a('page')
|
34
|
+
|
35
|
+
named_scope :of_a, lambda { |klass|
|
36
|
+
klass = klass.to_s.titleize
|
37
|
+
{
|
38
|
+
:conditions => "tagged_type = '#{klass}'",
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
# good housekeeping idea from tags extension.
|
44
|
+
# if all the taggings for a particular tag are deleted, we want to delete the tag too
|
45
|
+
|
46
|
+
def before_destroy
|
47
|
+
tag.destroy_without_callbacks if Tagging.with(tag).count < 1
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
- content_for :page_css do
|
2
|
+
:sass
|
3
|
+
p.title
|
4
|
+
:width 70%
|
5
|
+
p.keywords
|
6
|
+
:width 28%
|
7
|
+
:float right
|
8
|
+
:margin 0 1% 0 0
|
9
|
+
#content
|
10
|
+
form
|
11
|
+
.keywords
|
12
|
+
.textbox
|
13
|
+
:font-family Georgia, Palatino, "Times New Roman", Times, serif
|
14
|
+
:font-size 200%
|
15
|
+
:width 100%
|
16
|
+
:color #c00
|
17
|
+
:margin-top 4px
|
18
|
+
#attributes
|
19
|
+
:clear both
|
20
|
+
|
21
|
+
- fields_for :page, @page do |fields|
|
22
|
+
%p.keywords
|
23
|
+
%label{:for=>"page_keywords"}
|
24
|
+
Tags
|
25
|
+
= fields.text_field :keywords, :class => 'textbox'
|
26
|
+
%p.title
|
27
|
+
%label{:for=>"page_title"}= t('page_title')
|
28
|
+
= fields.text_field :title, :class => 'textbox', :maxlength => 255
|
@@ -0,0 +1,23 @@
|
|
1
|
+
= render_region :form_top
|
2
|
+
- render_region :form do |form_region|
|
3
|
+
.form-area
|
4
|
+
- form_region.edit_name do
|
5
|
+
%p.title
|
6
|
+
= form.label :title
|
7
|
+
= form.text_field :title, :class => 'textbox', :maxlength => 255
|
8
|
+
|
9
|
+
- form_region.edit_description do
|
10
|
+
%div
|
11
|
+
= form.label :description
|
12
|
+
= form.text_area :description, :class => "textarea", :style => "width: 100%"
|
13
|
+
|
14
|
+
- render_region :form_bottom do |form_bottom_region|
|
15
|
+
- form_bottom_region.edit_timestamp do
|
16
|
+
= updated_stamp @tag
|
17
|
+
|
18
|
+
- form_bottom_region.edit_buttons do
|
19
|
+
%p.buttons
|
20
|
+
= save_model_button(@tag)
|
21
|
+
= save_model_and_continue_editing_button(@tag)
|
22
|
+
or
|
23
|
+
= link_to "Cancel", admin_tags_url
|
@@ -0,0 +1,17 @@
|
|
1
|
+
- include_stylesheet 'admin/tags'
|
2
|
+
- url = Radiant::Config['tags.page'] || '/tags'
|
3
|
+
|
4
|
+
%h1 Tag cloud
|
5
|
+
|
6
|
+
%p
|
7
|
+
= link_to "← view as list", admin_tags_url
|
8
|
+
|
9
|
+
%div.cloud
|
10
|
+
- @tags.each do |tag|
|
11
|
+
= link_to tag.title, admin_tag_url(tag), :style => "font-size: #{tag.cloud_size.to_f * 2.5}em;"
|
12
|
+
|
13
|
+
#actions
|
14
|
+
%ul
|
15
|
+
%li= link_to image('plus') + " " + "new tag", new_admin_tag_url
|
16
|
+
%li= link_to "tag list", admin_tags_url, :class => 'minor'
|
17
|
+
%li= link_to "tag cloud", cloud_admin_tags_url, :class => 'minor'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
- include_stylesheet 'admin/tags'
|
2
|
+
|
3
|
+
- render_region :main do |main|
|
4
|
+
- main.edit_header do
|
5
|
+
%h1
|
6
|
+
Edit Tag
|
7
|
+
|
8
|
+
- main.edit_form do
|
9
|
+
- form_for :tag, :url => admin_tag_path(@tag), :html => { :method => "put", :multipart => true } do |form|
|
10
|
+
= render :partial => 'form', :object => form
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
|