polytag 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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