bmc 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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