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.
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