radiant-taggable-extension 1.2.0
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.
- 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
|