yeshoua_crm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +11 -0
  3. data/lib/yeshoua_crm.rb +87 -0
  4. data/lib/yeshoua_crm/acts_as_draftable/draft.rb +40 -0
  5. data/lib/yeshoua_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +154 -0
  6. data/lib/yeshoua_crm/acts_as_list/list.rb +282 -0
  7. data/lib/yeshoua_crm/acts_as_taggable/rcrm_acts_as_taggable.rb +350 -0
  8. data/lib/yeshoua_crm/acts_as_taggable/tag.rb +81 -0
  9. data/lib/yeshoua_crm/acts_as_taggable/tag_list.rb +111 -0
  10. data/lib/yeshoua_crm/acts_as_taggable/tagging.rb +16 -0
  11. data/lib/yeshoua_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
  12. data/lib/yeshoua_crm/acts_as_votable/rcrm_acts_as_votable.rb +80 -0
  13. data/lib/yeshoua_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
  14. data/lib/yeshoua_crm/acts_as_votable/votable.rb +323 -0
  15. data/lib/yeshoua_crm/acts_as_votable/vote.rb +28 -0
  16. data/lib/yeshoua_crm/acts_as_votable/voter.rb +131 -0
  17. data/lib/yeshoua_crm/assets_manager.rb +43 -0
  18. data/lib/yeshoua_crm/currency.rb +439 -0
  19. data/lib/yeshoua_crm/currency/formatting.rb +224 -0
  20. data/lib/yeshoua_crm/currency/heuristics.rb +151 -0
  21. data/lib/yeshoua_crm/currency/loader.rb +24 -0
  22. data/lib/yeshoua_crm/helpers/external_assets_helper.rb +17 -0
  23. data/lib/yeshoua_crm/helpers/form_tag_helper.rb +123 -0
  24. data/lib/yeshoua_crm/helpers/tags_helper.rb +13 -0
  25. data/lib/yeshoua_crm/helpers/vote_helper.rb +35 -0
  26. data/lib/yeshoua_crm/liquid/drops/cells_drop.rb +86 -0
  27. data/lib/yeshoua_crm/liquid/drops/issues_drop.rb +66 -0
  28. data/lib/yeshoua_crm/liquid/drops/news_drop.rb +54 -0
  29. data/lib/yeshoua_crm/liquid/drops/users_drop.rb +72 -0
  30. data/lib/yeshoua_crm/liquid/filters/arrays.rb +177 -0
  31. data/lib/yeshoua_crm/liquid/filters/base.rb +208 -0
  32. data/lib/yeshoua_crm/money_helper.rb +65 -0
  33. data/lib/yeshoua_crm/version.rb +3 -0
  34. data/test/acts_as_draftable/draft_test.rb +29 -0
  35. data/test/acts_as_draftable/rcrm_acts_as_draftable_test.rb +185 -0
  36. data/test/acts_as_taggable/rcrm_acts_as_taggable_test.rb +345 -0
  37. data/test/acts_as_taggable/tag_list_test.rb +34 -0
  38. data/test/acts_as_taggable/tag_test.rb +72 -0
  39. data/test/acts_as_taggable/tagging_test.rb +15 -0
  40. data/test/acts_as_viewed/rcrm_acts_as_viewed_test.rb +47 -0
  41. data/test/acts_as_votable/rcrm_acts_as_votable_test.rb +19 -0
  42. data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +14 -0
  43. data/test/acts_as_votable/votable_test.rb +507 -0
  44. data/test/acts_as_votable/voter_test.rb +296 -0
  45. data/test/currency_test.rb +292 -0
  46. data/test/liquid/drops/issues_drop_test.rb +34 -0
  47. data/test/liquid/drops/news_drop_test.rb +38 -0
  48. data/test/liquid/drops/projects_drop_test.rb +44 -0
  49. data/test/liquid/drops/uses_drop_test.rb +36 -0
  50. data/test/liquid/filters/arrays_filter_test.rb +24 -0
  51. data/test/liquid/filters/base_filter_test.rb +63 -0
  52. data/test/liquid/liquid_helper.rb +32 -0
  53. data/test/models/issue.rb +14 -0
  54. data/test/models/news.rb +3 -0
  55. data/test/models/project.rb +8 -0
  56. data/test/models/user.rb +11 -0
  57. data/test/models/vote_classes.rb +33 -0
  58. data/test/money_helper_test.rb +12 -0
  59. data/test/schema.rb +121 -0
  60. data/test/tags_helper_test.rb +29 -0
  61. data/test/test_helper.rb +66 -0
  62. data/test/vote_helper_test.rb +28 -0
  63. data/yeshoua_crm.gemspec +28 -0
  64. metadata +206 -0
@@ -0,0 +1,350 @@
1
+ require 'active_record'
2
+
3
+ module YeshouaCrm
4
+ module ActsAsTaggable #:nodoc:
5
+ module Taggable #:nodoc:
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def taggable?
12
+ false
13
+ end
14
+
15
+ def rcrm_acts_as_taggable
16
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => '::YeshouaCrm::ActsAsTaggable::Tagging'
17
+ has_many :tags, :through => :taggings, :class_name => '::YeshouaCrm::ActsAsTaggable::Tag'
18
+
19
+ before_save :save_cached_tag_list
20
+
21
+ after_create :save_tags
22
+ after_update :save_tags
23
+
24
+ include YeshouaCrm::ActsAsTaggable::Taggable::InstanceMethods
25
+ extend YeshouaCrm::ActsAsTaggable::Taggable::SingletonMethods
26
+
27
+ alias_method :reload_without_tag_list, :reload
28
+ alias_method :reload, :reload_with_tag_list
29
+
30
+ class_eval do
31
+ def self.taggable?
32
+ true
33
+ end
34
+ end
35
+ end
36
+
37
+ def cached_tag_list_column_name
38
+ 'cached_tag_list'
39
+ end
40
+
41
+ def set_cached_tag_list_column_name(value = nil, &block)
42
+ define_attr_method :cached_tag_list_column_name, value, &block
43
+ end
44
+
45
+ # Create the taggable tables
46
+ # === Options hash:
47
+ # * <tt>:table_name</tt> - use a table name other than viewings
48
+ # To be used during migration, but can also be used in other places
49
+ def create_taggable_table(options = {})
50
+ tag_name_table = options[:tags] || :tags
51
+
52
+ if !self.connection.table_exists?(tag_name_table)
53
+ self.connection.create_table(tag_name_table) do |t|
54
+ t.column :name, :string
55
+ end
56
+ end
57
+
58
+ taggings_name_table = options[:taggings] || :taggings
59
+ if !self.connection.table_exists?(taggings_name_table)
60
+ self.connection.create_table(taggings_name_table) do |t|
61
+ t.column :tag_id, :integer
62
+ t.column :taggable_id, :integer
63
+
64
+ # You should make sure that the column created is
65
+ # long enough to store the required class names.
66
+ t.column :taggable_type, :string
67
+
68
+ t.column :created_at, :datetime
69
+ end
70
+
71
+ self.connection.add_index :taggings, :tag_id
72
+ self.connection.add_index :taggings, [:taggable_id, :taggable_type]
73
+ end
74
+ end
75
+
76
+ def drop_taggable_table(options = {})
77
+ tag_name_table = options[:tags] || :tags
78
+ if self.connection.table_exists?(tag_name_table)
79
+ self.connection.drop_table tag_name_table
80
+ end
81
+
82
+ taggings_name_table = options[:taggings] || :taggings
83
+ if self.connection.table_exists?(taggings_name_table)
84
+ self.connection.drop_table taggings_name_table
85
+ end
86
+ end
87
+ end
88
+
89
+ module SingletonMethods
90
+ #Return all avalible tags for a cell or global
91
+ #Example: Question.available_tags(:cell => @cell_id )
92
+ def available_tags(options = {})
93
+ cell = options[:cell]
94
+ limit = options[:limit].to_i.zero? ? 30 : options[:limit].to_i
95
+ scope = Tag.where({})
96
+ class_name = quote_string_value(base_class.name)
97
+ join = []
98
+ join << "JOIN #{Tagging.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id "
99
+ join << "JOIN #{table_name} ON #{table_name}.id = #{Tagging.table_name}.taggable_id
100
+ AND #{Tagging.table_name}.taggable_type = #{class_name} "
101
+ if attribute_names.include?('cell_id') && cell
102
+ join << "JOIN #{Cell.table_name} ON #{Cell.table_name}.id = #{table_name}.cell_id"
103
+ scope = scope.where("#{table_name}.cell_id = ?", cell.id)
104
+ end
105
+
106
+ if options[:name_like]
107
+ scope = scope.where("LOWER(#{Tag.table_name}.name) LIKE LOWER(?)", "%#{options[:name_like]}%")
108
+ end
109
+
110
+ group_fields = ''
111
+ group_fields << ", #{Tag.table_name}.created_at" if Tag.respond_to?(:created_at)
112
+ group_fields << ", #{Tag.table_name}.updated_at" if Tag.respond_to?(:updated_at)
113
+
114
+ scope = scope.joins(join.join(' '))
115
+ scope = scope.select("#{Tag.table_name}.*, COUNT(DISTINCT #{Tagging.table_name}.taggable_id) AS count")
116
+ scope = scope.group("#{Tag.table_name}.id, #{Tag.table_name}.name #{group_fields}")
117
+ scope = scope.having('COUNT(*) > 0')
118
+ scope = scope.order("#{Tag.table_name}.name")
119
+ scope = scope.limit(limit)
120
+ scope
121
+ end
122
+ # Returns an array of related tags.
123
+ # Related tags are all the other tags that are found on the models tagged with the provided tags.
124
+ #
125
+ # Pass either a tag, string, or an array of strings or tags.
126
+ #
127
+ # Options:
128
+ # :order - SQL Order how to order the tags. Defaults to "count DESC, tags.name".
129
+ def find_related_tags(tags, options = {})
130
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
131
+
132
+ related_models = find_tagged_with(tags)
133
+
134
+ return [] if related_models.blank?
135
+
136
+ related_ids = related_models.map{|c| c.id }.join(",")
137
+ Tag.select(
138
+ "#{Tag.table_name}.*, COUNT(#{Tag.table_name}.id) AS count").joins(
139
+ "JOIN #{Tagging.table_name} ON #{Tagging.table_name}.taggable_type = '#{base_class.name}'
140
+ AND #{Tagging.table_name}.taggable_id IN (#{related_ids})
141
+ AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id").order(
142
+ options[:order] || "count DESC, #{Tag.table_name}.name").group(
143
+ "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING #{Tag.table_name}.name NOT IN (#{tags.map { |n| quote_string_value(n) }.join(",")})")
144
+ end
145
+
146
+ # Pass either a tag, string, or an array of strings or tags.
147
+ #
148
+ # Options:
149
+ # :exclude - Find models that are not tagged with the given tags
150
+ # :match_all - Find models that match all of the given tags, not just one
151
+ # :conditions - A piece of SQL conditions to add to the query
152
+ def find_tagged_with(*args)
153
+ options = find_options_for_find_tagged_with(*args)
154
+ options.blank? ? [] : select(options[:select]).where(options[:conditions]).joins(options[:joins]).order(options[:order]).to_a
155
+ end
156
+
157
+ alias_method :tagged_with, :find_tagged_with
158
+
159
+ def find_options_for_find_tagged_with(tags, options = {})
160
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
161
+ options = options.dup
162
+
163
+ return {} if tags.empty?
164
+
165
+ conditions = []
166
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
167
+
168
+ taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
169
+
170
+ joins = [
171
+ "INNER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_string_value(base_class.name)}",
172
+ "INNER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id"
173
+ ]
174
+
175
+ if options.delete(:exclude)
176
+ conditions << <<-END
177
+ #{table_name}.id NOT IN
178
+ (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name}
179
+ INNER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id
180
+ WHERE #{tags_condition(tags)} AND #{Tagging.table_name}.taggable_type = #{quote_string_value(base_class.name)})
181
+ END
182
+ else
183
+ if options.delete(:match_all)
184
+ joins << joins_for_match_all_tags(tags)
185
+ else
186
+ conditions << tags_condition(tags, tags_alias)
187
+ end
188
+ end
189
+
190
+ { :select => "DISTINCT #{table_name}.* ",
191
+ :joins => joins.join(" "),
192
+ :conditions => conditions.join(" AND ")
193
+ }.reverse_merge!(options)
194
+ end
195
+
196
+ def joins_for_match_all_tags(tags)
197
+ joins = []
198
+
199
+ tags.each_with_index do |tag, index|
200
+ taggings_alias, tags_alias = "taggings_#{index}", "tags_#{index}"
201
+
202
+ join = <<-END
203
+ INNER JOIN #{Tagging.table_name} #{taggings_alias} ON
204
+ #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND
205
+ #{taggings_alias}.taggable_type = #{quote_string_value(base_class.name)}
206
+
207
+ INNER JOIN #{Tag.table_name} #{tags_alias} ON
208
+ #{taggings_alias}.tag_id = #{tags_alias}.id AND
209
+ #{tags_alias}.name = ?
210
+ END
211
+
212
+ joins << sanitize_sql([join, tag])
213
+ end
214
+
215
+ joins.join(' ')
216
+ end
217
+
218
+ # Calculate the tag counts for all tags.
219
+ #
220
+ # See Tag.counts for available options.
221
+ def tag_counts(options = {})
222
+ # Tag.find(:all, find_options_for_tag_counts(options))
223
+ opt = find_options_for_tag_counts(options)
224
+ Tag.select(opt[:select]).where(opt[:conditions]).joins(opt[:joins]).group(opt[:group]).having(opt[:having]).order(opt[:order]).limit(options[:limit])
225
+ end
226
+ alias_method :all_tag_counts, :tag_counts
227
+
228
+ def find_options_for_tag_counts(options = {})
229
+ options = options.dup
230
+ scope = scope_attributes
231
+ # scope(:find)
232
+
233
+ conditions = []
234
+ conditions << send(:sanitize_sql_for_assignment, options.delete(:conditions)) if options[:conditions]
235
+ conditions << send(:sanitize_sql_for_assignment, scope) if scope
236
+ conditions << "#{Tagging.table_name}.taggable_type = #{quote_string_value(base_class.name)}"
237
+ conditions << type_condition unless descends_from_active_record?
238
+ conditions.delete('')
239
+ conditions.compact!
240
+ conditions = conditions.join(" AND ")
241
+
242
+ joins = ["INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"]
243
+ joins << options.delete(:joins) if options[:joins].present?
244
+ # joins << scope[:joins] if scope && scope[:joins].present?
245
+ joins = joins.join(" ")
246
+
247
+ options = { :conditions => conditions, :joins => joins }.update(options)
248
+
249
+ Tag.options_for_counts(options)
250
+ end
251
+
252
+ def caching_tag_list?
253
+ column_names.include?(cached_tag_list_column_name)
254
+ end
255
+
256
+ private
257
+
258
+ def quote_string_value(object)
259
+ connection.quote(object)
260
+ end
261
+
262
+ def tags_condition(tags, table_name = Tag.table_name)
263
+ condition = tags.map { |t| sanitize_sql(["#{table_name}.name LIKE ?", t]) }.join(" OR ")
264
+ "(" + condition + ")" unless condition.blank?
265
+ end
266
+
267
+ def merge_conditions(*conditions)
268
+ segments = []
269
+
270
+ conditions.each do |condition|
271
+ unless condition.blank?
272
+ sql = sanitize_sql(condition)
273
+ segments << sql unless sql.blank?
274
+ end
275
+ end
276
+
277
+ "(#{segments.join(') AND (')})" unless segments.empty?
278
+ end
279
+ end
280
+
281
+ module InstanceMethods
282
+ def tag_list
283
+ return @tag_list if @tag_list ||= nil
284
+
285
+ if self.class.caching_tag_list? && !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
286
+ @tag_list = TagList.from(cached_value)
287
+ else
288
+ @tag_list = TagList.new(*tags.map(&:name))
289
+ end
290
+ end
291
+
292
+ def tag_list=(value)
293
+ @tag_list = TagList.from(value)
294
+ end
295
+
296
+ def save_cached_tag_list
297
+ if self.class.caching_tag_list?
298
+ self[self.class.cached_tag_list_column_name] = tag_list.to_s
299
+ end
300
+ end
301
+
302
+ #build list from related tags
303
+ def all_tags_list
304
+ tags.pluck(:name)
305
+ end
306
+
307
+ def save_tags
308
+ return unless @tag_list
309
+
310
+ new_tag_names = @tag_list - tags.map(&:name)
311
+ old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
312
+
313
+ self.class.transaction do
314
+ if old_tags.any?
315
+ taggings.where("tag_id IN (?)", old_tags.map(&:id)).each(&:destroy)
316
+ taggings.reset
317
+ end
318
+ new_tag_names.each do |new_tag_name|
319
+ tags << Tag.find_or_create_with_like_by_name(new_tag_name)
320
+ end
321
+ end
322
+
323
+ true
324
+ end
325
+
326
+ # Calculate the tag counts for the tags used by this model.
327
+ #
328
+ # The possible options are the same as the tag_counts class method.
329
+ def tag_counts(options = {})
330
+ return [] if tag_list.blank?
331
+
332
+ options[:conditions] = self.class.send(:merge_conditions, options[:conditions], self.class.send(:tags_condition, tag_list))
333
+ self.class.tag_counts(options)
334
+ end
335
+
336
+ def reload_with_tag_list(*args) #:nodoc:
337
+ @tag_list = nil
338
+ reload_without_tag_list(*args)
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ ActiveRecord::Base.send(:include, YeshouaCrm::ActsAsTaggable::Taggable)
346
+
347
+ # Class aliases
348
+ YeshouaCrm::Tag = YeshouaCrm::ActsAsTaggable::Tag
349
+ YeshouaCrm::TagList = YeshouaCrm::ActsAsTaggable::TagList
350
+ YeshouaCrm::Tagging = YeshouaCrm::ActsAsTaggable::Tagging
@@ -0,0 +1,81 @@
1
+ module YeshouaCrm
2
+ module ActsAsTaggable #:nodoc:
3
+ class Tag < ActiveRecord::Base #:nodoc:
4
+ has_many :taggings, :dependent => :destroy
5
+
6
+ validates_presence_of :name
7
+ validates :name, :uniqueness => { :message => " not uniq tag" }
8
+ validates :name, :presence => true
9
+ cattr_accessor :destroy_unused
10
+ self.destroy_unused = false
11
+
12
+ attr_accessible :name if defined?(ActiveModel::MassAssignmentSecurity)
13
+
14
+ # LIKE is used for cross-database case-insensitivity
15
+ def self.find_or_create_with_like_by_name(name)
16
+ # find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
17
+ where("LOWER(name) LIKE LOWER(?)", name).first || create(:name => name)
18
+ end
19
+
20
+ def ==(object)
21
+ super || (object.is_a?(Tag) && name == object.name)
22
+ end
23
+
24
+ def to_s
25
+ name
26
+ end
27
+
28
+ def count
29
+ read_attribute(:count).to_i
30
+ end
31
+
32
+ class << self
33
+ # Calculate the tag counts for all tags.
34
+ # :start_at - Restrict the tags to those created after a certain time
35
+ # :end_at - Restrict the tags to those created before a certain time
36
+ # :conditions - A piece of SQL conditions to add to the query
37
+ # :limit - The maximum number of tags to return
38
+ # :order - A piece of SQL to order by. Eg 'count desc' or 'taggings.created_at desc'
39
+ # :at_least - Exclude tags with a frequency less than the given value
40
+ # :at_most - Exclude tags with a frequency greater than the given value
41
+ def counts(options = {})
42
+ opt = options_for_counts(options)
43
+ select(opt[:select]).where(opt[:conditions]).joins(opt[:joins]).group(opt[:group])
44
+ end
45
+
46
+ def options_for_counts(options = {})
47
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :joins
48
+ options = options.dup
49
+
50
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
51
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
52
+
53
+ conditions = [
54
+ (sanitize_sql(options.delete(:conditions)) if options[:conditions]),
55
+ start_at,
56
+ end_at
57
+ ].compact
58
+
59
+ conditions = conditions.join(' AND ') if conditions.any?
60
+
61
+ joins = ["INNER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
62
+ joins << options.delete(:joins) if options[:joins]
63
+
64
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
65
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
66
+ having = 'COUNT(*) > 0'
67
+ having = [having, at_least, at_most].compact.join(' AND ')
68
+ group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name"
69
+ # group_by << " AND #{having}" unless having.blank?
70
+
71
+ { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
72
+ :joins => joins.join(" "),
73
+ :conditions => conditions,
74
+ :group => group_by,
75
+ :having => having
76
+ }.update(options)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,111 @@
1
+ module YeshouaCrm
2
+ module ActsAsTaggable #:nodoc:
3
+ class TagList < Array #:nodoc:
4
+ cattr_accessor :delimiter
5
+ self.delimiter = ','
6
+
7
+ def initialize(*args)
8
+ add(*args)
9
+ end
10
+
11
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
12
+ #
13
+ # tag_list.add("Fun", "Happy")
14
+ #
15
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
16
+ #
17
+ # tag_list.add("Fun, Happy", :parse => true)
18
+ def add(*names)
19
+ extract_and_apply_options!(names)
20
+ concat(names)
21
+ clean!
22
+ self
23
+ end
24
+
25
+ # Remove specific tags from the tag_list.
26
+ #
27
+ # tag_list.remove("Sad", "Lonely")
28
+ #
29
+ # Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
30
+ #
31
+ # tag_list.remove("Sad, Lonely", :parse => true)
32
+ def remove(*names)
33
+ extract_and_apply_options!(names)
34
+ delete_if { |name| names.include?(name) }
35
+ self
36
+ end
37
+
38
+ # Toggle the presence of the given tags.
39
+ # If a tag is already in the list it is removed, otherwise it is added.
40
+ def toggle(*names)
41
+ extract_and_apply_options!(names)
42
+
43
+ names.each do |name|
44
+ include?(name) ? delete(name) : push(name)
45
+ end
46
+
47
+ clean!
48
+ self
49
+ end
50
+
51
+ # Transform the tag_list into a tag string suitable for edting in a form.
52
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
53
+ #
54
+ # tag_list = TagList.new("Round", "Square,Cube")
55
+ # tag_list.to_s # 'Round, "Square,Cube"'
56
+ def to_s
57
+ clean!
58
+
59
+ map do |name|
60
+ name.include?(delimiter) ? "\"#{name}\"" : name
61
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
62
+ end
63
+
64
+ private
65
+ # Remove whitespace, duplicates, and blanks.
66
+ def clean!
67
+ reject!(&:blank?)
68
+ map!(&:strip)
69
+ uniq!
70
+ end
71
+
72
+ def extract_and_apply_options!(args)
73
+ options = args.last.is_a?(Hash) ? args.pop : {}
74
+ options.assert_valid_keys :parse
75
+
76
+ args.map! { |a| self.class.from(a) } if options[:parse]
77
+
78
+ args.flatten!
79
+ end
80
+
81
+ class << self
82
+ # Returns a new TagList using the given tag string.
83
+ #
84
+ # tag_list = TagList.from("One , Two, Three")
85
+ # tag_list # ["One", "Two", "Three"]
86
+ def from(source)
87
+ tag_list = new
88
+
89
+ case source
90
+ when Array
91
+ tag_list.add(source)
92
+ else
93
+ string = source.to_s.dup
94
+
95
+ # Parse the quoted tags
96
+ [
97
+ /\s*#{delimiter}\s*(['"])(.*?)\1\s*/,
98
+ /^\s*(['"])(.*?)\1\s*#{delimiter}?/
99
+ ].each do |re|
100
+ string.gsub!(re) { tag_list << $2; "" }
101
+ end
102
+
103
+ tag_list.add(string.split(delimiter))
104
+ end
105
+
106
+ tag_list
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end