agilibox 1.0.0 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +321 -2
- data/Rakefile +4 -0
- data/app/assets/javascripts/agilibox/autocomplete.coffee +2 -0
- data/app/assets/javascripts/agilibox/checkboxes_dropdown.coffee +9 -0
- data/app/assets/javascripts/agilibox/modals.coffee +81 -55
- data/app/assets/stylesheets/agilibox/actions.sass +2 -0
- data/app/assets/stylesheets/agilibox/all.sass +5 -0
- data/app/assets/stylesheets/agilibox/buttons.sass +11 -0
- data/app/assets/stylesheets/agilibox/filters.sass +13 -3
- data/app/assets/stylesheets/agilibox/forms.sass +49 -0
- data/app/assets/stylesheets/agilibox/icons.sass +5 -0
- data/app/assets/stylesheets/agilibox/modals.sass +15 -7
- data/app/assets/stylesheets/agilibox/pagination.sass +7 -0
- data/app/assets/stylesheets/agilibox/print.sass +10 -0
- data/app/assets/stylesheets/agilibox/tables.sass +26 -0
- data/app/controllers/agilibox/application_controller.rb +2 -4
- data/app/controllers/agilibox/small_data/filters_controller.rb +8 -6
- data/app/controllers/concerns/agilibox/api_controller_concern.rb +69 -0
- data/app/controllers/concerns/agilibox/back_url_concern.rb +13 -5
- data/app/emails/agilibox/email.rb +113 -0
- data/app/filters/agilibox/small_data/filter.rb +19 -1
- data/app/filters/agilibox/small_data/filter_strategy.rb +1 -1
- data/app/filters/agilibox/small_data/filter_strategy_by_date_begin.rb +3 -2
- data/app/filters/agilibox/small_data/filter_strategy_by_date_end.rb +3 -2
- data/app/filters/agilibox/small_data/filter_strategy_by_date_or_datetime_period.rb +56 -0
- data/app/filters/agilibox/small_data/filter_strategy_by_date_period.rb +5 -0
- data/app/filters/agilibox/small_data/filter_strategy_by_datetime_period.rb +5 -0
- data/app/filters/agilibox/small_data/filter_strategy_by_key_value.rb +14 -3
- data/app/filters/agilibox/small_data/filter_strategy_by_key_values.rb +13 -0
- data/app/filters/agilibox/small_data/filter_strategy_by_tags.rb +1 -1
- data/app/forms/agilibox/mini_form_object.rb +29 -0
- data/app/helpers/agilibox/all_helpers.rb +2 -0
- data/app/helpers/agilibox/bootstrap_helper.rb +62 -3
- data/app/helpers/agilibox/button_helper.rb +22 -13
- data/app/helpers/agilibox/filters_helper.rb +7 -8
- data/app/helpers/agilibox/font_awesome_helper.rb +67 -0
- data/app/helpers/agilibox/form_helper.rb +37 -14
- data/app/helpers/agilibox/i18n_helper.rb +5 -0
- data/app/helpers/agilibox/link_helper.rb +1 -1
- data/app/helpers/agilibox/pagination_helper.rb +18 -2
- data/app/helpers/agilibox/routes_helper.rb +6 -2
- data/app/helpers/agilibox/sorting_helper.rb +9 -4
- data/app/helpers/agilibox/text_helper.rb +51 -26
- data/app/helpers/h.rb +3 -0
- data/app/jobs/concerns/agilibox/setup_job_concern.rb +35 -0
- data/app/libs/agilibox/collection_update.rb +32 -0
- data/app/libs/agilibox/fcm/notifier.rb +28 -0
- data/app/libs/agilibox/fcm/request.rb +48 -0
- data/app/libs/agilibox/initialize_with.rb +20 -0
- data/app/libs/agilibox/mini_model_serializer/serialize.rb +30 -0
- data/app/libs/agilibox/mini_model_serializer/serializer.rb +23 -0
- data/app/libs/agilibox/monkey.rb +39 -0
- data/app/libs/agilibox/phone_number_sanitizer.rb +15 -0
- data/app/libs/agilibox/sortable_uuid_generator.rb +1 -1
- data/app/libs/agilibox/token_generator.rb +37 -0
- data/app/mailers/agilibox/application_mailer.rb +2 -0
- data/app/mailers/agilibox/generic_mailer.rb +9 -0
- data/app/models/concerns/agilibox/active_record_uuid_concern.rb +4 -2
- data/app/models/concerns/agilibox/default_values_concern.rb +4 -3
- data/app/models/concerns/agilibox/model_i18n.rb +37 -6
- data/app/models/concerns/agilibox/pluck_distinct.rb +13 -0
- data/app/models/concerns/agilibox/pluck_to_hash.rb +9 -0
- data/app/models/concerns/agilibox/polymorphic_id.rb +5 -2
- data/app/models/concerns/agilibox/search.rb +11 -13
- data/app/models/concerns/agilibox/timestamp_helpers.rb +17 -0
- data/app/serializers/agilibox/serializers/base.rb +47 -1
- data/app/serializers/agilibox/serializers/xlsx.rb +25 -23
- data/app/services/agilibox/service.rb +17 -0
- data/app/sms/agilibox/sms/application_sms.rb +42 -0
- data/app/sms/agilibox/sms/message.rb +25 -0
- data/app/sms/agilibox/sms/strategies/amazon_sns.rb +48 -0
- data/app/sms/agilibox/sms/strategies/base.rb +15 -0
- data/app/sms/agilibox/sms/strategies/test.rb +10 -0
- data/app/sms/agilibox/sms.rb +35 -0
- data/app/sorters/agilibox/sorter.rb +26 -0
- data/app/views/agilibox/_flash.html.slim +9 -0
- data/app/views/agilibox/forms/_checkboxes_dropdown.html.slim +14 -0
- data/app/views/agilibox/search/_form.html.slim +1 -6
- data/app/views/agilibox/search/_form_bs3.html.slim +13 -0
- data/app/views/agilibox/search/_form_bs4.html.slim +9 -0
- data/app/views/agilibox/search/_form_bs5.html.slim +8 -0
- data/app/views/kaminari/bootstrap4/_first_page.html.slim +2 -0
- data/app/views/kaminari/bootstrap4/_gap.html.slim +2 -0
- data/app/views/kaminari/bootstrap4/_last_page.html.slim +2 -0
- data/app/views/kaminari/bootstrap4/_next_page.html.slim +2 -0
- data/app/views/kaminari/bootstrap4/_page.html.slim +6 -0
- data/app/views/kaminari/bootstrap4/_paginator.html.slim +12 -0
- data/app/views/kaminari/bootstrap4/_prev_page.html.slim +2 -0
- data/config/cucumber.yml +9 -0
- data/config/locales/actions.en.yml +76 -0
- data/config/locales/actions.fr.yml +76 -0
- data/config/locales/attributes.en.yml +108 -0
- data/config/locales/attributes.fr.yml +108 -0
- data/config/locales/common.en.yml +0 -160
- data/config/locales/common.fr.yml +0 -171
- data/config/locales/dates.fr.yml +8 -0
- data/config/locales/errors.en.yml +5 -0
- data/config/locales/errors.fr.yml +5 -0
- data/db/migrate/20000101000000_enable_agilibox_extensions.rb +7 -0
- data/lib/agilibox/active_model_custom_error_messages.rb +23 -0
- data/lib/agilibox/active_model_type_cast.rb +52 -0
- data/lib/agilibox/config.rb +19 -0
- data/lib/agilibox/core_and_rails_ext.rb +2 -1
- data/lib/agilibox/cucumber_config.rb +53 -0
- data/lib/agilibox/cucumber_helpers/agilibox.rb +2 -0
- data/lib/agilibox/cucumber_helpers/ajax.rb +18 -0
- data/lib/agilibox/cucumber_helpers/apparition.rb +18 -0
- data/lib/agilibox/cucumber_helpers/capybara.rb +12 -0
- data/lib/agilibox/cucumber_helpers/capybara_selectors.rb +7 -0
- data/lib/agilibox/cucumber_helpers/chrome_headless.rb +16 -0
- data/lib/agilibox/cucumber_helpers/common_steps.rb +139 -0
- data/lib/agilibox/cucumber_helpers/cuprite.rb +20 -0
- data/lib/agilibox/cucumber_helpers/database_cleaner.rb +8 -0
- data/lib/agilibox/cucumber_helpers/factory_bot.rb +1 -0
- data/lib/agilibox/cucumber_helpers/rails.rb +1 -0
- data/lib/agilibox/cucumber_helpers/rspec.rb +6 -0
- data/lib/agilibox/cucumber_helpers/screenshots.rb +3 -0
- data/lib/agilibox/cucumber_helpers/select2.rb +23 -0
- data/lib/agilibox/cucumber_helpers/sign_in.rb +17 -0
- data/lib/agilibox/cucumber_helpers/simplecov.rb +1 -0
- data/lib/agilibox/cucumber_helpers/timecop.rb +3 -0
- data/lib/agilibox/cucumber_helpers/turbolinks.rb +34 -0
- data/lib/agilibox/cucumber_helpers/zonebie.rb +4 -0
- data/lib/agilibox/engine.rb +15 -0
- data/lib/agilibox/engine_file.rb +10 -0
- data/lib/agilibox/errors_middleware.rb +50 -0
- data/lib/agilibox/form_back_url.rb +4 -4
- data/lib/agilibox/rspec.rb +29 -0
- data/lib/agilibox/test_helpers.rb +13 -0
- data/lib/agilibox/version.rb +1 -1
- data/lib/tasks/cucumber.rake +76 -0
- metadata +165 -9
- data/app/filters/agilibox/small_data/filter_strategy_by_time_period.rb +0 -37
- 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.
|
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
|
@@ -4,9 +4,11 @@ module Agilibox::ActiveRecordUUIDConcern
|
|
4
4
|
private
|
5
5
|
|
6
6
|
def assign_default_uuid
|
7
|
-
|
8
|
-
|
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
|
4
|
+
def assign_default_values
|
5
|
+
end
|
5
6
|
|
6
7
|
def assign_default(attribute, value)
|
7
|
-
|
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(
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
53
|
+
t(nil, count: 2)
|
23
54
|
end
|
24
55
|
end # class_methods
|
25
56
|
end # Agilibox::ModelI18n
|
@@ -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}-#{
|
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
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
23
|
+
sql_params = words.map.with_index { |word, index| ["w#{index}".to_sym, "%#{word}%"] }.to_h
|
26
24
|
|
27
|
-
|
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(
|
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
|
-
|
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
|
-
|
14
|
+
File.open(file_path, "w+b") do |f|
|
15
|
+
f.write(render_inline)
|
16
|
+
end
|
10
17
|
end
|
11
18
|
|
12
|
-
|
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
|
-
|
29
|
-
|
30
|
-
end
|
21
|
+
def range_styles(row)
|
22
|
+
return [] if row.nil?
|
31
23
|
|
32
|
-
|
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
|