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
@@ -0,0 +1,53 @@
|
|
1
|
+
- include_stylesheet 'admin/tags'
|
2
|
+
= render_region :top
|
3
|
+
|
4
|
+
#tags_table.outset
|
5
|
+
%table{ :class => "index", :cellpadding => "0", :cellspacing => "0", :border => "0"}
|
6
|
+
%thead
|
7
|
+
%tr
|
8
|
+
- render_region :thead do |thead|
|
9
|
+
- thead.title_header do
|
10
|
+
%th.tag-title Title
|
11
|
+
- thead.description_header do
|
12
|
+
%th.tag-description Description
|
13
|
+
- thead.usage_header do
|
14
|
+
%th.tag-usage Usage
|
15
|
+
- thead.modify_header do
|
16
|
+
%th.modify{:colspan =>"2"} Modify
|
17
|
+
|
18
|
+
%tbody
|
19
|
+
- for tag in @tags
|
20
|
+
%tr.node.level-1
|
21
|
+
- render_region :tbody do |tbody|
|
22
|
+
- tbody.title_cell do
|
23
|
+
%td.tag-title
|
24
|
+
= link_to image('tag', :alt => ''), edit_admin_tag_url(:id => tag.id)
|
25
|
+
= link_to tag.title, edit_admin_tag_url(:id => tag.id)
|
26
|
+
|
27
|
+
- tbody.description_cell do
|
28
|
+
%td.tag-description
|
29
|
+
= tag.description
|
30
|
+
|
31
|
+
- tbody.usage_cell do
|
32
|
+
%td.tag-usage
|
33
|
+
- if tag.use_count.to_i > 0
|
34
|
+
= link_to "#{tag.use_count} #{pluralize(tag.use_count.to_i, 'item')}", admin_tag_url(tag)
|
35
|
+
- else
|
36
|
+
\-
|
37
|
+
|
38
|
+
- tbody.modify_cell do
|
39
|
+
%td.actions
|
40
|
+
= link_to image('minus') + ' ' + t('remove'), remove_admin_tag_url(tag), :class => "action"
|
41
|
+
|
42
|
+
- render_region :bottom do |bottom|
|
43
|
+
- bottom.new_button do
|
44
|
+
#actions
|
45
|
+
%ul
|
46
|
+
%li= link_to image('plus') + " " + "new tag", new_admin_tag_url
|
47
|
+
%li= link_to "tag list", admin_tags_url, :class => 'minor'
|
48
|
+
%li= link_to "tag cloud", cloud_admin_tags_url, :class => 'minor'
|
49
|
+
|
50
|
+
%script{ :type => "text/javascript"}
|
51
|
+
// <! [CDATA[
|
52
|
+
new RuledTable('tags')
|
53
|
+
//]
|
@@ -0,0 +1,13 @@
|
|
1
|
+
- include_stylesheet 'admin/tags'
|
2
|
+
|
3
|
+
- render_region :main do |main|
|
4
|
+
- main.edit_header do
|
5
|
+
%h1
|
6
|
+
New Tag
|
7
|
+
%p
|
8
|
+
Tags are usually added while editing pages (or other tagged items). There you can create several tags at once just by typing a list into the 'keywords' field.
|
9
|
+
If you'd like a bit more control than that, you can also create and describe tags one at a time here:
|
10
|
+
|
11
|
+
- main.edit_form do
|
12
|
+
- form_for :tag, :url => admin_tags_path, :html => { :method => "post", :multipart => true } do |form|
|
13
|
+
= render :partial => 'form', :object => form
|
@@ -0,0 +1,32 @@
|
|
1
|
+
- include_stylesheet 'admin/tags'
|
2
|
+
|
3
|
+
- render_region :main do |main|
|
4
|
+
- main.show_header do
|
5
|
+
%h1
|
6
|
+
Tag:
|
7
|
+
= link_to @tag.title, edit_admin_tag_url(@tag)
|
8
|
+
- if @tag.description
|
9
|
+
%p
|
10
|
+
= @tag.description
|
11
|
+
|
12
|
+
- main.show_pages do
|
13
|
+
- page_taggings = @tag.taggings.of_a :page
|
14
|
+
- if page_taggings.any?
|
15
|
+
%h2
|
16
|
+
Tagged pages
|
17
|
+
%table.index
|
18
|
+
- page_taggings.each do |tagging|
|
19
|
+
- page = tagging.tagged
|
20
|
+
- dom_id = "tagging_#{tagging.id}"
|
21
|
+
%tr.page.level-1{:id => dom_id}
|
22
|
+
%td{:style => 'width: 48px'}
|
23
|
+
= link_to image('page', :class => "icon"), page.url
|
24
|
+
%td.name
|
25
|
+
= link_to %{ <span class="title">#{ h(page.title) }</span>}, page.url
|
26
|
+
%td.actions
|
27
|
+
= link_to_remote image('minus') + ' detach', |
|
28
|
+
:html => { :class => "action", :title => "Detach tag from page" }, |
|
29
|
+
:url => admin_tagging_url(tagging), :method => :delete, |
|
30
|
+
:after => "Effect.Fade('#{dom_id}', { duration: 0.5 })", |
|
31
|
+
:complete => "Element.remove('#{dom_id}');"
|
32
|
+
|
data/config/routes.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
class CreateTags < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :tags do |t|
|
4
|
+
t.column :title, :string
|
5
|
+
t.column :description, :text
|
6
|
+
t.column :created_by_id, :integer
|
7
|
+
t.column :updated_by_id, :integer
|
8
|
+
t.column :created_at, :datetime
|
9
|
+
t.column :updated_at, :datetime
|
10
|
+
t.column :site_id, :integer
|
11
|
+
end
|
12
|
+
add_index :tags, :title, :unique => true
|
13
|
+
|
14
|
+
create_table :taggings do |t|
|
15
|
+
t.column :tag_id, :integer
|
16
|
+
t.column :tagged_type, :string
|
17
|
+
t.column :tagged_id, :integer
|
18
|
+
end
|
19
|
+
add_index :taggings, [:tag_id, :tagged_id, :tagged_type], :unique => true
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.down
|
23
|
+
drop_table :tags
|
24
|
+
drop_table :taggings
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module TaggableAdminPageController # for inclusion into Admin::PagesController
|
2
|
+
|
3
|
+
# here we have a few more special cases for page tags.
|
4
|
+
# mostly just interface niceties
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
|
8
|
+
base.class_eval {
|
9
|
+
|
10
|
+
def initialize_meta_rows_and_buttons_with_tags
|
11
|
+
initialize_meta_rows_and_buttons_without_tags
|
12
|
+
@meta.delete(@meta.find{|m| m[:field] == 'keywords'})
|
13
|
+
end
|
14
|
+
alias_method_chain :initialize_meta_rows_and_buttons, :tags
|
15
|
+
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module TaggableAdminUI
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
|
6
|
+
attr_accessor :tag
|
7
|
+
alias_method :tags, :tag
|
8
|
+
|
9
|
+
def load_default_regions_with_tags
|
10
|
+
load_default_regions_without_tags
|
11
|
+
@tag = load_default_tag_regions
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method_chain :load_default_regions, :tags
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def load_default_tag_regions
|
19
|
+
returning OpenStruct.new do |tag|
|
20
|
+
tag.edit = Radiant::AdminUI::RegionSet.new do |edit|
|
21
|
+
edit.main.concat %w{edit_header edit_form}
|
22
|
+
edit.form.concat %w{edit_name edit_description}
|
23
|
+
edit.form_bottom.concat %w{edit_timestamp edit_buttons}
|
24
|
+
end
|
25
|
+
tag.show = Radiant::AdminUI::RegionSet.new do |show|
|
26
|
+
show.main.concat %w{show_header show_pages}
|
27
|
+
end
|
28
|
+
tag.index = Radiant::AdminUI::RegionSet.new do |index|
|
29
|
+
index.thead.concat %w{title_header description_header usage_header modify_header}
|
30
|
+
index.tbody.concat %w{title_cell description_cell usage_cell modify_cell}
|
31
|
+
index.bottom.concat %w{new_button}
|
32
|
+
end
|
33
|
+
tag.remove = tag.index
|
34
|
+
tag.new = tag.edit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module TaggableModel # for inclusion into ActiveRecord::Base
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
base.class_eval {
|
6
|
+
@@taggable_models = []
|
7
|
+
cattr_accessor :taggable_models
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def is_taggable?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_taggable
|
17
|
+
return if is_taggable?
|
18
|
+
|
19
|
+
has_many :taggings, :as => :tagged
|
20
|
+
has_many :attached_tags, :through => :taggings, :source => :tag # can't be just has_many :tags because that stomps on the radius tags in Page.
|
21
|
+
|
22
|
+
named_scope :from_tags, lambda { |tags|
|
23
|
+
{
|
24
|
+
:joins => "INNER JOIN taggings as tt on tt.tagged_id = #{self.table_name}.id AND tt.tagged_type = '#{self.to_s}'",
|
25
|
+
:conditions => ["tt.tag_id in(#{tags.map{ '?' }.join(',')})"] + tags.map(&:id),
|
26
|
+
:group => column_names.map { |n| table_name + '.' + n }.join(','), # postgres is strict and requires that we group by all selected (but not aggregated) columns
|
27
|
+
:order => "count(tt.id) DESC"
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
named_scope :from_all_tags, lambda { |tags|
|
32
|
+
{
|
33
|
+
:joins => "INNER JOIN taggings as tt on tt.tagged_id = #{self.table_name}.id AND tt.tagged_type = '#{self.to_s}'",
|
34
|
+
:conditions => ["tt.tag_id in(#{tags.map{ '?' }.join(',')})"] + tags.map(&:id),
|
35
|
+
:group => column_names.map { |n| table_name + '.' + n }.join(','), # postgres is strict and requires that we group by all selected (but not aggregated) columns
|
36
|
+
:having => "count(tt.id) >= #{tags.length}"
|
37
|
+
}
|
38
|
+
} do
|
39
|
+
# count is badly sugared here: it omits the group and having clauses.
|
40
|
+
# length performs the query and looks at the array: less neat, but more right
|
41
|
+
# this gives us back any? and empty? as well.
|
42
|
+
def count
|
43
|
+
length
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# this sets up eg Taggings.of_model
|
48
|
+
# and then uses that to define instance methods in Tag:
|
49
|
+
# tag.models
|
50
|
+
# tag.models_count
|
51
|
+
Tag.define_class_retrieval_methods(self.to_s)
|
52
|
+
|
53
|
+
class_eval {
|
54
|
+
extend TaggableModel::TaggableClassMethods
|
55
|
+
include TaggableModel::TaggableInstanceMethods
|
56
|
+
alias_method "related_#{self.to_s.underscore.pluralize}".intern, :related
|
57
|
+
alias_method "closely_related_#{self.to_s.underscore.pluralize}".intern, :closely_related
|
58
|
+
}
|
59
|
+
|
60
|
+
ActiveRecord::Base.taggable_models.push(self.to_s.intern)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module TaggableClassMethods
|
65
|
+
def tagged_with(somewords=[])
|
66
|
+
if somewords.is_a?(Array)
|
67
|
+
self.from_all_tags(somewords)
|
68
|
+
else
|
69
|
+
self.from_all_tags( Tag.from_list(somewords) )
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def is_taggable?
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
def tags_for_cloud_from(these, limit=50)
|
78
|
+
Tag.attached_to(these).most_popular(limit) # here popularity is use-count *within the group*
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module TaggableInstanceMethods
|
83
|
+
|
84
|
+
def add_tag(word=nil)
|
85
|
+
self.attached_tags << Tag.for(word) if word && !word.blank?
|
86
|
+
end
|
87
|
+
|
88
|
+
def remove_tag(word=nil)
|
89
|
+
tag = Tag.find_by_title(word) if word && !word.blank?
|
90
|
+
self.attached_tags.delete(tag) if tag
|
91
|
+
end
|
92
|
+
|
93
|
+
def related
|
94
|
+
self.attached_tags.empty? ? [] : self.class.from_tags(self.attached_tags) - [self]
|
95
|
+
end
|
96
|
+
|
97
|
+
def closely_related
|
98
|
+
self.attached_tags.empty? ? [] : self.class.from_all_tags(self.attached_tags) - [self]
|
99
|
+
end
|
100
|
+
|
101
|
+
# in the case of pages and anything else that keywords in the same way this overrides the existing column
|
102
|
+
# the rest of the time it's just another way of specifying tags.
|
103
|
+
|
104
|
+
def keywords
|
105
|
+
self.attached_tags.map {|t| t.title}.join(', ')
|
106
|
+
end
|
107
|
+
|
108
|
+
def keywords=(somewords="")
|
109
|
+
if somewords.blank?
|
110
|
+
self.attached_tags.clear
|
111
|
+
else
|
112
|
+
self.attached_tags = Tag.from_list(somewords)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def keywords_before_type_cast # for form_helper
|
117
|
+
keywords
|
118
|
+
end
|
119
|
+
|
120
|
+
def tags_from_keywords
|
121
|
+
if self.class.column_names.include?('keywords') && keys = read_attribute(:keywords)
|
122
|
+
self.attached_tags = Tag.from_list(keys)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module TaggablePage # for inclusion into Page
|
2
|
+
|
3
|
+
# here we have a few special cases for page tags.
|
4
|
+
# because of the page tree
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
|
8
|
+
base.class_eval {
|
9
|
+
named_scope :children_of, lambda { |these|
|
10
|
+
{ :conditions => ["parent_id IN (#{these.map{'?'}.join(',')})", *these.map{|t| t.id}] }
|
11
|
+
}
|
12
|
+
extend TaggablePage::ClassMethods
|
13
|
+
include TaggablePage::InstanceMethods
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
module InstanceMethods
|
22
|
+
|
23
|
+
# note varying logic here: tag clouds are used differently when describing a group.
|
24
|
+
# if only one object is relevant, all of its tags will be equally (locally) important.
|
25
|
+
# Presumably that cloud should show global tag importance.
|
26
|
+
# If several objects are relevant, either from a list or a tree of descendants, we
|
27
|
+
# probably want to show local tag importance, ie prominence within that list.
|
28
|
+
|
29
|
+
def tags_for_cloud(limit=50, bands=6)
|
30
|
+
tags = Tag.attached_to(self.with_children).most_popular(limit)
|
31
|
+
Tag.sized(tags, bands)
|
32
|
+
end
|
33
|
+
|
34
|
+
# the family-tree builder works with generations instead of individuals to cut down the number of retrieval steps
|
35
|
+
|
36
|
+
def with_children
|
37
|
+
this_generation = [self]
|
38
|
+
return this_generation unless self.respond_to?(:children) && self.children.any?
|
39
|
+
family = [self]
|
40
|
+
while this_generation.any? && next_generation = self.class.children_of(this_generation)
|
41
|
+
family.push(*next_generation)
|
42
|
+
this_generation = next_generation
|
43
|
+
end
|
44
|
+
family
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|