redmineup 1.0.2

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 (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/Gemfile +4 -0
  4. data/README.md +204 -0
  5. data/Rakefile +11 -0
  6. data/app/controllers/redmineup_controller.rb +26 -0
  7. data/app/views/redmine_crm/redmineup_calendar/_calendar.html.erb +34 -0
  8. data/app/views/redmineup/_money.html.erb +44 -0
  9. data/app/views/redmineup/settings.html.erb +10 -0
  10. data/bitbucket-pipelines.yml +50 -0
  11. data/config/currency_iso.json +2544 -0
  12. data/config/locales/cs.yml +13 -0
  13. data/config/locales/de.yml +13 -0
  14. data/config/locales/en.yml +13 -0
  15. data/config/locales/es.yml +13 -0
  16. data/config/locales/ru.yml +13 -0
  17. data/config/routes.rb +5 -0
  18. data/doc/CHANGELOG +14 -0
  19. data/doc/LICENSE.txt +339 -0
  20. data/lib/redmineup/acts_as_draftable/draft.rb +40 -0
  21. data/lib/redmineup/acts_as_draftable/up_acts_as_draftable.rb +172 -0
  22. data/lib/redmineup/acts_as_list/list.rb +282 -0
  23. data/lib/redmineup/acts_as_priceable/up_acts_as_priceable.rb +33 -0
  24. data/lib/redmineup/acts_as_taggable/tag.rb +81 -0
  25. data/lib/redmineup/acts_as_taggable/tag_list.rb +111 -0
  26. data/lib/redmineup/acts_as_taggable/tagging.rb +16 -0
  27. data/lib/redmineup/acts_as_taggable/up_acts_as_taggable.rb +357 -0
  28. data/lib/redmineup/acts_as_viewed/up_acts_as_viewed.rb +274 -0
  29. data/lib/redmineup/acts_as_votable/up_acts_as_votable.rb +80 -0
  30. data/lib/redmineup/acts_as_votable/up_acts_as_voter.rb +20 -0
  31. data/lib/redmineup/acts_as_votable/votable.rb +323 -0
  32. data/lib/redmineup/acts_as_votable/vote.rb +28 -0
  33. data/lib/redmineup/acts_as_votable/voter.rb +131 -0
  34. data/lib/redmineup/assets_manager.rb +43 -0
  35. data/lib/redmineup/colors_helper.rb +192 -0
  36. data/lib/redmineup/compatibility/application_controller_patch.rb +33 -0
  37. data/lib/redmineup/compatibility/routing_mapper_patch.rb +25 -0
  38. data/lib/redmineup/currency/formatting.rb +224 -0
  39. data/lib/redmineup/currency/heuristics.rb +151 -0
  40. data/lib/redmineup/currency/loader.rb +23 -0
  41. data/lib/redmineup/currency.rb +450 -0
  42. data/lib/redmineup/engine.rb +4 -0
  43. data/lib/redmineup/helpers/external_assets_helper.rb +20 -0
  44. data/lib/redmineup/helpers/form_tag_helper.rb +88 -0
  45. data/lib/redmineup/helpers/rup_calendar_helper.rb +146 -0
  46. data/lib/redmineup/helpers/tags_helper.rb +13 -0
  47. data/lib/redmineup/helpers/vote_helper.rb +35 -0
  48. data/lib/redmineup/hooks/views_layouts_hook.rb +11 -0
  49. data/lib/redmineup/liquid/drops/attachment_drop.rb +47 -0
  50. data/lib/redmineup/liquid/drops/issue_relations_drop.rb +41 -0
  51. data/lib/redmineup/liquid/drops/issues_drop.rb +217 -0
  52. data/lib/redmineup/liquid/drops/news_drop.rb +54 -0
  53. data/lib/redmineup/liquid/drops/projects_drop.rb +86 -0
  54. data/lib/redmineup/liquid/drops/time_entries_drop.rb +65 -0
  55. data/lib/redmineup/liquid/drops/users_drop.rb +68 -0
  56. data/lib/redmineup/liquid/filters/arrays.rb +254 -0
  57. data/lib/redmineup/liquid/filters/base.rb +249 -0
  58. data/lib/redmineup/liquid/filters/colors.rb +31 -0
  59. data/lib/redmineup/money_helper.rb +66 -0
  60. data/lib/redmineup/patches/liquid_patch.rb +33 -0
  61. data/lib/redmineup/settings/money.rb +46 -0
  62. data/lib/redmineup/settings.rb +53 -0
  63. data/lib/redmineup/version.rb +3 -0
  64. data/lib/redmineup.rb +108 -0
  65. data/redmineup.gemspec +29 -0
  66. data/test/acts_as_draftable/draft_test.rb +29 -0
  67. data/test/acts_as_draftable/rup_acts_as_draftable_test.rb +178 -0
  68. data/test/acts_as_taggable/rup_acts_as_taggable_test.rb +350 -0
  69. data/test/acts_as_taggable/tag_list_test.rb +34 -0
  70. data/test/acts_as_taggable/tag_test.rb +72 -0
  71. data/test/acts_as_taggable/tagging_test.rb +15 -0
  72. data/test/acts_as_viewed/rup_acts_as_viewed_test.rb +47 -0
  73. data/test/acts_as_votable/rup_acts_as_votable_test.rb +19 -0
  74. data/test/acts_as_votable/rup_acts_as_voter_test.rb +14 -0
  75. data/test/acts_as_votable/votable_test.rb +507 -0
  76. data/test/acts_as_votable/voter_test.rb +296 -0
  77. data/test/currency_test.rb +292 -0
  78. data/test/database.yml +17 -0
  79. data/test/fixtures/attachments.yml +14 -0
  80. data/test/fixtures/issues.yml +24 -0
  81. data/test/fixtures/news.yml +8 -0
  82. data/test/fixtures/projects.yml +10 -0
  83. data/test/fixtures/taggings.yml +32 -0
  84. data/test/fixtures/tags.yml +11 -0
  85. data/test/fixtures/users.yml +9 -0
  86. data/test/fixtures/votable_caches.yml +2 -0
  87. data/test/fixtures/votables.yml +4 -0
  88. data/test/fixtures/voters.yml +6 -0
  89. data/test/liquid/drops/attachment_drop_test.rb +15 -0
  90. data/test/liquid/drops/issue_relations_drop_test.rb +24 -0
  91. data/test/liquid/drops/issues_drop_test.rb +38 -0
  92. data/test/liquid/drops/news_drop_test.rb +38 -0
  93. data/test/liquid/drops/projects_drop_test.rb +44 -0
  94. data/test/liquid/drops/uses_drop_test.rb +36 -0
  95. data/test/liquid/filters/arrays_filter_test.rb +31 -0
  96. data/test/liquid/filters/base_filter_test.rb +67 -0
  97. data/test/liquid/filters/colors_filter_test.rb +33 -0
  98. data/test/liquid/liquid_helper.rb +34 -0
  99. data/test/models/attachment.rb +3 -0
  100. data/test/models/issue.rb +21 -0
  101. data/test/models/issue_relation.rb +10 -0
  102. data/test/models/news.rb +3 -0
  103. data/test/models/project.rb +8 -0
  104. data/test/models/user.rb +11 -0
  105. data/test/models/vote_classes.rb +33 -0
  106. data/test/money_helper_test.rb +12 -0
  107. data/test/schema.rb +144 -0
  108. data/test/tags_helper_test.rb +29 -0
  109. data/test/test_helper.rb +66 -0
  110. data/test/vote_helper_test.rb +28 -0
  111. data/vendor/assets/images/money.png +0 -0
  112. data/vendor/assets/images/vcard.png +0 -0
  113. data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
  114. data/vendor/assets/javascripts/select2.js +2 -0
  115. data/vendor/assets/javascripts/select2_helpers.js +192 -0
  116. data/vendor/assets/stylesheets/money.css +96 -0
  117. data/vendor/assets/stylesheets/select2.css +424 -0
  118. metadata +295 -0
@@ -0,0 +1,282 @@
1
+ module Redmineup
2
+ module ActsAsList
3
+ module List
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
+ # The class that has this specified needs to have a +position+ column defined as an integer on
10
+ # the mapped database table.
11
+ #
12
+ # Todo list example:
13
+ #
14
+ # class TodoList < ActiveRecord::Base
15
+ # has_many :todo_items, :order => "position"
16
+ # end
17
+ #
18
+ # class TodoItem < ActiveRecord::Base
19
+ # belongs_to :todo_list
20
+ # acts_as_list :scope => :todo_list
21
+ # end
22
+ #
23
+ # todo_list.first.move_to_bottom
24
+ # todo_list.last.move_higher
25
+ module ClassMethods
26
+ # Configuration options are:
27
+ #
28
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
+ def up_acts_as_list(options = {})
34
+
35
+ configuration = { :column => "position", :scope => "1 = 1" }
36
+ configuration.update(options) if options.is_a?(Hash)
37
+
38
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
39
+
40
+ if configuration[:scope].is_a?(Symbol)
41
+ scope_condition_method = %(
42
+ def scope_condition
43
+ if #{configuration[:scope].to_s}.nil?
44
+ "#{configuration[:scope].to_s} IS NULL"
45
+ else
46
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
47
+ end
48
+ end
49
+ )
50
+ else
51
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
52
+ end
53
+
54
+ class_eval <<-EOV
55
+ include Redmineup::ActsAsList::List::InstanceMethods
56
+
57
+ def up_acts_as_list_class
58
+ ::#{self.name}
59
+ end
60
+
61
+ def position_column
62
+ '#{configuration[:column]}'
63
+ end
64
+
65
+ #{scope_condition_method}
66
+
67
+ before_destroy :remove_from_list
68
+ before_create :add_to_list_bottom
69
+ EOV
70
+ end
71
+ end
72
+
73
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
74
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
75
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
76
+ # the first in the list of all chapters.
77
+ module InstanceMethods
78
+ # Insert the item at the given position (defaults to the top position of 1).
79
+ def insert_at(position = 1)
80
+ insert_at_position(position)
81
+ end
82
+
83
+ # Swap positions with the next lower item, if one exists.
84
+ def move_lower
85
+ return unless lower_item
86
+
87
+ up_acts_as_list_class.transaction do
88
+ lower_item.decrement_position
89
+ increment_position
90
+ end
91
+ end
92
+
93
+ # Swap positions with the next higher item, if one exists.
94
+ def move_higher
95
+ return unless higher_item
96
+
97
+ up_acts_as_list_class.transaction do
98
+ higher_item.increment_position
99
+ decrement_position
100
+ end
101
+ end
102
+
103
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
104
+ # position adjusted accordingly.
105
+ def move_to_bottom
106
+ return unless in_list?
107
+ up_acts_as_list_class.transaction do
108
+ decrement_positions_on_lower_items
109
+ assume_bottom_position
110
+ end
111
+ end
112
+
113
+ # Move to the top of the list. If the item is already in the list, the items above it have their
114
+ # position adjusted accordingly.
115
+ def move_to_top
116
+ return unless in_list?
117
+ up_acts_as_list_class.transaction do
118
+ increment_positions_on_higher_items
119
+ assume_top_position
120
+ end
121
+ end
122
+
123
+ # Move to the given position
124
+ def move_to=(pos)
125
+ case pos.to_s
126
+ when 'highest'
127
+ move_to_top
128
+ when 'higher'
129
+ move_higher
130
+ when 'lower'
131
+ move_lower
132
+ when 'lowest'
133
+ move_to_bottom
134
+ end
135
+ reset_positions_in_list
136
+ end
137
+
138
+ def reset_positions_in_list
139
+ up_acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
140
+ unless item.send(position_column) == (i + 1)
141
+ up_acts_as_list_class.where({:id => item.id}).
142
+ update_all({position_column => (i + 1)})
143
+ end
144
+ end
145
+ end
146
+
147
+ # Removes the item from the list.
148
+ def remove_from_list
149
+ if in_list?
150
+ decrement_positions_on_lower_items
151
+ update_attribute position_column, nil
152
+ end
153
+ end
154
+
155
+ # Increase the position of this item without adjusting the rest of the list.
156
+ def increment_position
157
+ return unless in_list?
158
+ update_attribute position_column, self.send(position_column).to_i + 1
159
+ end
160
+
161
+ # Decrease the position of this item without adjusting the rest of the list.
162
+ def decrement_position
163
+ return unless in_list?
164
+ update_attribute position_column, self.send(position_column).to_i - 1
165
+ end
166
+
167
+ # Return +true+ if this object is the first in the list.
168
+ def first?
169
+ return false unless in_list?
170
+ self.send(position_column) == 1
171
+ end
172
+
173
+ # Return +true+ if this object is the last in the list.
174
+ def last?
175
+ return false unless in_list?
176
+ self.send(position_column) == bottom_position_in_list
177
+ end
178
+
179
+ # Return the next higher item in the list.
180
+ def higher_item
181
+ return nil unless in_list?
182
+ up_acts_as_list_class.where(
183
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
184
+ ).first
185
+ end
186
+
187
+ # Return the next lower item in the list.
188
+ def lower_item
189
+ return nil unless in_list?
190
+ up_acts_as_list_class.where(
191
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
192
+ ).first
193
+ end
194
+
195
+ # Test if this record is in a list
196
+ def in_list?
197
+ !send(position_column).nil?
198
+ end
199
+
200
+ private
201
+
202
+ def add_to_list_top
203
+ increment_positions_on_all_items
204
+ end
205
+
206
+ def add_to_list_bottom
207
+ self[position_column] = bottom_position_in_list.to_i + 1
208
+ end
209
+
210
+ # Overwrite this method to define the scope of the list changes
211
+ def scope_condition() "1" end
212
+
213
+ # Returns the bottom position number in the list.
214
+ # bottom_position_in_list # => 2
215
+ def bottom_position_in_list(except = nil)
216
+ item = bottom_item(except)
217
+ item ? item.send(position_column) : 0
218
+ end
219
+
220
+ # Returns the bottom item
221
+ def bottom_item(except = nil)
222
+ conditions = scope_condition
223
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
224
+ up_acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
225
+ end
226
+
227
+ # Forces item to assume the bottom position in the list.
228
+ def assume_bottom_position
229
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
230
+ end
231
+
232
+ # Forces item to assume the top position in the list.
233
+ def assume_top_position
234
+ update_attribute(position_column, 1)
235
+ end
236
+
237
+ # This has the effect of moving all the higher items up one.
238
+ def decrement_positions_on_higher_items(position)
239
+ up_acts_as_list_class.
240
+ where("#{scope_condition} AND #{position_column} <= #{position}").
241
+ update_all("#{position_column} = (#{position_column} - 1)")
242
+ end
243
+
244
+ # This has the effect of moving all the lower items up one.
245
+ def decrement_positions_on_lower_items
246
+ return unless in_list?
247
+ up_acts_as_list_class.
248
+ where("#{scope_condition} AND #{position_column} > #{send(position_column).to_i}").
249
+ update_all("#{position_column} = (#{position_column} - 1)")
250
+ end
251
+
252
+ # This has the effect of moving all the higher items down one.
253
+ def increment_positions_on_higher_items
254
+ return unless in_list?
255
+ up_acts_as_list_class.
256
+ where("#{scope_condition} AND #{position_column} < #{send(position_column).to_i}").
257
+ update_all("#{position_column} = (#{position_column} + 1)")
258
+ end
259
+
260
+ # This has the effect of moving all the lower items down one.
261
+ def increment_positions_on_lower_items(position)
262
+ up_acts_as_list_class.
263
+ where("#{scope_condition} AND #{position_column} >= #{position}").
264
+ update_all("#{position_column} = (#{position_column} + 1)")
265
+ end
266
+
267
+ # Increments position (<tt>position_column</tt>) of all items in the list.
268
+ def increment_positions_on_all_items
269
+ up_acts_as_list_class.
270
+ where("#{scope_condition}").
271
+ update_all("#{position_column} = (#{position_column} + 1)")
272
+ end
273
+
274
+ def insert_at_position(position)
275
+ remove_from_list
276
+ increment_positions_on_lower_items(position)
277
+ self.update_attribute(position_column, position)
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,33 @@
1
+ module Redmineup
2
+ module ActsAsPriceable
3
+ module Base
4
+ def up_acts_as_priceable(*args)
5
+ priceable_options = args
6
+ priceable_options << :price if priceable_options.empty?
7
+ priceable_methods = ""
8
+ priceable_options.each do |priceable_attr|
9
+ priceable_methods << %(
10
+ def #{priceable_attr.to_s}_to_s
11
+ object_price(
12
+ self,
13
+ :#{priceable_attr},
14
+ {
15
+ :decimal_mark => Redmineup::Settings::Money.decimal_separator,
16
+ :thousands_separator => Redmineup::Settings::Money.thousands_delimiter
17
+ }
18
+ ) if self.respond_to?(:#{priceable_attr})
19
+ end
20
+ )
21
+ end
22
+
23
+ class_eval <<-EOV
24
+ include Redmineup::MoneyHelper
25
+
26
+ #{priceable_methods}
27
+ EOV
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ ActiveRecord::Base.extend Redmineup::ActsAsPriceable::Base
@@ -0,0 +1,81 @@
1
+ module Redmineup
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 Redmineup
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[-1] == (' ') ? 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
@@ -0,0 +1,16 @@
1
+ module Redmineup
2
+ module ActsAsTaggable #:nodoc:
3
+ class Tagging < ActiveRecord::Base #:nodoc:
4
+ belongs_to :tag
5
+ belongs_to :taggable, :polymorphic => true
6
+
7
+ after_destroy :destroy_tag_if_unused
8
+
9
+ private
10
+
11
+ def destroy_tag_if_unused
12
+ tag.destroy if Tag.destroy_unused && tag.taggings.count.zero?
13
+ end
14
+ end
15
+ end
16
+ end