make_taggable 0.7.4 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +79 -0
- data/CONTRIBUTING.md +65 -22
- data/LICENSE.md +18 -17
- data/README.md +73 -444
- data/UPGRADING.md +28 -2
- data/docs/caching.md +93 -0
- data/docs/configuration.md +108 -0
- data/docs/contexts.md +147 -0
- data/docs/database.md +106 -0
- data/docs/getting-started.md +114 -0
- data/docs/migrating-from-aato.md +67 -0
- data/docs/ownership.md +111 -0
- data/docs/parsers.md +109 -0
- data/docs/querying.md +163 -0
- data/docs/tag-clouds.md +74 -0
- data/lib/make_taggable/default_parser.rb +45 -32
- data/lib/make_taggable/engine.rb +6 -0
- data/lib/make_taggable/generic_parser.rb +30 -4
- data/lib/make_taggable/tag.rb +121 -19
- data/lib/make_taggable/tag_list.rb +77 -24
- data/lib/make_taggable/taggable/cache.rb +61 -11
- data/lib/make_taggable/taggable/collection.rb +102 -30
- data/lib/make_taggable/taggable/core.rb +154 -34
- data/lib/make_taggable/taggable/ownership.rb +82 -4
- data/lib/make_taggable/taggable/related.rb +66 -3
- data/lib/make_taggable/taggable/tag_list_type.rb +8 -0
- data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +10 -0
- data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +10 -0
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +10 -0
- data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +15 -0
- data/lib/make_taggable/taggable/tagged_with_query.rb +17 -0
- data/lib/make_taggable/taggable.rb +34 -33
- data/lib/make_taggable/tagger.rb +73 -12
- data/lib/make_taggable/tagging.rb +30 -3
- data/lib/make_taggable/tags_helper.rb +22 -1
- data/lib/make_taggable/utils.rb +40 -5
- data/lib/make_taggable/version.rb +8 -1
- data/lib/make_taggable.rb +153 -8
- data/make_taggable.gemspec +13 -20
- metadata +50 -165
- data/.dummyrc +0 -17
- data/.github/workflows/ci.yml +0 -140
- data/.github/workflows/standard-ci.yml +0 -27
- data/.gitignore +0 -17
- data/.rspec +0 -3
- data/Appraisals +0 -15
- data/Gemfile +0 -16
- data/Rakefile +0 -13
- data/gemfiles/rails_5.gemfile +0 -9
- data/gemfiles/rails_6.gemfile +0 -9
- data/gemfiles/rails_6_1.gemfile +0 -9
- data/gemfiles/rails_master.gemfile +0 -9
- data/lib/tasks/setup_test_db.rake +0 -8
|
@@ -1,11 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable::Taggable
|
|
4
|
+
##
|
|
5
|
+
# Finding records that share tags with this one.
|
|
6
|
+
#
|
|
7
|
+
# Results come back ordered by how many tags matched, most first, and carry that figure as a
|
|
8
|
+
# `count` attribute.
|
|
9
|
+
#
|
|
2
10
|
module Related
|
|
11
|
+
##
|
|
12
|
+
# @param base [Class] the model being made taggable
|
|
13
|
+
# @return [void]
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
#
|
|
3
17
|
def self.included(base)
|
|
4
18
|
base.extend MakeTaggable::Taggable::Related::ClassMethods
|
|
5
19
|
base.initialize_make_taggable_related
|
|
6
20
|
end
|
|
7
21
|
|
|
22
|
+
##
|
|
23
|
+
# Added to every taggable model.
|
|
24
|
+
#
|
|
8
25
|
module ClassMethods
|
|
26
|
+
##
|
|
27
|
+
# Defines `find_related_<context>` and `find_related_<context>_for` for each context.
|
|
28
|
+
#
|
|
29
|
+
# @return [void]
|
|
30
|
+
#
|
|
9
31
|
def initialize_make_taggable_related
|
|
10
32
|
tag_types.map(&:to_s).each do |tag_type|
|
|
11
33
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
@@ -21,29 +43,70 @@ module MakeTaggable::Taggable
|
|
|
21
43
|
end
|
|
22
44
|
end
|
|
23
45
|
|
|
46
|
+
##
|
|
47
|
+
# Adds contexts and refreshes the related-record finders.
|
|
48
|
+
#
|
|
49
|
+
# @param args [Array<Symbol, String>] the contexts to add
|
|
50
|
+
# @return [void]
|
|
51
|
+
#
|
|
24
52
|
def make_taggable(*args)
|
|
25
|
-
super
|
|
53
|
+
super
|
|
26
54
|
initialize_make_taggable_related
|
|
27
55
|
end
|
|
28
56
|
end
|
|
29
57
|
|
|
58
|
+
##
|
|
59
|
+
# Records of this model tagged, in one context, with the tags this record carries in another.
|
|
60
|
+
#
|
|
61
|
+
# @param search_context [Symbol, String] the context to take this record's tags from
|
|
62
|
+
# @param result_context [Symbol, String] the context to match them against
|
|
63
|
+
# @param options [Hash] reserved for future use
|
|
64
|
+
# @return [ActiveRecord::Relation] ordered by number of matching tags, descending
|
|
65
|
+
#
|
|
30
66
|
def find_matching_contexts(search_context, result_context, options = {})
|
|
31
67
|
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
|
32
68
|
end
|
|
33
69
|
|
|
70
|
+
##
|
|
71
|
+
# As {#find_matching_contexts}, against another model.
|
|
72
|
+
#
|
|
73
|
+
# @param klass [Class] the model to search
|
|
74
|
+
# @param search_context [Symbol, String] the context to take this record's tags from
|
|
75
|
+
# @param result_context [Symbol, String] the context to match them against
|
|
76
|
+
# @param options [Hash] reserved for future use
|
|
77
|
+
# @return [ActiveRecord::Relation] ordered by number of matching tags, descending
|
|
78
|
+
#
|
|
34
79
|
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
|
35
80
|
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
|
36
81
|
end
|
|
37
82
|
|
|
83
|
+
##
|
|
84
|
+
# Builds the relation behind {#find_matching_contexts}.
|
|
85
|
+
#
|
|
86
|
+
# @param search_context [Symbol, String] the context to take this record's tags from
|
|
87
|
+
# @param result_context [Symbol, String] the context to match them against
|
|
88
|
+
# @param klass [Class] the model to search
|
|
89
|
+
# @param options [Hash] reserved for future use
|
|
90
|
+
# @return [ActiveRecord::Relation]
|
|
91
|
+
#
|
|
38
92
|
def matching_contexts_for(search_context, result_context, klass, options = {})
|
|
39
93
|
tags_to_find = tags_on(search_context).map { |t| t.name }
|
|
40
|
-
related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id AND #{MakeTaggable::Tagging.table_name}.taggable_type =
|
|
94
|
+
related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id AND #{MakeTaggable::Tagging.table_name}.taggable_type = ? AND #{MakeTaggable::Tagging.table_name}.tag_id = #{MakeTaggable::Tag.table_name}.#{MakeTaggable::Tag.primary_key} AND #{MakeTaggable::Tag.table_name}.name IN (?) AND #{MakeTaggable::Tagging.table_name}.context = ?", klass.base_class.to_s, tags_to_find, result_context])
|
|
41
95
|
end
|
|
42
96
|
|
|
97
|
+
##
|
|
98
|
+
# Builds the relation behind each generated `find_related_<context>` method.
|
|
99
|
+
#
|
|
100
|
+
# @param context [Symbol, String] the context to match on
|
|
101
|
+
# @param klass [Class] the model to search
|
|
102
|
+
# @param options [Hash] the search options
|
|
103
|
+
# @option options [String, Array<String>] :ignore tags to leave out of the match
|
|
104
|
+
# @return [ActiveRecord::Relation] ordered by number of matching tags, descending
|
|
105
|
+
#
|
|
43
106
|
def related_tags_for(context, klass, options = {})
|
|
44
107
|
tags_to_ignore = Array.wrap(options[:ignore]).map(&:to_s) || []
|
|
45
108
|
tags_to_find = tags_on(context).map { |t| t.name }.reject { |t| tags_to_ignore.include? t }
|
|
46
|
-
related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id AND #{MakeTaggable::Tagging.table_name}.taggable_type =
|
|
109
|
+
related_where(klass, ["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id AND #{MakeTaggable::Tagging.table_name}.taggable_type = ? AND #{MakeTaggable::Tagging.table_name}.tag_id = #{MakeTaggable::Tag.table_name}.#{MakeTaggable::Tag.primary_key} AND #{MakeTaggable::Tag.table_name}.name IN (?) AND #{MakeTaggable::Tagging.table_name}.context = ?", klass.base_class.to_s, tags_to_find, context])
|
|
47
110
|
end
|
|
48
111
|
|
|
49
112
|
private
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable::Taggable
|
|
4
|
+
##
|
|
5
|
+
# The Active Model type backing each generated `*_list` attribute, so tag lists take part in
|
|
6
|
+
# dirty tracking alongside ordinary columns.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
#
|
|
2
10
|
class TagListType < ActiveModel::Type::Value
|
|
3
11
|
end
|
|
4
12
|
end
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable::Taggable::TaggedWithQuery
|
|
4
|
+
##
|
|
5
|
+
# Records carrying every tag in the list.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
2
9
|
class AllTagsQuery < QueryBase
|
|
10
|
+
##
|
|
11
|
+
# @return [ActiveRecord::Relation]
|
|
12
|
+
#
|
|
3
13
|
def build
|
|
4
14
|
taggable_model.joins(each_tag_in_list)
|
|
5
15
|
.group(by_taggable)
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable::Taggable::TaggedWithQuery
|
|
4
|
+
##
|
|
5
|
+
# Records carrying at least one tag in the list.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
2
9
|
class AnyTagsQuery < QueryBase
|
|
10
|
+
##
|
|
11
|
+
# @return [ActiveRecord::Relation]
|
|
12
|
+
#
|
|
3
13
|
def build
|
|
4
14
|
taggable_model.select(all_fields)
|
|
5
15
|
.where(model_has_at_least_one_tag)
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable::Taggable::TaggedWithQuery
|
|
4
|
+
##
|
|
5
|
+
# Records carrying none of the tags in the list.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
2
9
|
class ExcludeTagsQuery < QueryBase
|
|
10
|
+
##
|
|
11
|
+
# @return [ActiveRecord::Relation]
|
|
12
|
+
#
|
|
3
13
|
def build
|
|
4
14
|
taggable_model.joins(owning_to_tagger)
|
|
5
15
|
.where(tags_not_in_list)
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable::Taggable::TaggedWithQuery
|
|
4
|
+
##
|
|
5
|
+
# Shared Arel plumbing for the three query strategies.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
#
|
|
2
9
|
class QueryBase
|
|
10
|
+
##
|
|
11
|
+
# @param taggable_model [Class] the model being queried
|
|
12
|
+
# @param tag_model [Class] the tag model
|
|
13
|
+
# @param tagging_model [Class] the tagging model
|
|
14
|
+
# @param tag_list [MakeTaggable::TagList] the tags to match
|
|
15
|
+
# @param options [Hash] the options given to `tagged_with`
|
|
16
|
+
# @return [MakeTaggable::Taggable::TaggedWithQuery::QueryBase]
|
|
17
|
+
#
|
|
3
18
|
def initialize(taggable_model, tag_model, tagging_model, tag_list, options)
|
|
4
19
|
@taggable_model = taggable_model
|
|
5
20
|
@tag_model = tag_model
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative "tagged_with_query/query_base"
|
|
2
4
|
require_relative "tagged_with_query/exclude_tags_query"
|
|
3
5
|
require_relative "tagged_with_query/any_tags_query"
|
|
4
6
|
require_relative "tagged_with_query/all_tags_query"
|
|
5
7
|
|
|
8
|
+
##
|
|
9
|
+
# Builds the relation behind {MakeTaggable::Taggable::Core::ClassMethods#tagged_with}.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
#
|
|
6
13
|
module MakeTaggable::Taggable::TaggedWithQuery
|
|
14
|
+
##
|
|
15
|
+
# Picks the query strategy the options call for and builds the relation.
|
|
16
|
+
#
|
|
17
|
+
# @param taggable_model [Class] the model being queried
|
|
18
|
+
# @param tag_model [Class] the tag model
|
|
19
|
+
# @param tagging_model [Class] the tagging model
|
|
20
|
+
# @param tag_list [MakeTaggable::TagList] the tags to match
|
|
21
|
+
# @param options [Hash] the options given to `tagged_with`
|
|
22
|
+
# @return [ActiveRecord::Relation]
|
|
23
|
+
#
|
|
7
24
|
def self.build(taggable_model, tag_model, tagging_model, tag_list, options)
|
|
8
25
|
if options[:exclude].present?
|
|
9
26
|
ExcludeTagsQuery.new(taggable_model, tag_model, tagging_model, tag_list, options).build
|
|
@@ -1,55 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable
|
|
4
|
+
##
|
|
5
|
+
# Declaring a model taggable. Mixed into Active Record automatically, which is what puts
|
|
6
|
+
# {#make_taggable} on every model.
|
|
7
|
+
#
|
|
2
8
|
module Taggable
|
|
9
|
+
##
|
|
10
|
+
# Whether this model has been made taggable.
|
|
11
|
+
#
|
|
12
|
+
# @return [TrueClass, FalseClass] `false` until {#make_taggable} is called
|
|
13
|
+
#
|
|
3
14
|
def taggable?
|
|
4
15
|
false
|
|
5
16
|
end
|
|
6
17
|
|
|
7
18
|
##
|
|
8
|
-
#
|
|
19
|
+
# Make a model taggable on the given contexts.
|
|
9
20
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# acts_as_taggable
|
|
13
|
-
# end
|
|
14
|
-
def acts_as_taggable
|
|
15
|
-
make_taggable :tags
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
##
|
|
19
|
-
# This is an alias for calling <tt>acts_as_ordered_taggable_on :tags</tt>.
|
|
21
|
+
# Called without arguments it makes the model taggable on `:tags`, which is the context every
|
|
22
|
+
# other part of the library treats as the default.
|
|
20
23
|
#
|
|
21
|
-
#
|
|
24
|
+
# @param tag_types [Array<Symbol, String>] the contexts to tag on
|
|
25
|
+
# @return [void]
|
|
26
|
+
#
|
|
27
|
+
# @example A single, default context
|
|
22
28
|
# class Book < ActiveRecord::Base
|
|
23
|
-
#
|
|
29
|
+
# make_taggable
|
|
24
30
|
# end
|
|
25
|
-
def acts_as_ordered_taggable
|
|
26
|
-
acts_as_ordered_taggable_on :tags
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
##
|
|
30
|
-
# Make a model taggable on specified contexts.
|
|
31
|
-
#
|
|
32
|
-
# @param [Array] tag_types An array of taggable contexts
|
|
33
31
|
#
|
|
34
|
-
#
|
|
32
|
+
# @example Several named contexts
|
|
35
33
|
# class User < ActiveRecord::Base
|
|
36
34
|
# make_taggable :languages, :skills
|
|
37
35
|
# end
|
|
36
|
+
#
|
|
38
37
|
def make_taggable(*tag_types)
|
|
38
|
+
tag_types = [:tags] if tag_types.flatten.compact.empty?
|
|
39
|
+
|
|
39
40
|
taggable_on(false, tag_types)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
##
|
|
43
|
-
# Make a model taggable on
|
|
44
|
-
#
|
|
44
|
+
# Make a model taggable on the given contexts, preserving the order in which tags were added.
|
|
45
|
+
#
|
|
46
|
+
# Called without arguments it makes the model taggable on `:tags`.
|
|
45
47
|
#
|
|
46
|
-
# @param [Array]
|
|
48
|
+
# @param tag_types [Array<Symbol, String>] the contexts to tag on
|
|
49
|
+
# @return [void]
|
|
47
50
|
#
|
|
48
|
-
#
|
|
51
|
+
# @example
|
|
49
52
|
# class User < ActiveRecord::Base
|
|
50
|
-
#
|
|
53
|
+
# make_ordered_taggable :languages, :skills
|
|
51
54
|
# end
|
|
52
|
-
|
|
55
|
+
#
|
|
56
|
+
def make_ordered_taggable(*tag_types)
|
|
57
|
+
tag_types = [:tags] if tag_types.flatten.compact.empty?
|
|
58
|
+
|
|
53
59
|
taggable_on(true, tag_types)
|
|
54
60
|
end
|
|
55
61
|
|
|
@@ -58,11 +64,6 @@ module MakeTaggable
|
|
|
58
64
|
# Make a model taggable on specified contexts
|
|
59
65
|
# and optionally preserves the order in which tags are created
|
|
60
66
|
#
|
|
61
|
-
# Separate methods used above for backwards compatibility
|
|
62
|
-
# so that the original make_taggable method is unaffected
|
|
63
|
-
# as it's not possible to add another argument to the method
|
|
64
|
-
# without the tag_types being enclosed in square brackets
|
|
65
|
-
#
|
|
66
67
|
# NB: method overridden in core module in order to create tag type
|
|
67
68
|
# associations and methods after this logic has executed
|
|
68
69
|
#
|
data/lib/make_taggable/tagger.rb
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable
|
|
4
|
+
##
|
|
5
|
+
# Ownership of tags. Mixed into Active Record automatically, which is what puts {ClassMethods#make_tagger}
|
|
6
|
+
# on every model.
|
|
7
|
+
#
|
|
2
8
|
module Tagger
|
|
9
|
+
##
|
|
10
|
+
# @param base [Class] the class being extended
|
|
11
|
+
# @return [void]
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
#
|
|
3
15
|
def self.included(base)
|
|
4
16
|
base.extend ClassMethods
|
|
5
17
|
end
|
|
6
18
|
|
|
19
|
+
##
|
|
20
|
+
# Added to every Active Record model.
|
|
21
|
+
#
|
|
7
22
|
module ClassMethods
|
|
8
23
|
##
|
|
9
|
-
# Make a model a tagger
|
|
10
|
-
#
|
|
24
|
+
# Make a model a tagger, allowing its instances to claim ownership of the tags they apply.
|
|
25
|
+
#
|
|
26
|
+
# Adds an `owned_taggings` association and an `owned_tags` association to the model.
|
|
27
|
+
#
|
|
28
|
+
# @param opts [Hash] options forwarded to the generated `owned_taggings` association
|
|
29
|
+
# @option opts [Proc] :scope a scope applied to the `owned_taggings` association
|
|
30
|
+
# @return [void]
|
|
11
31
|
#
|
|
12
|
-
#
|
|
32
|
+
# @example
|
|
13
33
|
# class User < ActiveRecord::Base
|
|
14
|
-
#
|
|
34
|
+
# make_tagger
|
|
15
35
|
# end
|
|
16
|
-
|
|
36
|
+
#
|
|
37
|
+
def make_tagger(opts = {})
|
|
17
38
|
class_eval do
|
|
18
39
|
owned_taggings_scope = opts.delete(:scope)
|
|
19
40
|
|
|
@@ -34,26 +55,47 @@ module MakeTaggable
|
|
|
34
55
|
extend MakeTaggable::Tagger::SingletonMethods
|
|
35
56
|
end
|
|
36
57
|
|
|
58
|
+
##
|
|
59
|
+
# Whether this model claims ownership of the tags it applies.
|
|
60
|
+
#
|
|
61
|
+
# @return [TrueClass, FalseClass] `false` until {#make_tagger} is called
|
|
62
|
+
#
|
|
37
63
|
def tagger?
|
|
38
64
|
false
|
|
39
65
|
end
|
|
40
66
|
|
|
67
|
+
##
|
|
68
|
+
# @return [TrueClass, FalseClass]
|
|
69
|
+
# @see #tagger?
|
|
70
|
+
#
|
|
41
71
|
def is_tagger?
|
|
42
72
|
tagger?
|
|
43
73
|
end
|
|
44
74
|
end
|
|
45
75
|
|
|
76
|
+
##
|
|
77
|
+
# Added to a model once it is a tagger.
|
|
78
|
+
#
|
|
46
79
|
module InstanceMethods
|
|
47
80
|
##
|
|
48
|
-
#
|
|
81
|
+
# Tags a record, with this tagger as the owner of the tags.
|
|
82
|
+
#
|
|
83
|
+
# The taggable is saved unless `:skip_save` is given, so the tags are persisted immediately.
|
|
49
84
|
#
|
|
50
|
-
# @param taggable
|
|
51
|
-
# @param [Hash]
|
|
52
|
-
#
|
|
53
|
-
#
|
|
85
|
+
# @param taggable [ActiveRecord::Base] the record to tag
|
|
86
|
+
# @param opts [Hash] the tagging options
|
|
87
|
+
# @option opts [String, Array<String>] :with the tags to apply
|
|
88
|
+
# @option opts [Symbol, String] :on the context to apply them in
|
|
89
|
+
# @option opts [TrueClass, FalseClass] :skip_save whether to leave the taggable unsaved
|
|
90
|
+
# @option opts [TrueClass, FalseClass] :force whether to allow a context the model does not
|
|
91
|
+
# declare, on by default
|
|
92
|
+
# @return [TrueClass, FalseClass] whether the taggable saved, or `false` when it is not taggable
|
|
93
|
+
# @raise [RuntimeError] when `:on` or `:with` is missing, or the context is undeclared and
|
|
94
|
+
# `:force` is off
|
|
95
|
+
#
|
|
96
|
+
# @example
|
|
97
|
+
# @user.tag(@photo, with: "paris, normandy", on: :locations)
|
|
54
98
|
#
|
|
55
|
-
# Example:
|
|
56
|
-
# @user.tag(@photo, :with => "paris, normandy", :on => :locations)
|
|
57
99
|
def tag(taggable, opts = {})
|
|
58
100
|
opts.reverse_merge!(force: true)
|
|
59
101
|
skip_save = opts.delete(:skip_save)
|
|
@@ -67,20 +109,39 @@ module MakeTaggable
|
|
|
67
109
|
taggable.save unless skip_save
|
|
68
110
|
end
|
|
69
111
|
|
|
112
|
+
##
|
|
113
|
+
# Whether this record claims ownership of the tags it applies.
|
|
114
|
+
#
|
|
115
|
+
# @return [TrueClass, FalseClass]
|
|
116
|
+
#
|
|
70
117
|
def tagger?
|
|
71
118
|
self.class.is_tagger?
|
|
72
119
|
end
|
|
73
120
|
|
|
121
|
+
##
|
|
122
|
+
# @return [TrueClass, FalseClass]
|
|
123
|
+
# @see #tagger?
|
|
124
|
+
#
|
|
74
125
|
def is_tagger?
|
|
75
126
|
tagger?
|
|
76
127
|
end
|
|
77
128
|
end
|
|
78
129
|
|
|
130
|
+
##
|
|
131
|
+
# Replaces {ClassMethods#tagger?} once a model is a tagger.
|
|
132
|
+
#
|
|
79
133
|
module SingletonMethods
|
|
134
|
+
##
|
|
135
|
+
# @return [TrueClass, FalseClass] always `true`
|
|
136
|
+
#
|
|
80
137
|
def tagger?
|
|
81
138
|
true
|
|
82
139
|
end
|
|
83
140
|
|
|
141
|
+
##
|
|
142
|
+
# @return [TrueClass, FalseClass]
|
|
143
|
+
# @see #tagger?
|
|
144
|
+
#
|
|
84
145
|
def is_tagger?
|
|
85
146
|
tagger?
|
|
86
147
|
end
|
|
@@ -1,8 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable
|
|
2
|
-
|
|
4
|
+
##
|
|
5
|
+
# The join between a tag and the record it was applied to.
|
|
6
|
+
#
|
|
7
|
+
# A tagging records the context the tag was applied in, and optionally the tagger who applied it.
|
|
8
|
+
# Records own their taggings, so destroying a taggable destroys its taggings with it.
|
|
9
|
+
#
|
|
10
|
+
# @!attribute [rw] context
|
|
11
|
+
# The context the tag was applied in, such as `"skills"`.
|
|
12
|
+
# @return [String]
|
|
13
|
+
# @!attribute [rw] tag
|
|
14
|
+
# The tag being applied.
|
|
15
|
+
# @return [MakeTaggable::Tag]
|
|
16
|
+
# @!attribute [rw] taggable
|
|
17
|
+
# The record being tagged.
|
|
18
|
+
# @return [ActiveRecord::Base]
|
|
19
|
+
# @!attribute [rw] tagger
|
|
20
|
+
# The record that applied the tag, when it was applied by a tagger.
|
|
21
|
+
# @return [ActiveRecord::Base, NilClass]
|
|
22
|
+
#
|
|
23
|
+
class Tagging < ::ActiveRecord::Base
|
|
24
|
+
##
|
|
25
|
+
# The context used when none is given.
|
|
26
|
+
#
|
|
27
|
+
# @return [String]
|
|
28
|
+
#
|
|
29
|
+
DEFAULT_CONTEXT = "tags"
|
|
30
|
+
|
|
3
31
|
self.table_name = MakeTaggable.taggings_table
|
|
4
32
|
|
|
5
|
-
DEFAULT_CONTEXT = "tags"
|
|
6
33
|
belongs_to :tag, class_name: "::MakeTaggable::Tag", counter_cache: MakeTaggable.tags_counter
|
|
7
34
|
belongs_to :taggable, polymorphic: true
|
|
8
35
|
|
|
@@ -11,7 +38,7 @@ module MakeTaggable
|
|
|
11
38
|
scope :owned_by, ->(owner) { where(tagger: owner) }
|
|
12
39
|
scope :not_owned, -> { where(tagger_id: nil, tagger_type: nil) }
|
|
13
40
|
|
|
14
|
-
scope :by_contexts, ->(contexts) { where(context:
|
|
41
|
+
scope :by_contexts, ->(contexts) { where(context: contexts || DEFAULT_CONTEXT) }
|
|
15
42
|
scope :by_context, ->(context = DEFAULT_CONTEXT) { by_contexts(context.to_s) }
|
|
16
43
|
|
|
17
44
|
validates_presence_of :context
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable
|
|
4
|
+
##
|
|
5
|
+
# View helpers for rendering tags. Mixed into Action View automatically.
|
|
6
|
+
#
|
|
2
7
|
module TagsHelper
|
|
3
|
-
|
|
8
|
+
##
|
|
9
|
+
# Yields each tag with the CSS class matching how often it is used, for building a tag cloud.
|
|
10
|
+
#
|
|
11
|
+
# Classes are handed out in the order given, from least to most used, so the first class is the
|
|
12
|
+
# smallest and the last is the largest.
|
|
13
|
+
#
|
|
14
|
+
# @param tags [Array<MakeTaggable::Tag>] tags carrying a `taggings_count`
|
|
15
|
+
# @param classes [Array<String>] the CSS classes, smallest first
|
|
16
|
+
# @yieldparam tag [MakeTaggable::Tag] the tag
|
|
17
|
+
# @yieldparam css_class [String] the class for that tag's frequency
|
|
18
|
+
# @return [Array] the tags, or an empty array when none were given
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# <% tag_cloud(@tags, %w[css1 css2 css3 css4]) do |tag, css_class| %>
|
|
22
|
+
# <%= link_to tag.name, tag_path(tag.name), class: css_class %>
|
|
23
|
+
# <% end %>
|
|
24
|
+
#
|
|
4
25
|
def tag_cloud(tags, classes)
|
|
5
26
|
return [] if tags.empty?
|
|
6
27
|
|
data/lib/make_taggable/utils.rb
CHANGED
|
@@ -1,31 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module MakeTaggable
|
|
4
|
+
##
|
|
5
|
+
# Database differences the rest of the library needs to work around.
|
|
6
|
+
#
|
|
2
7
|
module Utils
|
|
3
8
|
class << self
|
|
9
|
+
##
|
|
10
|
+
# The connection tags are read and written through.
|
|
11
|
+
#
|
|
12
|
+
# @return [ActiveRecord::ConnectionAdapters::AbstractAdapter]
|
|
13
|
+
#
|
|
4
14
|
def connection
|
|
5
15
|
MakeTaggable::Tag.connection
|
|
6
16
|
end
|
|
7
17
|
|
|
18
|
+
##
|
|
19
|
+
# Whether tags are stored in PostgreSQL.
|
|
20
|
+
#
|
|
21
|
+
# @return [TrueClass, FalseClass]
|
|
22
|
+
#
|
|
8
23
|
def using_postgresql?
|
|
9
24
|
connection && connection.adapter_name == "PostgreSQL"
|
|
10
25
|
end
|
|
11
26
|
|
|
27
|
+
##
|
|
28
|
+
# Whether tags are stored in MySQL.
|
|
29
|
+
#
|
|
30
|
+
# @return [TrueClass, FalseClass]
|
|
31
|
+
#
|
|
12
32
|
def using_mysql?
|
|
13
33
|
connection && connection.adapter_name == "Mysql2"
|
|
14
34
|
end
|
|
15
35
|
|
|
36
|
+
##
|
|
37
|
+
# A short digest of a string, used to keep generated SQL aliases unique and within the
|
|
38
|
+
# identifier length databases allow.
|
|
39
|
+
#
|
|
40
|
+
# @param string [String] the value to digest
|
|
41
|
+
# @return [String] the first seven characters of the SHA1 hex digest
|
|
42
|
+
#
|
|
16
43
|
def sha_prefix(string)
|
|
17
44
|
Digest::SHA1.hexdigest(string)[0..6]
|
|
18
45
|
end
|
|
19
46
|
|
|
47
|
+
##
|
|
48
|
+
# The case-insensitive pattern operator for the current adapter.
|
|
49
|
+
#
|
|
50
|
+
# @return [String] `"ILIKE"` on PostgreSQL, otherwise `"LIKE"`
|
|
51
|
+
#
|
|
20
52
|
def like_operator
|
|
21
53
|
using_postgresql? ? "ILIKE" : "LIKE"
|
|
22
54
|
end
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
#
|
|
56
|
+
##
|
|
57
|
+
# Escapes the SQL pattern wildcards in a string so it matches literally.
|
|
58
|
+
#
|
|
59
|
+
# Pair it with an `ESCAPE '!'` clause.
|
|
60
|
+
#
|
|
61
|
+
# @param str [String] the value to escape
|
|
62
|
+
# @return [String] with `!`, `%` and `_` escaped
|
|
63
|
+
#
|
|
29
64
|
def escape_like(str)
|
|
30
65
|
str.gsub(/[!%_]/) { |x| "!" + x }
|
|
31
66
|
end
|