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.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/Gemfile +1 -1
- data/README.md +166 -33
- data/Rakefile +3 -12
- data/bitbucket-pipelines.yml +42 -0
- data/config/currency_iso.json +12 -0
- data/doc/CHANGELOG +81 -2
- data/lib/redmine_crm/acts_as_draftable/draft.rb +40 -0
- data/lib/redmine_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +170 -0
- data/lib/redmine_crm/acts_as_list/list.rb +282 -0
- data/lib/redmine_crm/{rcrm_acts_as_taggable.rb → acts_as_taggable/rcrm_acts_as_taggable.rb} +105 -93
- data/lib/redmine_crm/acts_as_taggable/tag.rb +81 -0
- data/lib/redmine_crm/acts_as_taggable/tag_list.rb +111 -0
- data/lib/redmine_crm/acts_as_taggable/tagging.rb +16 -0
- data/lib/redmine_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
- data/lib/redmine_crm/{rcrm_acts_as_votable.rb → acts_as_votable/rcrm_acts_as_votable.rb} +15 -14
- data/lib/redmine_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
- data/lib/redmine_crm/{votable.rb → acts_as_votable/votable.rb} +54 -65
- data/lib/redmine_crm/{vote.rb → acts_as_votable/vote.rb} +6 -8
- data/lib/redmine_crm/{voter.rb → acts_as_votable/voter.rb} +29 -34
- data/lib/redmine_crm/assets_manager.rb +43 -0
- data/lib/redmine_crm/colors_helper.rb +192 -0
- data/lib/redmine_crm/compatibility/application_controller_patch.rb +33 -0
- data/lib/redmine_crm/currency/formatting.rb +0 -3
- data/lib/redmine_crm/currency/heuristics.rb +1 -1
- data/lib/redmine_crm/currency/loader.rb +5 -6
- data/lib/redmine_crm/helpers/external_assets_helper.rb +19 -0
- data/lib/redmine_crm/helpers/form_tag_helper.rb +76 -0
- data/lib/redmine_crm/helpers/tags_helper.rb +1 -3
- data/lib/redmine_crm/helpers/vote_helper.rb +29 -32
- data/lib/redmine_crm/liquid/drops/issues_drop.rb +191 -0
- data/lib/redmine_crm/liquid/drops/news_drop.rb +54 -0
- data/lib/redmine_crm/liquid/drops/projects_drop.rb +86 -0
- data/lib/redmine_crm/liquid/drops/time_entries_drop.rb +65 -0
- data/lib/redmine_crm/liquid/drops/users_drop.rb +68 -0
- data/lib/redmine_crm/liquid/filters/arrays.rb +187 -0
- data/lib/redmine_crm/liquid/filters/base.rb +217 -0
- data/lib/redmine_crm/liquid/filters/colors.rb +31 -0
- data/lib/redmine_crm/money_helper.rb +2 -4
- data/lib/redmine_crm/version.rb +1 -1
- data/lib/redmine_crm.rb +56 -21
- data/redmine_crm.gemspec +9 -4
- data/test/acts_as_draftable/draft_test.rb +29 -0
- data/test/acts_as_draftable/rcrm_acts_as_draftable_test.rb +179 -0
- data/test/{acts_as_taggable_test.rb → acts_as_taggable/rcrm_acts_as_taggable_test.rb} +117 -156
- 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/{viewed_test.rb → acts_as_viewed/rcrm_acts_as_viewed_test.rb} +17 -15
- data/test/acts_as_votable/rcrm_acts_as_votable_test.rb +19 -0
- data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +14 -0
- data/test/{votable_model_test.rb → acts_as_votable/votable_test.rb} +34 -5
- data/test/{voter_model_test.rb → acts_as_votable/voter_test.rb} +8 -8
- data/test/currency_test.rb +10 -10
- data/test/database.yml +14 -14
- data/test/fixtures/issues.yml +13 -1
- data/test/fixtures/news.yml +8 -0
- data/test/fixtures/projects.yml +10 -0
- data/test/fixtures/users.yml +6 -2
- data/test/liquid/drops/issues_drop_test.rb +34 -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 +63 -0
- data/test/liquid/filters/colors_filter_test.rb +33 -0
- data/test/liquid/liquid_helper.rb +34 -0
- data/test/models/issue.rb +14 -0
- data/test/models/news.rb +3 -0
- data/test/models/project.rb +8 -0
- data/test/{fixtures → models}/user.rb +5 -1
- data/test/{fixtures → models}/vote_classes.rb +0 -21
- data/test/money_helper_test.rb +5 -5
- data/test/schema.rb +33 -10
- data/test/test_helper.rb +20 -72
- data/vendor/assets/images/vcard.png +0 -0
- data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
- data/vendor/assets/javascripts/select2.js +3 -0
- data/vendor/assets/javascripts/select2_helpers.js +186 -0
- data/vendor/assets/stylesheets/select2.css +414 -0
- metadata +162 -38
- data/lib/redmine_crm/rcrm_acts_as_viewed.rb +0 -287
- data/lib/redmine_crm/rcrm_acts_as_voter.rb +0 -27
- data/lib/redmine_crm/tag.rb +0 -81
- data/lib/redmine_crm/tag_list.rb +0 -112
- data/lib/redmine_crm/tagging.rb +0 -20
- data/test/fixtures/issue.rb +0 -14
- data/test/tag_test.rb +0 -64
- data/test/tagging_test.rb +0 -14
- 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
|
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'
|
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::
|
27
|
-
extend RedmineCrm::
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
51
|
-
tag_name_table
|
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
|
79
|
-
tag_name_table
|
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(
|
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 =
|
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
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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}
|
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)
|
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(
|
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|
|
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 = #{
|
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 = #{
|
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 = #{
|
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(:
|
234
|
-
conditions << send(:
|
235
|
-
conditions << "#{Tagging.table_name}.taggable_type = #{
|
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
|
-
|
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?
|
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::
|
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
|