redmineup 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/Gemfile +4 -0
  4. data/README.md +204 -0
  5. data/Rakefile +11 -0
  6. data/app/controllers/redmineup_controller.rb +26 -0
  7. data/app/views/redmine_crm/redmineup_calendar/_calendar.html.erb +34 -0
  8. data/app/views/redmineup/_money.html.erb +44 -0
  9. data/app/views/redmineup/settings.html.erb +10 -0
  10. data/bitbucket-pipelines.yml +50 -0
  11. data/config/currency_iso.json +2544 -0
  12. data/config/locales/cs.yml +13 -0
  13. data/config/locales/de.yml +13 -0
  14. data/config/locales/en.yml +13 -0
  15. data/config/locales/es.yml +13 -0
  16. data/config/locales/ru.yml +13 -0
  17. data/config/routes.rb +5 -0
  18. data/doc/CHANGELOG +14 -0
  19. data/doc/LICENSE.txt +339 -0
  20. data/lib/redmineup/acts_as_draftable/draft.rb +40 -0
  21. data/lib/redmineup/acts_as_draftable/up_acts_as_draftable.rb +172 -0
  22. data/lib/redmineup/acts_as_list/list.rb +282 -0
  23. data/lib/redmineup/acts_as_priceable/up_acts_as_priceable.rb +33 -0
  24. data/lib/redmineup/acts_as_taggable/tag.rb +81 -0
  25. data/lib/redmineup/acts_as_taggable/tag_list.rb +111 -0
  26. data/lib/redmineup/acts_as_taggable/tagging.rb +16 -0
  27. data/lib/redmineup/acts_as_taggable/up_acts_as_taggable.rb +357 -0
  28. data/lib/redmineup/acts_as_viewed/up_acts_as_viewed.rb +274 -0
  29. data/lib/redmineup/acts_as_votable/up_acts_as_votable.rb +80 -0
  30. data/lib/redmineup/acts_as_votable/up_acts_as_voter.rb +20 -0
  31. data/lib/redmineup/acts_as_votable/votable.rb +323 -0
  32. data/lib/redmineup/acts_as_votable/vote.rb +28 -0
  33. data/lib/redmineup/acts_as_votable/voter.rb +131 -0
  34. data/lib/redmineup/assets_manager.rb +43 -0
  35. data/lib/redmineup/colors_helper.rb +192 -0
  36. data/lib/redmineup/compatibility/application_controller_patch.rb +33 -0
  37. data/lib/redmineup/compatibility/routing_mapper_patch.rb +25 -0
  38. data/lib/redmineup/currency/formatting.rb +224 -0
  39. data/lib/redmineup/currency/heuristics.rb +151 -0
  40. data/lib/redmineup/currency/loader.rb +23 -0
  41. data/lib/redmineup/currency.rb +450 -0
  42. data/lib/redmineup/engine.rb +4 -0
  43. data/lib/redmineup/helpers/external_assets_helper.rb +20 -0
  44. data/lib/redmineup/helpers/form_tag_helper.rb +88 -0
  45. data/lib/redmineup/helpers/rup_calendar_helper.rb +146 -0
  46. data/lib/redmineup/helpers/tags_helper.rb +13 -0
  47. data/lib/redmineup/helpers/vote_helper.rb +35 -0
  48. data/lib/redmineup/hooks/views_layouts_hook.rb +11 -0
  49. data/lib/redmineup/liquid/drops/attachment_drop.rb +47 -0
  50. data/lib/redmineup/liquid/drops/issue_relations_drop.rb +41 -0
  51. data/lib/redmineup/liquid/drops/issues_drop.rb +217 -0
  52. data/lib/redmineup/liquid/drops/news_drop.rb +54 -0
  53. data/lib/redmineup/liquid/drops/projects_drop.rb +86 -0
  54. data/lib/redmineup/liquid/drops/time_entries_drop.rb +65 -0
  55. data/lib/redmineup/liquid/drops/users_drop.rb +68 -0
  56. data/lib/redmineup/liquid/filters/arrays.rb +254 -0
  57. data/lib/redmineup/liquid/filters/base.rb +249 -0
  58. data/lib/redmineup/liquid/filters/colors.rb +31 -0
  59. data/lib/redmineup/money_helper.rb +66 -0
  60. data/lib/redmineup/patches/liquid_patch.rb +33 -0
  61. data/lib/redmineup/settings/money.rb +46 -0
  62. data/lib/redmineup/settings.rb +53 -0
  63. data/lib/redmineup/version.rb +3 -0
  64. data/lib/redmineup.rb +108 -0
  65. data/redmineup.gemspec +29 -0
  66. data/test/acts_as_draftable/draft_test.rb +29 -0
  67. data/test/acts_as_draftable/rup_acts_as_draftable_test.rb +178 -0
  68. data/test/acts_as_taggable/rup_acts_as_taggable_test.rb +350 -0
  69. data/test/acts_as_taggable/tag_list_test.rb +34 -0
  70. data/test/acts_as_taggable/tag_test.rb +72 -0
  71. data/test/acts_as_taggable/tagging_test.rb +15 -0
  72. data/test/acts_as_viewed/rup_acts_as_viewed_test.rb +47 -0
  73. data/test/acts_as_votable/rup_acts_as_votable_test.rb +19 -0
  74. data/test/acts_as_votable/rup_acts_as_voter_test.rb +14 -0
  75. data/test/acts_as_votable/votable_test.rb +507 -0
  76. data/test/acts_as_votable/voter_test.rb +296 -0
  77. data/test/currency_test.rb +292 -0
  78. data/test/database.yml +17 -0
  79. data/test/fixtures/attachments.yml +14 -0
  80. data/test/fixtures/issues.yml +24 -0
  81. data/test/fixtures/news.yml +8 -0
  82. data/test/fixtures/projects.yml +10 -0
  83. data/test/fixtures/taggings.yml +32 -0
  84. data/test/fixtures/tags.yml +11 -0
  85. data/test/fixtures/users.yml +9 -0
  86. data/test/fixtures/votable_caches.yml +2 -0
  87. data/test/fixtures/votables.yml +4 -0
  88. data/test/fixtures/voters.yml +6 -0
  89. data/test/liquid/drops/attachment_drop_test.rb +15 -0
  90. data/test/liquid/drops/issue_relations_drop_test.rb +24 -0
  91. data/test/liquid/drops/issues_drop_test.rb +38 -0
  92. data/test/liquid/drops/news_drop_test.rb +38 -0
  93. data/test/liquid/drops/projects_drop_test.rb +44 -0
  94. data/test/liquid/drops/uses_drop_test.rb +36 -0
  95. data/test/liquid/filters/arrays_filter_test.rb +31 -0
  96. data/test/liquid/filters/base_filter_test.rb +67 -0
  97. data/test/liquid/filters/colors_filter_test.rb +33 -0
  98. data/test/liquid/liquid_helper.rb +34 -0
  99. data/test/models/attachment.rb +3 -0
  100. data/test/models/issue.rb +21 -0
  101. data/test/models/issue_relation.rb +10 -0
  102. data/test/models/news.rb +3 -0
  103. data/test/models/project.rb +8 -0
  104. data/test/models/user.rb +11 -0
  105. data/test/models/vote_classes.rb +33 -0
  106. data/test/money_helper_test.rb +12 -0
  107. data/test/schema.rb +144 -0
  108. data/test/tags_helper_test.rb +29 -0
  109. data/test/test_helper.rb +66 -0
  110. data/test/vote_helper_test.rb +28 -0
  111. data/vendor/assets/images/money.png +0 -0
  112. data/vendor/assets/images/vcard.png +0 -0
  113. data/vendor/assets/javascripts/Chart.bundle.min.js +16 -0
  114. data/vendor/assets/javascripts/select2.js +2 -0
  115. data/vendor/assets/javascripts/select2_helpers.js +192 -0
  116. data/vendor/assets/stylesheets/money.css +96 -0
  117. data/vendor/assets/stylesheets/select2.css +424 -0
  118. 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