yeshoua_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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +11 -0
  3. data/lib/yeshoua_crm.rb +87 -0
  4. data/lib/yeshoua_crm/acts_as_draftable/draft.rb +40 -0
  5. data/lib/yeshoua_crm/acts_as_draftable/rcrm_acts_as_draftable.rb +154 -0
  6. data/lib/yeshoua_crm/acts_as_list/list.rb +282 -0
  7. data/lib/yeshoua_crm/acts_as_taggable/rcrm_acts_as_taggable.rb +350 -0
  8. data/lib/yeshoua_crm/acts_as_taggable/tag.rb +81 -0
  9. data/lib/yeshoua_crm/acts_as_taggable/tag_list.rb +111 -0
  10. data/lib/yeshoua_crm/acts_as_taggable/tagging.rb +16 -0
  11. data/lib/yeshoua_crm/acts_as_viewed/rcrm_acts_as_viewed.rb +274 -0
  12. data/lib/yeshoua_crm/acts_as_votable/rcrm_acts_as_votable.rb +80 -0
  13. data/lib/yeshoua_crm/acts_as_votable/rcrm_acts_as_voter.rb +20 -0
  14. data/lib/yeshoua_crm/acts_as_votable/votable.rb +323 -0
  15. data/lib/yeshoua_crm/acts_as_votable/vote.rb +28 -0
  16. data/lib/yeshoua_crm/acts_as_votable/voter.rb +131 -0
  17. data/lib/yeshoua_crm/assets_manager.rb +43 -0
  18. data/lib/yeshoua_crm/currency.rb +439 -0
  19. data/lib/yeshoua_crm/currency/formatting.rb +224 -0
  20. data/lib/yeshoua_crm/currency/heuristics.rb +151 -0
  21. data/lib/yeshoua_crm/currency/loader.rb +24 -0
  22. data/lib/yeshoua_crm/helpers/external_assets_helper.rb +17 -0
  23. data/lib/yeshoua_crm/helpers/form_tag_helper.rb +123 -0
  24. data/lib/yeshoua_crm/helpers/tags_helper.rb +13 -0
  25. data/lib/yeshoua_crm/helpers/vote_helper.rb +35 -0
  26. data/lib/yeshoua_crm/liquid/drops/cells_drop.rb +86 -0
  27. data/lib/yeshoua_crm/liquid/drops/issues_drop.rb +66 -0
  28. data/lib/yeshoua_crm/liquid/drops/news_drop.rb +54 -0
  29. data/lib/yeshoua_crm/liquid/drops/users_drop.rb +72 -0
  30. data/lib/yeshoua_crm/liquid/filters/arrays.rb +177 -0
  31. data/lib/yeshoua_crm/liquid/filters/base.rb +208 -0
  32. data/lib/yeshoua_crm/money_helper.rb +65 -0
  33. data/lib/yeshoua_crm/version.rb +3 -0
  34. data/test/acts_as_draftable/draft_test.rb +29 -0
  35. data/test/acts_as_draftable/rcrm_acts_as_draftable_test.rb +185 -0
  36. data/test/acts_as_taggable/rcrm_acts_as_taggable_test.rb +345 -0
  37. data/test/acts_as_taggable/tag_list_test.rb +34 -0
  38. data/test/acts_as_taggable/tag_test.rb +72 -0
  39. data/test/acts_as_taggable/tagging_test.rb +15 -0
  40. data/test/acts_as_viewed/rcrm_acts_as_viewed_test.rb +47 -0
  41. data/test/acts_as_votable/rcrm_acts_as_votable_test.rb +19 -0
  42. data/test/acts_as_votable/rcrm_acts_as_voter_test.rb +14 -0
  43. data/test/acts_as_votable/votable_test.rb +507 -0
  44. data/test/acts_as_votable/voter_test.rb +296 -0
  45. data/test/currency_test.rb +292 -0
  46. data/test/liquid/drops/issues_drop_test.rb +34 -0
  47. data/test/liquid/drops/news_drop_test.rb +38 -0
  48. data/test/liquid/drops/projects_drop_test.rb +44 -0
  49. data/test/liquid/drops/uses_drop_test.rb +36 -0
  50. data/test/liquid/filters/arrays_filter_test.rb +24 -0
  51. data/test/liquid/filters/base_filter_test.rb +63 -0
  52. data/test/liquid/liquid_helper.rb +32 -0
  53. data/test/models/issue.rb +14 -0
  54. data/test/models/news.rb +3 -0
  55. data/test/models/project.rb +8 -0
  56. data/test/models/user.rb +11 -0
  57. data/test/models/vote_classes.rb +33 -0
  58. data/test/money_helper_test.rb +12 -0
  59. data/test/schema.rb +121 -0
  60. data/test/tags_helper_test.rb +29 -0
  61. data/test/test_helper.rb +66 -0
  62. data/test/vote_helper_test.rb +28 -0
  63. data/yeshoua_crm.gemspec +28 -0
  64. metadata +206 -0
@@ -0,0 +1,224 @@
1
+ # encoding: UTF-8
2
+ module YeshouaCrm
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 YeshouaCrm
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 YeshouaCrm
2
+ class Currency
3
+ module Loader
4
+ DATA_PATH = File.expand_path("../../../../config", __FILE__)
5
+
6
+ # Loads and returns the currencies stored in JSON files in the config directory.
7
+ #
8
+ # @return [Hash]
9
+ def load_currencies
10
+ currencies = parse_currency_file("currency_iso.json")
11
+ # currencies.merge! parse_currency_file("currency_non_iso.json")
12
+ # currencies.merge! parse_currency_file("currency_backwards_compatible.json")
13
+ end
14
+
15
+ private
16
+
17
+ def parse_currency_file(filename)
18
+ json = File.read("#{DATA_PATH}/#{filename}")
19
+ json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
20
+ JSON.parse(json, :symbolize_names => true)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module YeshouaCrm
2
+ module ExternalAssetsHelper
3
+ include ActionView::Helpers::JavaScriptHelper
4
+
5
+ def select2_assets
6
+ return if @select2_tag_included
7
+ @select2_tag_included = true
8
+ javascript_include_tag('select2', :plugin => 'yeshoua_crm') + stylesheet_link_tag('select2', :plugin => 'yeshoua_crm')
9
+ end
10
+
11
+ def chartjs_assets
12
+ return if @chartjs_tag_included
13
+ @chartjs_tag_included = true
14
+ javascript_include_tag('Chart.bundle.min', :plugin => 'yeshoua_crm')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,123 @@
1
+ module YeshouaCrm
2
+ module FormTagHelper
3
+ # Allows include select2 into your views.
4
+ #
5
+ # ==== Examples
6
+ # select2_tag 'city_id', '<option value="1">Lisbon</option>...'
7
+ # select2_tag 'city_id', options_for_select(...)
8
+ # select2_tag 'tag_list', nil, :multiple => true, :data => [{ id: 0, text: 'deal' }, ...], :tags => true, :include_hidden => false %>
9
+ # select2_tag 'tag_list', options_for_select(...), :multiple => true, :style => 'width: 100%;', :url => '/tags', :placeholder => '+ add tag', :tags => true %>
10
+ #
11
+ # You may use select_tag options and additional options.
12
+ #
13
+ # ==== Additional options
14
+ # * <tt>:url</tt> Allows searches for remote data using the ajax.
15
+ # * <tt>:data</tt> Load dropdown options from a local array if +url+ option not set.
16
+ # * <tt>:placeholder</tt> Supports displaying a placeholder value.
17
+ # * <tt>:include_hidden</tt> Adds hidden field after select when +multiple+ option true. Default value true.
18
+ #
19
+ # <b>Note:</b> The HTML specification says when +multiple+ parameter passed to select and all options got deselected
20
+ # web browsers do not send any value to server.
21
+ #
22
+ # In case if you don't want the helper to generate this hidden field you can specify
23
+ # <tt>include_hidden: false</tt> option.
24
+ #
25
+ # Also aliased as: select2
26
+ #
27
+ # select2 'city_id', options_for_select(...)
28
+ #
29
+ def select2_tag(name, option_tags = nil, options = {})
30
+ id = sanitize_to_id(name)
31
+ placeholder = options[:placeholder] || 'Select ...'
32
+
33
+ content_for(:header_tags) { select2_assets }
34
+ result = select_tag(name, option_tags, options)
35
+ if options[:multiple] && options.fetch(:include_hidden, true)
36
+ result << hidden_field_tag("#{name}[]", '')
37
+ end
38
+
39
+ result << javascript_tag(<<-JS)
40
+ $(function () {
41
+ $('select#' + '#{id}').select2({
42
+ #{select2_data_source_options(options)},
43
+ #{select2_tags_options(options)},
44
+ placeholder: '#{placeholder}'
45
+ });
46
+ });
47
+ JS
48
+ end
49
+
50
+ alias select2 select2_tag
51
+
52
+ # Transforms select filter field into select2
53
+ #
54
+ # ==== Examples
55
+ # transform_to_select2 'issue_tags', url: auto_complete_tags_url
56
+ # transform_to_select2 'manager_id', format_state: 'formatStateWithAvatar', min_input_length: 1, url: '/managers'
57
+ #
58
+ # ==== Options
59
+ # * <tt>:url</tt> Defines URL to search remote data using the ajax.
60
+ # * <tt>:format_state</tt> Defines template of search results in the drop-down.
61
+ # * <tt>:min_input_length</tt> Minimum number of characters required to start a search. Default value 0.
62
+ # * <tt>:width</tt> Sets the width of the control. Default value '60%'.
63
+ #
64
+ def transform_to_select2(field, options = {})
65
+ return if field.empty?
66
+
67
+ result = ''.html_safe
68
+ unless @transform_to_select2_included
69
+ result << javascript_include_tag('select2_helpers', plugin: 'yeshoua_crm')
70
+ @transform_to_select2_included = true
71
+ end
72
+
73
+ result << javascript_tag(<<-JS)
74
+ select2Filters['#{field}'] = {
75
+ url: '#{options[:url].to_s}',
76
+ formatState: #{options.fetch(:format_state, 'undefined')},
77
+ minimumInputLength: #{options.fetch(:min_input_length, 0)},
78
+ width: '#{options.fetch(:with, '60%')}'
79
+ };
80
+ JS
81
+ end
82
+
83
+ private
84
+
85
+ def select2_data_source_options(options = {})
86
+ if options[:url].to_s.empty?
87
+ "data: #{options.fetch(:data, []).to_json}"
88
+ else
89
+ "ajax: {
90
+ url: '#{options[:url]}',
91
+ dataType: 'json',
92
+ delay: 250,
93
+ data: function (params) {
94
+ return { q: params.term };
95
+ },
96
+ processResults: function (data, params) {
97
+ return { results: data };
98
+ },
99
+ cache: true
100
+ }"
101
+ end
102
+ end
103
+
104
+ def select2_tags_options(options = {})
105
+ if options.fetch(:tags, false)
106
+ "tags: true,
107
+ tokenSeparators: [','],
108
+ createTag: function (params) {
109
+ var term = $.trim(params.term);
110
+ if (term === '' || term.indexOf(',') > -1) {
111
+ return null; // Return null to disable tag creation
112
+ }
113
+ return {
114
+ id: term,
115
+ text: term
116
+ }
117
+ }"
118
+ else
119
+ 'tags: false'
120
+ end
121
+ end
122
+ end
123
+ end