redmineup 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/README.md +204 -0
- data/Rakefile +11 -0
- data/app/controllers/redmineup_controller.rb +26 -0
- data/app/views/redmine_crm/redmineup_calendar/_calendar.html.erb +34 -0
- data/app/views/redmineup/_money.html.erb +44 -0
- data/app/views/redmineup/settings.html.erb +10 -0
- data/bitbucket-pipelines.yml +50 -0
- data/config/currency_iso.json +2544 -0
- data/config/locales/cs.yml +13 -0
- data/config/locales/de.yml +13 -0
- data/config/locales/en.yml +13 -0
- data/config/locales/es.yml +13 -0
- data/config/locales/ru.yml +13 -0
- data/config/routes.rb +5 -0
- data/doc/CHANGELOG +14 -0
- data/doc/LICENSE.txt +339 -0
- data/lib/redmineup/acts_as_draftable/draft.rb +40 -0
- data/lib/redmineup/acts_as_draftable/up_acts_as_draftable.rb +172 -0
- data/lib/redmineup/acts_as_list/list.rb +282 -0
- data/lib/redmineup/acts_as_priceable/up_acts_as_priceable.rb +33 -0
- data/lib/redmineup/acts_as_taggable/tag.rb +81 -0
- data/lib/redmineup/acts_as_taggable/tag_list.rb +111 -0
- data/lib/redmineup/acts_as_taggable/tagging.rb +16 -0
- data/lib/redmineup/acts_as_taggable/up_acts_as_taggable.rb +357 -0
- data/lib/redmineup/acts_as_viewed/up_acts_as_viewed.rb +274 -0
- data/lib/redmineup/acts_as_votable/up_acts_as_votable.rb +80 -0
- data/lib/redmineup/acts_as_votable/up_acts_as_voter.rb +20 -0
- data/lib/redmineup/acts_as_votable/votable.rb +323 -0
- data/lib/redmineup/acts_as_votable/vote.rb +28 -0
- data/lib/redmineup/acts_as_votable/voter.rb +131 -0
- data/lib/redmineup/assets_manager.rb +43 -0
- data/lib/redmineup/colors_helper.rb +192 -0
- data/lib/redmineup/compatibility/application_controller_patch.rb +33 -0
- data/lib/redmineup/compatibility/routing_mapper_patch.rb +25 -0
- data/lib/redmineup/currency/formatting.rb +224 -0
- data/lib/redmineup/currency/heuristics.rb +151 -0
- data/lib/redmineup/currency/loader.rb +23 -0
- data/lib/redmineup/currency.rb +450 -0
- data/lib/redmineup/engine.rb +4 -0
- data/lib/redmineup/helpers/external_assets_helper.rb +20 -0
- data/lib/redmineup/helpers/form_tag_helper.rb +88 -0
- data/lib/redmineup/helpers/rup_calendar_helper.rb +146 -0
- data/lib/redmineup/helpers/tags_helper.rb +13 -0
- data/lib/redmineup/helpers/vote_helper.rb +35 -0
- data/lib/redmineup/hooks/views_layouts_hook.rb +11 -0
- data/lib/redmineup/liquid/drops/attachment_drop.rb +47 -0
- data/lib/redmineup/liquid/drops/issue_relations_drop.rb +41 -0
- data/lib/redmineup/liquid/drops/issues_drop.rb +217 -0
- data/lib/redmineup/liquid/drops/news_drop.rb +54 -0
- data/lib/redmineup/liquid/drops/projects_drop.rb +86 -0
- data/lib/redmineup/liquid/drops/time_entries_drop.rb +65 -0
- data/lib/redmineup/liquid/drops/users_drop.rb +68 -0
- data/lib/redmineup/liquid/filters/arrays.rb +254 -0
- data/lib/redmineup/liquid/filters/base.rb +249 -0
- data/lib/redmineup/liquid/filters/colors.rb +31 -0
- data/lib/redmineup/money_helper.rb +66 -0
- data/lib/redmineup/patches/liquid_patch.rb +33 -0
- data/lib/redmineup/settings/money.rb +46 -0
- data/lib/redmineup/settings.rb +53 -0
- data/lib/redmineup/version.rb +3 -0
- data/lib/redmineup.rb +108 -0
- data/redmineup.gemspec +29 -0
- data/test/acts_as_draftable/draft_test.rb +29 -0
- data/test/acts_as_draftable/rup_acts_as_draftable_test.rb +178 -0
- data/test/acts_as_taggable/rup_acts_as_taggable_test.rb +350 -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/rup_acts_as_viewed_test.rb +47 -0
- data/test/acts_as_votable/rup_acts_as_votable_test.rb +19 -0
- data/test/acts_as_votable/rup_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/database.yml +17 -0
- data/test/fixtures/attachments.yml +14 -0
- data/test/fixtures/issues.yml +24 -0
- data/test/fixtures/news.yml +8 -0
- data/test/fixtures/projects.yml +10 -0
- data/test/fixtures/taggings.yml +32 -0
- data/test/fixtures/tags.yml +11 -0
- data/test/fixtures/users.yml +9 -0
- data/test/fixtures/votable_caches.yml +2 -0
- data/test/fixtures/votables.yml +4 -0
- data/test/fixtures/voters.yml +6 -0
- data/test/liquid/drops/attachment_drop_test.rb +15 -0
- data/test/liquid/drops/issue_relations_drop_test.rb +24 -0
- data/test/liquid/drops/issues_drop_test.rb +38 -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 +67 -0
- data/test/liquid/filters/colors_filter_test.rb +33 -0
- data/test/liquid/liquid_helper.rb +34 -0
- data/test/models/attachment.rb +3 -0
- data/test/models/issue.rb +21 -0
- data/test/models/issue_relation.rb +10 -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 +144 -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/vendor/assets/images/money.png +0 -0
- data/vendor/assets/images/vcard.png +0 -0
- data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
- data/vendor/assets/javascripts/select2.js +2 -0
- data/vendor/assets/javascripts/select2_helpers.js +192 -0
- data/vendor/assets/stylesheets/money.css +96 -0
- data/vendor/assets/stylesheets/select2.css +424 -0
- metadata +295 -0
@@ -0,0 +1,192 @@
|
|
1
|
+
module Redmineup
|
2
|
+
class ColorsHelper
|
3
|
+
NAMED_COLORS = {
|
4
|
+
'aliceblue' => 'f0f8ff',
|
5
|
+
'antiquewhite' => 'faebd7',
|
6
|
+
'aqua' => '00ffff',
|
7
|
+
'aquamarine' => '7fffd4',
|
8
|
+
'azure' => 'f0ffff',
|
9
|
+
'beige' => 'f5f5dc',
|
10
|
+
'bisque' => 'ffe4c4',
|
11
|
+
'black' => '000',
|
12
|
+
'blanchedalmond' => 'ffebcd',
|
13
|
+
'blue' => '0000ff',
|
14
|
+
'blueviolet' => '8a2be2',
|
15
|
+
'brown' => 'a52a2a',
|
16
|
+
'burlywood' => 'deb887',
|
17
|
+
'burntsienna' => 'ea7e5d',
|
18
|
+
'cadetblue' => '5f9ea0',
|
19
|
+
'chartreuse' => '7fff00',
|
20
|
+
'chocolate' => 'd2691e',
|
21
|
+
'coral' => 'ff7f50',
|
22
|
+
'cornflowerblue' => '6495ed',
|
23
|
+
'cornsilk' => 'fff8dc',
|
24
|
+
'crimson' => 'dc143c',
|
25
|
+
'cyan' => '00ffff',
|
26
|
+
'darkblue' => '00008b',
|
27
|
+
'darkcyan' => '008b8b',
|
28
|
+
'darkgoldenrod' => 'b8860b',
|
29
|
+
'darkgray' => 'a9a9a9',
|
30
|
+
'darkgreen' => '006400',
|
31
|
+
'darkgrey' => 'a9a9a9',
|
32
|
+
'darkkhaki' => 'bdb76b',
|
33
|
+
'darkmagenta' => '8b008b',
|
34
|
+
'darkolivegreen' => '556b2f',
|
35
|
+
'darkorange' => 'ff8c00',
|
36
|
+
'darkorchid' => '9932cc',
|
37
|
+
'darkred' => '8b0000',
|
38
|
+
'darksalmon' => 'e9967a',
|
39
|
+
'darkseagreen' => '8fbc8f',
|
40
|
+
'darkslateblue' => '483d8b',
|
41
|
+
'darkslategray' => '2f4f4f',
|
42
|
+
'darkslategrey' => '2f4f4f',
|
43
|
+
'darkturquoise' => '00ced1',
|
44
|
+
'darkviolet' => '9400d3',
|
45
|
+
'deeppink' => 'ff1493',
|
46
|
+
'deepskyblue' => '00bfff',
|
47
|
+
'dimgray' => '696969',
|
48
|
+
'dimgrey' => '696969',
|
49
|
+
'dodgerblue' => '1e90ff',
|
50
|
+
'firebrick' => 'b22222',
|
51
|
+
'floralwhite' => 'fffaf0',
|
52
|
+
'forestgreen' => '228b22',
|
53
|
+
'fuchsia' => 'ff00ff',
|
54
|
+
'gainsboro' => 'dcdcdc',
|
55
|
+
'ghostwhite' => 'f8f8ff',
|
56
|
+
'gold' => 'ffd700',
|
57
|
+
'goldenrod' => 'daa520',
|
58
|
+
'gray' => '808080',
|
59
|
+
'green' => '008000',
|
60
|
+
'greenyellow' => 'adff2f',
|
61
|
+
'grey' => '808080',
|
62
|
+
'honeydew' => 'f0fff0',
|
63
|
+
'hotpink' => 'ff69b4',
|
64
|
+
'indianred' => 'cd5c5c',
|
65
|
+
'indigo' => '4b0082',
|
66
|
+
'ivory' => 'fffff0',
|
67
|
+
'khaki' => 'f0e68c',
|
68
|
+
'lavender' => 'e6e6fa',
|
69
|
+
'lavenderblush' => 'fff0f5',
|
70
|
+
'lawngreen' => '7cfc00',
|
71
|
+
'lemonchiffon' => 'fffacd',
|
72
|
+
'lightblue' => 'add8e6',
|
73
|
+
'lightcoral' => 'f08080',
|
74
|
+
'lightcyan' => 'e0ffff',
|
75
|
+
'lightgoldenrodyellow' => 'fafad2',
|
76
|
+
'lightgray' => 'd3d3d3',
|
77
|
+
'lightgreen' => '90ee90',
|
78
|
+
'lightgrey' => 'd3d3d3',
|
79
|
+
'lightpink' => 'ffb6c1',
|
80
|
+
'lightsalmon' => 'ffa07a',
|
81
|
+
'lightseagreen' => '20b2aa',
|
82
|
+
'lightskyblue' => '87cefa',
|
83
|
+
'lightslategray' => '789',
|
84
|
+
'lightslategrey' => '789',
|
85
|
+
'lightsteelblue' => 'b0c4de',
|
86
|
+
'lightyellow' => 'ffffe0',
|
87
|
+
'lime' => '00ff00',
|
88
|
+
'limegreen' => '32cd32',
|
89
|
+
'linen' => 'faf0e6',
|
90
|
+
'magenta' => 'ff00ff',
|
91
|
+
'maroon' => '800000',
|
92
|
+
'mediumaquamarine' => '66cdaa',
|
93
|
+
'mediumblue' => '0000cd',
|
94
|
+
'mediumorchid' => 'ba55d3',
|
95
|
+
'mediumpurple' => '9370db',
|
96
|
+
'mediumseagreen' => '3cb371',
|
97
|
+
'mediumslateblue' => '7b68ee',
|
98
|
+
'mediumspringgreen' => '00fa9a',
|
99
|
+
'mediumturquoise' => '48d1cc',
|
100
|
+
'mediumvioletred' => 'c71585',
|
101
|
+
'midnightblue' => '191970',
|
102
|
+
'mintcream' => 'f5fffa',
|
103
|
+
'mistyrose' => 'ffe4e1',
|
104
|
+
'moccasin' => 'ffe4b5',
|
105
|
+
'navajowhite' => 'ffdead',
|
106
|
+
'navy' => '000080',
|
107
|
+
'oldlace' => 'fdf5e6',
|
108
|
+
'olive' => '808000',
|
109
|
+
'olivedrab' => '6b8e23',
|
110
|
+
'orange' => 'ffa500',
|
111
|
+
'orangered' => 'ff4500',
|
112
|
+
'orchid' => 'da70d6',
|
113
|
+
'palegoldenrod' => 'eee8aa',
|
114
|
+
'palegreen' => '98fb98',
|
115
|
+
'paleturquoise' => 'afeeee',
|
116
|
+
'palevioletred' => 'db7093',
|
117
|
+
'papayawhip' => 'ffefd5',
|
118
|
+
'peachpuff' => 'ffdab9',
|
119
|
+
'peru' => 'cd853f',
|
120
|
+
'pink' => 'ffc0cb',
|
121
|
+
'plum' => 'dda0dd',
|
122
|
+
'powderblue' => 'b0e0e6',
|
123
|
+
'purple' => '800080',
|
124
|
+
'rebeccapurple' => '663399',
|
125
|
+
'red' => 'ff0000',
|
126
|
+
'rosybrown' => 'bc8f8f',
|
127
|
+
'royalblue' => '4169e1',
|
128
|
+
'saddlebrown' => '8b4513',
|
129
|
+
'salmon' => 'fa8072',
|
130
|
+
'sandybrown' => 'f4a460',
|
131
|
+
'seagreen' => '2e8b57',
|
132
|
+
'seashell' => 'fff5ee',
|
133
|
+
'sienna' => 'a0522d',
|
134
|
+
'silver' => 'c0c0c0',
|
135
|
+
'skyblue' => '87ceeb',
|
136
|
+
'slateblue' => '6a5acd',
|
137
|
+
'slategray' => '708090',
|
138
|
+
'slategrey' => '708090',
|
139
|
+
'snow' => 'fffafa',
|
140
|
+
'springgreen' => '00ff7f',
|
141
|
+
'steelblue' => '4682b4',
|
142
|
+
'tan' => 'd2b48c',
|
143
|
+
'teal' => '008080',
|
144
|
+
'thistle' => 'd8bfd8',
|
145
|
+
'tomato' => 'ff6347',
|
146
|
+
'turquoise' => '40e0d0',
|
147
|
+
'violet' => 'ee82ee',
|
148
|
+
'wheat' => 'f5deb3',
|
149
|
+
'white' => 'fff',
|
150
|
+
'whitesmoke' => 'f5f5f5',
|
151
|
+
'yellow' => 'ffff00',
|
152
|
+
'yellowgreen' => '9acd32'
|
153
|
+
}
|
154
|
+
|
155
|
+
class << self
|
156
|
+
# Amount should be a decimal between 0 and 1. Lower means darker
|
157
|
+
|
158
|
+
def darken_color(input_color, amount=0.4)
|
159
|
+
hex_color = hex_color(input_color).gsub('#','')
|
160
|
+
rgb = hex_color.scan(/../).map {|color| color.hex}
|
161
|
+
rgb[0] = (rgb[0].to_i * amount).round
|
162
|
+
rgb[1] = (rgb[1].to_i * amount).round
|
163
|
+
rgb[2] = (rgb[2].to_i * amount).round
|
164
|
+
"#%02x%02x%02x" % rgb
|
165
|
+
end
|
166
|
+
|
167
|
+
# Amount should be a decimal between 0 and 1. Higher lightgreen lighter
|
168
|
+
def lighten_color(input_color, amount=0.6)
|
169
|
+
hex_color = hex_color(input_color).gsub('#','')
|
170
|
+
rgb = hex_color.scan(/../).map {|color| color.hex}
|
171
|
+
rgb[0] = [(rgb[0].to_i + 255 * amount).round, 255].min
|
172
|
+
rgb[1] = [(rgb[1].to_i + 255 * amount).round, 255].min
|
173
|
+
rgb[2] = [(rgb[2].to_i + 255 * amount).round, 255].min
|
174
|
+
"#%02x%02x%02x" % rgb
|
175
|
+
end
|
176
|
+
|
177
|
+
def contrasting_text_color(input_color)
|
178
|
+
color = hex_color(input_color).gsub('#','')
|
179
|
+
convert_to_brightness_value(color) > 382.5 ? darken_color(color) : lighten_color(color)
|
180
|
+
end
|
181
|
+
|
182
|
+
def hex_color(input_color)
|
183
|
+
"##{NAMED_COLORS[input_color] || input_color}"
|
184
|
+
end
|
185
|
+
|
186
|
+
def convert_to_brightness_value(input_color)
|
187
|
+
(hex_color(input_color).scan(/../).map {|color| color.hex}).sum
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Redmineup
|
2
|
+
module Patches
|
3
|
+
module ApplicationControllerPatch
|
4
|
+
def self.included(base) # :nodoc:
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.class_eval do
|
7
|
+
unloadable # Send unloadable so it will not be unloaded in development
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def before_action(*filters, &block)
|
13
|
+
before_filter(*filters, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def after_action(*filters, &block)
|
17
|
+
after_filter(*filters, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def skip_before_action(*filters, &block)
|
21
|
+
skip_before_filter(*filters, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
unless ActionController::Base.methods.include?(:before_action)
|
29
|
+
ActionController::Base.send(
|
30
|
+
:include,
|
31
|
+
Redmineup::Patches::ApplicationControllerPatch
|
32
|
+
)
|
33
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Redmineup
|
2
|
+
module Patches
|
3
|
+
module RoutingMapperPatch
|
4
|
+
def self.included(base)
|
5
|
+
base.send(:include, InstanceMethods)
|
6
|
+
|
7
|
+
base.class_eval do
|
8
|
+
alias_method :constraints_without_redmineup, :constraints
|
9
|
+
alias_method :constraints, :constraints_with_redmineup
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
def constraints_with_redmineup(options = {}, &block)
|
15
|
+
options[:object_type] = /.+/ if options[:object_type] && options[:object_type].is_a?(Regexp)
|
16
|
+
constraints_without_redmineup(options, &block)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
unless ActionDispatch::Routing::Mapper.included_modules.include?(Redmineup::Patches::RoutingMapperPatch)
|
24
|
+
ActionDispatch::Routing::Mapper.send(:include, Redmineup::Patches::RoutingMapperPatch)
|
25
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Redmineup
|
3
|
+
class Currency
|
4
|
+
module Formatting
|
5
|
+
def self.included(base)
|
6
|
+
[
|
7
|
+
[:thousands_separator, :delimiter, Redmineup::Settings::Money.thousands_delimiter],
|
8
|
+
[:decimal_mark, :separator, Redmineup::Settings::Money.decimal_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: Redmineup::Settings::Money.decimal_separator || '.',
|
119
|
+
thousands_separator: Redmineup::Settings::Money.thousands_delimiter || ',',
|
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 Redmineup
|
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,23 @@
|
|
1
|
+
module Redmineup
|
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
|
7
|
+
# in the config directory.
|
8
|
+
#
|
9
|
+
# @return [Hash]
|
10
|
+
def load_currencies
|
11
|
+
parse_currency_file('currency_iso.json')
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parse_currency_file(filename)
|
17
|
+
json = File.read("#{DATA_PATH}/#{filename}")
|
18
|
+
json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
|
19
|
+
JSON.parse(json, symbolize_names: true)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|