redmineup 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/README.md +204 -0
- data/Rakefile +11 -0
- data/app/controllers/redmineup_controller.rb +26 -0
- data/app/views/redmine_crm/redmineup_calendar/_calendar.html.erb +34 -0
- data/app/views/redmineup/_money.html.erb +44 -0
- data/app/views/redmineup/settings.html.erb +10 -0
- data/bitbucket-pipelines.yml +50 -0
- data/config/currency_iso.json +2544 -0
- data/config/locales/cs.yml +13 -0
- data/config/locales/de.yml +13 -0
- data/config/locales/en.yml +13 -0
- data/config/locales/es.yml +13 -0
- data/config/locales/ru.yml +13 -0
- data/config/routes.rb +5 -0
- data/doc/CHANGELOG +14 -0
- data/doc/LICENSE.txt +339 -0
- data/lib/redmineup/acts_as_draftable/draft.rb +40 -0
- data/lib/redmineup/acts_as_draftable/up_acts_as_draftable.rb +172 -0
- data/lib/redmineup/acts_as_list/list.rb +282 -0
- data/lib/redmineup/acts_as_priceable/up_acts_as_priceable.rb +33 -0
- data/lib/redmineup/acts_as_taggable/tag.rb +81 -0
- data/lib/redmineup/acts_as_taggable/tag_list.rb +111 -0
- data/lib/redmineup/acts_as_taggable/tagging.rb +16 -0
- data/lib/redmineup/acts_as_taggable/up_acts_as_taggable.rb +357 -0
- data/lib/redmineup/acts_as_viewed/up_acts_as_viewed.rb +274 -0
- data/lib/redmineup/acts_as_votable/up_acts_as_votable.rb +80 -0
- data/lib/redmineup/acts_as_votable/up_acts_as_voter.rb +20 -0
- data/lib/redmineup/acts_as_votable/votable.rb +323 -0
- data/lib/redmineup/acts_as_votable/vote.rb +28 -0
- data/lib/redmineup/acts_as_votable/voter.rb +131 -0
- data/lib/redmineup/assets_manager.rb +43 -0
- data/lib/redmineup/colors_helper.rb +192 -0
- data/lib/redmineup/compatibility/application_controller_patch.rb +33 -0
- data/lib/redmineup/compatibility/routing_mapper_patch.rb +25 -0
- data/lib/redmineup/currency/formatting.rb +224 -0
- data/lib/redmineup/currency/heuristics.rb +151 -0
- data/lib/redmineup/currency/loader.rb +23 -0
- data/lib/redmineup/currency.rb +450 -0
- data/lib/redmineup/engine.rb +4 -0
- data/lib/redmineup/helpers/external_assets_helper.rb +20 -0
- data/lib/redmineup/helpers/form_tag_helper.rb +88 -0
- data/lib/redmineup/helpers/rup_calendar_helper.rb +146 -0
- data/lib/redmineup/helpers/tags_helper.rb +13 -0
- data/lib/redmineup/helpers/vote_helper.rb +35 -0
- data/lib/redmineup/hooks/views_layouts_hook.rb +11 -0
- data/lib/redmineup/liquid/drops/attachment_drop.rb +47 -0
- data/lib/redmineup/liquid/drops/issue_relations_drop.rb +41 -0
- data/lib/redmineup/liquid/drops/issues_drop.rb +217 -0
- data/lib/redmineup/liquid/drops/news_drop.rb +54 -0
- data/lib/redmineup/liquid/drops/projects_drop.rb +86 -0
- data/lib/redmineup/liquid/drops/time_entries_drop.rb +65 -0
- data/lib/redmineup/liquid/drops/users_drop.rb +68 -0
- data/lib/redmineup/liquid/filters/arrays.rb +254 -0
- data/lib/redmineup/liquid/filters/base.rb +249 -0
- data/lib/redmineup/liquid/filters/colors.rb +31 -0
- data/lib/redmineup/money_helper.rb +66 -0
- data/lib/redmineup/patches/liquid_patch.rb +33 -0
- data/lib/redmineup/settings/money.rb +46 -0
- data/lib/redmineup/settings.rb +53 -0
- data/lib/redmineup/version.rb +3 -0
- data/lib/redmineup.rb +108 -0
- data/redmineup.gemspec +29 -0
- data/test/acts_as_draftable/draft_test.rb +29 -0
- data/test/acts_as_draftable/rup_acts_as_draftable_test.rb +178 -0
- data/test/acts_as_taggable/rup_acts_as_taggable_test.rb +350 -0
- data/test/acts_as_taggable/tag_list_test.rb +34 -0
- data/test/acts_as_taggable/tag_test.rb +72 -0
- data/test/acts_as_taggable/tagging_test.rb +15 -0
- data/test/acts_as_viewed/rup_acts_as_viewed_test.rb +47 -0
- data/test/acts_as_votable/rup_acts_as_votable_test.rb +19 -0
- data/test/acts_as_votable/rup_acts_as_voter_test.rb +14 -0
- data/test/acts_as_votable/votable_test.rb +507 -0
- data/test/acts_as_votable/voter_test.rb +296 -0
- data/test/currency_test.rb +292 -0
- data/test/database.yml +17 -0
- data/test/fixtures/attachments.yml +14 -0
- data/test/fixtures/issues.yml +24 -0
- data/test/fixtures/news.yml +8 -0
- data/test/fixtures/projects.yml +10 -0
- data/test/fixtures/taggings.yml +32 -0
- data/test/fixtures/tags.yml +11 -0
- data/test/fixtures/users.yml +9 -0
- data/test/fixtures/votable_caches.yml +2 -0
- data/test/fixtures/votables.yml +4 -0
- data/test/fixtures/voters.yml +6 -0
- data/test/liquid/drops/attachment_drop_test.rb +15 -0
- data/test/liquid/drops/issue_relations_drop_test.rb +24 -0
- data/test/liquid/drops/issues_drop_test.rb +38 -0
- data/test/liquid/drops/news_drop_test.rb +38 -0
- data/test/liquid/drops/projects_drop_test.rb +44 -0
- data/test/liquid/drops/uses_drop_test.rb +36 -0
- data/test/liquid/filters/arrays_filter_test.rb +31 -0
- data/test/liquid/filters/base_filter_test.rb +67 -0
- data/test/liquid/filters/colors_filter_test.rb +33 -0
- data/test/liquid/liquid_helper.rb +34 -0
- data/test/models/attachment.rb +3 -0
- data/test/models/issue.rb +21 -0
- data/test/models/issue_relation.rb +10 -0
- data/test/models/news.rb +3 -0
- data/test/models/project.rb +8 -0
- data/test/models/user.rb +11 -0
- data/test/models/vote_classes.rb +33 -0
- data/test/money_helper_test.rb +12 -0
- data/test/schema.rb +144 -0
- data/test/tags_helper_test.rb +29 -0
- data/test/test_helper.rb +66 -0
- data/test/vote_helper_test.rb +28 -0
- data/vendor/assets/images/money.png +0 -0
- data/vendor/assets/images/vcard.png +0 -0
- data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
- data/vendor/assets/javascripts/select2.js +2 -0
- data/vendor/assets/javascripts/select2_helpers.js +192 -0
- data/vendor/assets/stylesheets/money.css +96 -0
- data/vendor/assets/stylesheets/select2.css +424 -0
- 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
|