easy_tag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ ## EasyTag
2
+
3
+ This is a very simple tagging system for Rails. Because it is so simple, you should fork and modify it for your own purpose.
4
+
5
+ This gem is used in my other projects, thus, it will be kept updated, albeit slowly.
6
+
7
+ ## Install
8
+
9
+ rails generate easy_tag:migration
10
+ rake db:migrate
11
+ rake db:test:prepare
12
+
13
+ ## Basic Usage
14
+
15
+ EasyTag offers very few methods to manipulate tags. You mostly work
16
+ on adding and removing tags on taggable. For advanced usage, try
17
+ work on the assocation of EasyTag models, or fork this project
18
+ and extend its functionality.
19
+
20
+ ### Tagger
21
+
22
+ You need to define __acts_as_tagger__ in tagger model like this:
23
+
24
+ class User < ActiveRecord::Base
25
+ acts_as_tagger
26
+ end
27
+
28
+ Only **ONE** model can be tagger.
29
+
30
+ You can find tags of tagger:
31
+
32
+ tagger.tags
33
+
34
+ Tags in a context can be retrieved like this:
35
+
36
+ tagger.tags.in_context(:skill)
37
+
38
+ ### Taggable
39
+
40
+ You need to define __acts_as_taggable__ in tagger model like this:
41
+
42
+ class Post < ActiveRecord::Base
43
+ acts_as_taggable
44
+ end
45
+
46
+ You can manage tags of taggable
47
+
48
+ taggable.tags: return tags associated with this taggable
49
+ taggable.tags=: set tags of this taggable without context and tagger
50
+ taggable.set_tags: set tags of this taggable and remove old tags
51
+
52
+ All of setting methods (except tags=) can add :context and :tagger as optioanl parameters.
53
+ For example:
54
+
55
+ taggable.set_tags ['ruby', 'rvm'], :context => 'skill', :tagger => current_user
56
+
57
+ :context and :tagger also accept number as id for faster database query:
58
+
59
+ taggable.set_tags, :context => 'skill', :tagger => current_user.id
60
+
61
+ If not specified, default context is nil and default tagger is nil.
62
+
63
+ Tags should be an array of strings, or by default, a string with comma(,) to
64
+ divide tags.
65
+ Delimiter can be specified in :delimiter, for example:
66
+
67
+ taggable.set_tags 'rails; ruby', :context => 'skill', :delimiter => ';'
68
+
69
+ Space, single and double quotation will be trimmed automatically.
70
+ For more complicated processing of tag string, use block:
71
+
72
+ taggable.set_tags 'rails; ruby', :context => 'skill' do |tag_list|
73
+ return tag_list.split(',')
74
+ end
75
+
76
+ To retrieve tags in a context, use
77
+
78
+ taggable.tags.in_context(:skill)
79
+
80
+ To retreieve tags tagged by a tagger, use
81
+
82
+ taggable.tags.by_tagger(User.first)
83
+
84
+ __in_context__ and __by_tagger__ can be chained together. They are methods in scopes.
85
+
86
+ To retrieve taggable tagged with tags, use
87
+
88
+ Taggable.with_tags('ruby, rvm', :match => :any)
89
+
90
+ Options for __:match__ can be :any (default), :all (expensive).
91
+
92
+ __with_tags__ can also be used with __in_context__ and __by_tagger__
93
+
94
+ To delete tags, set __nil__:
95
+
96
+ taggable.set_tags(nil)
97
+
98
+ To delete tags associated with context and tagger, include options __context__ and __tagger__:
99
+
100
+ taggable.set_tags(nil, :context => :skill, :tagger = User.first)
101
+
102
+ ### Tag
103
+
104
+ tag.taggers: taggers who use this tag
105
+ tag.taggings.collect(&:taggable): array of taggable tagged with this tag
106
+
107
+ To retrieve tags in a context, use
108
+
109
+ tags.in_context(:skill)
110
+
111
+ To retreieve tags tagged by a tagger, use
112
+
113
+ tagger.tags
114
+
115
+ ### Tagging
116
+
117
+ No public methods for now.
118
+
119
+ This is the core model to link together tag, context, taggable and tagger.
120
+ If you need to work on low-level database query, you probably need to look
121
+ at this one.
122
+
123
+ ### Tag Context
124
+
125
+ No public methods for now.
126
+
127
+ Just a place to keep records on tag context.
128
+
129
+ ## FAQ
130
+
131
+ #### How do I get tag list from returned EasyTag::Tag ?
132
+
133
+ tags.pluck(:name).join(', ')
134
+
135
+
136
+ #### What's the difference between these two ?
137
+
138
+ user.posts.tags
139
+
140
+ user.posts.by_tagger(user)
141
+
142
+
143
+ The first returns all tags associated with posts, including tags tagged by others. The second return all tags associated with posts AND tagged by tagger.
144
+
145
+ #### What's the difference between these two ?
146
+
147
+ Post.with_tags('ruby')
148
+
149
+ Post.with_tags('ruby').in_context(nil)
150
+
151
+ The first returns all tags with 'ruby', regardless the context. In another word, it return all taggables with any context. The second returns all tags with 'ruby' with nil context. In another word, it will not return taggables where context exists. The same idea applies to __by_tagger__.
152
+
153
+ ## Acknowledges
154
+
155
+ EasyTag is heavily influenced by [rocket_tag](https://github.com/bradphelan/rocket_tag), but not a direct fork.
156
+
157
+ ## License
158
+
159
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'EasyTag'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
24
+ load 'rails/tasks/engine.rake'
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
@@ -0,0 +1,29 @@
1
+ class EasyTag::Tag < ActiveRecord::Base
2
+ has_many :taggings, :dependent => :destroy,
3
+ :class_name => 'EasyTag::Tagging'
4
+ has_many :taggers, :through => :taggings
5
+
6
+ # Setup accessible (or protected) attributes for your model
7
+ attr_accessible :name
8
+
9
+ def self.compact_tag_list(tag_list, options = {})
10
+ options.reverse_merge! :downcase => true,
11
+ :delimiter => ','
12
+ if (tag_list.is_a?(String))
13
+ tag_list.downcase! if options[:downcase]
14
+ tags = tag_list.to_tags(options[:delimiter])
15
+ elsif (tag_list.is_a?(Array))
16
+ if options[:downcase]
17
+ tags = tag_list.collect { |t| t.downcase }
18
+ else
19
+ tags = tag_list
20
+ end
21
+ elsif tag_list.blank?
22
+ tags = nil
23
+ else
24
+ raise EasyTag::InvalidTagList
25
+ end
26
+
27
+ tags
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ class EasyTag::TagContext < ActiveRecord::Base
2
+ has_many :taggings, :dependent => :destroy,
3
+ :class_name => 'EasyTag::Tagging'
4
+ has_many :tags, :through => :taggings
5
+
6
+ # Setup accessible (or protected) attributes for your model
7
+ attr_accessible :name
8
+
9
+ def self.get_id(context)
10
+ if context.is_a?(String) || context.is_a?(Symbol)
11
+ context_id = self.where(:name => context.to_s).first
12
+ elsif context.is_a?(Integer)
13
+ context_id = context
14
+ else
15
+ raise EasyTag::InvalidContext
16
+ end
17
+ context_id
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ class EasyTag::Tagging < ActiveRecord::Base
2
+ belongs_to :tag, :class_name => 'EasyTag::Tag'
3
+ belongs_to :tag_context, :class_name => 'EasyTag::TagContext'
4
+ belongs_to :taggable, :polymorphic => true
5
+ end
@@ -0,0 +1,8 @@
1
+ # For some reason, it does not work. Thus, it is put in lib/easy_tag.rb
2
+ =begin
3
+ module EasyTag
4
+ def self.table_name_prefix
5
+ 'easy_tag_'
6
+ end
7
+ end
8
+ =end
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,4 @@
1
+ module EasyTag
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,141 @@
1
+ module EasyTag
2
+ module Taggable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :taggings, :as => :taggable, :dependent => :destroy,
7
+ :class_name => 'EasyTag::Tagging'
8
+ has_many :tags, :through => :taggings, :uniq => true do
9
+ def in_context(context)
10
+ context_id = EasyTag::TagContext.get_id(context)
11
+ where('easy_tag_taggings.tag_context_id = ?', context_id)
12
+ end
13
+
14
+ def by_tagger(tagger)
15
+ tagger_id = EasyTag::Tagger.get_id(tagger)
16
+ where('easy_tag_taggings.tagger_id = ?', tagger_id)
17
+ end
18
+ end # end of has_many :tags
19
+
20
+ scope :with_tags, ->(tag_list, options = {}) {
21
+ options.reverse_merge! :match => :any
22
+
23
+ if block_given?
24
+ tags = yield(klass)
25
+ else
26
+ tags = EasyTag::Tag.compact_tag_list(tag_list, options.slice(:downcase, :delimiter))
27
+ end
28
+
29
+ return where('1 == 2') if tags.nil?
30
+
31
+ query = tags.collect { |t| "name = '#{t}'" }.join(' OR ')
32
+ tag_ids = EasyTag::Tag.where(query).pluck(:id)
33
+
34
+ if options[:match] == :all
35
+ ids = nil
36
+ tag_ids.each do |tag_id|
37
+ # p EasyTag::Tag.find(tag_id)
38
+ taggable_ids = EasyTag::Tagging.where(:tag_id => tag_id).where(:taggable_type => self.model_name).pluck(:taggable_id).to_a
39
+ if ids
40
+ ids = ids & taggable_ids
41
+ else
42
+ ids = taggable_ids # first tag
43
+ end
44
+ end
45
+ joins(:taggings).where(:id => ids).uniq
46
+ else
47
+ # :any
48
+ joins(:taggings).where('easy_tag_taggings.tag_id' => tag_ids).uniq
49
+ end
50
+
51
+ } do
52
+ def in_context(context)
53
+ context_id = EasyTag::TagContext.get_id(context)
54
+ where('easy_tag_taggings.tag_context_id = ?', context_id)
55
+ end
56
+
57
+ def by_tagger(tagger)
58
+ tagger_id = EasyTag::Tagger.get_id(tagger)
59
+ where('easy_tag_taggings.tagger_id = ?', tagger_id)
60
+ end
61
+ end # end of scope :with_tags
62
+ end # end of included
63
+
64
+ module ClassMethods
65
+ end # end of class methods
66
+
67
+ def set_tags(tag_list, options = {})
68
+ options.reverse_merge! :context => nil,
69
+ :tagger => nil,
70
+ :downcase => true,
71
+ :delimiter => ','
72
+
73
+ if block_given?
74
+ tags = yield(klass)
75
+ else
76
+ tags = EasyTag::Tag.compact_tag_list(tag_list, options.slice(:downcase, :delimiter))
77
+ end
78
+
79
+ context = compact_context(options[:context])
80
+ tagger = compact_tagger(options[:tagger])
81
+
82
+ # Remove old tags
83
+ self.taggings.where(:tag_context_id => context.try(:id), :tagger_id => tagger.try(:id)).destroy_all
84
+ # TODO: should remove unused tags and contexts
85
+
86
+ if tags
87
+ tags.each do |t|
88
+ tag = EasyTag::Tag.where(:name => t).first_or_create
89
+ raise SimgleTag::InvalidTag if tag.nil?
90
+ self.taggings.where(:tagger_id => tagger.try(:id), :tag_context_id => context.try(:id), :tag_id => tag.id).first_or_create
91
+ end
92
+ end
93
+ end
94
+
95
+ def tags=(tag_list)
96
+ self.set_tags(tag_list)
97
+ end
98
+
99
+ def is_taggable?
100
+ return true
101
+ end
102
+
103
+ protected
104
+
105
+ def compact_context(context)
106
+ return nil if context.blank?
107
+
108
+ if (context.is_a?(String) || context.is_a?(Symbol))
109
+ return EasyTag::TagContext.where(:name => context.to_s).first_or_create
110
+ elsif (context.is_a?(Integer))
111
+ return EasyTag::TagContext.where(:id => context).first_or_create
112
+ end
113
+
114
+ raise EasyTag::InvalidTagContext
115
+ end
116
+
117
+ def compact_tagger(tagger)
118
+ return nil if tagger.blank?
119
+ return tagger if tagger.is_tagger?
120
+
121
+ if (tagger.is_a?(Integer))
122
+ if EasyTag::Tagger.class_variable_defined?(:@@tagger_class)
123
+ klass = EasyTag::Tagger.class_variable_get(:@@tagger_class)
124
+ return klass.where(:id => tagger).first_or_create
125
+ else
126
+ raise EasyTag::NoTaggerDefined
127
+ end
128
+ end
129
+
130
+ raise EasyTag::InvalidTagger
131
+ end
132
+ end
133
+ end
134
+
135
+ class String
136
+ def to_tags(delimiter = ',')
137
+ self.split(delimiter).collect do |t|
138
+ t.strip.gsub(/\A["']|["']\Z/, '')
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,36 @@
1
+ module EasyTag
2
+ module Tagger
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ has_many :taggings, :dependent => :destroy,
7
+ :class_name => 'EasyTag::Tagging',
8
+ :foreign_key => 'tagger_id'
9
+ has_many :tags, :through => :taggings, :uniq => true do
10
+ def in_context(context)
11
+ context_id = EasyTag::TagContext.get_id(context)
12
+ where('easy_tag_taggings.tag_context_id = ?', context_id)
13
+ end
14
+ end
15
+ end # end of included
16
+
17
+ module ClassMethods
18
+ def get_id(tagger)
19
+ if tagger.is_a?(Integer)
20
+ tagger_id = tagger
21
+ elsif tagger.is_tagger?
22
+ tagger_id = tagger.id
23
+ else
24
+ raise EasyTag::InvalidTagger
25
+ end
26
+ tagger_id
27
+ end
28
+ end # end of class methods
29
+
30
+ extend ClassMethods # to be use in EasyTag::Tag module
31
+
32
+ def is_tagger?
33
+ return true
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module EasyTag
2
+ VERSION = "0.1.0"
3
+ end
data/lib/easy_tag.rb ADDED
@@ -0,0 +1,36 @@
1
+ require 'easy_tag/engine'
2
+ require 'easy_tag/taggable.rb'
3
+ require 'easy_tag/tagger.rb'
4
+
5
+ # For some reason, this does not work when set in app/models/easy_tag.rb
6
+ # Therefore, it is put here.
7
+ module EasyTag
8
+ def self.table_name_prefix
9
+ 'easy_tag_'
10
+ end
11
+ end
12
+
13
+ if defined?(ActiveRecord::Base)
14
+ class ActiveRecord::Base
15
+ def is_taggable?
16
+ return false
17
+ end
18
+
19
+ def is_tagger?
20
+ return false
21
+ end
22
+
23
+ def self.acts_as_taggable(options = {})
24
+ options.reverse_merge!({})
25
+ include EasyTag::Taggable
26
+ end
27
+
28
+ def self.acts_as_tagger(options = {})
29
+ options.reverse_merge!({})
30
+ include EasyTag::Tagger
31
+
32
+ EasyTag::Tagger.class_variable_set(:@@tagger_class, self)
33
+ EasyTag::Tagging.belongs_to :tagger, :class_name => self.model_name
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+ require 'rails/generators/active_record'
4
+
5
+ module EasyTag
6
+ class MigrationGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ desc 'add easy tag'
10
+
11
+ source_root File.expand_path('../templates', __FILE__)
12
+
13
+ def self.next_migration_number(path)
14
+ ActiveRecord::Generators::Base.next_migration_number(path)
15
+ end
16
+
17
+ def create_migration_file
18
+ migration_template 'migration.rb', 'db/migrate/easy_tag_migration'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ class EasyTagMigration < ActiveRecord::Migration
2
+
3
+ def change
4
+ create_table :easy_tag_tags do |t|
5
+ t.string :name
6
+
7
+ t.timestamps
8
+ end
9
+
10
+ create_table :easy_tag_tag_contexts do |t|
11
+ t.string :name
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ create_table :easy_tag_taggings do |t|
17
+ t.references :tag
18
+ t.references :tag_context
19
+ t.references :tagger
20
+
21
+ # You should make sure that the column created is
22
+ # long enough to store the required class names.
23
+ t.references :taggable, :polymorphic => true
24
+
25
+ t.timestamps
26
+ end
27
+
28
+ add_index :easy_tag_taggings, :tag_id
29
+ add_index :easy_tag_taggings, :tagger_id
30
+ add_index :easy_tag_taggings, :tag_context_id
31
+ add_index :easy_tag_taggings, [:taggable_id, :taggable_type]
32
+ end
33
+
34
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :easy_tag do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easy_tag
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Yen-Ju Chen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.11
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.11
30
+ - !ruby/object:Gem::Dependency
31
+ name: sqlite3
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A very simple tagging system for Rails to be forked or extended.
47
+ email:
48
+ - yjchenx@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - app/models/easy_tag/tag.rb
54
+ - app/models/easy_tag/tag_context.rb
55
+ - app/models/easy_tag/tagging.rb
56
+ - app/models/easy_tag.rb
57
+ - config/routes.rb
58
+ - lib/easy_tag/engine.rb
59
+ - lib/easy_tag/taggable.rb
60
+ - lib/easy_tag/tagger.rb
61
+ - lib/easy_tag/version.rb
62
+ - lib/easy_tag.rb
63
+ - lib/generators/easy_tag/migration/migration_generator.rb
64
+ - lib/generators/easy_tag/migration/templates/migration.rb
65
+ - lib/tasks/easy_tag_tasks.rake
66
+ - MIT-LICENSE
67
+ - Rakefile
68
+ - README.md
69
+ homepage: https://github.com/yjchen/easy_tag
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.23
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: A very simple tagging system for Rails
93
+ test_files: []