redmine_crm 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)