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