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.
Files changed (45) hide show
  1. data/.DS_Store +0 -0
  2. data/.pryrc +27 -0
  3. data/.rspec +3 -1
  4. data/README.md +26 -4
  5. data/lib/.DS_Store +0 -0
  6. data/lib/generators/polytag/.DS_Store +0 -0
  7. data/lib/generators/polytag/install/.DS_Store +0 -0
  8. data/lib/generators/polytag/install/templates/create_polytag_tables.rb +25 -11
  9. data/lib/polytag.rb +221 -58
  10. data/lib/polytag/concerns/tag_owner.rb +31 -0
  11. data/lib/polytag/concerns/tag_owner/association_extensions.rb +15 -0
  12. data/lib/polytag/concerns/tag_owner/association_extensions/owned_tags.rb +16 -0
  13. data/lib/polytag/concerns/tag_owner/class_helpers.rb +25 -0
  14. data/lib/polytag/concerns/tag_owner/model_helpers.rb +78 -0
  15. data/lib/polytag/concerns/taggable.rb +24 -0
  16. data/lib/polytag/concerns/taggable/association_extensions.rb +15 -0
  17. data/lib/polytag/concerns/taggable/class_helpers.rb +16 -0
  18. data/lib/polytag/concerns/taggable/model_helpers.rb +60 -0
  19. data/lib/polytag/connection.rb +27 -0
  20. data/lib/polytag/exceptions.rb +7 -0
  21. data/lib/polytag/tag.rb +6 -24
  22. data/lib/polytag/tag_group.rb +8 -46
  23. data/lib/polytag/version.rb +1 -1
  24. data/polytag.gemspec +7 -3
  25. data/spec/spec_helper.rb +15 -10
  26. data/spec/specs/owner_spec.rb +104 -0
  27. data/spec/specs/taggable_with_owner_spec.rb +120 -0
  28. data/spec/specs/taggable_without_owner_spec.rb +59 -0
  29. data/spec/support/active_record.rb +6 -6
  30. data/spec/support/owner.rb +1 -1
  31. data/spec/support/taggable.rb +3 -0
  32. metadata +115 -46
  33. checksums.yaml +0 -7
  34. data/circle.yml +0 -3
  35. data/lib/polytag/tag_group/owner.rb +0 -17
  36. data/lib/polytag/tag_relation.rb +0 -15
  37. data/spec/specs/polytag_querying_spec.rb +0 -56
  38. data/spec/specs/polytag_test_taggable_four_spec.rb +0 -54
  39. data/spec/specs/polytag_test_taggable_one_spec.rb +0 -47
  40. data/spec/specs/polytag_test_taggable_three_spec.rb +0 -51
  41. data/spec/specs/polytag_test_taggable_two_spec.rb +0 -51
  42. data/spec/support/test_taggable_four.rb +0 -7
  43. data/spec/support/test_taggable_one.rb +0 -3
  44. data/spec/support/test_taggable_three.rb +0 -8
  45. data/spec/support/test_taggable_two.rb +0 -7
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
@@ -1,4 +1,6 @@
1
1
  --color
2
- --format documentation
2
+ --format Fuubar
3
3
  --backtrace
4
4
  --profile
5
+ --order default
6
+ --fail-fast
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Polytag
2
2
 
3
- ![CircleCI Build Status](https://circleci.com/gh/JIFFinc/polytag.png)
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
- Please see specs in the meantime.
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
 
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
- # Create the relations table
11
- create_table :polytag_tag_relations do |t|
12
- t.belongs_to :tagged, polymorphic: true, index: true
13
- t.belongs_to :polytag_tag, index: true
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
- # Create the tag groups table
18
- create_table :polytag_tag_groups do |t|
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.string :name, index: true, null: true, default: nil
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], unique: true
26
- add_index :polytag_tag_groups, [:owner_type, :owner_id, :name], unique: true
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 :polytag_tag_relations
44
+ drop_table :polytag_tag_groups
45
+ drop_table :polytag_tag_connections
32
46
  end
33
47
  end
@@ -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
- def self.included(base)
9
- base.extend(ClassMethods)
10
- base.has_many :polytag_tag_relations, class_name: '::Polytag::TagRelation',
11
- as: :tagged
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
- base.has_many :tag_relations, class_name: '::Polytag::TagRelation',
14
- as: :tagged
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
- base.has_many :polytag_tags, class_name: '::Polytag::Tag',
17
- through: :polytag_tag_relations
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
- base.has_many :tags, class_name: '::Polytag::Tag',
20
- through: :polytag_tag_relations,
21
- source: :polytag_tag
22
- end
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
- def tag_group(_tag_group = nil)
25
- @__polytag_tag_group_hash__ ||= {}
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
- if _tag_group
28
- @__polytag_tag_group_hash__.merge!(_tag_group)
29
- @__polytag_tag_group__ = Polytag::TagGroup.search_by_hash(@__polytag_tag_group_hash__).first_or_create
30
- else
31
- @__polytag_tag_group_hash__ = {} if _tag_group.is_a?(Hash) && _tag_group.empty?
32
- @__polytag_tag_group__
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
- def add_tag(tag, _tag_group = {})
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
- def add_tag!(tag, _tag_group = {})
41
- polytag_tags << polytag_tags.where(name: tag, polytag_tag_group_id: tag_group(_tag_group).try(&:id)).first_or_create
42
- end
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
- def add_tags(*_tags)
45
- _tag_group = _tags.pop if _tags.last.is_a?(Hash)
46
- _tags.map { |x| add_tag(x, _tag_group || {}) }
47
- end
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
- def add_tags!(*_tags)
50
- _tag_group = _tags.pop if _tags.last.is_a?(Hash)
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
- def remove_tag!(tag, _tag_group = {})
55
- tag_id = polytag_tags.where(name: tag, polytag_tag_group_id: tag_group(_tag_group).try(&:id)).first.try(:id)
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
- module ClassMethods
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
- # Get records with tags
62
- def has_tags(*tags)
63
- includes(polytag_tag_relations: :polytag_tag).references(:polytag_tags).where("polytag_tags.name IN (?)", tags).group("#{table_name}.id")
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
- alias_method :has_tag, :has_tags
223
+ private
67
224
 
68
- def in_tag_group(_tag_group = {})
69
- if _tag_group[:group_ids]
70
- tag_groups = _tag_group[:group_ids]
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
- return tag_groups if _tag_group[:multi]
231
+ def __connection_processor(data)
232
+ foc = data.delete(:foc)
76
233
 
77
- includes(polytag_tag_relations: :polytag_tag).references(:polytag_tags).where('polytag_tags.polytag_tag_group_id IN (?)', tag_groups)
78
- end
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
- def in_tag_groups(*tag_groups)
81
- tag_groups = tag_groups.map{ |tg| in_tag_group(tg.merge(multi: true)) }.flatten.compact
82
- in_tag_group(group_ids: tag_groups)
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
- # @TODO: Implement results must be connected to all tags
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