radiant-taggable-extension 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +125 -0
  2. data/Rakefile +136 -0
  3. data/VERSION +1 -0
  4. data/app/controllers/admin/taggings_controller.rb +8 -0
  5. data/app/controllers/admin/tags_controller.rb +17 -0
  6. data/app/models/tag.rb +200 -0
  7. data/app/models/tagging.rb +50 -0
  8. data/app/views/admin/pages/_edit_title.html.haml +28 -0
  9. data/app/views/admin/tags/_form.html.haml +23 -0
  10. data/app/views/admin/tags/_search_results.html.haml +10 -0
  11. data/app/views/admin/tags/cloud.html.haml +17 -0
  12. data/app/views/admin/tags/edit.html.haml +14 -0
  13. data/app/views/admin/tags/index.html.haml +53 -0
  14. data/app/views/admin/tags/new.html.haml +13 -0
  15. data/app/views/admin/tags/show.html.haml +32 -0
  16. data/config/routes.rb +6 -0
  17. data/db/migrate/001_create_tags.rb +26 -0
  18. data/db/migrate/002_import_keywords.rb +11 -0
  19. data/lib/taggable_admin_page_controller.rb +18 -0
  20. data/lib/taggable_admin_ui.rb +41 -0
  21. data/lib/taggable_model.rb +128 -0
  22. data/lib/taggable_page.rb +48 -0
  23. data/lib/taggable_tags.rb +456 -0
  24. data/lib/tasks/taggable_extension_tasks.rake +55 -0
  25. data/pkg/radiant-taggable-extension-1.2.0.gem +0 -0
  26. data/public/images/admin/new-tag.png +0 -0
  27. data/public/images/admin/tag.png +0 -0
  28. data/public/images/furniture/detag.png +0 -0
  29. data/public/stylesheets/admin/tags.css +20 -0
  30. data/public/stylesheets/sass/tagcloud.sass +29 -0
  31. data/radiant-taggable-extension.gemspec +84 -0
  32. data/spec/datasets/tag_sites_dataset.rb +9 -0
  33. data/spec/datasets/tags_dataset.rb +43 -0
  34. data/spec/lib/taggable_page_spec.rb +93 -0
  35. data/spec/models/tag_spec.rb +39 -0
  36. data/spec/spec.opts +6 -0
  37. data/spec/spec_helper.rb +36 -0
  38. data/taggable_extension.rb +36 -0
  39. 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,6 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ map.namespace :admin do |admin|
3
+ admin.resources :tags, :member => {:remove => :get }, :collection => {:cloud => :get}
4
+ admin.resources :taggings, :only => [:destroy]
5
+ end
6
+ end
@@ -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,11 @@
1
+ class ImportKeywords < ActiveRecord::Migration
2
+ def self.up
3
+ Page.find(:all).each do |page|
4
+ page.tags_from_keywords
5
+ end
6
+ end
7
+
8
+ def self.down
9
+
10
+ end
11
+ 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