redmine_crm 0.0.23 → 0.0.53

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/README.md +166 -33
  4. data/Rakefile +3 -12
  5. data/app/controllers/redmine_crm_controller.rb +26 -0
  6. data/app/views/redmine_crm/_money.html.erb +44 -0
  7. data/app/views/redmine_crm/settings.html.erb +10 -0
  8. data/bitbucket-pipelines.yml +54 -0
  9. data/config/currency_iso.json +12 -0
  10. data/config/locales/en.yml +13 -0
  11. data/config/locales/ru.yml +13 -0
  12. data/config/routes.rb +5 -0
  13. data/doc/CHANGELOG +123 -2
  14. data/lib/redmine_crm/acts_as_draftable/draft.rb +40 -0
  15. data/lib/redmine_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +172 -0
  16. data/lib/redmine_crm/acts_as_list/list.rb +282 -0
  17. data/lib/redmine_crm/{rcrm_acts_as_taggable.rb → acts_as_taggable/rcrm_acts_as_taggable.rb} +112 -93
  18. data/lib/redmine_crm/acts_as_taggable/tag.rb +81 -0
  19. data/lib/redmine_crm/acts_as_taggable/tag_list.rb +111 -0
  20. data/lib/redmine_crm/acts_as_taggable/tagging.rb +16 -0
  21. data/lib/redmine_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
  22. data/lib/redmine_crm/{rcrm_acts_as_votable.rb → acts_as_votable/rcrm_acts_as_votable.rb} +15 -14
  23. data/lib/redmine_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
  24. data/lib/redmine_crm/{votable.rb → acts_as_votable/votable.rb} +54 -65
  25. data/lib/redmine_crm/{vote.rb → acts_as_votable/vote.rb} +6 -8
  26. data/lib/redmine_crm/{voter.rb → acts_as_votable/voter.rb} +29 -34
  27. data/lib/redmine_crm/assets_manager.rb +43 -0
  28. data/lib/redmine_crm/colors_helper.rb +192 -0
  29. data/lib/redmine_crm/compatibility/application_controller_patch.rb +33 -0
  30. data/lib/redmine_crm/currency/formatting.rb +5 -8
  31. data/lib/redmine_crm/currency/heuristics.rb +1 -1
  32. data/lib/redmine_crm/currency/loader.rb +5 -6
  33. data/lib/redmine_crm/currency.rb +28 -17
  34. data/lib/redmine_crm/engine.rb +4 -0
  35. data/lib/redmine_crm/helpers/external_assets_helper.rb +19 -0
  36. data/lib/redmine_crm/helpers/form_tag_helper.rb +76 -0
  37. data/lib/redmine_crm/helpers/tags_helper.rb +1 -3
  38. data/lib/redmine_crm/helpers/vote_helper.rb +29 -32
  39. data/lib/redmine_crm/hooks/views_layouts_hook.rb +11 -0
  40. data/lib/redmine_crm/liquid/drops/issues_drop.rb +191 -0
  41. data/lib/redmine_crm/liquid/drops/news_drop.rb +54 -0
  42. data/lib/redmine_crm/liquid/drops/projects_drop.rb +86 -0
  43. data/lib/redmine_crm/liquid/drops/time_entries_drop.rb +65 -0
  44. data/lib/redmine_crm/liquid/drops/users_drop.rb +68 -0
  45. data/lib/redmine_crm/liquid/filters/arrays.rb +187 -0
  46. data/lib/redmine_crm/liquid/filters/base.rb +217 -0
  47. data/lib/redmine_crm/liquid/filters/colors.rb +31 -0
  48. data/lib/redmine_crm/money_helper.rb +17 -18
  49. data/lib/redmine_crm/settings/money.rb +46 -0
  50. data/lib/redmine_crm/settings.rb +53 -0
  51. data/lib/redmine_crm/version.rb +1 -1
  52. data/lib/redmine_crm.rb +60 -21
  53. data/redmine_crm.gemspec +12 -6
  54. data/test/acts_as_draftable/draft_test.rb +29 -0
  55. data/test/acts_as_draftable/rcrm_acts_as_draftable_test.rb +178 -0
  56. data/test/{acts_as_taggable_test.rb → acts_as_taggable/rcrm_acts_as_taggable_test.rb} +117 -156
  57. data/test/acts_as_taggable/tag_list_test.rb +34 -0
  58. data/test/acts_as_taggable/tag_test.rb +72 -0
  59. data/test/acts_as_taggable/tagging_test.rb +15 -0
  60. data/test/{viewed_test.rb → acts_as_viewed/rcrm_acts_as_viewed_test.rb} +17 -15
  61. data/test/acts_as_votable/rcrm_acts_as_votable_test.rb +19 -0
  62. data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +14 -0
  63. data/test/{votable_model_test.rb → acts_as_votable/votable_test.rb} +34 -5
  64. data/test/{voter_model_test.rb → acts_as_votable/voter_test.rb} +8 -8
  65. data/test/currency_test.rb +10 -10
  66. data/test/database.yml +14 -14
  67. data/test/fixtures/issues.yml +13 -1
  68. data/test/fixtures/news.yml +8 -0
  69. data/test/fixtures/projects.yml +10 -0
  70. data/test/fixtures/users.yml +6 -2
  71. data/test/liquid/drops/issues_drop_test.rb +34 -0
  72. data/test/liquid/drops/news_drop_test.rb +38 -0
  73. data/test/liquid/drops/projects_drop_test.rb +44 -0
  74. data/test/liquid/drops/uses_drop_test.rb +36 -0
  75. data/test/liquid/filters/arrays_filter_test.rb +31 -0
  76. data/test/liquid/filters/base_filter_test.rb +63 -0
  77. data/test/liquid/filters/colors_filter_test.rb +33 -0
  78. data/test/liquid/liquid_helper.rb +34 -0
  79. data/test/models/issue.rb +14 -0
  80. data/test/models/news.rb +3 -0
  81. data/test/models/project.rb +8 -0
  82. data/test/{fixtures → models}/user.rb +5 -1
  83. data/test/{fixtures → models}/vote_classes.rb +0 -21
  84. data/test/money_helper_test.rb +5 -5
  85. data/test/schema.rb +33 -10
  86. data/test/test_helper.rb +20 -72
  87. data/vendor/assets/images/money.png +0 -0
  88. data/vendor/assets/images/vcard.png +0 -0
  89. data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
  90. data/vendor/assets/javascripts/select2.js +2 -0
  91. data/vendor/assets/javascripts/select2_helpers.js +192 -0
  92. data/vendor/assets/stylesheets/money.css +3 -0
  93. data/vendor/assets/stylesheets/select2.css +424 -0
  94. metadata +190 -40
  95. data/lib/redmine_crm/rcrm_acts_as_viewed.rb +0 -287
  96. data/lib/redmine_crm/rcrm_acts_as_voter.rb +0 -27
  97. data/lib/redmine_crm/tag.rb +0 -81
  98. data/lib/redmine_crm/tag_list.rb +0 -112
  99. data/lib/redmine_crm/tagging.rb +0 -20
  100. data/test/fixtures/issue.rb +0 -14
  101. data/test/tag_test.rb +0 -64
  102. data/test/tagging_test.rb +0 -14
  103. data/test/votable_test.rb +0 -17
@@ -1,32 +1,31 @@
1
1
  require 'active_record'
2
2
 
3
- # module ActiveRecord #:nodoc:
4
3
  module RedmineCrm
5
- module Acts #:nodoc:
4
+ module ActsAsTaggable #:nodoc:
6
5
  module Taggable #:nodoc:
7
6
  def self.included(base)
8
7
  base.extend(ClassMethods)
9
8
  end
10
-
9
+
11
10
  module ClassMethods
12
-
13
11
  def taggable?
14
12
  false
15
13
  end
16
14
 
17
15
  def rcrm_acts_as_taggable
18
- has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => '::RedmineCrm::Tagging'#, :include => :tag
19
- has_many :tags, :through => :taggings, :class_name => '::RedmineCrm::Tag'
20
-
16
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => '::RedmineCrm::ActsAsTaggable::Tagging'
17
+ has_many :tags, :through => :taggings, :class_name => '::RedmineCrm::ActsAsTaggable::Tag'
18
+
21
19
  before_save :save_cached_tag_list
22
-
20
+
23
21
  after_create :save_tags
24
22
  after_update :save_tags
25
-
26
- include RedmineCrm::Acts::Taggable::InstanceMethods
27
- extend RedmineCrm::Acts::Taggable::SingletonMethods
28
-
29
- alias_method_chain :reload, :tag_list
23
+
24
+ include RedmineCrm::ActsAsTaggable::Taggable::InstanceMethods
25
+ extend RedmineCrm::ActsAsTaggable::Taggable::SingletonMethods
26
+
27
+ alias_method :reload_without_tag_list, :reload
28
+ alias_method :reload, :reload_with_tag_list
30
29
 
31
30
  class_eval do
32
31
  def self.taggable?
@@ -34,11 +33,11 @@ module RedmineCrm
34
33
  end
35
34
  end
36
35
  end
37
-
36
+
38
37
  def cached_tag_list_column_name
39
- "cached_tag_list"
38
+ 'cached_tag_list'
40
39
  end
41
-
40
+
42
41
  def set_cached_tag_list_column_name(value = nil, &block)
43
42
  define_attr_method :cached_tag_list_column_name, value, &block
44
43
  end
@@ -47,9 +46,9 @@ module RedmineCrm
47
46
  # === Options hash:
48
47
  # * <tt>:table_name</tt> - use a table name other than viewings
49
48
  # To be used during migration, but can also be used in other places
50
- def create_taggable_table options = {}
51
- tag_name_table = options[:tags] || :tags
52
-
49
+ def create_taggable_table(options = {})
50
+ tag_name_table = options[:tags] || :tags
51
+
53
52
  if !self.connection.table_exists?(tag_name_table)
54
53
  self.connection.create_table(tag_name_table) do |t|
55
54
  t.column :name, :string
@@ -61,89 +60,98 @@ module RedmineCrm
61
60
  self.connection.create_table(taggings_name_table) do |t|
62
61
  t.column :tag_id, :integer
63
62
  t.column :taggable_id, :integer
64
-
63
+
65
64
  # You should make sure that the column created is
66
65
  # long enough to store the required class names.
67
66
  t.column :taggable_type, :string
68
-
67
+
69
68
  t.column :created_at, :datetime
70
69
  end
71
70
 
72
71
  self.connection.add_index :taggings, :tag_id
73
72
  self.connection.add_index :taggings, [:taggable_id, :taggable_type]
74
73
  end
75
-
76
74
  end
77
-
78
- def drop_taggable_table options = {}
79
- tag_name_table = options[:tags] || :tags
75
+
76
+ def drop_taggable_table(options = {})
77
+ tag_name_table = options[:tags] || :tags
80
78
  if self.connection.table_exists?(tag_name_table)
81
79
  self.connection.drop_table tag_name_table
82
80
  end
81
+
83
82
  taggings_name_table = options[:taggings] || :taggings
84
83
  if self.connection.table_exists?(taggings_name_table)
85
84
  self.connection.drop_table taggings_name_table
86
85
  end
87
-
88
86
  end
89
87
  end
90
-
88
+
91
89
  module SingletonMethods
92
90
  #Return all avalible tags for a project or global
93
- #Example: Question.available_tags(@project_id )
94
- def available_tags(project=nil, limit=30)
91
+ #Example: Question.available_tags(:project => @project_id )
92
+ def available_tags(options = {})
93
+ project = options[:project]
94
+ limit = options[:limit].to_i.zero? ? 30 : options[:limit].to_i
95
95
  scope = Tag.where({})
96
- class_name = quote_value(base_class.name, nil)
96
+ class_name = quote_string_value(base_class.name)
97
97
  join = []
98
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
99
+ join << "JOIN #{table_name} ON #{table_name}.id = #{Tagging.table_name}.taggable_id
100
100
  AND #{Tagging.table_name}.taggable_type = #{class_name} "
101
- if self.attribute_names.include? "project_id"
102
- if project
103
- join << "JOIN #{Project.table_name} ON #{Project.table_name}.id = #{table_name}.project_id"
104
- else
105
- scope = scope.where("#{table_name}.project_id IS NULL")
106
- end
101
+ if attribute_names.include?('project_id') && project
102
+ join << "JOIN #{Project.table_name} ON #{Project.table_name}.id = #{table_name}.project_id"
103
+ scope = scope.where("#{table_name}.project_id = ?", project.id)
104
+ end
105
+
106
+ if options[:name_like]
107
+ scope = scope.where("LOWER(#{Tag.table_name}.name) LIKE LOWER(?)", "%#{options[:name_like]}%")
107
108
  end
108
109
 
109
- group_fields = ""
110
+ group_fields = ''
110
111
  group_fields << ", #{Tag.table_name}.created_at" if Tag.respond_to?(:created_at)
111
112
  group_fields << ", #{Tag.table_name}.updated_at" if Tag.respond_to?(:updated_at)
112
113
 
114
+ if base_class.respond_to?(:visible_condition)
115
+ visible_condition = base_class.visible_condition(User.current)
116
+ if visible_condition.include?('project_id') && join.all? { |jn| jn.exclude?('JOIN projects') }
117
+ join << "JOIN #{Project.table_name} ON #{Project.table_name}.id = #{table_name}.project_id"
118
+ end
119
+ scope = scope.where(visible_condition)
120
+ end
113
121
  scope = scope.joins(join.join(' '))
114
122
  scope = scope.select("#{Tag.table_name}.*, COUNT(DISTINCT #{Tagging.table_name}.taggable_id) AS count")
115
- scope = scope.group("#{Tag.table_name}.id, #{Tag.table_name}.name #{group_fields} HAVING COUNT(*) > 0")
123
+ scope = scope.group("#{Tag.table_name}.id, #{Tag.table_name}.name #{group_fields}")
124
+ scope = scope.having('COUNT(*) > 0')
116
125
  scope = scope.order("#{Tag.table_name}.name")
117
- scope = scope.limit(limit) if limit
126
+ scope = scope.limit(limit)
118
127
  scope
119
128
  end
120
129
  # Returns an array of related tags.
121
130
  # Related tags are all the other tags that are found on the models tagged with the provided tags.
122
- #
131
+ #
123
132
  # Pass either a tag, string, or an array of strings or tags.
124
- #
133
+ #
125
134
  # Options:
126
135
  # :order - SQL Order how to order the tags. Defaults to "count DESC, tags.name".
127
136
  def find_related_tags(tags, options = {})
128
137
  tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
129
-
138
+
130
139
  related_models = find_tagged_with(tags)
131
-
140
+
132
141
  return [] if related_models.blank?
133
-
142
+
134
143
  related_ids = related_models.map{|c| c.id }.join(",")
135
- Tag.select( #find(:all, options.merge({
144
+ Tag.select(
136
145
  "#{Tag.table_name}.*, COUNT(#{Tag.table_name}.id) AS count").joins(
137
146
  "JOIN #{Tagging.table_name} ON #{Tagging.table_name}.taggable_type = '#{base_class.name}'
138
147
  AND #{Tagging.table_name}.taggable_id IN (#{related_ids})
139
148
  AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id").order(
140
149
  options[:order] || "count DESC, #{Tag.table_name}.name").group(
141
- "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING #{Tag.table_name}.name NOT IN (#{tags.map { |n| quote_value(n, nil) }.join(",")})")
142
- # }))
150
+ "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING #{Tag.table_name}.name NOT IN (#{tags.map { |n| quote_string_value(n) }.join(",")})")
143
151
  end
144
-
152
+
145
153
  # Pass either a tag, string, or an array of strings or tags.
146
- #
154
+ #
147
155
  # Options:
148
156
  # :exclude - Find models that are not tagged with the given tags
149
157
  # :match_all - Find models that match all of the given tags, not just one
@@ -151,32 +159,32 @@ module RedmineCrm
151
159
  def find_tagged_with(*args)
152
160
  options = find_options_for_find_tagged_with(*args)
153
161
  options.blank? ? [] : select(options[:select]).where(options[:conditions]).joins(options[:joins]).order(options[:order]).to_a
154
- # find(:all, options)
155
162
  end
163
+
156
164
  alias_method :tagged_with, :find_tagged_with
157
-
165
+
158
166
  def find_options_for_find_tagged_with(tags, options = {})
159
167
  tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
160
168
  options = options.dup
161
-
169
+
162
170
  return {} if tags.empty?
163
-
171
+
164
172
  conditions = []
165
173
  conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
166
-
174
+
167
175
  taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
168
-
176
+
169
177
  joins = [
170
- "INNER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}",
178
+ "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)}",
171
179
  "INNER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id"
172
180
  ]
173
-
181
+
174
182
  if options.delete(:exclude)
175
183
  conditions << <<-END
176
184
  #{table_name}.id NOT IN
177
185
  (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name}
178
186
  INNER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id
179
- WHERE #{tags_condition(tags)} AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})
187
+ WHERE #{tags_condition(tags)} AND #{Tagging.table_name}.taggable_type = #{quote_string_value(base_class.name)})
180
188
  END
181
189
  else
182
190
  if options.delete(:match_all)
@@ -185,23 +193,23 @@ module RedmineCrm
185
193
  conditions << tags_condition(tags, tags_alias)
186
194
  end
187
195
  end
188
-
196
+
189
197
  { :select => "DISTINCT #{table_name}.* ",
190
198
  :joins => joins.join(" "),
191
199
  :conditions => conditions.join(" AND ")
192
200
  }.reverse_merge!(options)
193
201
  end
194
-
202
+
195
203
  def joins_for_match_all_tags(tags)
196
204
  joins = []
197
-
205
+
198
206
  tags.each_with_index do |tag, index|
199
207
  taggings_alias, tags_alias = "taggings_#{index}", "tags_#{index}"
200
208
 
201
209
  join = <<-END
202
210
  INNER JOIN #{Tagging.table_name} #{taggings_alias} ON
203
211
  #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND
204
- #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}
212
+ #{taggings_alias}.taggable_type = #{quote_string_value(base_class.name)}
205
213
 
206
214
  INNER JOIN #{Tag.table_name} #{tags_alias} ON
207
215
  #{taggings_alias}.tag_id = #{tags_alias}.id AND
@@ -210,12 +218,12 @@ module RedmineCrm
210
218
 
211
219
  joins << sanitize_sql([join, tag])
212
220
  end
213
-
214
- joins.join(" ")
221
+
222
+ joins.join(' ')
215
223
  end
216
-
224
+
217
225
  # Calculate the tag counts for all tags.
218
- #
226
+ #
219
227
  # See Tag.counts for available options.
220
228
  def tag_counts(options = {})
221
229
  # Tag.find(:all, find_options_for_tag_counts(options))
@@ -223,35 +231,41 @@ module RedmineCrm
223
231
  Tag.select(opt[:select]).where(opt[:conditions]).joins(opt[:joins]).group(opt[:group]).having(opt[:having]).order(opt[:order]).limit(options[:limit])
224
232
  end
225
233
  alias_method :all_tag_counts, :tag_counts
226
-
234
+
227
235
  def find_options_for_tag_counts(options = {})
228
236
  options = options.dup
229
237
  scope = scope_attributes
230
238
  # scope(:find)
231
-
239
+
232
240
  conditions = []
233
- conditions << send(:sanitize_conditions, options.delete(:conditions)) if options[:conditions]
234
- conditions << send(:sanitize_conditions, scope) if scope
235
- conditions << "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}"
236
- conditions << type_condition unless descends_from_active_record?
241
+ conditions << send(:sanitize_sql_for_assignment, options.delete(:conditions)) if options[:conditions]
242
+ conditions << send(:sanitize_sql_for_assignment, scope) if scope
243
+ conditions << "#{Tagging.table_name}.taggable_type = #{quote_string_value(base_class.name)}"
244
+ conditions << type_condition unless descends_from_active_record?
245
+ conditions.delete('')
237
246
  conditions.compact!
238
247
  conditions = conditions.join(" AND ")
239
-
248
+
240
249
  joins = ["INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"]
241
250
  joins << options.delete(:joins) if options[:joins].present?
242
251
  # joins << scope[:joins] if scope && scope[:joins].present?
243
252
  joins = joins.join(" ")
244
-
253
+
245
254
  options = { :conditions => conditions, :joins => joins }.update(options)
246
-
255
+
247
256
  Tag.options_for_counts(options)
248
257
  end
249
258
 
250
259
  def caching_tag_list?
251
260
  column_names.include?(cached_tag_list_column_name)
252
261
  end
253
-
254
- private
262
+
263
+ private
264
+
265
+ def quote_string_value(object)
266
+ connection.quote(object)
267
+ end
268
+
255
269
  def tags_condition(tags, table_name = Tag.table_name)
256
270
  condition = tags.map { |t| sanitize_sql(["#{table_name}.name LIKE ?", t]) }.join(" OR ")
257
271
  "(" + condition + ")" unless condition.blank?
@@ -270,39 +284,39 @@ module RedmineCrm
270
284
  "(#{segments.join(') AND (')})" unless segments.empty?
271
285
  end
272
286
  end
273
-
287
+
274
288
  module InstanceMethods
275
289
  def tag_list
276
- return @tag_list if @tag_list
277
-
278
- if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
290
+ return @tag_list if @tag_list ||= nil
291
+
292
+ if self.class.caching_tag_list? && !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
279
293
  @tag_list = TagList.from(cached_value)
280
294
  else
281
295
  @tag_list = TagList.new(*tags.map(&:name))
282
296
  end
283
297
  end
284
-
298
+
285
299
  def tag_list=(value)
286
300
  @tag_list = TagList.from(value)
287
301
  end
288
-
302
+
289
303
  def save_cached_tag_list
290
304
  if self.class.caching_tag_list?
291
305
  self[self.class.cached_tag_list_column_name] = tag_list.to_s
292
306
  end
293
307
  end
294
-
308
+
295
309
  #build list from related tags
296
310
  def all_tags_list
297
311
  tags.pluck(:name)
298
312
  end
299
-
313
+
300
314
  def save_tags
301
315
  return unless @tag_list
302
-
316
+
303
317
  new_tag_names = @tag_list - tags.map(&:name)
304
318
  old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
305
-
319
+
306
320
  self.class.transaction do
307
321
  if old_tags.any?
308
322
  taggings.where("tag_id IN (?)", old_tags.map(&:id)).each(&:destroy)
@@ -312,20 +326,20 @@ module RedmineCrm
312
326
  tags << Tag.find_or_create_with_like_by_name(new_tag_name)
313
327
  end
314
328
  end
315
-
329
+
316
330
  true
317
331
  end
318
-
332
+
319
333
  # Calculate the tag counts for the tags used by this model.
320
334
  #
321
335
  # The possible options are the same as the tag_counts class method.
322
336
  def tag_counts(options = {})
323
337
  return [] if tag_list.blank?
324
-
338
+
325
339
  options[:conditions] = self.class.send(:merge_conditions, options[:conditions], self.class.send(:tags_condition, tag_list))
326
340
  self.class.tag_counts(options)
327
341
  end
328
-
342
+
329
343
  def reload_with_tag_list(*args) #:nodoc:
330
344
  @tag_list = nil
331
345
  reload_without_tag_list(*args)
@@ -335,4 +349,9 @@ module RedmineCrm
335
349
  end
336
350
  end
337
351
 
338
- ActiveRecord::Base.send(:include, RedmineCrm::Acts::Taggable)
352
+ ActiveRecord::Base.send(:include, RedmineCrm::ActsAsTaggable::Taggable)
353
+
354
+ # Class aliases
355
+ RedmineCrm::Tag = RedmineCrm::ActsAsTaggable::Tag
356
+ RedmineCrm::TagList = RedmineCrm::ActsAsTaggable::TagList
357
+ RedmineCrm::Tagging = RedmineCrm::ActsAsTaggable::Tagging
@@ -0,0 +1,81 @@
1
+ module RedmineCrm
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 RedmineCrm
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
@@ -0,0 +1,16 @@
1
+ module RedmineCrm
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