polytag 0.0.4 → 0.1.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/.DS_Store +0 -0
- data/.pryrc +27 -0
- data/.rspec +3 -1
- data/README.md +26 -4
- data/lib/.DS_Store +0 -0
- data/lib/generators/polytag/.DS_Store +0 -0
- data/lib/generators/polytag/install/.DS_Store +0 -0
- data/lib/generators/polytag/install/templates/create_polytag_tables.rb +25 -11
- data/lib/polytag.rb +221 -58
- data/lib/polytag/concerns/tag_owner.rb +31 -0
- data/lib/polytag/concerns/tag_owner/association_extensions.rb +15 -0
- data/lib/polytag/concerns/tag_owner/association_extensions/owned_tags.rb +16 -0
- data/lib/polytag/concerns/tag_owner/class_helpers.rb +25 -0
- data/lib/polytag/concerns/tag_owner/model_helpers.rb +78 -0
- data/lib/polytag/concerns/taggable.rb +24 -0
- data/lib/polytag/concerns/taggable/association_extensions.rb +15 -0
- data/lib/polytag/concerns/taggable/class_helpers.rb +16 -0
- data/lib/polytag/concerns/taggable/model_helpers.rb +60 -0
- data/lib/polytag/connection.rb +27 -0
- data/lib/polytag/exceptions.rb +7 -0
- data/lib/polytag/tag.rb +6 -24
- data/lib/polytag/tag_group.rb +8 -46
- data/lib/polytag/version.rb +1 -1
- data/polytag.gemspec +7 -3
- data/spec/spec_helper.rb +15 -10
- data/spec/specs/owner_spec.rb +104 -0
- data/spec/specs/taggable_with_owner_spec.rb +120 -0
- data/spec/specs/taggable_without_owner_spec.rb +59 -0
- data/spec/support/active_record.rb +6 -6
- data/spec/support/owner.rb +1 -1
- data/spec/support/taggable.rb +3 -0
- metadata +115 -46
- checksums.yaml +0 -7
- data/circle.yml +0 -3
- data/lib/polytag/tag_group/owner.rb +0 -17
- data/lib/polytag/tag_relation.rb +0 -15
- data/spec/specs/polytag_querying_spec.rb +0 -56
- data/spec/specs/polytag_test_taggable_four_spec.rb +0 -54
- data/spec/specs/polytag_test_taggable_one_spec.rb +0 -47
- data/spec/specs/polytag_test_taggable_three_spec.rb +0 -51
- data/spec/specs/polytag_test_taggable_two_spec.rb +0 -51
- data/spec/support/test_taggable_four.rb +0 -7
- data/spec/support/test_taggable_one.rb +0 -3
- data/spec/support/test_taggable_three.rb +0 -8
- data/spec/support/test_taggable_two.rb +0 -7
data/.DS_Store
ADDED
Binary file
|
data/.pryrc
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
begin
|
2
|
+
require 'awesome_print'
|
3
|
+
Pry.config.print = proc { |output, value| output.puts value.ai }
|
4
|
+
rescue LoadError => err
|
5
|
+
puts "no awesome_print :("
|
6
|
+
end
|
7
|
+
|
8
|
+
GEM_DIR = File.dirname(__FILE__)
|
9
|
+
SPEC_DIR = File.join(GEM_DIR, 'spec')
|
10
|
+
|
11
|
+
# Load dependencies
|
12
|
+
require 'rspec/rails/extensions/active_record/base'
|
13
|
+
require 'active_support'
|
14
|
+
require 'active_record'
|
15
|
+
|
16
|
+
# Load in the support for AR
|
17
|
+
require "#{SPEC_DIR}/support/active_record"
|
18
|
+
|
19
|
+
# Load in the gem we are testing
|
20
|
+
require 'polytag'
|
21
|
+
|
22
|
+
# The test models
|
23
|
+
require "#{SPEC_DIR}/support/owner"
|
24
|
+
require "#{SPEC_DIR}/support/test_taggable_one"
|
25
|
+
require "#{SPEC_DIR}/support/test_taggable_two"
|
26
|
+
require "#{SPEC_DIR}/support/test_taggable_three"
|
27
|
+
require "#{SPEC_DIR}/support/test_taggable_four"
|
data/.rspec
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Polytag
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Adds the ability to easily add tags with optional tag groups to models.
|
3
|
+
TODO: Write a gem description
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -20,7 +18,31 @@ Or install it yourself as:
|
|
20
18
|
|
21
19
|
## Usage
|
22
20
|
|
23
|
-
|
21
|
+
Basic usage with a model
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
include Polytag::Concerns::Taggable
|
26
|
+
end
|
27
|
+
|
28
|
+
user = User.new
|
29
|
+
|
30
|
+
# Add a tag
|
31
|
+
user.tag.new('apple')
|
32
|
+
=> #<Polytag::Tag>
|
33
|
+
|
34
|
+
# Remove a tag
|
35
|
+
user.tag.del('apple')
|
36
|
+
=> true/false
|
37
|
+
|
38
|
+
# Check for tag
|
39
|
+
user.tag.has_tag?('apple')
|
40
|
+
=> true/false
|
41
|
+
|
42
|
+
# Get all tags
|
43
|
+
user.tags
|
44
|
+
=> #<ActiveRecord::Relation>
|
45
|
+
```
|
24
46
|
|
25
47
|
## Contributing
|
26
48
|
|
data/lib/.DS_Store
ADDED
Binary file
|
Binary file
|
Binary file
|
@@ -2,32 +2,46 @@ class CreatePolytagTables < ActiveRecord::Migration
|
|
2
2
|
def self.up
|
3
3
|
# Create the tags table
|
4
4
|
create_table :polytag_tags do |t|
|
5
|
-
t.belongs_to :polytag_tag_group, index:true, null: true, default: nil
|
6
5
|
t.string :name, index: true
|
6
|
+
t.belongs_to :polytag_tag_group, index: true
|
7
7
|
t.timestamps
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
create_table :
|
12
|
-
t.
|
13
|
-
t.belongs_to :
|
10
|
+
# Create the tag groups table
|
11
|
+
create_table :polytag_tag_groups do |t|
|
12
|
+
t.string :name, index: true
|
13
|
+
t.belongs_to :owner, polymorphic: true, index: true
|
14
14
|
t.timestamps
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
create_table :
|
17
|
+
# Create the tag group relations table
|
18
|
+
create_table :polytag_connections do |t|
|
19
|
+
t.belongs_to :polytag_tag, index: true
|
20
|
+
t.belongs_to :polytag_tag_group, index: true
|
19
21
|
t.belongs_to :owner, polymorphic: true, index: true
|
20
|
-
t.
|
22
|
+
t.belongs_to :tagged, polymorphic: true, index: true
|
21
23
|
t.timestamps
|
22
24
|
end
|
23
25
|
|
24
26
|
# Index for the category and name
|
25
|
-
add_index :polytag_tags, [:polytag_tag_group_id, :name],
|
26
|
-
|
27
|
+
add_index :polytag_tags, [:polytag_tag_group_id, :name],
|
28
|
+
name: :polytag_tags_unique,
|
29
|
+
unique: true
|
30
|
+
|
31
|
+
add_index :polytag_tag_groups,
|
32
|
+
[:owner_type, :owner_id, :name],
|
33
|
+
name: :polytag_tag_groups_unique,
|
34
|
+
unique: true
|
35
|
+
|
36
|
+
add_index :polytag_connections,
|
37
|
+
[:polytag_tag_id, :polytag_tag_group_id, :owner_type, :owner_id, :tagged_type, :tagged_id],
|
38
|
+
name: :polytag_connections_unique,
|
39
|
+
unique: true
|
27
40
|
end
|
28
41
|
|
29
42
|
def self.down
|
30
43
|
drop_table :polytag_tags
|
31
|
-
drop_table :
|
44
|
+
drop_table :polytag_tag_groups
|
45
|
+
drop_table :polytag_tag_connections
|
32
46
|
end
|
33
47
|
end
|
data/lib/polytag.rb
CHANGED
@@ -1,87 +1,250 @@
|
|
1
1
|
require "polytag/version"
|
2
|
+
require "polytag/exceptions"
|
3
|
+
|
4
|
+
# Real work
|
5
|
+
require "polytag/connection"
|
2
6
|
require "polytag/tag"
|
3
7
|
require "polytag/tag_group"
|
4
|
-
require "polytag/tag_group/owner"
|
5
|
-
require "polytag/tag_relation"
|
6
8
|
|
9
|
+
# Taggable Concerns
|
10
|
+
require "polytag/concerns/taggable/association_extensions"
|
11
|
+
require "polytag/concerns/taggable/class_helpers"
|
12
|
+
require "polytag/concerns/taggable/model_helpers"
|
13
|
+
require "polytag/concerns/taggable"
|
14
|
+
|
15
|
+
# Tag Owner Concerns
|
16
|
+
require "polytag/concerns/tag_owner/association_extensions/owned_tags"
|
17
|
+
require "polytag/concerns/tag_owner/association_extensions"
|
18
|
+
require "polytag/concerns/tag_owner/class_helpers"
|
19
|
+
require "polytag/concerns/tag_owner/model_helpers"
|
20
|
+
require "polytag/concerns/tag_owner"
|
21
|
+
|
22
|
+
# Polytag Module
|
7
23
|
module Polytag
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
24
|
+
class << self
|
25
|
+
def get(type = :tag, foc = nil, data = {})
|
26
|
+
force_name_search = false
|
27
|
+
retried = false
|
28
|
+
|
29
|
+
# Allow hashes to be passed
|
30
|
+
if type.is_a?(Hash) || type.is_a?(ActiveRecord::Base)
|
31
|
+
data = type
|
32
|
+
type = nil
|
33
|
+
foc = nil
|
34
|
+
elsif foc.is_a?(Hash) || foc.is_a?(ActiveRecord::Base)
|
35
|
+
data = foc
|
36
|
+
foc = nil
|
37
|
+
end
|
12
38
|
|
13
|
-
|
14
|
-
|
39
|
+
# Reject nil or false data keys
|
40
|
+
# also grab the foc value
|
41
|
+
if data.is_a?(Hash)
|
42
|
+
data = data.delete_if { |k, v| v.nil? || v == false }
|
43
|
+
foc = data.delete(:foc) if data.has_key?(:foc)
|
44
|
+
data[:tag_group] = :default if data[:owner] && ! data[:tag_group]
|
45
|
+
end
|
15
46
|
|
16
|
-
|
17
|
-
|
47
|
+
# Ensure that we are processing the right data if the data comes in in a unexpected way
|
48
|
+
if data.is_a?(Hash) && data.keys.size == 1 && [:tag, :tag_group].include?(data.keys.first)
|
49
|
+
type = data.keys.first
|
50
|
+
data = data[data.keys.first]
|
51
|
+
end
|
18
52
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
53
|
+
# Return the model if the model passed matches
|
54
|
+
if data.is_a?(ActiveRecord::Base)
|
55
|
+
return data if data.instance_of?(Tag) && (type ? type == :tag : true)
|
56
|
+
return data if data.instance_of?(TagGroup) && (type ? type == :tag_group : true)
|
57
|
+
return data if data.instance_of?(Connection) && (type ? type == :connection : true)
|
58
|
+
raise NotAPolytagModel, "#{data.inspect} is not a Polytag model in the requested form."
|
59
|
+
elsif data.is_a?(String) || data.is_a?(Symbol)
|
60
|
+
data = "#{data}".strip
|
61
|
+
end
|
23
62
|
|
24
|
-
|
25
|
-
|
63
|
+
# Handle how the results are returned
|
64
|
+
if data.is_a?(Hash) && data.keys.sort == [:owner, :tag, :tag_group, :tagged]
|
65
|
+
|
66
|
+
# Get the tag owner
|
67
|
+
tag_owner = get_tag_owner_or_taggable(:hash, data[:owner])
|
68
|
+
|
69
|
+
# Get the tag group
|
70
|
+
querydata = {owner: tag_owner, tag_group: data[:tag_group], foc: foc}
|
71
|
+
tag_group = get(:tag_group, foc, querydata)
|
72
|
+
|
73
|
+
# Get the tag
|
74
|
+
querydata = {tag: data[:tag], tag_group: tag_group, foc: foc}
|
75
|
+
tag = get(:tag, foc, querydata)
|
76
|
+
|
77
|
+
# Create the data we are using to create the connection
|
78
|
+
querydata = tag_owner.merge foc: foc,
|
79
|
+
polytag_tag_id: tag.id,
|
80
|
+
polytag_tag_group_id: tag_group.id,
|
81
|
+
tagged: data[:tagged]
|
82
|
+
|
83
|
+
__connection_processor(querydata)
|
84
|
+
elsif data.is_a?(Hash) && data.keys.sort == [:tag, :tag_group, :tagged]
|
85
|
+
|
86
|
+
# Get the tag group
|
87
|
+
querydata = {tag_group: data[:tag_group], foc: foc}
|
88
|
+
tag_group = get(:tag_group, foc, querydata)
|
89
|
+
|
90
|
+
# Get the tag
|
91
|
+
querydata = {tag: data[:tag], tag_group: tag_group, foc: foc}
|
92
|
+
tag = get(:tag, foc, querydata)
|
93
|
+
|
94
|
+
# Create the data we are using to create the connection
|
95
|
+
querydata = {}.merge foc: foc,
|
96
|
+
polytag_tag_group_id: tag_group.id,
|
97
|
+
polytag_tag_id: tag.id,
|
98
|
+
tagged: data[:tagged]
|
99
|
+
|
100
|
+
__connection_processor(querydata)
|
101
|
+
elsif data.is_a?(Hash) && data.keys.sort == [:tag, :tagged]
|
102
|
+
|
103
|
+
# Get the tag
|
104
|
+
querydata = {tag: data[:tag], foc: foc}
|
105
|
+
tag = get(:tag, foc, querydata) do |ar|
|
106
|
+
ar.where(polytag_tag_group_id: nil)
|
107
|
+
end
|
108
|
+
|
109
|
+
# If we expected a result and we don't have one raise
|
110
|
+
raise CantFindPolytagModel if foc && ! tag
|
111
|
+
|
112
|
+
# Create the data we are using to create the connection
|
113
|
+
querydata = {}.merge foc: foc,
|
114
|
+
polytag_tag_group_id: nil,
|
115
|
+
polytag_tag_id: tag.id,
|
116
|
+
tagged: data[:tagged]
|
117
|
+
|
118
|
+
__connection_processor(querydata)
|
119
|
+
elsif data.is_a?(Hash) && data.keys.sort == [:owner, :tag, :tag_group]
|
120
|
+
|
121
|
+
# Get the tag owner
|
122
|
+
tag_owner = get_tag_owner_or_taggable(data[:owner])
|
123
|
+
|
124
|
+
# Get the tag group
|
125
|
+
querydata = {owner: tag_owner, tag_group: data[:tag_group], foc: foc}
|
126
|
+
tag_group = get(:tag_group, foc || :first, querydata)
|
127
|
+
|
128
|
+
# Get the tag
|
129
|
+
querydata = {tag: data[:tag], tag_group: tag_group, foc: foc}
|
130
|
+
tag = get(:tag, foc, querydata)
|
131
|
+
|
132
|
+
return tag
|
133
|
+
elsif data.is_a?(Hash) && data.keys.sort == [:owner, :tag_group]
|
134
|
+
# Get the tag group with owner
|
135
|
+
tag_owner = get_tag_owner_or_taggable(:hash, data[:owner])
|
136
|
+
|
137
|
+
get(:tag_group, foc, data[:tag_group]) do |ar|
|
138
|
+
ar.where(tag_owner)
|
139
|
+
end
|
140
|
+
elsif data.is_a?(Hash) && data.keys.sort == [:tag, :tag_group]
|
141
|
+
# Get the tag with tag group
|
142
|
+
tag_group = get(:tag_group, foc || :first, data[:tag_group])
|
143
|
+
|
144
|
+
get(:tag, foc, data[:tag]) do |ar|
|
145
|
+
ar.where(polytag_tag_group_id: tag_group.id)
|
146
|
+
end
|
147
|
+
elsif type && ! force_name_search && __numerical_string_id?(data)
|
148
|
+
|
149
|
+
# Force foc to be a first if the option is requested
|
150
|
+
foc = :first if foc
|
151
|
+
|
152
|
+
result = const_get("#{type}".camelize).where(id: data.to_i)
|
153
|
+
result = yield(result) if block_given?
|
154
|
+
result = result.__send__(foc) if foc
|
155
|
+
return result
|
156
|
+
elsif type && (force_name_search || data.is_a?(String))
|
157
|
+
|
158
|
+
result = const_get("#{type}".camelize).where(name: data)
|
159
|
+
result = yield(result) if block_given?
|
160
|
+
result = result.__send__(foc) if foc
|
161
|
+
return result
|
162
|
+
else
|
163
|
+
raise CantFindPolytagModel, "Can't find a polytag model with #{data.inspect}."
|
164
|
+
end
|
165
|
+
rescue ActiveRecord::RecordNotFound => e
|
166
|
+
raise e if e.instance_of?(CantFindPolytagModel) || retried
|
167
|
+
force_name_search = true
|
168
|
+
retried = true
|
169
|
+
foc = :first
|
170
|
+
retry
|
171
|
+
end
|
26
172
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
173
|
+
def tag_group_owner?(owner = {}, raise_on_error = false)
|
174
|
+
owner = get_polymorphic(owner)
|
175
|
+
return true if __inherits(data.class, ActiveRecord::Base, Concerns::TagOwner)
|
176
|
+
raise NotTagOwner, "This model #{owner.inspect} is not a polytag tag owner."
|
177
|
+
rescue NotTagOwner, NotTagOwnerOrTaggable => e
|
178
|
+
raise e unless raise_on_error
|
179
|
+
false
|
33
180
|
end
|
34
|
-
end
|
35
181
|
|
36
|
-
|
37
|
-
polytag_tags << polytag_tags.where(name: tag, polytag_tag_group_id: tag_group(_tag_group).try(&:id)).first_or_initialize
|
38
|
-
end
|
182
|
+
def get_tag_owner_or_taggable(result = :object, data = {})
|
39
183
|
|
40
|
-
|
41
|
-
|
42
|
-
|
184
|
+
# Ensure result is is always set
|
185
|
+
if result.is_a?(Hash) || result.is_a?(ActiveRecord::Base)
|
186
|
+
data = result
|
187
|
+
result = :object
|
188
|
+
end
|
43
189
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
190
|
+
if data.is_a?(Hash)
|
191
|
+
data = if data.keys.sort == [:id, :type]
|
192
|
+
data[:type].camelize.constantize.find(data[:id])
|
193
|
+
elsif data.keys.sort == [:owner_id, :owner_type]
|
194
|
+
data[:owner_type].camelize.constantize.find(data[:owner_id])
|
195
|
+
elsif data.keys.sort == [:tagged_id, :tagged_type]
|
196
|
+
data[:tagged_type].camelize.constantize.find(data[:tagged_id])
|
197
|
+
end
|
198
|
+
end
|
48
199
|
|
49
|
-
|
50
|
-
|
51
|
-
_tags.map { |x| add_tag!(x, _tag_group || {}) }
|
52
|
-
end
|
200
|
+
# The class we need to test
|
201
|
+
dclass = data.class
|
53
202
|
|
54
|
-
|
55
|
-
|
56
|
-
tag_relations.where("polytag_tag_relations.polytag_tag_id = ?", tag_id).delete_all
|
57
|
-
end
|
203
|
+
# Ensure that the model we return is a taggable or tag owner
|
204
|
+
if __inherits(dclass, ActiveRecord::Base, Concerns::TagOwner) || __inherits(dclass, ActiveRecord::Base, Concerns::Taggable)
|
58
205
|
|
59
|
-
|
206
|
+
# Return hash options for the type
|
207
|
+
if result == :hash
|
208
|
+
if __inherits(dclass, ActiveRecord::Base, Concerns::TagOwner)
|
209
|
+
return {owner_type: "#{dclass}", owner_id: data.id}
|
210
|
+
elsif __inherits(dclass, ActiveRecord::Base, Concerns::Taggable)
|
211
|
+
return {tagged_type: "#{dclass}", tagged_id: data.id}
|
212
|
+
end
|
213
|
+
end
|
60
214
|
|
61
|
-
|
62
|
-
|
63
|
-
|
215
|
+
# Return the raw object
|
216
|
+
return data if result == :object
|
217
|
+
end
|
218
|
+
|
219
|
+
# Raise if not a taggable or tag owner object
|
220
|
+
raise NotTagOwnerOrTaggable, "The model #{data.inspect} is not a polytag tag owner or taggable model."
|
64
221
|
end
|
65
222
|
|
66
|
-
|
223
|
+
private
|
67
224
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
else
|
72
|
-
tag_groups = Polytag::TagGroup.select('polytag_tag_groups.id').search_by_hash(_tag_group).map{ |tg| tg.try(:id) }.flatten
|
225
|
+
def __inherits(klass, *ancestors)
|
226
|
+
ancestors.inject(true) do |result, ancestor|
|
227
|
+
result && klass.ancestors.include?(ancestor)
|
73
228
|
end
|
229
|
+
end
|
74
230
|
|
75
|
-
|
231
|
+
def __connection_processor(data)
|
232
|
+
foc = data.delete(:foc)
|
76
233
|
|
77
|
-
|
78
|
-
|
234
|
+
# Get the item we are trying to tag
|
235
|
+
taggable = get_tag_owner_or_taggable(:hash, data.delete(:tagged))
|
236
|
+
|
237
|
+
# Get the connection where statement
|
238
|
+
connection_hash = data.merge(taggable)
|
79
239
|
|
80
|
-
|
81
|
-
|
82
|
-
|
240
|
+
# Find the object and create it if applicable
|
241
|
+
result = Connection.where(connection_hash)
|
242
|
+
result = result.__send__(foc) if foc
|
243
|
+
result
|
83
244
|
end
|
84
245
|
|
85
|
-
|
246
|
+
def __numerical_string_id?(data)
|
247
|
+
(data.is_a?(String) && data.match(/^\d+$/)) || data.is_a?(Fixnum)
|
248
|
+
end
|
86
249
|
end
|
87
250
|
end
|