redmine_crm 0.0.4

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.
@@ -0,0 +1,151 @@
1
+ module RedmineCrm
2
+ class Currency
3
+ module Heuristics
4
+
5
+ # An robust and efficient algorithm for finding currencies in
6
+ # text. Using several algorithms it can find symbols, iso codes and
7
+ # even names of currencies.
8
+ # Although not recommendable, it can also attempt to find the given
9
+ # currency in an entire sentence
10
+ #
11
+ # Returns: Array (matched results)
12
+ def analyze(str)
13
+ return Analyzer.new(str, search_tree).process
14
+ end
15
+
16
+ private
17
+
18
+ # Build a search tree from the currency database
19
+ def search_tree
20
+ @_search_tree ||= {
21
+ :by_symbol => currencies_by_symbol,
22
+ :by_iso_code => currencies_by_iso_code,
23
+ :by_name => currencies_by_name
24
+ }
25
+ end
26
+
27
+ def currencies_by_symbol
28
+ {}.tap do |r|
29
+ table.each do |dummy, c|
30
+ symbol = (c[:symbol]||"").downcase
31
+ symbol.chomp!('.')
32
+ (r[symbol] ||= []) << c
33
+
34
+ (c[:alternate_symbols]||[]).each do |ac|
35
+ ac = ac.downcase
36
+ ac.chomp!('.')
37
+ (r[ac] ||= []) << c
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def currencies_by_iso_code
44
+ {}.tap do |r|
45
+ table.each do |dummy,c|
46
+ (r[c[:iso_code].downcase] ||= []) << c
47
+ end
48
+ end
49
+ end
50
+
51
+ def currencies_by_name
52
+ {}.tap do |r|
53
+ table.each do |dummy,c|
54
+ name_parts = c[:name].downcase.split
55
+ name_parts.each {|part| part.chomp!('.')}
56
+
57
+ # construct one branch per word
58
+ root = r
59
+ while name_part = name_parts.shift
60
+ root = (root[name_part] ||= {})
61
+ end
62
+
63
+ # the leaf is a currency
64
+ (root[:value] ||= []) << c
65
+ end
66
+ end
67
+ end
68
+
69
+ class Analyzer
70
+ attr_reader :search_tree, :words
71
+ attr_accessor :str, :currencies
72
+
73
+ def initialize str, search_tree
74
+ @str = (str||'').dup
75
+ @search_tree = search_tree
76
+ @currencies = []
77
+ end
78
+
79
+ def process
80
+ format
81
+ return [] if str.empty?
82
+
83
+ search_by_symbol
84
+ search_by_iso_code
85
+ search_by_name
86
+
87
+ prepare_reply
88
+ end
89
+
90
+ def format
91
+ str.gsub!(/[\r\n\t]/,'')
92
+ str.gsub!(/[0-9][\.,:0-9]*[0-9]/,'')
93
+ str.gsub!(/[0-9]/, '')
94
+ str.downcase!
95
+ @words = str.split
96
+ @words.each {|word| word.chomp!('.'); word.chomp!(',') }
97
+ end
98
+
99
+ def search_by_symbol
100
+ words.each do |word|
101
+ if found = search_tree[:by_symbol][word]
102
+ currencies.concat(found)
103
+ end
104
+ end
105
+ end
106
+
107
+ def search_by_iso_code
108
+ words.each do |word|
109
+ if found = search_tree[:by_iso_code][word]
110
+ currencies.concat(found)
111
+ end
112
+ end
113
+ end
114
+
115
+ def search_by_name
116
+ # remember, the search tree by name is a construct of branches and leaf!
117
+ # We need to try every combination of words within the sentence, so we
118
+ # end up with a x^2 equation, which should be fine as most names are either
119
+ # one or two words, and this is multiplied with the words of given sentence
120
+
121
+ search_words = words.dup
122
+
123
+ while search_words.length > 0
124
+ root = search_tree[:by_name]
125
+
126
+ search_words.each do |word|
127
+ if root = root[word]
128
+ if root[:value]
129
+ currencies.concat(root[:value])
130
+ end
131
+ else
132
+ break
133
+ end
134
+ end
135
+
136
+ search_words.delete_at(0)
137
+ end
138
+ end
139
+
140
+ def prepare_reply
141
+ codes = currencies.map do |currency|
142
+ currency[:iso_code]
143
+ end
144
+ codes.uniq!
145
+ codes.sort!
146
+ codes
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,24 @@
1
+ module RedmineCrm
2
+ class Currency
3
+ module Loader
4
+ DATA_PATH = File.expand_path("../../../../config", __FILE__)
5
+
6
+ # Loads and returns the currencies stored in JSON files in the config directory.
7
+ #
8
+ # @return [Hash]
9
+ def load_currencies
10
+ currencies = parse_currency_file("currency_iso.json")
11
+ # currencies.merge! parse_currency_file("currency_non_iso.json")
12
+ # currencies.merge! parse_currency_file("currency_backwards_compatible.json")
13
+ end
14
+
15
+ private
16
+
17
+ def parse_currency_file(filename)
18
+ json = File.read("#{DATA_PATH}/#{filename}")
19
+ json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
20
+ JSON.parse(json, :symbolize_names => true)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,301 @@
1
+ require 'active_record'
2
+
3
+ # module ActiveRecord #:nodoc:
4
+ module RedmineCrm
5
+ module Acts #:nodoc:
6
+ module Taggable #:nodoc:
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def rcrm_acts_as_taggable
14
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :class_name => '::RedmineCrm::Tagging'#, :include => :tag
15
+ has_many :tags, :through => :taggings, :class_name => '::RedmineCrm::Tag'
16
+
17
+ before_save :save_cached_tag_list
18
+
19
+ after_create :save_tags
20
+ after_update :save_tags
21
+
22
+ include RedmineCrm::Acts::Taggable::InstanceMethods
23
+ extend RedmineCrm::Acts::Taggable::SingletonMethods
24
+
25
+ alias_method_chain :reload, :tag_list
26
+ end
27
+
28
+ def cached_tag_list_column_name
29
+ "cached_tag_list"
30
+ end
31
+
32
+ def set_cached_tag_list_column_name(value = nil, &block)
33
+ define_attr_method :cached_tag_list_column_name, value, &block
34
+ end
35
+
36
+ # Create the taggable tables
37
+ # === Options hash:
38
+ # * <tt>:table_name</tt> - use a table name other than viewings
39
+ # To be used during migration, but can also be used in other places
40
+ def create_taggable_table options = {}
41
+ tag_name_table = options[:tags] || :tags
42
+
43
+ if !self.connection.table_exists?(tag_name_table)
44
+ self.connection.create_table(tag_name_table) do |t|
45
+ t.column :name, :string
46
+ end
47
+ end
48
+
49
+ taggings_name_table = options[:taggings] || :taggings
50
+ if !self.connection.table_exists?(taggings_name_table)
51
+ self.connection.create_table(taggings_name_table) do |t|
52
+ t.column :tag_id, :integer
53
+ t.column :taggable_id, :integer
54
+
55
+ # You should make sure that the column created is
56
+ # long enough to store the required class names.
57
+ t.column :taggable_type, :string
58
+
59
+ t.column :created_at, :datetime
60
+ end
61
+
62
+ self.connection.add_index :taggings, :tag_id
63
+ self.connection.add_index :taggings, [:taggable_id, :taggable_type]
64
+ end
65
+
66
+ end
67
+
68
+ def drop_taggable_table options = {}
69
+ tag_name_table = options[:tags] || :tags
70
+ if !self.connection.table_exists?(tag_name_table)
71
+ self.connection.drop_table tag_name_table
72
+ end
73
+ taggings_name_table = options[:taggings] || :taggings
74
+ if !self.connection.table_exists?(taggings_name_table)
75
+ self.connection.drop_table taggings_name_table
76
+ end
77
+
78
+ end
79
+ end
80
+
81
+ module SingletonMethods
82
+ # Returns an array of related tags.
83
+ # Related tags are all the other tags that are found on the models tagged with the provided tags.
84
+ #
85
+ # Pass either a tag, string, or an array of strings or tags.
86
+ #
87
+ # Options:
88
+ # :order - SQL Order how to order the tags. Defaults to "count DESC, tags.name".
89
+ def find_related_tags(tags, options = {})
90
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
91
+
92
+ related_models = find_tagged_with(tags)
93
+
94
+ return [] if related_models.blank?
95
+
96
+ related_ids = related_models.ids.join(",")
97
+ Tag.select( #find(:all, options.merge({
98
+ "#{Tag.table_name}.*, COUNT(#{Tag.table_name}.id) AS count").joins(
99
+ "JOIN #{Tagging.table_name} ON #{Tagging.table_name}.taggable_type = '#{base_class.name}'
100
+ AND #{Tagging.table_name}.taggable_id IN (#{related_ids})
101
+ AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id").order(
102
+ options[:order] || "count DESC, #{Tag.table_name}.name").group(
103
+ "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING #{Tag.table_name}.name NOT IN (#{tags.map { |n| quote_value(n, nil) }.join(",")})")
104
+ # }))
105
+ end
106
+
107
+ # Pass either a tag, string, or an array of strings or tags.
108
+ #
109
+ # Options:
110
+ # :exclude - Find models that are not tagged with the given tags
111
+ # :match_all - Find models that match all of the given tags, not just one
112
+ # :conditions - A piece of SQL conditions to add to the query
113
+ def find_tagged_with(*args)
114
+ options = find_options_for_find_tagged_with(*args)
115
+ options.blank? ? [] : select(options[:select]).where(options[:conditions]).joins(options[:joins]).order(options[:order])
116
+ # find(:all, options)
117
+ end
118
+ alias_method :tagged_with, :find_tagged_with
119
+
120
+ def find_options_for_find_tagged_with(tags, options = {})
121
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
122
+ options = options.dup
123
+
124
+ return {} if tags.empty?
125
+
126
+ conditions = []
127
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
128
+
129
+ taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
130
+
131
+ joins = [
132
+ "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)}",
133
+ "INNER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id"
134
+ ]
135
+
136
+ if options.delete(:exclude)
137
+ conditions << <<-END
138
+ #{table_name}.id NOT IN
139
+ (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name}
140
+ INNER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id
141
+ WHERE #{tags_condition(tags)} AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})
142
+ END
143
+ else
144
+ if options.delete(:match_all)
145
+ joins << joins_for_match_all_tags(tags)
146
+ else
147
+ conditions << tags_condition(tags, tags_alias)
148
+ end
149
+ end
150
+
151
+ { :select => "DISTINCT #{table_name}.* ",
152
+ :joins => joins.join(" "),
153
+ :conditions => conditions.join(" AND ")
154
+ }.reverse_merge!(options)
155
+ end
156
+
157
+ def joins_for_match_all_tags(tags)
158
+ joins = []
159
+
160
+ tags.each_with_index do |tag, index|
161
+ taggings_alias, tags_alias = "taggings_#{index}", "tags_#{index}"
162
+
163
+ join = <<-END
164
+ INNER JOIN #{Tagging.table_name} #{taggings_alias} ON
165
+ #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND
166
+ #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}
167
+
168
+ INNER JOIN #{Tag.table_name} #{tags_alias} ON
169
+ #{taggings_alias}.tag_id = #{tags_alias}.id AND
170
+ #{tags_alias}.name = ?
171
+ END
172
+
173
+ joins << sanitize_sql([join, tag])
174
+ end
175
+
176
+ joins.join(" ")
177
+ end
178
+
179
+ # Calculate the tag counts for all tags.
180
+ #
181
+ # See Tag.counts for available options.
182
+ def tag_counts(options = {})
183
+ # Tag.find(:all, find_options_for_tag_counts(options))
184
+ opt = find_options_for_tag_counts(options)
185
+ Tag.select(opt[:select]).where(opt[:conditions]).joins(opt[:joins]).group(opt[:group]).having(opt[:having]).order(opt[:order]).limit(options[:limit])
186
+ end
187
+ alias_method :all_tag_counts, :tag_counts
188
+
189
+ def find_options_for_tag_counts(options = {})
190
+ options = options.dup
191
+ scope = scope_attributes
192
+ # scope(:find)
193
+
194
+ conditions = []
195
+ conditions << send(:sanitize_conditions, options.delete(:conditions)) if options[:conditions]
196
+ conditions << send(:sanitize_conditions, scope) if scope
197
+ conditions << "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}"
198
+ conditions << type_condition unless descends_from_active_record?
199
+ conditions.compact!
200
+ conditions = conditions.join(" AND ")
201
+
202
+ joins = ["INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"]
203
+ joins << options.delete(:joins) if options[:joins].present?
204
+ # joins << scope[:joins] if scope && scope[:joins].present?
205
+ joins = joins.join(" ")
206
+
207
+ options = { :conditions => conditions, :joins => joins }.update(options)
208
+
209
+ Tag.options_for_counts(options)
210
+ end
211
+
212
+ def caching_tag_list?
213
+ column_names.include?(cached_tag_list_column_name)
214
+ end
215
+
216
+ private
217
+ def tags_condition(tags, table_name = Tag.table_name)
218
+ condition = tags.map { |t| sanitize_sql(["#{table_name}.name LIKE ?", t]) }.join(" OR ")
219
+ "(" + condition + ")" unless condition.blank?
220
+ end
221
+
222
+ def merge_conditions(*conditions)
223
+ segments = []
224
+
225
+ conditions.each do |condition|
226
+ unless condition.blank?
227
+ sql = sanitize_sql(condition)
228
+ segments << sql unless sql.blank?
229
+ end
230
+ end
231
+
232
+ "(#{segments.join(') AND (')})" unless segments.empty?
233
+ end
234
+ end
235
+
236
+ module InstanceMethods
237
+ def tag_list
238
+ return @tag_list if @tag_list
239
+
240
+ if self.class.caching_tag_list? and !(cached_value = send(self.class.cached_tag_list_column_name)).nil?
241
+ @tag_list = TagList.from(cached_value)
242
+ else
243
+ @tag_list = TagList.new(*tags.map(&:name))
244
+ end
245
+ end
246
+
247
+ def tag_list=(value)
248
+ @tag_list = TagList.from(value)
249
+ end
250
+
251
+ def save_cached_tag_list
252
+ if self.class.caching_tag_list?
253
+ self[self.class.cached_tag_list_column_name] = tag_list.to_s
254
+ end
255
+ end
256
+
257
+ #build list from related tags
258
+ def all_tags_list
259
+ tags.pluck(:name)
260
+ end
261
+
262
+ def save_tags
263
+ return unless @tag_list
264
+
265
+ new_tag_names = @tag_list - tags.map(&:name)
266
+ old_tags = tags.reject { |tag| @tag_list.include?(tag.name) }
267
+
268
+ self.class.transaction do
269
+ if old_tags.any?
270
+ taggings.where("tag_id IN (?)", old_tags.map(&:id)).each(&:destroy)
271
+ taggings.reset
272
+ end
273
+
274
+ new_tag_names.each do |new_tag_name|
275
+ tags << Tag.find_or_create_with_like_by_name(new_tag_name)
276
+ end
277
+ end
278
+
279
+ true
280
+ end
281
+
282
+ # Calculate the tag counts for the tags used by this model.
283
+ #
284
+ # The possible options are the same as the tag_counts class method.
285
+ def tag_counts(options = {})
286
+ return [] if tag_list.blank?
287
+
288
+ options[:conditions] = self.class.send(:merge_conditions, options[:conditions], self.class.send(:tags_condition, tag_list))
289
+ self.class.tag_counts(options)
290
+ end
291
+
292
+ def reload_with_tag_list(*args) #:nodoc:
293
+ @tag_list = nil
294
+ reload_without_tag_list(*args)
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
300
+
301
+ ActiveRecord::Base.send(:include, RedmineCrm::Acts::Taggable)