Class: NoFlyList::TaggingProxy

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Naming
Includes:
ActiveModel::Conversion, ActiveModel::Validations, Enumerable
Defined in:
lib/no_fly_list/tagging_proxy.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, tag_model, context, transformer: "ApplicationTagTransformer", restrict_to_existing: false, limit: nil) ⇒ TaggingProxy

Creates a new tagging proxy

Parameters:

  • model (ActiveRecord::Base)

    Model being tagged

  • tag_model (Class)

    Tag model class

  • context (Symbol)

    Tagging context (e.g. :colors)

  • transformer (Class) (defaults to: "ApplicationTagTransformer")

    Class for transforming tag strings

  • restrict_to_existing (Boolean) (defaults to: false)

    Only allow existing tags

  • limit (Integer, nil) (defaults to: nil)

    Maximum number of tags allowed



21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/no_fly_list/tagging_proxy.rb', line 21

def initialize(model, tag_model, context,
               transformer: "ApplicationTagTransformer",
               restrict_to_existing: false,
               limit: nil)
  @model = model
  @tag_model = tag_model
  @context = context
  @transformer = resolve_transformer(transformer)
  @restrict_to_existing = restrict_to_existing
  @limit = limit
  @pending_changes = nil # Use nil to indicate no changes yet
  @clear_operation = false
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/no_fly_list/tagging_proxy.rb', line 55

def method_missing(method_name, *args)
  if current_list.respond_to?(method_name)
    current_list.send(method_name, *args)
  else
    case method_name.to_s
    when /\A(.+)_list=\z/
      set_list(::Regexp.last_match(1), args.first)
    when /\A(.+)_list\z/
      get_list(::Regexp.last_match(1))
    else
      super
    end
  end
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



9
10
11
# File 'lib/no_fly_list/tagging_proxy.rb', line 9

def context
  @context
end

#modelObject (readonly)

Returns the value of attribute model.



9
10
11
# File 'lib/no_fly_list/tagging_proxy.rb', line 9

def model
  @model
end

#tag_modelObject (readonly)

Returns the value of attribute tag_model.



9
10
11
# File 'lib/no_fly_list/tagging_proxy.rb', line 9

def tag_model
  @tag_model
end

#transformerObject (readonly)

Returns the value of attribute transformer.



9
10
11
# File 'lib/no_fly_list/tagging_proxy.rb', line 9

def transformer
  @transformer
end

Instance Method Details

#add(*tags) ⇒ TaggingProxy

Adds one or more tags to the current tag list

Parameters:

  • *tags (Array<String, Array<String>>)

    Tags to add:

    • Single string with comma-separated values ("tag1, tag2")
    • Single array of strings (["tag1", "tag2"])
    • Multiple string arguments ("tag1", "tag2")

Returns:



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/no_fly_list/tagging_proxy.rb', line 229

def add(*tags)
  return self if limit_reached?

  @clear_operation = false
  new_tags = if tags.size == 1 && tags.first.is_a?(String)
               transformer.parse_tags(tags.first)
  else
               tags.flatten.map { |tag| transformer.parse_tags(tag) }.flatten
  end
  return self if new_tags.empty?

  # Initialize @pending_changes with database values if not yet initialized
  @pending_changes = current_list_from_database if @pending_changes.nil?

  @pending_changes = @pending_changes + new_tags
  @pending_changes.uniq!
  mark_record_dirty
  self
end

#add!(*tags) ⇒ Object



249
250
251
252
# File 'lib/no_fly_list/tagging_proxy.rb', line 249

def add!(*tags)
  add(*tags)
  save
end

#additionsArray<String>

Returns tags that will be added (not in database but in pending changes)

Returns:

  • (Array<String>)

    Tags to be added



185
186
187
188
189
190
# File 'lib/no_fly_list/tagging_proxy.rb', line 185

def additions
  return [] if @clear_operation
  return [] if @pending_changes.nil?

  @pending_changes - current_list_from_database
end

#changed?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Determines if tags have changed from database state

Returns:

  • (Boolean)

    True if pending changes differ from database



51
52
53
# File 'lib/no_fly_list/tagging_proxy.rb', line 51

def changed?
  @clear_operation || (!@pending_changes.nil? && @pending_changes != current_list_from_database)
end

#clearTaggingProxy

Clears all tags

Examples:

Clear all tags

tags.clear #=> []

Returns:



289
290
291
292
293
294
295
# File 'lib/no_fly_list/tagging_proxy.rb', line 289

def clear
  @clear_operation = true
  @pending_changes = []
  mark_record_dirty if current_list_from_database.any?
  model.write_attribute("#{@context}_count", 0) if setup[:counter_cache]
  self
end

#clear!TaggingProxy

Forces clearing all tags by destroying records

Examples:

Force clear tags

tags.clear! #=> []

Returns:

Raises:

  • (ActiveRecord::RecordNotDestroyed)

    If destroy fails



302
303
304
305
306
307
308
# File 'lib/no_fly_list/tagging_proxy.rb', line 302

def clear!
  @model.send(@context.to_s).destroy_all
  @pending_changes = []
  @clear_operation = false
  @model.update_column("#{@context}_count", 0) if setup[:counter_cache]
  self
end

#coerce(other) ⇒ Array

Handles numeric coercion

Parameters:

  • other (Object)

    Object to coerce with

Returns:

  • (Array)

    Two-element array for coercion



78
79
80
# File 'lib/no_fly_list/tagging_proxy.rb', line 78

def coerce(other)
  [ other, to_a ]
end

#countInteger

Returns:

  • (Integer)


148
149
150
151
# File 'lib/no_fly_list/tagging_proxy.rb', line 148

def count
  # Always return the database count for count operations
  @model.send(@context.to_s).count
end

#empty?Boolean

Checks if tag list is empty

Returns:

  • (Boolean)

    True if no tags exist



319
320
321
# File 'lib/no_fly_list/tagging_proxy.rb', line 319

def empty?
  current_list.empty?
end

#include?(tag) ⇒ Boolean

Checks if a tag exists in the list

Parameters:

  • tag (String)

    Tag to check for

Returns:

  • (Boolean)

    True if tag exists



313
314
315
# File 'lib/no_fly_list/tagging_proxy.rb', line 313

def include?(tag)
  current_list.include?(tag.to_s.strip)
end

#inspectString

Returns:

  • (String)


205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/no_fly_list/tagging_proxy.rb', line 205

def inspect
  if @clear_operation
    db_tags = current_list_from_database
    "#<#{self.class.name} tags=[] changes=[CLEARING ALL (#{db_tags.size}): #{db_tags.inspect}] transformer_with=#{transformer_name}>"
  elsif !@pending_changes.nil?
    add_list = additions
    remove_list = removals
    changes = []
    changes << "+#{add_list.inspect}" if add_list.any?
    changes << "-#{remove_list.inspect}" if remove_list.any?
    changes_str = changes.join(", ")

    "#<#{self.class.name} tags=#{current_list.inspect} changes=[#{changes_str}] transformer_with=#{transformer_name}>"
  else
    "#<#{self.class.name} tags=#{current_list.inspect} transformer_with=#{transformer_name}>"
  end
end

#persisted?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Required by ActiveModel::Validations

Returns:

  • (Boolean)

    Always returns false since proxy isn't persisted

See Also:



327
328
329
# File 'lib/no_fly_list/tagging_proxy.rb', line 327

def persisted?
  false
end

#removalsArray<String>

Returns tags that will be removed (in database but not in pending changes)

Returns:

  • (Array<String>)

    Tags to be removed



194
195
196
197
198
199
200
201
202
# File 'lib/no_fly_list/tagging_proxy.rb', line 194

def removals
  if @clear_operation
    current_list_from_database
  elsif @pending_changes.nil?
    []
  else
    current_list_from_database - @pending_changes
  end
end

#remove(*tags) ⇒ TaggingProxy

Removes one or more tags from the current tag list

Parameters:

  • *tags (Array<String, Array<String>>)

    Tags to remove:

    • Single string with comma-separated values ("tag1, tag2")
    • Single array of strings (["tag1", "tag2"])
    • Multiple string arguments ("tag1", "tag2")

Returns:

Raises:

  • (ActiveRecord::RecordInvalid)

    If validation fails



261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/no_fly_list/tagging_proxy.rb', line 261

def remove(*tags)
  @clear_operation = false

  # Initialize @pending_changes with database values if not yet initialized
  @pending_changes = current_list_from_database if @pending_changes.nil?

  old_list = @pending_changes.dup

  tags_to_remove = if tags.size == 1 && tags.first.is_a?(String)
                     transformer.parse_tags(tags.first)
  else
                     tags.flatten.map { |tag| tag.to_s.strip }
  end

  @pending_changes = @pending_changes - tags_to_remove
  mark_record_dirty if @pending_changes != old_list
  self
end

#remove!(tag) ⇒ Object



280
281
282
283
# File 'lib/no_fly_list/tagging_proxy.rb', line 280

def remove!(tag)
  remove(tag)
  save
end

#resolve_transformer(trans) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/no_fly_list/tagging_proxy.rb', line 35

def resolve_transformer(trans)
  const = trans
  const = const.constantize if const.is_a?(String)
  unless const.respond_to?(:parse_tags) && const.respond_to?(:recreate_string)
    warn "NoFlyList: transformer #{trans.inspect} is invalid. Falling back to DefaultTransformer"
    const = NoFlyList::DefaultTransformer
  end
  const
rescue NameError
  warn "NoFlyList: transformer #{trans.inspect} not found. Falling back to DefaultTransformer"
  NoFlyList::DefaultTransformer
end

#respond_to_missing?(method_name, _include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


70
71
72
73
# File 'lib/no_fly_list/tagging_proxy.rb', line 70

def respond_to_missing?(method_name, _include_private = false)
  current_list.respond_to?(method_name) ||
    method_name.to_s =~ /\A(.+)_list(=)?\z/
end

#saveBoolean

Returns true if the proxy is valid.

Returns:

  • (Boolean)

    true if the proxy is valid



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/no_fly_list/tagging_proxy.rb', line 87

def save
  return true unless changed?
  return false unless valid?

  # Prevent recursive validation
  @saving = true
  begin
    model.class.transaction do
      # Always save parent first if needed
      if model.new_record? && !model.save
        errors.add(:base, "Failed to save parent record")
        raise ActiveRecord::Rollback
      end

      # Clear existing tags
      old_count = model.send(context_taggings).count
      model.send(context_taggings).delete_all

      # Update counter
      model.update_column("#{@context}_count", 0) if setup[:counter_cache]

      # Create new tags
      pending_list.each do |tag_name|
        tag = find_or_create_tag(tag_name)
        next unless tag

        attributes = {
          tag: tag,
          context: @context.to_s.singularize
        }

        if setup[:polymorphic]
          attributes[:taggable_type] = model.class.name
          attributes[:taggable_id] = model.id
        end

        # Use create! to ensure we catch any errors
        model.send(context_taggings).create!(attributes)
      end
    end
    # Update counter to match the actual count
    model.update_column("#{@context}_count", pending_list.size) if setup[:counter_cache]

    refresh_from_database
    true
  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
    errors.add(:base, e.message)
    false
  ensure
    @saving = false
  end
end

#save!Boolean

Returns true if the proxy is valid and the changes were saved.

Returns:

  • (Boolean)

    true if the proxy is valid and the changes were saved

Raises:

  • (ActiveModel::ValidationError)

    if the proxy is not valid



142
143
144
145
# File 'lib/no_fly_list/tagging_proxy.rb', line 142

def save!
  valid? || raise(ActiveModel::ValidationError, self)
  save
end

#sizeInteger

Returns:

  • (Integer)


154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/no_fly_list/tagging_proxy.rb', line 154

def size
  # For size, return the database count if we've had a validation error
  if !valid?
    count
    # Otherwise show pending changes
  elsif @clear_operation
    0
  elsif !@pending_changes.nil?
    @pending_changes.size
  else
    count
  end
end

#to_aArray<String>

Returns:

  • (Array<String>)


169
170
171
# File 'lib/no_fly_list/tagging_proxy.rb', line 169

def to_a
  current_list
end

#to_aryObject



82
83
84
# File 'lib/no_fly_list/tagging_proxy.rb', line 82

def to_ary
  current_list
end

#to_sString

Returns:

  • (String)


174
175
176
# File 'lib/no_fly_list/tagging_proxy.rb', line 174

def to_s
  transformer.recreate_string(current_list)
end

#transformer_nameString

Returns The name of the parser used to transform tags.

Returns:

  • (String)

    The name of the parser used to transform tags



179
180
181
# File 'lib/no_fly_list/tagging_proxy.rb', line 179

def transformer_name
  @transformer_name ||= transformer.name
end