yeshua_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/yeshua_crm/acts_as_draftable/draft.rb +40 -0
- data/lib/yeshua_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +154 -0
- data/lib/yeshua_crm/acts_as_list/list.rb +282 -0
- data/lib/yeshua_crm/acts_as_taggable/rcrm_acts_as_taggable.rb +350 -0
- data/lib/yeshua_crm/acts_as_taggable/tag.rb +81 -0
- data/lib/yeshua_crm/acts_as_taggable/tag_list.rb +111 -0
- data/lib/yeshua_crm/acts_as_taggable/tagging.rb +16 -0
- data/lib/yeshua_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
- data/lib/yeshua_crm/acts_as_votable/rcrm_acts_as_votable.rb +80 -0
- data/lib/yeshua_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
- data/lib/yeshua_crm/acts_as_votable/votable.rb +323 -0
- data/lib/yeshua_crm/acts_as_votable/vote.rb +28 -0
- data/lib/yeshua_crm/acts_as_votable/voter.rb +131 -0
- data/lib/yeshua_crm/assets_manager.rb +43 -0
- data/lib/yeshua_crm/currency/formatting.rb +224 -0
- data/lib/yeshua_crm/currency/heuristics.rb +151 -0
- data/lib/yeshua_crm/currency/loader.rb +24 -0
- data/lib/yeshua_crm/currency.rb +439 -0
- data/lib/yeshua_crm/helpers/external_assets_helper.rb +17 -0
- data/lib/yeshua_crm/helpers/form_tag_helper.rb +123 -0
- data/lib/yeshua_crm/helpers/tags_helper.rb +13 -0
- data/lib/yeshua_crm/helpers/vote_helper.rb +35 -0
- data/lib/yeshua_crm/liquid/drops/cells_drop.rb +86 -0
- data/lib/yeshua_crm/liquid/drops/issues_drop.rb +66 -0
- data/lib/yeshua_crm/liquid/drops/news_drop.rb +54 -0
- data/lib/yeshua_crm/liquid/drops/users_drop.rb +72 -0
- data/lib/yeshua_crm/liquid/filters/arrays.rb +177 -0
- data/lib/yeshua_crm/liquid/filters/base.rb +208 -0
- data/lib/yeshua_crm/money_helper.rb +65 -0
- data/lib/yeshua_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/yeshua_crm.gemspec +28 -0
- metadata +206 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'yeshua_crm/helpers/vote_helper'
|
|
2
|
+
|
|
3
|
+
module YeshuaCrm
|
|
4
|
+
module ActsAsVotable
|
|
5
|
+
class Vote < ActiveRecord::Base
|
|
6
|
+
include Helpers::Words
|
|
7
|
+
|
|
8
|
+
if defined?(ProtectedAttributes) || ::ActiveRecord::VERSION::MAJOR < 4
|
|
9
|
+
attr_accessible :votable_id, :votable_type,
|
|
10
|
+
:voter_id, :voter_type,
|
|
11
|
+
:votable, :voter,
|
|
12
|
+
:vote_flag, :vote_scope,
|
|
13
|
+
:vote_ip
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
belongs_to :votable, :polymorphic => true
|
|
17
|
+
belongs_to :voter, :polymorphic => true
|
|
18
|
+
|
|
19
|
+
scope :up, lambda { where(:vote_flag => true) }
|
|
20
|
+
scope :down, lambda { where(:vote_flag => false) }
|
|
21
|
+
scope :for_type, lambda { |klass| where(:votable_type => klass.to_s) }
|
|
22
|
+
scope :by_type, lambda { |klass| where(:voter_type => klass.to_s) }
|
|
23
|
+
|
|
24
|
+
validates_presence_of :votable_id
|
|
25
|
+
validates_presence_of :voter_id
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module YeshuaCrm
|
|
2
|
+
module ActsAsVotable
|
|
3
|
+
module Voter
|
|
4
|
+
def self.included(base)
|
|
5
|
+
# allow user to define these
|
|
6
|
+
aliases = {
|
|
7
|
+
:vote_up_for => [:likes, :upvotes, :up_votes],
|
|
8
|
+
:vote_down_for => [:dislikes, :downvotes, :down_votes],
|
|
9
|
+
:unvote_for => [:unlike, :undislike],
|
|
10
|
+
:voted_on? => [:voted_for?],
|
|
11
|
+
:voted_up_on? => [:voted_up_for?, :liked?],
|
|
12
|
+
:voted_down_on? => [:voted_down_for?, :disliked?],
|
|
13
|
+
:voted_as_when_voting_on => [:voted_as_when_voted_on, :voted_as_when_voting_for, :voted_as_when_voted_for],
|
|
14
|
+
:find_up_voted_items => [:find_liked_items],
|
|
15
|
+
:find_down_voted_items => [:find_disliked_items]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
base.class_eval do
|
|
19
|
+
has_many :votes, :class_name => 'YeshuaCrm::ActsAsVotable::Vote', :as => :voter, :dependent => :destroy do
|
|
20
|
+
def votables
|
|
21
|
+
includes(:votable).map(&:votable)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
aliases.each do |method, links|
|
|
26
|
+
links.each do |new_method|
|
|
27
|
+
alias_method(new_method, method)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# voting
|
|
34
|
+
def vote(args)
|
|
35
|
+
args[:votable].vote_by args.merge(:voter => self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def vote_up_for(model = nil, args = {})
|
|
39
|
+
vote :votable => model, :vote_scope => args[:vote_scope], :vote => true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def vote_down_for(model = nil, args = {})
|
|
43
|
+
vote :votable => model, :vote_scope => args[:vote_scope], :vote => false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unvote_for(model, args = {})
|
|
47
|
+
model.unvote :voter => self, :vote_scope => args[:vote_scope]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# results
|
|
51
|
+
def voted_on?(votable, args = {})
|
|
52
|
+
votes = find_votes(:votable_id => votable.id, :votable_type => votable.class.base_class.name,
|
|
53
|
+
:vote_scope => args[:vote_scope])
|
|
54
|
+
!votes.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def voted_up_on?(votable, args = {})
|
|
58
|
+
votes = find_votes(:votable_id => votable.id, :votable_type => votable.class.base_class.name,
|
|
59
|
+
:vote_scope => args[:vote_scope], :vote_flag => true)
|
|
60
|
+
!votes.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def voted_down_on?(votable, args = {})
|
|
64
|
+
votes = find_votes(:votable_id => votable.id, :votable_type => votable.class.base_class.name,
|
|
65
|
+
:vote_scope => args[:vote_scope], :vote_flag => false)
|
|
66
|
+
!votes.empty?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def voted_as_when_voting_on(votable, args = {})
|
|
70
|
+
vote = find_votes(:votable_id => votable.id, :votable_type => votable.class.base_class.name,
|
|
71
|
+
:vote_scope => args[:vote_scope]).select(:vote_flag).last
|
|
72
|
+
return nil unless vote
|
|
73
|
+
vote.vote_flag
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def find_votes(extra_conditions = {})
|
|
77
|
+
votes.where(extra_conditions)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def find_up_votes(args = {})
|
|
81
|
+
find_votes :vote_flag => true, :vote_scope => args[:vote_scope]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def find_down_votes(args = {})
|
|
85
|
+
find_votes :vote_flag => false, :vote_scope => args[:vote_scope]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def find_votes_for_class(klass, extra_conditions = {})
|
|
89
|
+
find_votes extra_conditions.merge(:votable_type => klass.name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def find_up_votes_for_class(klass, args = {})
|
|
93
|
+
find_votes_for_class klass, :vote_flag => true, :vote_scope => args[:vote_scope]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def find_down_votes_for_class(klass, args = {})
|
|
97
|
+
find_votes_for_class klass, :vote_flag => false, :vote_scope => args[:vote_scope]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Including polymporphic relations for eager loading
|
|
101
|
+
def include_objects
|
|
102
|
+
YeshuaCrm::ActsAsVotable::Vote.includes(:votable)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def find_voted_items(extra_conditions = {})
|
|
106
|
+
options = extra_conditions.merge :voter_id => id, :voter_type => self.class.base_class.name
|
|
107
|
+
include_objects.where(options).collect(&:votable)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def find_up_voted_items(extra_conditions = {})
|
|
111
|
+
find_voted_items extra_conditions.merge(:vote_flag => true)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def find_down_voted_items(extra_conditions = {})
|
|
115
|
+
find_voted_items extra_conditions.merge(:vote_flag => false)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def get_voted(klass, extra_conditions = {})
|
|
119
|
+
klass.joins(:votes_for).merge find_votes(extra_conditions)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def get_up_voted(klass)
|
|
123
|
+
klass.joins(:votes_for).merge find_up_votes
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_down_voted(klass)
|
|
127
|
+
klass.joins(:votes_for).merge find_down_votes
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module YeshuaCrm
|
|
2
|
+
class AssetsManager
|
|
3
|
+
def self.install_assets
|
|
4
|
+
return unless Gem.loaded_specs['yeshua_crm']
|
|
5
|
+
source = File.join(Gem.loaded_specs['yeshua_crm'].full_gem_path, 'vendor', 'assets')
|
|
6
|
+
destination = File.join(Dir.pwd, 'public', 'plugin_assets', 'yeshua_crm')
|
|
7
|
+
return unless File.directory?(source)
|
|
8
|
+
|
|
9
|
+
source_files = Dir[source + '/**/*']
|
|
10
|
+
source_dirs = source_files.select { |d| File.directory?(d) }
|
|
11
|
+
source_files -= source_dirs
|
|
12
|
+
|
|
13
|
+
unless source_files.empty?
|
|
14
|
+
base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
|
|
15
|
+
begin
|
|
16
|
+
FileUtils.mkdir_p(base_target_dir)
|
|
17
|
+
rescue Exception => e
|
|
18
|
+
raise "Could not create directory #{base_target_dir}: " + e.message
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
source_dirs.each do |dir|
|
|
23
|
+
target_dir = File.join(destination, dir.gsub(source, ''))
|
|
24
|
+
begin
|
|
25
|
+
FileUtils.mkdir_p(target_dir)
|
|
26
|
+
rescue Exception => e
|
|
27
|
+
raise "Could not create directory #{target_dir}: " + e.message
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
source_files.each do |file|
|
|
32
|
+
begin
|
|
33
|
+
target = File.join(destination, file.gsub(source, ''))
|
|
34
|
+
unless File.exist?(target) && FileUtils.identical?(file, target)
|
|
35
|
+
FileUtils.cp(file, target)
|
|
36
|
+
end
|
|
37
|
+
rescue Exception => e
|
|
38
|
+
raise "Could not copy #{file} to #{target}: " + e.message
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
module YeshuaCrm
|
|
3
|
+
class Currency
|
|
4
|
+
module Formatting
|
|
5
|
+
def self.included(base)
|
|
6
|
+
[
|
|
7
|
+
[:thousands_separator, :delimiter, ","],
|
|
8
|
+
[:decimal_mark, :separator, "."]
|
|
9
|
+
].each do |method, name, character|
|
|
10
|
+
define_i18n_method(method, name, character)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.define_i18n_method(method, name, character)
|
|
15
|
+
define_method(method) do
|
|
16
|
+
if self.class.use_i18n
|
|
17
|
+
begin
|
|
18
|
+
I18n.t name, :scope => "number.currency.format", :raise => true
|
|
19
|
+
rescue I18n::MissingTranslationData
|
|
20
|
+
I18n.t name, :scope =>"number.format", :default => (currency.send(method) || character)
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
currency.send(method) || character
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
alias_method name, method
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def format(value, currency, *rules)
|
|
30
|
+
# support for old format parameters
|
|
31
|
+
rules = normalize_formatting_rules(rules)
|
|
32
|
+
if currency
|
|
33
|
+
rules = self.localize_formatting_rules(rules, currency)
|
|
34
|
+
rules = self.translate_formatting_rules(rules, currency.code) if rules[:translate]
|
|
35
|
+
rules[:decimal_mark] = currency.decimal_mark if rules[:decimal_mark].nil?
|
|
36
|
+
rules[:decimal_places] = currency.decimal_places
|
|
37
|
+
rules[:subunit_to_unit] = currency.subunit_to_unit
|
|
38
|
+
rules[:thousands_separator] = currency.thousands_separator if rules[:thousands_separator].nil?
|
|
39
|
+
end
|
|
40
|
+
rules = Currency.default_formatting_rules.merge(rules){|key, v1, v2| v2.nil? ? v1 : v2}
|
|
41
|
+
|
|
42
|
+
# if fractional == 0
|
|
43
|
+
if rules[:display_free].respond_to?(:to_str)
|
|
44
|
+
return rules[:display_free]
|
|
45
|
+
elsif rules[:display_free]
|
|
46
|
+
return "free"
|
|
47
|
+
end
|
|
48
|
+
# end
|
|
49
|
+
|
|
50
|
+
symbol_value = currency.try(:symbol) || ""
|
|
51
|
+
|
|
52
|
+
formatted = value.abs.to_s
|
|
53
|
+
|
|
54
|
+
# if rules[:rounded_infinite_precision]
|
|
55
|
+
if currency
|
|
56
|
+
formatted.gsub!(/#{rules[:decimal_mark]}/, '.') unless '.' == rules[:decimal_mark]
|
|
57
|
+
formatted = ((BigDecimal(formatted) * currency.subunit_to_unit).round / BigDecimal(currency.subunit_to_unit.to_s)).to_s("F")
|
|
58
|
+
formatted.gsub!(/\..*/) do |decimal_part|
|
|
59
|
+
decimal_part << '0' while decimal_part.length < (currency.decimal_places + 1)
|
|
60
|
+
decimal_part
|
|
61
|
+
end
|
|
62
|
+
formatted.gsub!(/\./, rules[:decimal_mark]) unless '.' == rules[:decimal_mark]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sign = value < 0 ? '-' : ''
|
|
66
|
+
|
|
67
|
+
if rules[:no_cents] || (rules[:no_cents_if_whole] && cents % currency.subunit_to_unit == 0)
|
|
68
|
+
formatted = "#{formatted.to_i}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# thousands_separator_value = currency.thousands_separator
|
|
72
|
+
# Determine thousands_separator
|
|
73
|
+
if rules.has_key?(:thousands_separator)
|
|
74
|
+
thousands_separator_value = rules[:thousands_separator] || ''
|
|
75
|
+
end
|
|
76
|
+
decimal_mark = rules[:decimal_mark]
|
|
77
|
+
# Apply thousands_separator
|
|
78
|
+
formatted.gsub!(regexp_format(formatted, rules, decimal_mark, symbol_value),
|
|
79
|
+
"\\1#{thousands_separator_value}")
|
|
80
|
+
|
|
81
|
+
symbol_position = symbol_position_from(rules, currency) if currency
|
|
82
|
+
|
|
83
|
+
if rules[:sign_positive] == true && (value >= 0)
|
|
84
|
+
sign = '+'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if rules[:sign_before_symbol] == true
|
|
88
|
+
sign_before = sign
|
|
89
|
+
sign = ''
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if symbol_value && !symbol_value.empty?
|
|
93
|
+
symbol_value = "<span class=\"currency_symbol\">#{symbol_value}</span>" if rules[:html_wrap_symbol]
|
|
94
|
+
formatted = if symbol_position == :before
|
|
95
|
+
symbol_space = rules[:symbol_before_without_space] === false ? " " : ""
|
|
96
|
+
"#{sign_before}#{symbol_value}#{symbol_space}#{sign}#{formatted}"
|
|
97
|
+
else
|
|
98
|
+
symbol_space = rules[:symbol_after_without_space] ? "" : " "
|
|
99
|
+
"#{sign_before}#{sign}#{formatted}#{symbol_space}#{symbol_value}"
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
formatted="#{sign_before}#{sign}#{formatted}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# apply_decimal_mark_from_rules(formatted, rules)
|
|
106
|
+
|
|
107
|
+
if rules[:with_currency]
|
|
108
|
+
formatted << " "
|
|
109
|
+
formatted << '<span class="currency">' if rules[:html]
|
|
110
|
+
formatted << currency.to_s
|
|
111
|
+
formatted << '</span>' if rules[:html]
|
|
112
|
+
end
|
|
113
|
+
formatted
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def default_formatting_rules
|
|
117
|
+
{
|
|
118
|
+
:decimal_mark =>".",
|
|
119
|
+
:thousands_separator => ",",
|
|
120
|
+
:subunit_to_unit => 100
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def regexp_format(formatted, rules, decimal_mark, symbol_value)
|
|
125
|
+
regexp_decimal = Regexp.escape(decimal_mark)
|
|
126
|
+
if rules[:south_asian_number_formatting]
|
|
127
|
+
/(\d+?)(?=(\d\d)+(\d)(?:\.))/
|
|
128
|
+
else
|
|
129
|
+
# Symbols may contain decimal marks (E.g "դր.")
|
|
130
|
+
if formatted.sub(symbol_value.to_s, "") =~ /#{regexp_decimal}/
|
|
131
|
+
/(\d)(?=(?:\d{3})+(?:#{regexp_decimal}))/
|
|
132
|
+
else
|
|
133
|
+
/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def translate_formatting_rules(rules, iso_code)
|
|
139
|
+
begin
|
|
140
|
+
rules[:symbol] = I18n.t iso_code, :scope => "number.currency.symbol", :raise => true
|
|
141
|
+
rescue I18n::MissingTranslationData
|
|
142
|
+
# Do nothing
|
|
143
|
+
end
|
|
144
|
+
rules
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def localize_formatting_rules(rules, currency)
|
|
148
|
+
if currency.iso_code == "JPY" && I18n.locale == :ja
|
|
149
|
+
rules[:symbol] = "円" unless rules[:symbol] == false
|
|
150
|
+
rules[:symbol_position] = :after
|
|
151
|
+
rules[:symbol_after_without_space] = true
|
|
152
|
+
elsif currency.iso_code == "CHF"
|
|
153
|
+
rules[:symbol_before_without_space] = false
|
|
154
|
+
end
|
|
155
|
+
rules
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def symbol_value_from(rules)
|
|
159
|
+
if rules.has_key?(:symbol)
|
|
160
|
+
if rules[:symbol] === true
|
|
161
|
+
symbol
|
|
162
|
+
elsif rules[:symbol]
|
|
163
|
+
rules[:symbol]
|
|
164
|
+
else
|
|
165
|
+
""
|
|
166
|
+
end
|
|
167
|
+
elsif rules[:html]
|
|
168
|
+
currency.html_entity == '' ? currency.symbol : currency.html_entity
|
|
169
|
+
elsif rules[:disambiguate] and currency.disambiguate_symbol
|
|
170
|
+
currency.disambiguate_symbol
|
|
171
|
+
else
|
|
172
|
+
symbol
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def symbol_position_from(rules, currency)
|
|
177
|
+
if rules.has_key?(:symbol_position)
|
|
178
|
+
if [:before, :after].include?(rules[:symbol_position])
|
|
179
|
+
return rules[:symbol_position]
|
|
180
|
+
else
|
|
181
|
+
raise ArgumentError, ":symbol_position must be ':before' or ':after'"
|
|
182
|
+
end
|
|
183
|
+
elsif currency.symbol_first?
|
|
184
|
+
:before
|
|
185
|
+
else
|
|
186
|
+
:after
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Cleans up formatting rules.
|
|
193
|
+
#
|
|
194
|
+
# @param [Hash] rules
|
|
195
|
+
#
|
|
196
|
+
# @return [Hash]
|
|
197
|
+
def normalize_formatting_rules(rules)
|
|
198
|
+
if rules.size == 0
|
|
199
|
+
rules = {}
|
|
200
|
+
elsif rules.size == 1
|
|
201
|
+
rules = rules.pop
|
|
202
|
+
rules = { rules => true } if rules.is_a?(Symbol)
|
|
203
|
+
end
|
|
204
|
+
rules[:decimal_mark] = rules[:separator] || rules[:decimal_mark]
|
|
205
|
+
rules[:thousands_separator] = rules[:delimiter] || rules[:thousands_separator]
|
|
206
|
+
rules
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Applies decimal mark from rules to formatted
|
|
210
|
+
#
|
|
211
|
+
# @param [String] formatted
|
|
212
|
+
# @param [Hash] rules
|
|
213
|
+
def apply_decimal_mark_from_rules(formatted, rules)
|
|
214
|
+
if rules.has_key?(:decimal_mark) && rules[:decimal_mark]
|
|
215
|
+
# && rules[:decimal_mark] != decimal_mark
|
|
216
|
+
|
|
217
|
+
regexp_decimal = Regexp.escape(rules[:decimal_mark])
|
|
218
|
+
formatted.sub!(/(.*)(#{regexp_decimal})(.*)\Z/,
|
|
219
|
+
"\\1#{rules[:decimal_mark]}\\3")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
module YeshuaCrm
|
|
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 YeshuaCrm
|
|
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
|