no_fly_list 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoFlyList
4
+ module TaggableRecord
5
+ module Query
6
+ module PostgresqlStrategy
7
+ extend BaseStrategy
8
+
9
+ module_function
10
+
11
+ def define_query_methods(setup)
12
+ context = setup.context
13
+ taggable_klass = setup.taggable_klass
14
+ tagging_klass = setup.tagging_class_name.constantize
15
+ tagging_table = tagging_klass.arel_table
16
+ tag_klass = setup.tag_class_name.constantize
17
+ tag_table = tag_klass.arel_table
18
+ singular_name = context.to_s.singularize
19
+
20
+ taggable_klass.class_eval do
21
+ # Find records with any of the specified tags
22
+ scope "with_any_#{context}", lambda { |*tags|
23
+ tags = tags.flatten.compact.uniq
24
+ return none if tags.empty?
25
+
26
+ query = Arel::SelectManager.new(self)
27
+ .from(table_name)
28
+ .project(arel_table[primary_key])
29
+ .distinct
30
+ .join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
31
+ .join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
32
+ .where(tagging_table[:context].eq(singular_name))
33
+ .where(tag_table[:name].in(tags))
34
+
35
+ where(arel_table[primary_key].in(query))
36
+ }
37
+
38
+ scope "with_all_#{context}", lambda { |*tags|
39
+ tags = tags.flatten.compact.uniq
40
+ return none if tags.empty?
41
+
42
+ count_function = Arel::Nodes::NamedFunction.new(
43
+ 'COUNT',
44
+ [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:name]])]
45
+ )
46
+
47
+ query = Arel::SelectManager.new(self)
48
+ .from(table_name)
49
+ .project(arel_table[primary_key])
50
+ .join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
51
+ .join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
52
+ .where(tagging_table[:context].eq(singular_name))
53
+ .where(tag_table[:name].in(tags))
54
+ .group(arel_table[primary_key])
55
+ .having(count_function.eq(tags.size))
56
+
57
+ where(arel_table[primary_key].in(query))
58
+ }
59
+
60
+ # Find records without any of the specified tags
61
+ scope "without_any_#{context}", lambda { |*tags|
62
+ tags = tags.flatten.compact.uniq
63
+ return all if tags.empty?
64
+
65
+ subquery = joins(tagging_table)
66
+ .where(tagging_table[:context].eq(singular_name))
67
+ .where(tagging_table.name => { context: singular_name })
68
+ .where(tag_table.name => { name: tags })
69
+ .select(primary_key)
70
+
71
+ where.not(primary_key => subquery)
72
+ }
73
+
74
+ # Find records without any tags
75
+ scope "without_#{context}", lambda {
76
+ subquery = if setup.polymorphic
77
+ setup.tagging_class_name.constantize
78
+ .where(context: singular_name)
79
+ .where(taggable_type: name)
80
+ .select(:taggable_id)
81
+ else
82
+ setup.tagging_class_name.constantize
83
+ .where(context: singular_name)
84
+ .select(:taggable_id)
85
+ end
86
+
87
+ where.not(primary_key => subquery)
88
+ }
89
+
90
+ # Find records with exactly these tags
91
+ scope "with_exact_#{context}", lambda { |*tags|
92
+ tags = tags.flatten.compact.uniq
93
+
94
+ if tags.empty?
95
+ send("without_#{context}")
96
+ else
97
+ Arel::Nodes::NamedFunction.new(
98
+ 'COUNT',
99
+ [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
100
+ )
101
+
102
+ # Build the query for records having exactly the tags
103
+ all_tags_query = select(arel_table[primary_key])
104
+ .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
105
+ .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
106
+ .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
107
+ .where("#{tag_table.name}.name IN (?)", tags)
108
+ .group(arel_table[primary_key])
109
+ .having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
110
+
111
+ # Build query for records with other tags
112
+ other_tags_query = select(arel_table[primary_key])
113
+ .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
114
+ .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
115
+ .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
116
+ .where("#{tag_table.name}.name NOT IN (?)", tags)
117
+
118
+ # Combine queries
119
+ where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
120
+ .where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
121
+ end
122
+ }
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoFlyList
4
+ module TaggableRecord
5
+ module Query
6
+ module SqliteStrategy
7
+ extend BaseStrategy
8
+
9
+ module_function
10
+
11
+ def define_query_methods(setup)
12
+ context = setup.context
13
+ taggable_klass = setup.taggable_klass
14
+ tagging_klass = setup.tagging_class_name.constantize
15
+ tagging_table = tagging_klass.arel_table
16
+ tag_klass = setup.tag_class_name.constantize
17
+ tag_table = tag_klass.arel_table
18
+ singular_name = context.to_s.singularize
19
+
20
+ taggable_klass.class_eval do
21
+ # Find records with any of the specified tags
22
+ scope "with_any_#{context}", lambda { |*tags|
23
+ tags = tags.flatten.compact.uniq
24
+ return none if tags.empty?
25
+
26
+ query = Arel::SelectManager.new(self)
27
+ .from(table_name)
28
+ .project(arel_table[primary_key])
29
+ .distinct
30
+ .join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
31
+ .join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
32
+ .where(tagging_table[:context].eq(singular_name))
33
+ .where(tag_table[:name].in(tags))
34
+
35
+ where(arel_table[primary_key].in(query))
36
+ }
37
+
38
+ scope "with_all_#{context}", lambda { |*tags|
39
+ tags = tags.flatten.compact.uniq
40
+ return none if tags.empty?
41
+
42
+ count_function = Arel::Nodes::NamedFunction.new(
43
+ 'COUNT',
44
+ [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:name]])]
45
+ )
46
+
47
+ query = Arel::SelectManager.new(self)
48
+ .from(table_name)
49
+ .project(arel_table[primary_key])
50
+ .join(tagging_table).on(tagging_table[:taggable_id].eq(arel_table[primary_key]))
51
+ .join(tag_table).on(tag_table[:id].eq(tagging_table[:tag_id]))
52
+ .where(tagging_table[:context].eq(singular_name))
53
+ .where(tag_table[:name].in(tags))
54
+ .group(arel_table[primary_key])
55
+ .having(count_function.eq(tags.size))
56
+
57
+ where(arel_table[primary_key].in(query))
58
+ }
59
+
60
+ # Find records without any of the specified tags
61
+ scope "without_any_#{context}", lambda { |*tags|
62
+ tags = tags.flatten.compact.uniq
63
+ return all if tags.empty?
64
+
65
+ # Build dynamic joins
66
+ tagged_ids = distinct
67
+ .joins("INNER JOIN #{tagging_table} ON #{tagging_table}.taggable_id = #{table_name}.id")
68
+ .joins("INNER JOIN #{context} ON #{context}.id = #{tagging_table}.tag_id")
69
+ .where("#{context}.name IN (?)", tags)
70
+ .pluck("#{table_name}.id")
71
+
72
+ # Handle empty tagged_ids explicitly for SQLite compatibility
73
+ where("#{table_name}.id NOT IN (?)", tagged_ids.present? ? tagged_ids : [-1])
74
+ }
75
+
76
+ # Find records without any tags
77
+ scope "without_#{context}", lambda {
78
+ subquery = if setup.polymorphic
79
+ setup.tagging_class_name.constantize
80
+ .where(context: singular_name, taggable_type: name)
81
+ .select(:taggable_id)
82
+ else
83
+ setup.tagging_class_name.constantize
84
+ .where(context: singular_name)
85
+ .select(:taggable_id)
86
+ end
87
+ where('id NOT IN (?)', subquery)
88
+ }
89
+
90
+ # Find records with exactly these tags
91
+ scope "with_exact_#{context}", lambda { |*tags|
92
+ tags = tags.flatten.compact.uniq
93
+
94
+ if tags.empty?
95
+ send("without_#{context}")
96
+ else
97
+ Arel::Nodes::NamedFunction.new(
98
+ 'COUNT',
99
+ [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
100
+ )
101
+
102
+ # Build the query for records having exactly the tags
103
+ all_tags_query = select(arel_table[primary_key])
104
+ .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
105
+ .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
106
+ .where("#{tagging_table.name}.context = ?", singular_name)
107
+ .where("#{tag_table.name}.name IN (?)", tags)
108
+ .group(arel_table[primary_key])
109
+ .having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
110
+
111
+ # Build query for records with other tags
112
+ other_tags_query = select(arel_table[primary_key])
113
+ .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
114
+ .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
115
+ .where("#{tagging_table.name}.context = ?", singular_name)
116
+ .where("#{tag_table.name}.name NOT IN (?)", tags)
117
+
118
+ # Combine queries using subqueries
119
+ where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
120
+ .where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
121
+ end
122
+ }
123
+
124
+ # Add tag counts
125
+ # Find records with exactly these tags
126
+ scope "with_exact_#{context}", lambda { |*tags|
127
+ tags = tags.flatten.compact.uniq
128
+
129
+ if tags.empty?
130
+ send("without_#{context}")
131
+ else
132
+ Arel::Nodes::NamedFunction.new(
133
+ 'COUNT',
134
+ [Arel::Nodes::NamedFunction.new('DISTINCT', [tag_table[:id]])]
135
+ )
136
+
137
+ # Build the query for records having exactly the tags
138
+ all_tags_query = select(arel_table[primary_key])
139
+ .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
140
+ .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
141
+ .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
142
+ .where("#{tag_table.name}.name IN (?)", tags)
143
+ .group(arel_table[primary_key])
144
+ .having("COUNT(DISTINCT #{tag_table.name}.id) = ?", tags.size)
145
+
146
+ # Build query for records with other tags
147
+ other_tags_query = select(arel_table[primary_key])
148
+ .joins("INNER JOIN #{tagging_table.name} ON #{tagging_table.name}.taggable_id = #{table_name}.#{primary_key}")
149
+ .joins("INNER JOIN #{tag_table.name} ON #{tag_table.name}.id = #{tagging_table.name}.tag_id")
150
+ .where("#{tagging_table.name}.context = ?", context.to_s.singularize)
151
+ .where("#{tag_table.name}.name NOT IN (?)", tags)
152
+
153
+ # Combine queries
154
+ where("#{table_name}.#{primary_key} IN (?)", all_tags_query)
155
+ .where("#{table_name}.#{primary_key} NOT IN (?)", other_tags_query)
156
+ end
157
+ }
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -6,93 +6,25 @@ module NoFlyList
6
6
  module_function
7
7
 
8
8
  def define_query_methods(setup)
9
- context = setup.context
10
- taggable_klass = setup.taggable_klass
11
- singular_name = context.to_s.singularize
12
-
13
- taggable_klass.class_eval do
14
- # Find records with any of the specified tags
15
- scope "with_any_#{context}", lambda { |*tags|
16
- tags = tags.flatten.compact.uniq
17
- return none if tags.empty?
18
-
19
- joins(:"#{singular_name}_taggings")
20
- .joins(context)
21
- .where("#{singular_name}_taggings": { context: singular_name })
22
- .where(context => { name: tags })
23
- .distinct
24
- }
25
-
26
- # Find records without any tags
27
- scope "without_#{context}", lambda {
28
- where.not(
29
- id: setup.tagging_class_name.constantize.where(context: singular_name).select(:taggable_id)
30
- )
31
- }
32
-
33
- # Find records with all specified tags
34
- scope "with_all_#{context}", lambda { |*tags|
35
- tags = tags.flatten.compact.uniq
36
- return none if tags.empty?
37
-
38
- tag_count = tags.size
39
- joins(:"#{singular_name}_taggings")
40
- .joins(context)
41
- .where("#{singular_name}_taggings": { context: singular_name })
42
- .where(context => { name: tags })
43
- .group(:id)
44
- .having("COUNT(DISTINCT #{context}.name) = ?", tag_count)
45
- }
46
-
47
- # Find records without specific tags
48
- scope "without_any_#{context}", lambda { |*tags|
49
- tags = tags.flatten.compact.uniq
50
- return all if tags.empty?
51
-
52
- where.not(
53
- id: joins(:"#{singular_name}_taggings")
54
- .joins(context)
55
- .where("#{singular_name}_taggings": { context: singular_name })
56
- .where(context => { name: tags })
57
- .select(:id)
58
- )
59
- }
60
-
61
- # Find records with exactly these tags
62
- scope "with_exact_#{context}", lambda { |*tags|
63
- tags = tags.flatten.compact.uniq
9
+ case setup.adapter
10
+ when :postgresql
11
+ PostgresqlStrategy.define_query_methods(setup)
12
+ when :mysql
13
+ MysqlStrategy.define_query_methods(setup)
14
+ else
15
+ SqliteStrategy.define_query_methods(setup)
16
+ end
17
+ end
64
18
 
65
- if tags.empty?
66
- send("without_#{context}")
67
- else
68
- # Get records with the exact count of specified tags
69
- having_exact_tags =
70
- joins(:"#{singular_name}_taggings")
71
- .joins(context)
72
- .where("#{singular_name}_taggings": { context: singular_name })
73
- .where(context => { name: tags })
74
- .group(:id)
75
- .having("COUNT(DISTINCT #{context}.name) = ?", tags.size)
76
- .select(:id)
19
+ module BaseStrategy
20
+ module_function
77
21
 
78
- # Exclude records that have any other tags
79
- having_exact_tags.where.not(
80
- id: joins(:"#{singular_name}_taggings")
81
- .joins(context)
82
- .where("#{singular_name}_taggings": { context: singular_name })
83
- .where.not(context => { name: tags })
84
- .select(:id)
85
- )
86
- end
87
- }
22
+ def case_insensitive_where(table, column, values)
23
+ raise NotImplementedError
24
+ end
88
25
 
89
- # Count tags for each record
90
- scope "#{context}_count", lambda {
91
- left_joins(:"#{singular_name}_taggings")
92
- .where("#{singular_name}_taggings": { context: singular_name })
93
- .group(:id)
94
- .select("#{table_name}.*, COUNT(DISTINCT #{singular_name}_taggings.id) as #{context}_count")
95
- }
26
+ def define_query_methods(setup)
27
+ raise NotImplementedError
96
28
  end
97
29
  end
98
30
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NoFlyList
4
+ module TaggableRecord
5
+ class TagSetup
6
+ attr_reader :taggable_klass, :context, :transformer, :polymorphic,
7
+ :restrict_to_existing, :limit,
8
+ :tag_class_name, :tagging_class_name, :adapter
9
+
10
+ def initialize(taggable_klass, context, options = {})
11
+ @taggable_klass = taggable_klass
12
+ @context = context
13
+ @transformer = options.fetch(:transformer, ApplicationTagTransformer)
14
+ @polymorphic = options.fetch(:polymorphic, false)
15
+ @restrict_to_existing = options.fetch(:restrict_to_existing, false)
16
+ @limit = options.fetch(:limit, nil)
17
+ @tag_class_name = determine_tag_class_name(taggable_klass, options)
18
+ @tagging_class_name = determine_tagging_class_name(taggable_klass, options)
19
+ @adapter = determine_adapter
20
+ end
21
+
22
+ private
23
+
24
+ def determine_adapter
25
+ case ActiveRecord::Base.connection.adapter_name.downcase
26
+ when 'postgresql'
27
+ :postgresql
28
+ when 'mysql2'
29
+ :mysql
30
+ else
31
+ :sqlite
32
+ end
33
+ end
34
+
35
+ def determine_tag_class_name(taggable_klass, options)
36
+ if options[:polymorphic]
37
+ Rails.application.config.no_fly_list.tag_class_name
38
+ else
39
+ options.fetch(:tag_class_name, "#{taggable_klass.name}Tag")
40
+ end
41
+ end
42
+
43
+ def determine_tagging_class_name(taggable_klass, options)
44
+ if options[:polymorphic]
45
+ Rails.application.config.no_fly_list.tagging_class_name
46
+ else
47
+ options.fetch(:tagging_class_name, "#{taggable_klass.name}::Tagging")
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -5,22 +5,73 @@ module NoFlyList
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
+ class_attribute :_no_fly_list, instance_writer: false
9
+ self._no_fly_list = Config.new self
10
+
8
11
  before_save :save_tag_proxies
12
+ before_validation :validate_tag_proxies
9
13
  end
10
14
 
11
15
  private
12
16
 
17
+ def validate_tag_proxies
18
+ return true if @validating_proxies
19
+
20
+ @validating_proxies = true
21
+ begin
22
+ instance_variables.each do |var|
23
+ next unless var.to_s.match?(/_list_proxy$/)
24
+
25
+ proxy = instance_variable_get(var)
26
+ next if proxy.nil? || proxy.valid?
27
+
28
+ proxy.errors.each do |error|
29
+ errors.add(:base, error.message)
30
+ end
31
+ return false
32
+ end
33
+ true
34
+ ensure
35
+ @validating_proxies = false
36
+ end
37
+ end
38
+
13
39
  def save_tag_proxies
14
- instance_variables.each do |var|
15
- next unless var.to_s.match?(/_list_proxy$/)
40
+ return true if @saving_proxies
41
+
42
+ @saving_proxies = true
43
+ begin
44
+ instance_variables.each do |var|
45
+ next unless var.to_s.match?(/_list_proxy$/)
16
46
 
17
- proxy = instance_variable_get(var)
18
- return false unless proxy.save
47
+ proxy = instance_variable_get(var)
48
+ next if proxy.nil?
49
+ return false unless proxy.save
50
+ end
51
+ true
52
+ ensure
53
+ @saving_proxies = false
19
54
  end
20
55
  end
21
56
 
57
+ def no_fly_list_config
58
+ self.class._no_fly_list
59
+ end
60
+
61
+ def tag_contexts
62
+ no_fly_list_config.tag_contexts
63
+ end
64
+
65
+ def options_for_context(context)
66
+ tag_contexts[context.to_sym]
67
+ end
68
+
22
69
  class_methods do
23
70
  def has_tags(*contexts, **options)
71
+ contexts.each do |context|
72
+ _no_fly_list.add_context(context, options)
73
+ end
74
+
24
75
  Configuration.setup_tagging(self, contexts, options)
25
76
  end
26
77
  end
@@ -57,19 +57,48 @@ module NoFlyList
57
57
  # @return [Boolean] true if the proxy is valid
58
58
  def save
59
59
  return true unless @pending_changes.any?
60
+ return false unless valid?
61
+
62
+ # Prevent recursive validation
63
+ @saving = true
64
+ begin
65
+ model.class.transaction do
66
+ # Always save parent first if needed
67
+ if model.new_record? && !model.save
68
+ errors.add(:base, 'Failed to save parent record')
69
+ raise ActiveRecord::Rollback
70
+ end
71
+
72
+ # Clear existing tags
73
+ model.send(context_taggings).delete_all
60
74
 
61
- if valid?
62
- @model.transaction do
63
- @model.send(@context.to_s).destroy_all
75
+ # Create new tags
64
76
  @pending_changes.each do |tag_name|
65
77
  tag = find_or_create_tag(tag_name)
66
- @model.send(@context.to_s) << tag if tag
78
+ next unless tag
79
+
80
+ attributes = {
81
+ tag: tag,
82
+ context: @context.to_s.singularize
83
+ }
84
+
85
+ if setup[:polymorphic]
86
+ attributes[:taggable_type] = model.class.name
87
+ attributes[:taggable_id] = model.id
88
+ end
89
+
90
+ # Use create! to ensure we catch any errors
91
+ model.send(context_taggings).create!(attributes)
67
92
  end
68
93
  end
94
+
69
95
  refresh_from_database
70
96
  true
71
- else
97
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
98
+ errors.add(:base, e.message)
72
99
  false
100
+ ensure
101
+ @saving = false
73
102
  end
74
103
  end
75
104
 
@@ -175,14 +204,6 @@ module NoFlyList
175
204
  current_list
176
205
  end
177
206
 
178
- def current_list
179
- if @pending_changes.any?
180
- @pending_changes
181
- else
182
- @model.send(@context.to_s).pluck(:name)
183
- end
184
- end
185
-
186
207
  def refresh_from_database
187
208
  @pending_changes = []
188
209
  end
@@ -198,7 +219,9 @@ module NoFlyList
198
219
  return unless @restrict_to_existing
199
220
  return if @pending_changes.empty?
200
221
 
201
- existing_tags = @tag_model.where(name: @pending_changes).pluck(:name)
222
+ # Transform tags to lowercase for comparison
223
+ normalized_changes = @pending_changes.map(&:downcase)
224
+ existing_tags = @tag_model.where('LOWER(name) IN (?)', normalized_changes).pluck(:name)
202
225
  missing_tags = @pending_changes - existing_tags
203
226
 
204
227
  return unless missing_tags.any?
@@ -206,6 +229,17 @@ module NoFlyList
206
229
  errors.add(:base, "The following tags do not exist: #{missing_tags.join(', ')}")
207
230
  end
208
231
 
232
+ def context_taggings
233
+ @context_taggings ||= "#{@context.to_s.singularize}_taggings"
234
+ end
235
+
236
+ def setup
237
+ @setup ||= begin
238
+ context = @context.to_sym
239
+ @model.class._no_fly_list.tag_contexts[context]
240
+ end
241
+ end
242
+
209
243
  def find_or_create_tag(tag_name)
210
244
  if @restrict_to_existing
211
245
  @tag_model.find_by(name: tag_name)
@@ -214,6 +248,49 @@ module NoFlyList
214
248
  end
215
249
  end
216
250
 
251
+ def save_changes
252
+ # Clear existing tags
253
+ model.send(context_taggings).delete_all
254
+
255
+ # Create new tags
256
+ @pending_changes.each do |tag_name|
257
+ tag = find_or_create_tag(tag_name)
258
+ next unless tag
259
+
260
+ attributes = {
261
+ tag: tag,
262
+ context: @context.to_s.singularize
263
+ }
264
+
265
+ # Add polymorphic attributes for polymorphic tags
266
+ if setup[:polymorphic]
267
+ attributes[:taggable_type] = model.class.name
268
+ attributes[:taggable_id] = model.id
269
+ end
270
+
271
+ # Use create! to ensure we catch any errors
272
+ model.send(context_taggings).create!(attributes)
273
+ end
274
+
275
+ refresh_from_database
276
+ true
277
+ end
278
+
279
+ def current_list
280
+ if @pending_changes.any?
281
+ @pending_changes
282
+ elsif setup[:polymorphic]
283
+ tagging_table = setup[:tagging_class_name].tableize
284
+ @model.send(@context.to_s)
285
+ .joins("INNER JOIN #{tagging_table} ON #{tagging_table}.tag_id = tags.id")
286
+ .where("#{tagging_table}.taggable_type = ? AND #{tagging_table}.taggable_id = ?",
287
+ @model.class.name, @model.id)
288
+ .pluck(:name)
289
+ else
290
+ @model.send(@context.to_s).pluck(:name)
291
+ end
292
+ end
293
+
217
294
  def limit_reached?
218
295
  @limit && current_list.size >= @limit
219
296
  end