agilibox 1.0.0 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +321 -2
  3. data/Rakefile +4 -0
  4. data/app/assets/javascripts/agilibox/autocomplete.coffee +2 -0
  5. data/app/assets/javascripts/agilibox/checkboxes_dropdown.coffee +9 -0
  6. data/app/assets/javascripts/agilibox/modals.coffee +81 -55
  7. data/app/assets/stylesheets/agilibox/actions.sass +2 -0
  8. data/app/assets/stylesheets/agilibox/all.sass +5 -0
  9. data/app/assets/stylesheets/agilibox/buttons.sass +11 -0
  10. data/app/assets/stylesheets/agilibox/filters.sass +13 -3
  11. data/app/assets/stylesheets/agilibox/forms.sass +49 -0
  12. data/app/assets/stylesheets/agilibox/icons.sass +5 -0
  13. data/app/assets/stylesheets/agilibox/modals.sass +15 -7
  14. data/app/assets/stylesheets/agilibox/pagination.sass +7 -0
  15. data/app/assets/stylesheets/agilibox/print.sass +10 -0
  16. data/app/assets/stylesheets/agilibox/tables.sass +26 -0
  17. data/app/controllers/agilibox/application_controller.rb +2 -4
  18. data/app/controllers/agilibox/small_data/filters_controller.rb +8 -6
  19. data/app/controllers/concerns/agilibox/api_controller_concern.rb +69 -0
  20. data/app/controllers/concerns/agilibox/back_url_concern.rb +13 -5
  21. data/app/emails/agilibox/email.rb +113 -0
  22. data/app/filters/agilibox/small_data/filter.rb +19 -1
  23. data/app/filters/agilibox/small_data/filter_strategy.rb +1 -1
  24. data/app/filters/agilibox/small_data/filter_strategy_by_date_begin.rb +3 -2
  25. data/app/filters/agilibox/small_data/filter_strategy_by_date_end.rb +3 -2
  26. data/app/filters/agilibox/small_data/filter_strategy_by_date_or_datetime_period.rb +56 -0
  27. data/app/filters/agilibox/small_data/filter_strategy_by_date_period.rb +5 -0
  28. data/app/filters/agilibox/small_data/filter_strategy_by_datetime_period.rb +5 -0
  29. data/app/filters/agilibox/small_data/filter_strategy_by_key_value.rb +14 -3
  30. data/app/filters/agilibox/small_data/filter_strategy_by_key_values.rb +13 -0
  31. data/app/filters/agilibox/small_data/filter_strategy_by_tags.rb +1 -1
  32. data/app/forms/agilibox/mini_form_object.rb +29 -0
  33. data/app/helpers/agilibox/all_helpers.rb +2 -0
  34. data/app/helpers/agilibox/bootstrap_helper.rb +62 -3
  35. data/app/helpers/agilibox/button_helper.rb +22 -13
  36. data/app/helpers/agilibox/filters_helper.rb +7 -8
  37. data/app/helpers/agilibox/font_awesome_helper.rb +67 -0
  38. data/app/helpers/agilibox/form_helper.rb +37 -14
  39. data/app/helpers/agilibox/i18n_helper.rb +5 -0
  40. data/app/helpers/agilibox/link_helper.rb +1 -1
  41. data/app/helpers/agilibox/pagination_helper.rb +18 -2
  42. data/app/helpers/agilibox/routes_helper.rb +6 -2
  43. data/app/helpers/agilibox/sorting_helper.rb +9 -4
  44. data/app/helpers/agilibox/text_helper.rb +51 -26
  45. data/app/helpers/h.rb +3 -0
  46. data/app/jobs/concerns/agilibox/setup_job_concern.rb +35 -0
  47. data/app/libs/agilibox/collection_update.rb +32 -0
  48. data/app/libs/agilibox/fcm/notifier.rb +28 -0
  49. data/app/libs/agilibox/fcm/request.rb +48 -0
  50. data/app/libs/agilibox/initialize_with.rb +20 -0
  51. data/app/libs/agilibox/mini_model_serializer/serialize.rb +30 -0
  52. data/app/libs/agilibox/mini_model_serializer/serializer.rb +23 -0
  53. data/app/libs/agilibox/monkey.rb +39 -0
  54. data/app/libs/agilibox/phone_number_sanitizer.rb +15 -0
  55. data/app/libs/agilibox/sortable_uuid_generator.rb +1 -1
  56. data/app/libs/agilibox/token_generator.rb +37 -0
  57. data/app/mailers/agilibox/application_mailer.rb +2 -0
  58. data/app/mailers/agilibox/generic_mailer.rb +9 -0
  59. data/app/models/concerns/agilibox/active_record_uuid_concern.rb +4 -2
  60. data/app/models/concerns/agilibox/default_values_concern.rb +4 -3
  61. data/app/models/concerns/agilibox/model_i18n.rb +37 -6
  62. data/app/models/concerns/agilibox/pluck_distinct.rb +13 -0
  63. data/app/models/concerns/agilibox/pluck_to_hash.rb +9 -0
  64. data/app/models/concerns/agilibox/polymorphic_id.rb +5 -2
  65. data/app/models/concerns/agilibox/search.rb +11 -13
  66. data/app/models/concerns/agilibox/timestamp_helpers.rb +17 -0
  67. data/app/serializers/agilibox/serializers/base.rb +47 -1
  68. data/app/serializers/agilibox/serializers/xlsx.rb +25 -23
  69. data/app/services/agilibox/service.rb +17 -0
  70. data/app/sms/agilibox/sms/application_sms.rb +42 -0
  71. data/app/sms/agilibox/sms/message.rb +25 -0
  72. data/app/sms/agilibox/sms/strategies/amazon_sns.rb +48 -0
  73. data/app/sms/agilibox/sms/strategies/base.rb +15 -0
  74. data/app/sms/agilibox/sms/strategies/test.rb +10 -0
  75. data/app/sms/agilibox/sms.rb +35 -0
  76. data/app/sorters/agilibox/sorter.rb +26 -0
  77. data/app/views/agilibox/_flash.html.slim +9 -0
  78. data/app/views/agilibox/forms/_checkboxes_dropdown.html.slim +14 -0
  79. data/app/views/agilibox/search/_form.html.slim +1 -6
  80. data/app/views/agilibox/search/_form_bs3.html.slim +13 -0
  81. data/app/views/agilibox/search/_form_bs4.html.slim +9 -0
  82. data/app/views/agilibox/search/_form_bs5.html.slim +8 -0
  83. data/app/views/kaminari/bootstrap4/_first_page.html.slim +2 -0
  84. data/app/views/kaminari/bootstrap4/_gap.html.slim +2 -0
  85. data/app/views/kaminari/bootstrap4/_last_page.html.slim +2 -0
  86. data/app/views/kaminari/bootstrap4/_next_page.html.slim +2 -0
  87. data/app/views/kaminari/bootstrap4/_page.html.slim +6 -0
  88. data/app/views/kaminari/bootstrap4/_paginator.html.slim +12 -0
  89. data/app/views/kaminari/bootstrap4/_prev_page.html.slim +2 -0
  90. data/config/cucumber.yml +9 -0
  91. data/config/locales/actions.en.yml +76 -0
  92. data/config/locales/actions.fr.yml +76 -0
  93. data/config/locales/attributes.en.yml +108 -0
  94. data/config/locales/attributes.fr.yml +108 -0
  95. data/config/locales/common.en.yml +0 -160
  96. data/config/locales/common.fr.yml +0 -171
  97. data/config/locales/dates.fr.yml +8 -0
  98. data/config/locales/errors.en.yml +5 -0
  99. data/config/locales/errors.fr.yml +5 -0
  100. data/db/migrate/20000101000000_enable_agilibox_extensions.rb +7 -0
  101. data/lib/agilibox/active_model_custom_error_messages.rb +23 -0
  102. data/lib/agilibox/active_model_type_cast.rb +52 -0
  103. data/lib/agilibox/config.rb +19 -0
  104. data/lib/agilibox/core_and_rails_ext.rb +2 -1
  105. data/lib/agilibox/cucumber_config.rb +53 -0
  106. data/lib/agilibox/cucumber_helpers/agilibox.rb +2 -0
  107. data/lib/agilibox/cucumber_helpers/ajax.rb +18 -0
  108. data/lib/agilibox/cucumber_helpers/apparition.rb +18 -0
  109. data/lib/agilibox/cucumber_helpers/capybara.rb +12 -0
  110. data/lib/agilibox/cucumber_helpers/capybara_selectors.rb +7 -0
  111. data/lib/agilibox/cucumber_helpers/chrome_headless.rb +16 -0
  112. data/lib/agilibox/cucumber_helpers/common_steps.rb +139 -0
  113. data/lib/agilibox/cucumber_helpers/cuprite.rb +20 -0
  114. data/lib/agilibox/cucumber_helpers/database_cleaner.rb +8 -0
  115. data/lib/agilibox/cucumber_helpers/factory_bot.rb +1 -0
  116. data/lib/agilibox/cucumber_helpers/rails.rb +1 -0
  117. data/lib/agilibox/cucumber_helpers/rspec.rb +6 -0
  118. data/lib/agilibox/cucumber_helpers/screenshots.rb +3 -0
  119. data/lib/agilibox/cucumber_helpers/select2.rb +23 -0
  120. data/lib/agilibox/cucumber_helpers/sign_in.rb +17 -0
  121. data/lib/agilibox/cucumber_helpers/simplecov.rb +1 -0
  122. data/lib/agilibox/cucumber_helpers/timecop.rb +3 -0
  123. data/lib/agilibox/cucumber_helpers/turbolinks.rb +34 -0
  124. data/lib/agilibox/cucumber_helpers/zonebie.rb +4 -0
  125. data/lib/agilibox/engine.rb +15 -0
  126. data/lib/agilibox/engine_file.rb +10 -0
  127. data/lib/agilibox/errors_middleware.rb +50 -0
  128. data/lib/agilibox/form_back_url.rb +4 -4
  129. data/lib/agilibox/rspec.rb +29 -0
  130. data/lib/agilibox/test_helpers.rb +13 -0
  131. data/lib/agilibox/version.rb +1 -1
  132. data/lib/tasks/cucumber.rake +76 -0
  133. metadata +165 -9
  134. data/app/filters/agilibox/small_data/filter_strategy_by_time_period.rb +0 -37
  135. data/lib/agilibox/active_record_comma_type_cast.rb +0 -12
@@ -0,0 +1,48 @@
1
+ class Agilibox::FCM::Request < Agilibox::Service
2
+ URL = "https://fcm.googleapis.com/fcm/send"
3
+
4
+ class << self
5
+ attr_writer :api_key
6
+
7
+ def api_key
8
+ @api_key ||= ENV["FCM_API_KEY"]
9
+ end
10
+ end
11
+
12
+ initialize_with :request_body
13
+
14
+ attr_reader :response_json
15
+
16
+ def call
17
+ response_body = HTTParty.post(URL,
18
+ :body => request_body.to_json,
19
+ :headers => {
20
+ "Content-Type" => "application/json",
21
+ "Authorization" => "key=#{self.class.api_key}",
22
+ },
23
+ ).body
24
+
25
+ @response_json = JSON.parse(response_body).deep_symbolize_keys
26
+
27
+ self
28
+ rescue JSON::ParserError
29
+ @response_json = {success: 0, failure: 1, results: [{:error => "InvalidJsonResponse"}]}
30
+ self
31
+ end
32
+
33
+ def ok?
34
+ response_json[:success].positive? && response_json[:failure].zero?
35
+ end
36
+
37
+ def error?
38
+ !ok?
39
+ end
40
+
41
+ def errors
42
+ response_json[:results].pluck(:error).compact
43
+ end
44
+
45
+ def invalid_token?
46
+ errors.include?("NotRegistered") || errors.include?("InvalidRegistration")
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ module Agilibox::InitializeWith
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def initialize_with(*attrs)
6
+ attr_reader(*attrs)
7
+
8
+ define_method(:initialize) do |*args|
9
+ if attrs.length != args.length
10
+ message = "wrong number of arguments (given #{args.length}, expected #{attrs.length})"
11
+ raise ArgumentError, message
12
+ end
13
+
14
+ attrs.length.times do |i|
15
+ instance_variable_set("@#{attrs[i]}", args[i])
16
+ end
17
+ end
18
+ end
19
+ end # class_methods
20
+ end
@@ -0,0 +1,30 @@
1
+ class Agilibox::MiniModelSerializer::Serialize
2
+ attr_reader :object, :options
3
+
4
+ def initialize(object, options = {})
5
+ @object = object
6
+ @options = options
7
+ end
8
+
9
+ def call
10
+ if object.is_a?(Hash)
11
+ object.map { |k, v| [k.to_s, serialize(v)] }.to_h
12
+ elsif object.is_a?(Enumerable)
13
+ object.map { |e| serialize(e) }
14
+ elsif (serializer = "#{object.class}Serializer".safe_constantize)
15
+ serializer.call(object, options)
16
+ else
17
+ object.as_json
18
+ end
19
+ end
20
+
21
+ def self.call(*args)
22
+ new(*args).call
23
+ end
24
+
25
+ private
26
+
27
+ def serialize(object)
28
+ Agilibox::MiniModelSerializer::Serialize.call(object, options)
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ class Agilibox::MiniModelSerializer::Serializer < Agilibox::MiniModelSerializer::Serialize
2
+ def attributes
3
+ raise NotImplementedError
4
+ end
5
+
6
+ def call
7
+ serialize attributes.map { |k| [k, send(k)] }.to_h
8
+ end
9
+
10
+ private
11
+
12
+ def method_missing(m, *args)
13
+ if respond_to_missing?(m)
14
+ object.send(m, *args)
15
+ else
16
+ super
17
+ end
18
+ end
19
+
20
+ def respond_to_missing?(m, *)
21
+ super || object.respond_to?(m, true)
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ module Agilibox::Monkey
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def prepend_instances(&block)
6
+ m = Module.new(&block)
7
+ send(:prepend, m)
8
+ end
9
+
10
+ def prepend_class(&block)
11
+ m = Module.new(&block)
12
+ singleton_class.send(:prepend, m)
13
+ end
14
+
15
+ def prepend_instance_method(name, &block)
16
+ check_instance_method_exist!(name)
17
+
18
+ m = Module.new
19
+ m.send(:define_method, name, &block)
20
+ send(:prepend, m)
21
+ end
22
+
23
+ def prepend_class_method(name, &block)
24
+ check_class_method_exist!(name)
25
+
26
+ m = Module.new
27
+ m.send(:define_method, name, &block)
28
+ singleton_class.send(:prepend, m)
29
+ end
30
+
31
+ def check_instance_method_exist!(name)
32
+ raise "instance method `#{name}` does not exist" unless instance_methods.include?(name)
33
+ end
34
+
35
+ def check_class_method_exist!(name)
36
+ raise "class method `#{name}` does not exist" unless methods.include?(name)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ class Agilibox::PhoneNumberSanitizer
2
+ attr_reader :phone_number
3
+
4
+ def initialize(phone_number)
5
+ @phone_number = phone_number
6
+ end
7
+
8
+ def call
9
+ phone_number.to_s.gsub(/[^0-9+]+/, "").presence
10
+ end
11
+
12
+ def self.call(*args)
13
+ new(*args).call
14
+ end
15
+ end
@@ -3,7 +3,7 @@ class Agilibox::SortableUUIDGenerator
3
3
  REGEX_WITH_DASHES = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/
4
4
  REGEX_WITHOUT_DASHES = /^([0-9a-f]{8})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{12})$/
5
5
 
6
- def self.generate
6
+ def self.call
7
7
  prefix = Time.zone.now.strftime("%s%9N").to_i.to_s(16)
8
8
  suffix = SecureRandom.hex(8)
9
9
  (prefix + suffix).gsub(REGEX_WITHOUT_DASHES, '\1-\2-\3-\4-\5')
@@ -0,0 +1,37 @@
1
+ class Agilibox::TokenGenerator
2
+ attr_reader :options
3
+
4
+ def initialize(options = {})
5
+ @options = options
6
+ end
7
+
8
+ def size
9
+ options[:size] || self.class.default_size
10
+ end
11
+
12
+ def alphabet
13
+ options[:alphabet] || self.class.default_alphabet
14
+ end
15
+
16
+ def call
17
+ (alphabet * size).sample(size).join
18
+ end
19
+
20
+ class << self
21
+ def call(*args)
22
+ new(*args).call
23
+ end
24
+
25
+ attr_writer :default_size
26
+
27
+ def default_size
28
+ @default_size ||= 64
29
+ end
30
+
31
+ attr_writer :default_alphabet
32
+
33
+ def default_alphabet
34
+ @default_alphabet ||= (0..9).to_a + ("a".."z").to_a + ("A".."Z").to_a
35
+ end
36
+ end # class << self
37
+ end
@@ -0,0 +1,2 @@
1
+ class Agilibox::ApplicationMailer < Agilibox.parent_mailer
2
+ end
@@ -0,0 +1,9 @@
1
+ class Agilibox::GenericMailer < Agilibox::ApplicationMailer
2
+ def generic_email(data)
3
+ data.delete(:attachments).to_h.each do |filename, content|
4
+ attachments[filename] = content
5
+ end
6
+
7
+ mail(data)
8
+ end
9
+ end
@@ -4,9 +4,11 @@ module Agilibox::ActiveRecordUUIDConcern
4
4
  private
5
5
 
6
6
  def assign_default_uuid
7
- if id.nil? && self.class.columns_hash["id"].type == :uuid
8
- self.id = ::Agilibox::SortableUUIDGenerator.generate
7
+ unless self.class.columns_hash["id"].type == :uuid
8
+ raise "invalid id type, please change to uuid"
9
9
  end
10
+
11
+ self.id ||= ::Agilibox::SortableUUIDGenerator.call
10
12
  end
11
13
 
12
14
  included do
@@ -1,13 +1,14 @@
1
1
  module Agilibox::DefaultValuesConcern
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def assign_default_values; end
4
+ def assign_default_values
5
+ end
5
6
 
6
7
  def assign_default(attribute, value)
7
- self.send("#{attribute}=", value) if self.send(attribute).nil?
8
+ send("#{attribute}=", value) if send(attribute).nil?
8
9
  end
9
10
 
10
11
  included do
11
- after_initialize :assign_default_values
12
+ after_initialize :assign_default_values, if: :new_record?
12
13
  end
13
14
  end
@@ -1,6 +1,12 @@
1
1
  module Agilibox::ModelI18n
2
2
  extend ActiveSupport::Concern
3
3
 
4
+ MissingTranslationError = Class.new(StandardError)
5
+ class << self
6
+ attr_accessor :raise_on_missing_translations
7
+ end
8
+ self.raise_on_missing_translations = false
9
+
4
10
  def t(*args)
5
11
  self.class.t(*args)
6
12
  end
@@ -9,17 +15,42 @@ module Agilibox::ModelI18n
9
15
  self.class.ts
10
16
  end
11
17
 
18
+ def tv(attribute)
19
+ value = public_send(attribute)
20
+ t("#{attribute}.#{value}") if value.present?
21
+ end
22
+
12
23
  class_methods do
13
- def t(*args)
14
- if args.any?
15
- human_attribute_name(*args)
16
- else
17
- model_name.human
24
+ def t(attribute = nil, options = {})
25
+ should_raise = Agilibox::ModelI18n.raise_on_missing_translations
26
+ exception_class = Agilibox::ModelI18n::MissingTranslationError
27
+
28
+ if should_raise
29
+ options = options.merge(default: "")
30
+ end
31
+
32
+ if should_raise && attribute.nil?
33
+ human = model_name.human(options)
34
+ type = options[:count].to_i > 1 ? "plural" : "singular"
35
+ raise exception_class, "translation missing: #{self} #{type} model name" if human.blank?
36
+ return human
37
+ end
38
+
39
+ if should_raise && attribute
40
+ human = human_attribute_name(attribute, options)
41
+ raise exception_class, "translation missing: #{self}##{attribute}" if human.blank?
42
+ return human
18
43
  end
44
+
45
+ if attribute.nil?
46
+ return model_name.human(options)
47
+ end
48
+
49
+ human_attribute_name(attribute, options) if attribute
19
50
  end
20
51
 
21
52
  def ts
22
- model_name.human(count: 2)
53
+ t(nil, count: 2)
23
54
  end
24
55
  end # class_methods
25
56
  end # Agilibox::ModelI18n
@@ -0,0 +1,13 @@
1
+ module Agilibox::PluckDistinct
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def pluck_distinct(column)
6
+ all
7
+ .reorder(column)
8
+ .select(column)
9
+ .distinct
10
+ .pluck(column)
11
+ end
12
+ end # class_methods
13
+ end
@@ -0,0 +1,9 @@
1
+ module Agilibox::PluckToHash
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def pluck_to_hash(*attributes)
6
+ pluck(*attributes).map { |values| attributes.zip(values).to_h }
7
+ end
8
+ end # class_methods
9
+ end
@@ -11,7 +11,7 @@ module Agilibox::PolymorphicId
11
11
  def guid
12
12
  return nil if new_record?
13
13
 
14
- "#{self.class.base_class}-#{self.id}"
14
+ "#{self.class.base_class}-#{id}"
15
15
  end
16
16
  end # included
17
17
  end # module
@@ -26,7 +26,10 @@ Module.new do
26
26
  end
27
27
 
28
28
  def relation_guid=(guid)
29
- return self.relation = nil if guid.blank?
29
+ if guid.blank?
30
+ self.relation = nil
31
+ return
32
+ end
30
33
 
31
34
  type, id = guid.split("-", 2)
32
35
  self.relation = type.constantize.find(id)
@@ -3,28 +3,26 @@ module Agilibox::Search
3
3
 
4
4
  class_methods do
5
5
  def default_search_fields
6
- fields = columns.map do |column|
7
- "#{table_name}.#{column.name}"
8
- end
6
+ columns
7
+ .select { |column| column.type.in?([:string, :text]) }
8
+ .map { |column| "#{table_name}.#{column.name}" }
9
9
  end # def default_search_fields
10
10
 
11
11
  def search(q, *fields)
12
12
  words = q.to_s.parameterize.split("-")
13
13
  fields = default_search_fields if fields.empty?
14
14
 
15
- sql_query = words.map.with_index do |word, index|
16
- fields.map do |field|
17
- "(LOWER(CAST(#{field} AS TEXT)) LIKE :w#{index})"
18
- end.join(" OR ")
19
- end.map{ |e| "(#{e})" }.join(" AND ")
15
+ return all if words.empty?
20
16
 
21
- sql_params_a = words.map.with_index do |word, index|
22
- ["w#{index}".to_sym, "%#{word}%"]
23
- end
17
+ sql_query = words.map.with_index { |_word, index|
18
+ fields.map { |field|
19
+ "(UNACCENT(CAST(#{field} AS TEXT)) ILIKE :w#{index})"
20
+ }.join(" OR ")
21
+ }.map { |e| "(#{e})" }.join(" AND ")
24
22
 
25
- sql_params_h = Hash[sql_params_a]
23
+ sql_params = words.map.with_index { |word, index| ["w#{index}".to_sym, "%#{word}%"] }.to_h
26
24
 
27
- self.where(sql_query, sql_params_h)
25
+ where(sql_query, sql_params)
28
26
  end # def search
29
27
  end # class_methods
30
28
  end # class Agilibox::Search
@@ -0,0 +1,17 @@
1
+ module Agilibox::TimestampHelpers
2
+ extend ActiveSupport::Concern
3
+
4
+ class_methods do
5
+ def first_created
6
+ reorder(:created_at, :id).first
7
+ end
8
+
9
+ def last_created
10
+ reorder(:created_at, :id).last
11
+ end
12
+
13
+ def last_updated
14
+ reorder(:updated_at, :id).last
15
+ end
16
+ end
17
+ end
@@ -6,12 +6,58 @@ class Agilibox::Serializers::Base
6
6
  @options = options
7
7
  end
8
8
 
9
+ def formatted_data
10
+ data.map do |line|
11
+ line.map { |value| self.class.format(value) }
12
+ end
13
+ end
14
+
9
15
  def render_inline
10
16
  raise NotImplementedError
11
17
  end
12
18
 
13
- def render_file(file_path)
19
+ def render_file(_file_path)
14
20
  raise NotImplementedError
15
21
  end
16
22
 
23
+ class << self
24
+ def decimals_precision
25
+ @decimals_precision ||= 2
26
+ end
27
+
28
+ attr_writer :decimals_precision
29
+
30
+ def format_integer(value)
31
+ value
32
+ end
33
+
34
+ def format_decimal(value)
35
+ value.to_f.round(decimals_precision) # Fix BigDecimal and 0.1 + 0.2
36
+ end
37
+
38
+ def format_boolean(value)
39
+ I18n.t(value.to_s)
40
+ end
41
+
42
+ def format_date_or_time(value)
43
+ value
44
+ end
45
+
46
+ def format_default(value)
47
+ value.to_s
48
+ end
49
+
50
+ def formatter_for(value)
51
+ return :integer if value.is_a?(Integer)
52
+ return :decimal if value.is_a?(Numeric)
53
+ return :boolean if value.is_a?(TrueClass) || value.is_a?(FalseClass)
54
+ return :date_or_time if value.is_a?(Date) || value.is_a?(Time)
55
+ return :default
56
+ end
57
+
58
+ def format(value)
59
+ formatter = "format_" + formatter_for(value).to_s
60
+ public_send(formatter, value)
61
+ end
62
+ end # class << self
17
63
  end
@@ -1,36 +1,38 @@
1
- require "axlsx"
2
-
3
1
  class Agilibox::Serializers::XLSX < Agilibox::Serializers::Base
4
2
  def render_inline
5
- xlsx.to_stream.read.force_encoding("BINARY")
3
+ headers, *data = formatted_data
4
+
5
+ SpreadsheetArchitect.to_xlsx(
6
+ headers: headers,
7
+ data: data,
8
+ freeze_headers: true,
9
+ range_styles: range_styles(data[0]),
10
+ )
6
11
  end
7
12
 
8
13
  def render_file(file_path)
9
- xlsx.serialize(file_path)
14
+ File.open(file_path, "w+b") do |f|
15
+ f.write(render_inline)
16
+ end
10
17
  end
11
18
 
12
- def xlsx
13
- @xlsx ||= Axlsx::Package.new do |p|
14
- p.workbook.add_worksheet do |sheet|
15
- data.each do |line|
16
- values = line.map do |value|
17
- if value.is_a?(Integer)
18
- value
19
- elsif value.is_a?(Numeric)
20
- value.to_f # Fix BigDecimal
21
- elsif value == true || value == false
22
- I18n.t(value.to_s)
23
- else
24
- value.to_s
25
- end
26
- end
19
+ private
27
20
 
28
- sheet.add_row(values)
29
- end
30
- end
21
+ def range_styles(row)
22
+ return [] if row.nil?
31
23
 
32
- p.use_shared_strings = true
24
+ date_range_styles(row) + time_range_styles(row)
25
+ end
26
+
27
+ def date_range_styles(row)
28
+ row.each_index.select { row[_1].is_a?(Date) }.map do
29
+ {range: {rows: :all, columns: _1}, styles: {format_code: "dd/mm/yyyy"}}
33
30
  end
34
31
  end
35
32
 
33
+ def time_range_styles(row)
34
+ row.each_index.select { row[_1].is_a?(Time) }.map do
35
+ {range: {rows: :all, columns: _1}, styles: {format_code: "dd/mm/yyyy hh:mm:ss"}}
36
+ end
37
+ end
36
38
  end
@@ -0,0 +1,17 @@
1
+ class Agilibox::Service
2
+ include Agilibox::InitializeWith
3
+
4
+ if RUBY_VERSION >= "2.7.0"
5
+ def self.call(*args, **kwargs)
6
+ new(*args, **kwargs).call
7
+ end
8
+ else
9
+ def self.call(*args)
10
+ new(*args).call
11
+ end
12
+ end
13
+
14
+ def call(*)
15
+ raise NotImplementedError
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ class Agilibox::SMS::ApplicationSMS
2
+ attr_reader :options
3
+
4
+ def initialize(options)
5
+ @options = options
6
+ end
7
+
8
+ private
9
+
10
+ def action_name
11
+ options[:action_name]
12
+ end
13
+
14
+ def t(key, **args)
15
+ if key.start_with?(".")
16
+ path = self.class.to_s.underscore.tr("/", ".")
17
+ key = "#{path}.#{action_name}#{key}"
18
+ end
19
+
20
+ I18n.t(key, **args)
21
+ end
22
+
23
+ def sms(data)
24
+ Agilibox::SMS::Message.new(data)
25
+ end
26
+
27
+ class << self
28
+ private :new
29
+
30
+ def method_missing(m, *args)
31
+ if respond_to_missing?(m)
32
+ new(action_name: m).public_send(m, *args)
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ def respond_to_missing?(m, *)
39
+ super || public_instance_methods.include?(m)
40
+ end
41
+ end # class << self
42
+ end
@@ -0,0 +1,25 @@
1
+ class Agilibox::SMS::Message
2
+ attr_reader :data
3
+
4
+ def initialize(data)
5
+ @data = data
6
+ end
7
+
8
+ def strategy=(value)
9
+ @strategy = Agilibox::SMS.parse_strategy(value)
10
+ end
11
+
12
+ def strategy(value = :no_argument)
13
+ self.strategy = value unless value == :no_argument
14
+ @strategy || Agilibox::SMS.strategy
15
+ end
16
+
17
+ def deliver_now
18
+ strategy.call(data)
19
+ end
20
+
21
+ # TODO : Delay
22
+ def deliver_later
23
+ deliver_now
24
+ end
25
+ end