bmc 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 (97) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +28 -0
  4. data/Rakefile +41 -0
  5. data/app/assets/config/bmc_manifest.js +2 -0
  6. data/app/controllers/bmc/application_controller.rb +3 -0
  7. data/app/controllers/bmc/filters_controller.rb +38 -0
  8. data/app/controllers/concerns/bmc/api_controller_concern.rb +69 -0
  9. data/app/controllers/concerns/bmc/back_url_concern.rb +26 -0
  10. data/app/emails/bmc/email.rb +113 -0
  11. data/app/filters/bmc/filter/by_date.rb +6 -0
  12. data/app/filters/bmc/filter/by_date_begin.rb +7 -0
  13. data/app/filters/bmc/filter/by_date_end.rb +7 -0
  14. data/app/filters/bmc/filter/by_date_or_datetime_period.rb +56 -0
  15. data/app/filters/bmc/filter/by_date_period.rb +5 -0
  16. data/app/filters/bmc/filter/by_datetime_period.rb +5 -0
  17. data/app/filters/bmc/filter/by_key_value.rb +27 -0
  18. data/app/filters/bmc/filter/by_key_values.rb +13 -0
  19. data/app/filters/bmc/filter.rb +86 -0
  20. data/app/forms/bmc/mini_form_object.rb +29 -0
  21. data/app/helpers/bmc/all_helpers.rb +15 -0
  22. data/app/helpers/bmc/bootstrap_helper.rb +63 -0
  23. data/app/helpers/bmc/button_helper.rb +150 -0
  24. data/app/helpers/bmc/filters_helper.rb +16 -0
  25. data/app/helpers/bmc/font_awesome_helper.rb +67 -0
  26. data/app/helpers/bmc/form_helper.rb +24 -0
  27. data/app/helpers/bmc/i18n_helper.rb +5 -0
  28. data/app/helpers/bmc/link_helper.rb +42 -0
  29. data/app/helpers/bmc/pagination_helper.rb +22 -0
  30. data/app/helpers/bmc/routes_helper.rb +24 -0
  31. data/app/helpers/bmc/sorting_helper.rb +55 -0
  32. data/app/helpers/bmc/text_helper.rb +127 -0
  33. data/app/helpers/h.rb +3 -0
  34. data/app/jobs/concerns/bmc/setup_job_concern.rb +35 -0
  35. data/app/libs/bmc/collection_update.rb +38 -0
  36. data/app/libs/bmc/fcm/notifier.rb +32 -0
  37. data/app/libs/bmc/fcm/request.rb +54 -0
  38. data/app/libs/bmc/mini_model_serializer/serialize.rb +30 -0
  39. data/app/libs/bmc/mini_model_serializer/serializer.rb +23 -0
  40. data/app/libs/bmc/monkey.rb +39 -0
  41. data/app/libs/bmc/sortable_uuid_generator.rb +11 -0
  42. data/app/libs/bmc/token_generator.rb +37 -0
  43. data/app/mailers/bmc/application_mailer.rb +2 -0
  44. data/app/mailers/bmc/generic_mailer.rb +9 -0
  45. data/app/models/concerns/bmc/active_record_uuid_concern.rb +17 -0
  46. data/app/models/concerns/bmc/default_values_concern.rb +14 -0
  47. data/app/models/concerns/bmc/model_i18n.rb +56 -0
  48. data/app/models/concerns/bmc/model_to_s.rb +9 -0
  49. data/app/models/concerns/bmc/pluck_distinct.rb +13 -0
  50. data/app/models/concerns/bmc/pluck_to_hash.rb +9 -0
  51. data/app/models/concerns/bmc/polymorphic_id.rb +37 -0
  52. data/app/models/concerns/bmc/search.rb +28 -0
  53. data/app/models/concerns/bmc/timestamp_helpers.rb +17 -0
  54. data/app/serializers/bmc/serializers/base.rb +63 -0
  55. data/app/serializers/bmc/serializers/xlsx.rb +38 -0
  56. data/app/serializers/bmc/serializers.rb +2 -0
  57. data/app/sms/bmc/sms/application_sms.rb +42 -0
  58. data/app/sms/bmc/sms/message.rb +25 -0
  59. data/app/sms/bmc/sms/strategies/amazon_sns.rb +48 -0
  60. data/app/sms/bmc/sms/strategies/base.rb +15 -0
  61. data/app/sms/bmc/sms/strategies/test.rb +10 -0
  62. data/app/sms/bmc/sms.rb +35 -0
  63. data/app/sorters/bmc/sorter.rb +26 -0
  64. data/app/views/bmc/search/_form.html.slim +1 -0
  65. data/app/views/bmc/search/_form_bs3.html.slim +13 -0
  66. data/app/views/bmc/search/_form_bs4.html.slim +9 -0
  67. data/app/views/bmc/search/_form_bs5.html.slim +8 -0
  68. data/app/views/kaminari/bootstrap4/_first_page.html.slim +2 -0
  69. data/app/views/kaminari/bootstrap4/_gap.html.slim +2 -0
  70. data/app/views/kaminari/bootstrap4/_last_page.html.slim +2 -0
  71. data/app/views/kaminari/bootstrap4/_next_page.html.slim +2 -0
  72. data/app/views/kaminari/bootstrap4/_page.html.slim +6 -0
  73. data/app/views/kaminari/bootstrap4/_paginator.html.slim +12 -0
  74. data/app/views/kaminari/bootstrap4/_prev_page.html.slim +2 -0
  75. data/config/locales/actions.en.yml +76 -0
  76. data/config/locales/actions.fr.yml +76 -0
  77. data/config/locales/attributes.en.yml +108 -0
  78. data/config/locales/attributes.fr.yml +108 -0
  79. data/config/locales/common.en.yml +39 -0
  80. data/config/locales/common.fr.yml +39 -0
  81. data/config/locales/dates.fr.yml +8 -0
  82. data/config/locales/errors.en.yml +5 -0
  83. data/config/locales/errors.fr.yml +5 -0
  84. data/config/routes.rb +3 -0
  85. data/db/migrate/20000101000000_enable_bmc_extensions.rb +7 -0
  86. data/lib/bmc/active_model_custom_error_messages.rb +23 -0
  87. data/lib/bmc/active_model_type_cast.rb +52 -0
  88. data/lib/bmc/config.rb +19 -0
  89. data/lib/bmc/core_and_rails_ext.rb +3 -0
  90. data/lib/bmc/engine.rb +36 -0
  91. data/lib/bmc/engine_file.rb +10 -0
  92. data/lib/bmc/errors_middleware.rb +50 -0
  93. data/lib/bmc/form_back_url.rb +18 -0
  94. data/lib/bmc/version.rb +3 -0
  95. data/lib/bmc.rb +5 -0
  96. data/lib/tasks/bmc_tasks.rake +4 -0
  97. metadata +236 -0
@@ -0,0 +1,150 @@
1
+ module BMC::ButtonHelper
2
+ class << self
3
+ attr_writer :default_size, :default_style
4
+
5
+ def default_size
6
+ @default_size ||= :sm
7
+ end
8
+
9
+ def default_style
10
+ @default_style ||= :outline_primary
11
+ end
12
+ end
13
+
14
+ def bs_button( # rubocop:disable Metrics/ParameterLists
15
+ url,
16
+ method: :get,
17
+ confirm: nil,
18
+ text: nil,
19
+ icon:,
20
+ action: nil,
21
+ title: text,
22
+ btn_size: BMC::ButtonHelper.default_size,
23
+ btn_style: BMC::ButtonHelper.default_style,
24
+ add_class: [],
25
+ **options
26
+ )
27
+ text = ta(action) if text.nil? && action
28
+
29
+ content = icon(icon).concat(" ").concat(tag.span(text, class: "text"))
30
+
31
+ if method != :get
32
+ options[:method] = method
33
+ confirm = true if confirm.nil?
34
+ end
35
+
36
+ unless options[:class]
37
+ options[:class] = [
38
+ "btn",
39
+ "btn-#{btn_size}",
40
+ "btn-#{btn_style.to_s.tr('_', '-')}",
41
+ ("link-#{action}" if action),
42
+ *add_class,
43
+ ]
44
+ end
45
+
46
+ options[:title] = title
47
+
48
+ unless confirm.nil?
49
+ confirm = ta(:confirm) if confirm == true
50
+ options.deep_merge!(data: {confirm: confirm})
51
+ end
52
+
53
+ link_to(content, url, options.sort.to_h)
54
+ end
55
+
56
+ def new_button(url = url_for(action: :new), **options)
57
+ options = {
58
+ :icon => :plus,
59
+ :action => :new,
60
+ :btn_style => :success,
61
+ }.merge(options)
62
+
63
+ bs_button(url, **options)
64
+ end
65
+
66
+ def read_button(url, **options)
67
+ options = {
68
+ :icon => :info_circle,
69
+ :action => :read,
70
+ }.merge(options)
71
+
72
+ bs_button(url, **options)
73
+ end
74
+
75
+ def edit_button(url, **options)
76
+ options = {
77
+ :icon => :pencil_alt,
78
+ :action => :edit,
79
+ }.merge(options)
80
+
81
+ bs_button(url, **options)
82
+ end
83
+
84
+ def delete_button(url, **options)
85
+ options = {
86
+ :icon => :trash,
87
+ :action => :delete,
88
+ :btn_style => :danger,
89
+ :method => :delete,
90
+ }.merge(options)
91
+
92
+ bs_button(url, **options)
93
+ end
94
+
95
+ def print_button(**options)
96
+ options = {
97
+ :icon => :print,
98
+ :action => :print,
99
+ :onclick => "print(); return false;",
100
+ }.merge(options)
101
+
102
+ bs_button("#", **options)
103
+ end
104
+
105
+ def download_button(url, **options)
106
+ options = {
107
+ :icon => :cloud_download_alt,
108
+ :action => :download,
109
+ :target => :_blank,
110
+ }.merge(options)
111
+
112
+ bs_button(url, **options)
113
+ end
114
+
115
+ def export_button(url, **options)
116
+ ext = url.split(".").last
117
+
118
+ if ext.length <= 4
119
+ text = "#{ta(:export)} (#{ext.upcase})"
120
+ else
121
+ text = ta(:export)
122
+ end
123
+
124
+ options = {
125
+ :icon => :download,
126
+ :action => :export,
127
+ :text => text,
128
+ }.merge(options)
129
+
130
+ download_button(url, **options)
131
+ end
132
+
133
+ def import_button(url, **options)
134
+ options = {
135
+ :icon => :upload,
136
+ :action => :import,
137
+ }.merge(options)
138
+
139
+ bs_button(url, **options)
140
+ end
141
+
142
+ def copy_button(url, **options)
143
+ options = {
144
+ :icon => :copy,
145
+ :action => :copy,
146
+ }.merge(options)
147
+
148
+ bs_button(url, **options)
149
+ end
150
+ end
@@ -0,0 +1,16 @@
1
+ module BMC::FiltersHelper
2
+ def bmc_time_periods_for_select
3
+ {
4
+ t("time_periods.all_time") => "",
5
+ t("time_periods.today") => "today",
6
+ t("time_periods.yesterday") => "yesterday",
7
+ t("time_periods.this_week") => "this_week",
8
+ t("time_periods.last_week") => "last_week",
9
+ t("time_periods.this_month") => "this_month",
10
+ t("time_periods.last_month") => "last_month",
11
+ t("time_periods.this_year") => "this_year",
12
+ t("time_periods.last_year") => "last_year",
13
+ t("time_periods.custom_date") => "custom_date",
14
+ }
15
+ end
16
+ end
@@ -0,0 +1,67 @@
1
+ module BMC::FontAwesomeHelper
2
+ def icon(id, fa_style: nil, size: nil, spin: false, **options)
3
+ id = id.to_s.tr("_", "-").to_sym
4
+
5
+ if fa_style.nil?
6
+ fa_style = BMC::FontAwesomeHelper.default_fa_style_for_id(id)
7
+ end
8
+
9
+ css_classes = options.delete(:class).to_s.split
10
+ css_classes << "icon"
11
+ css_classes << "fa-#{id}"
12
+ css_classes << "fa#{fa_style.to_s[0]}"
13
+ css_classes << "fa-#{size}" if size
14
+ css_classes << "fa-spin" if spin
15
+
16
+ attributes = options.merge(class: css_classes.sort.join(" ")).sort.to_h
17
+
18
+ tag.span(**attributes)
19
+ end
20
+
21
+ class << self
22
+ def database
23
+ @database ||= YAML.safe_load(database_yml).deep_symbolize_keys
24
+ end
25
+
26
+ def database_path
27
+ Rails.root.join("tmp", "fa_database_#{version}.yml")
28
+ end
29
+
30
+ def database_yml
31
+ download_database! unless File.size?(database_path)
32
+ File.read(database_path)
33
+ end
34
+
35
+ def database_url
36
+ short_version = version.split(".")[0, 3].join(".") # 1.20.14.2 => 1.20.14
37
+ url = "https://raw.githubusercontent.com/FortAwesome/Font-Awesome/#{short_version}/metadata/icons.yml"
38
+
39
+ if Gem::Version.new(short_version) < Gem::Version.new("5.6.0")
40
+ url = url.gsub("/metadata", "/advanced-options/metadata")
41
+ end
42
+
43
+ url
44
+ end
45
+
46
+ def download_database!
47
+ require "open-uri"
48
+ data = URI.parse(database_url).open.read
49
+ File.write(database_path, data)
50
+ end
51
+
52
+ def version
53
+ require "font_awesome/sass/version"
54
+ FontAwesome::Sass::VERSION
55
+ end
56
+
57
+ def default_fa_style_for_id(id)
58
+ return if version.start_with?("4")
59
+
60
+ if version.start_with?("5")
61
+ return BMC::FontAwesomeHelper.database.dig(id, :styles).to_a.first
62
+ end
63
+
64
+ raise "invalid font-awesome-sass version"
65
+ end
66
+ end # class << self
67
+ end
@@ -0,0 +1,24 @@
1
+ module BMC::FormHelper
2
+ include BMC::I18nHelper
3
+
4
+ # Hidden submit to be the default triggered on <enter> keypress on a form
5
+ # Does not work if display:none
6
+ def form_hidden_submit
7
+ css = "position:absolute;top:-9999px;left:-9999px;opacity:0;height:0;width:0;visibility: hidden"
8
+ tag(:input, type: "submit", class: "hidden-submit", style: css)
9
+ end
10
+
11
+ def search_form(action: request.fullpath)
12
+ render "bmc/search/form", action: action
13
+ end
14
+
15
+ def hidden_inputs_for_get_form(url, except: [])
16
+ query_values = Addressable::URI.parse(url).query_values.to_h
17
+ .with_indifferent_access
18
+ .except(*except)
19
+
20
+ return if query_values.empty?
21
+
22
+ query_values.sum { |k, v| tag.input(type: "hidden", name: k, value: v) }
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module BMC::I18nHelper
2
+ def ta(action)
3
+ t("actions.#{action}")
4
+ end
5
+ end
@@ -0,0 +1,42 @@
1
+ module BMC::LinkHelper
2
+ def link_to_object(obj, options = {})
3
+ return if obj.nil?
4
+
5
+ if policy(obj).read?
6
+ link_to(obj.to_s, engine_polymorphic_path(obj), options)
7
+ else
8
+ obj.to_s
9
+ end
10
+ end
11
+
12
+ def icon_link_to(icon, name, options = nil, html_options = nil, &block)
13
+ name = icon(icon).concat(" ").concat(name)
14
+ link_to(name, options, html_options, &block)
15
+ end
16
+
17
+ def web_link(text, opts = {})
18
+ return if text.to_s.blank?
19
+
20
+ href = text
21
+ href = "http://#{text}" unless text.include?("://")
22
+
23
+ link_to(text, href, opts)
24
+ end
25
+
26
+ def email_link(text, opts = {})
27
+ return if text.to_s.blank?
28
+
29
+ href = "mailto:#{text}"
30
+
31
+ link_to(text, href, opts)
32
+ end
33
+
34
+ def tel_link(text, opts = {})
35
+ return if text.to_s.blank?
36
+
37
+ value = text.delete(" ")
38
+ href = "tel:#{value}"
39
+
40
+ link_to(text, href, opts)
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ module BMC::PaginationHelper
2
+ class << self
3
+ attr_writer :theme
4
+
5
+ def theme
6
+ @theme ||= "bootstrap4"
7
+ end
8
+ end
9
+
10
+ def paginate(objects, options = {})
11
+ options = {theme: BMC::PaginationHelper.theme}.merge(options)
12
+ super(objects, **options).gsub(/>(\s+)</, "><").html_safe
13
+ end
14
+
15
+ def pagination_infos(collection)
16
+ tag.p(class: "pagination-infos") { page_entries_info(collection) }
17
+ end
18
+
19
+ def pagination_and_infos(collection)
20
+ paginate(collection) + pagination_infos(collection)
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module BMC::RoutesHelper
2
+ def engine_polymorphic_path(obj, opts = {})
3
+ if Rails::VERSION::STRING >= "6.0.0"
4
+ engine = obj.class.module_parents[-2]
5
+ else
6
+ engine = obj.class.parents[-2]
7
+ end
8
+
9
+ if engine.nil?
10
+ routes = main_app
11
+ else
12
+ routes = engine::Engine.routes
13
+ end
14
+
15
+ opts = {
16
+ :controller => "/#{obj.class.to_s.tableize}",
17
+ :action => :show,
18
+ :id => obj.to_param,
19
+ :only_path => true,
20
+ }.merge(opts)
21
+
22
+ routes.url_for(opts)
23
+ end
24
+ end
@@ -0,0 +1,55 @@
1
+ module BMC::SortingHelper
2
+ def sortable_column(name, column, options = {})
3
+ unless column.is_a?(Symbol)
4
+ raise ArgumentError, "invalid column, please use symbol"
5
+ end
6
+
7
+ current_column, current_direction = sortable_column_order
8
+
9
+ if current_column == column
10
+ if current_direction == :asc
11
+ name = "#{name} ↓"
12
+ new_sort_param = "-#{column}"
13
+ end
14
+
15
+ if current_direction == :desc
16
+ name = "#{name} ↑"
17
+ new_sort_param = column
18
+ end
19
+
20
+ klass = "sort #{current_direction}"
21
+ else
22
+ new_sort_param = column
23
+ klass = "sort"
24
+ end
25
+
26
+ unless (url_params = options.delete(:url_params))
27
+ url_params = (params.try(:permit!) || params).to_h.symbolize_keys
28
+ end
29
+ url_params[:sort] = new_sort_param
30
+
31
+ html_options = {class: klass}.merge(options)
32
+
33
+ link_to(name, url_params, html_options)
34
+ end
35
+
36
+ def sortable_column_order(sort_param = params[:sort])
37
+ sort_param = sort_param.to_s
38
+
39
+ if sort_param.present?
40
+ if sort_param.start_with?("-")
41
+ column = sort_param[1..].to_sym
42
+ direction = :desc
43
+ else
44
+ column = sort_param.to_sym
45
+ direction = :asc
46
+ end
47
+ end
48
+
49
+ if block_given?
50
+ yield(column, direction)
51
+ else
52
+ [column, direction]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,127 @@
1
+ module BMC::TextHelper
2
+ include ::ActionView::Helpers::NumberHelper
3
+ include ::ActionView::Helpers::SanitizeHelper
4
+ include ::ActionView::Helpers::TextHelper
5
+
6
+ def nbsp(text = :no_argument)
7
+ if text == :no_argument
8
+ "\u00A0"
9
+ else
10
+ text.to_s.gsub(" ", nbsp)
11
+ end
12
+ end
13
+
14
+ def euros(n)
15
+ currency(n, "€")
16
+ end
17
+
18
+ def currency(n, u)
19
+ return if n.nil?
20
+
21
+ I18n.t("number.currency.format.format")
22
+ .gsub("%n", number(n))
23
+ .gsub("%u", u)
24
+ .tr(" ", nbsp)
25
+ end
26
+
27
+ def percentage(n)
28
+ return if n.nil?
29
+
30
+ number(n) + nbsp + "%"
31
+ end
32
+
33
+ def number(n)
34
+ return if n.nil?
35
+
36
+ opts = {}
37
+
38
+ if n.class.to_s.match?(/Float|Decimal/i)
39
+ opts[:precision] = 2
40
+ else
41
+ opts[:precision] = 0
42
+ end
43
+
44
+ opts[:delimiter] = I18n.t("number.format.delimiter")
45
+ opts[:separator] = I18n.t("number.format.separator")
46
+
47
+ number_with_precision(n, opts).tr(" ", nbsp)
48
+ end
49
+
50
+ def date(d, **args)
51
+ I18n.l(d, **args) unless d.nil?
52
+ end
53
+
54
+ def boolean_icon(bool)
55
+ return if bool.nil?
56
+
57
+ return icon(:check, style: "color: green") if bool == true
58
+ return icon(:times, style: "color: red") if bool == false
59
+
60
+ raise "#{bool} is not a boolean"
61
+ end
62
+
63
+ def text2html(str)
64
+ return if str.to_s.blank?
65
+
66
+ str = str.delete("\r").strip
67
+ strip_tags(str).gsub("\n", "<br />").html_safe
68
+ end
69
+
70
+ def lf2br(str)
71
+ return if str.to_s.blank?
72
+
73
+ str.delete("\r").gsub("\n", "<br />").html_safe
74
+ end
75
+
76
+ def info( # rubocop:disable Metrics/ParameterLists
77
+ object,
78
+ attribute,
79
+ value: nil,
80
+ default: nil,
81
+ helper: nil,
82
+ tag: :div,
83
+ separator: " : ",
84
+ label: object.t(attribute)
85
+ )
86
+ if default == :hide
87
+ value = fma(object, attribute, value: value, helper: helper)
88
+ return if value.nil?
89
+ end
90
+
91
+ value = fma(object, attribute, value: value, default: default, helper: helper)
92
+
93
+ separator = " :<br/>".html_safe if separator == :br
94
+ klass = object.is_a?(Module) ? object : object.class
95
+ object_type = klass.to_s.split("::").last.underscore
96
+
97
+ html_label = self.tag.strong(class: "info-label") { label }
98
+ span_css_class = "info-value #{object_type}-#{attribute}"
99
+ html_value = self.tag.span(class: span_css_class) { value }
100
+ separator_html = self.tag.span(class: "info-separator") { separator }
101
+
102
+ container_css_class = ["info", ("blank" if value.blank?)]
103
+
104
+ content_tag(tag, class: container_css_class) do
105
+ [html_label, separator_html, html_value].sum
106
+ end
107
+ end # def info
108
+
109
+ # format model attribute
110
+ def fma(object, attribute, value: nil, default: nil, helper: nil)
111
+ value = object.public_send(attribute) if value.nil?
112
+ value = default if value != false && value.blank?
113
+
114
+ i18n_scope = object.model_name.instance_variable_get(:@klass).i18n_scope
115
+ i18n_key = "#{object.model_name.i18n_key}/#{attribute}"
116
+ nested = I18n.t("#{i18n_scope}.attributes.#{i18n_key}", default: "").is_a?(Hash)
117
+
118
+ value = object.tv(attribute) if nested
119
+ value = send(helper, value) if helper
120
+ value = t("yes") if value == true
121
+ value = t("no") if value == false
122
+ value = number(value) if value.is_a?(Numeric)
123
+ value = date(value) if value.is_a?(Date) || value.is_a?(Time)
124
+
125
+ value.to_s.presence
126
+ end
127
+ end
data/app/helpers/h.rb ADDED
@@ -0,0 +1,3 @@
1
+ # Short alias
2
+ # Example : H.number(123)
3
+ H = BMC::AllHelpers
@@ -0,0 +1,35 @@
1
+ module BMC::SetupJobConcern
2
+ extend ActiveSupport::Concern
3
+
4
+ def perform(*args)
5
+ setup(*args)
6
+ call
7
+ end
8
+
9
+ def call
10
+ raise NotImplementedError
11
+ end
12
+
13
+ class_methods do
14
+ def setup_with(*attrs)
15
+ attr_reader(*attrs)
16
+
17
+ define_method(:setup) do |*args|
18
+ if attrs.length != args.length
19
+ message = "wrong number of arguments (given #{args.length}, expected #{attrs.length})"
20
+ raise ArgumentError, message
21
+ end
22
+
23
+ attrs.length.times do |i|
24
+ instance_variable_set("@#{attrs[i]}", args[i])
25
+ end
26
+ end
27
+ end
28
+
29
+ private :setup_with
30
+
31
+ def setup(*args)
32
+ new.tap { |instance| instance.setup(*args) }
33
+ end
34
+ end # class_methods
35
+ end
@@ -0,0 +1,38 @@
1
+ class BMC::CollectionUpdate
2
+ extend AttrExtras.mixin
3
+
4
+ attr_reader_initialize :scope, :params_array
5
+
6
+ def self.call(...)
7
+ new(...).call
8
+ end
9
+
10
+ def call
11
+ return update
12
+ end
13
+
14
+ def update!
15
+ ApplicationRecord.transaction do
16
+ params_array.each do |object_param|
17
+ if object_param[:id].present?
18
+ object = scope.find(object_param[:id])
19
+ else
20
+ object = scope.new
21
+ end
22
+
23
+ if object_param.delete(:_destroy).to_i == 1
24
+ object.destroy!
25
+ else
26
+ object.update!(object_param)
27
+ end
28
+ end
29
+ end
30
+ true
31
+ end
32
+
33
+ def update
34
+ update!
35
+ rescue ActiveRecord::RecordInvalid
36
+ false
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ class BMC::FCM::Notifier
2
+ attr_reader :to, :title, :body, :badge, :sound, :data
3
+
4
+ # rubocop:disable Metrics/ParameterLists
5
+ def initialize(to:, title: nil, body:, badge: 0, sound: "default", data: {})
6
+ super()
7
+ @to = to
8
+ @title = title
9
+ @body = body
10
+ @badge = badge
11
+ @sound = sound
12
+ @data = data
13
+ end
14
+ # rubocop:enable Metrics/ParameterLists
15
+
16
+ def call
17
+ BMC::FCM::Request.call(
18
+ :to => to,
19
+ :notification => {
20
+ :title => title,
21
+ :body => body,
22
+ :badge => badge,
23
+ :sound => sound,
24
+ },
25
+ :data => data,
26
+ )
27
+ end
28
+
29
+ def self.call(...)
30
+ new(...).call
31
+ end
32
+ end
@@ -0,0 +1,54 @@
1
+ class BMC::FCM::Request
2
+ extend AttrExtras.mixin
3
+
4
+ URL = "https://fcm.googleapis.com/fcm/send"
5
+
6
+ class << self
7
+ attr_writer :api_key
8
+
9
+ def api_key
10
+ @api_key ||= ENV["FCM_API_KEY"]
11
+ end
12
+ end
13
+
14
+ attr_reader_initialize :request_body
15
+
16
+ attr_reader :response_json
17
+
18
+ def self.call(...)
19
+ new(...).call
20
+ end
21
+
22
+ def call
23
+ response_body = HTTParty.post(URL,
24
+ :body => request_body.to_json,
25
+ :headers => {
26
+ "Content-Type" => "application/json",
27
+ "Authorization" => "key=#{self.class.api_key}",
28
+ },
29
+ ).body
30
+
31
+ @response_json = JSON.parse(response_body).deep_symbolize_keys
32
+
33
+ self
34
+ rescue JSON::ParserError
35
+ @response_json = {success: 0, failure: 1, results: [{:error => "InvalidJsonResponse"}]}
36
+ self
37
+ end
38
+
39
+ def ok?
40
+ response_json[:success].positive? && response_json[:failure].zero?
41
+ end
42
+
43
+ def error?
44
+ !ok?
45
+ end
46
+
47
+ def errors
48
+ response_json[:results].pluck(:error).compact
49
+ end
50
+
51
+ def invalid_token?
52
+ errors.include?("NotRegistered") || errors.include?("InvalidRegistration")
53
+ end
54
+ end