redmine_crm 0.0.23 → 0.0.43

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