redmineup 1.0.2
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/.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
|