motor-admin 0.3.17 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/channels/motor/application_cable/channel.rb +14 -0
- data/app/channels/motor/application_cable/connection.rb +27 -0
- data/app/channels/motor/notes_channel.rb +9 -0
- data/app/channels/motor/notifications_channel.rb +9 -0
- data/app/controllers/concerns/motor/current_user_method.rb +2 -0
- data/app/controllers/motor/note_tags_controller.rb +13 -0
- data/app/controllers/motor/notes_controller.rb +58 -0
- data/app/controllers/motor/notifications_controller.rb +33 -0
- data/app/controllers/motor/reminders_controller.rb +38 -0
- data/app/controllers/motor/run_queries_controller.rb +1 -1
- data/app/controllers/motor/send_alerts_controller.rb +2 -2
- data/app/controllers/motor/slack_conversations_controller.rb +11 -0
- data/app/controllers/motor/tags_controller.rb +1 -1
- data/app/controllers/motor/ui_controller.rb +9 -1
- data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
- data/app/jobs/motor/alert_sending_job.rb +1 -1
- data/app/jobs/motor/notify_note_mentions_job.rb +9 -0
- data/app/jobs/motor/notify_reminder_job.rb +9 -0
- data/app/mailers/motor/alerts_mailer.rb +6 -29
- data/app/mailers/motor/application_mailer.rb +27 -1
- data/app/mailers/motor/notifications_mailer.rb +33 -0
- data/app/models/motor/note.rb +18 -0
- data/app/models/motor/note_tag.rb +7 -0
- data/app/models/motor/note_tag_tag.rb +8 -0
- data/app/models/motor/notification.rb +14 -0
- data/app/models/motor/reminder.rb +13 -0
- data/app/views/layouts/motor/application.html.erb +4 -1
- data/app/views/layouts/motor/mailer.html.erb +72 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +52 -124
- data/app/views/motor/notifications_mailer/notify_mention_email.html.erb +28 -0
- data/app/views/motor/notifications_mailer/notify_reminder_email.html.erb +28 -0
- data/config/locales/el.yml +25 -0
- data/config/locales/en.yml +33 -2
- data/config/locales/es.yml +33 -2
- data/config/locales/pt.yml +33 -2
- data/config/routes.rb +9 -0
- data/lib/generators/motor/install_notes_generator.rb +22 -0
- data/lib/generators/motor/templates/install.rb +77 -0
- data/lib/generators/motor/templates/install_notes.rb +83 -0
- data/lib/generators/motor/upgrade_generator.rb +13 -6
- data/lib/motor/admin.rb +13 -1
- data/lib/motor/alerts/slack_sender.rb +74 -0
- data/lib/motor/alerts.rb +42 -0
- data/lib/motor/api_query/apply_scope.rb +1 -0
- data/lib/motor/build_schema/apply_permissions.rb +8 -0
- data/lib/motor/build_schema/defaults.rb +15 -1
- data/lib/motor/build_schema/load_from_rails.rb +4 -1
- data/lib/motor/build_schema.rb +7 -0
- data/lib/motor/configs/build_ui_app_tag.rb +73 -8
- data/lib/motor/configs.rb +3 -4
- data/lib/motor/notes/notify_mentions.rb +73 -0
- data/lib/motor/notes/notify_reminder.rb +49 -0
- data/lib/motor/notes/persist.rb +36 -0
- data/lib/motor/notes/reminders_scheduler.rb +39 -0
- data/lib/motor/notes/tags.rb +34 -0
- data/lib/motor/notes.rb +12 -0
- data/lib/motor/queries/run_query.rb +66 -3
- data/lib/motor/resources/fetch_configured_model.rb +19 -3
- data/lib/motor/resources.rb +1 -1
- data/lib/motor/slack/client.rb +62 -0
- data/lib/motor/slack.rb +16 -0
- data/lib/motor/version.rb +1 -1
- data/lib/motor.rb +19 -0
- data/ui/dist/{main-726aa7f6805676af4d21.css.gz → main-94c4b9cbc80e23602ab9.css.gz} +0 -0
- data/ui/dist/main-94c4b9cbc80e23602ab9.js.gz +0 -0
- data/ui/dist/manifest.json +5 -5
- metadata +36 -4
- data/ui/dist/main-726aa7f6805676af4d21.js.gz +0 -0
@@ -15,22 +15,29 @@ module Motor
|
|
15
15
|
source_root File.expand_path('templates', __dir__)
|
16
16
|
|
17
17
|
def copy_migration
|
18
|
-
has_api_actions = Motor::Resource.all.any? do |resource|
|
19
|
-
resource.preferences[:actions]&.any? { |action| action[:action_type] == 'api' }
|
20
|
-
end
|
21
|
-
|
22
18
|
unless Motor::ApiConfig.table_exists?
|
23
19
|
migration_template 'install_api_configs.rb', 'db/migrate/install_motor_api_configs.rb'
|
24
20
|
end
|
25
21
|
|
26
|
-
|
22
|
+
if with_api_actions?
|
23
|
+
migration_template 'upgrade_motor_api_actions.rb', 'db/migrate/upgrade_motor_api_actions.rb'
|
24
|
+
end
|
25
|
+
|
26
|
+
migration_template 'install_notes.rb', 'db/migrate/install_motor_notes.rb' unless Motor::Note.table_exists?
|
27
27
|
|
28
|
-
if Motor::ApiConfig.table_exists? && !
|
28
|
+
if Motor::ApiConfig.table_exists? && !with_api_actions? && Motor::Note.table_exists?
|
29
29
|
puts 'The latest Motor Admin features are already configured'
|
30
30
|
else
|
31
31
|
puts 'Run `rake db:migrate` to update DB schema'
|
32
32
|
end
|
33
33
|
end
|
34
|
+
|
35
|
+
def with_api_actions?
|
36
|
+
@with_api_actions ||=
|
37
|
+
Motor::Resource.all.any? do |resource|
|
38
|
+
resource.preferences[:actions]&.any? { |action| action[:action_type] == 'api' }
|
39
|
+
end
|
40
|
+
end
|
34
41
|
end
|
35
42
|
end
|
36
43
|
end
|
data/lib/motor/admin.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
module Motor
|
4
4
|
class Admin < ::Rails::Engine
|
5
|
+
config.custom_html = ''
|
6
|
+
|
5
7
|
initializer 'motor.startup_message' do
|
6
8
|
config.after_initialize do
|
7
9
|
next unless Motor.server?
|
@@ -48,6 +50,7 @@ module Motor
|
|
48
50
|
next unless Rails.env.production?
|
49
51
|
|
50
52
|
Motor::Alerts::Scheduler::SCHEDULER_TASK.execute
|
53
|
+
Motor::Notes::RemindersScheduler::SCHEDULER_TASK.execute
|
51
54
|
end
|
52
55
|
end
|
53
56
|
|
@@ -78,6 +81,15 @@ module Motor
|
|
78
81
|
end
|
79
82
|
end
|
80
83
|
|
84
|
+
initializer 'action_cable.connection_class' do
|
85
|
+
config.after_initialize do
|
86
|
+
next if defined?(::ApplicationCable::Connection)
|
87
|
+
next unless defined?(::ActionCable)
|
88
|
+
|
89
|
+
ActionCable.server.config.connection_class = -> { Motor::ApplicationCable::Connection }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
81
93
|
initializer 'motor.active_storage.extensions' do
|
82
94
|
config.after_initialize do
|
83
95
|
next unless defined?(ActiveStorage::Engine)
|
@@ -107,7 +119,7 @@ module Motor
|
|
107
119
|
raise
|
108
120
|
end
|
109
121
|
|
110
|
-
|
122
|
+
if !Motor::ApiConfig.table_exists? || !Motor::Note.table_exists?
|
111
123
|
puts
|
112
124
|
puts ' => Run `rails g motor:upgrade && rake db:migrate`' \
|
113
125
|
' to perform data migration and enable the latest features'
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Alerts
|
5
|
+
module SlackSender
|
6
|
+
MARKDOWN_LINK_TRANSFORM_REGEXP = /\[(.+?)\]\((.+?)\)/.freeze
|
7
|
+
MARKDOWN_PATHS_TRANSFORM_REGEXP = /\n\s*\#{1,5}\s*(.*)\n?/.freeze
|
8
|
+
MARKDOWN_HEADING_TRANSFORM_REGEXP = /\n\s*\#{1,5}\s*(.*)\n?/.freeze
|
9
|
+
MARKDOWN_BOLD_TRANSFORM_REGEXP = /[*_]{2}/.freeze
|
10
|
+
|
11
|
+
DEFAULT_FILENAME = 'data.csv'
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
def call(alert, conversation_id, slack_user: nil)
|
16
|
+
query_result = load_data(alert, slack_user)
|
17
|
+
|
18
|
+
if alert.query.preferences[:visualization] == 'markdown'
|
19
|
+
text = [build_query_link(alert), render_markdown(alert, query_result)].join
|
20
|
+
|
21
|
+
Slack::Client.send_message(channel: conversation_id,
|
22
|
+
as_user: slack_user.present?,
|
23
|
+
text: text)
|
24
|
+
else
|
25
|
+
Slack::Client.send_message(channel: conversation_id,
|
26
|
+
as_user: slack_user.present?,
|
27
|
+
text: build_alert_message(alert))
|
28
|
+
Slack::Client.send_file(channels: conversation_id,
|
29
|
+
content: generate_csv(query_result),
|
30
|
+
filename: DEFAULT_FILENAME)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_alert_message(alert)
|
35
|
+
<<~MARKDOWN
|
36
|
+
#{build_query_link(alert)}
|
37
|
+
#{alert.description}
|
38
|
+
MARKDOWN
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_query_link(alert)
|
42
|
+
link = Motor::Admin.routes.url_helpers.motor_ui_query_url(alert.query_id, host: Motor.app_host)
|
43
|
+
|
44
|
+
"*<#{link}|#{alert.name}>*"
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_markdown(alert, query_result)
|
48
|
+
params = query_result.columns.pluck(:name).zip(query_result.data[0]).to_h.symbolize_keys
|
49
|
+
|
50
|
+
markdown =
|
51
|
+
["\n", Mustache.render(alert.query.preferences[:visualization_options][:markdown], **params)].join
|
52
|
+
|
53
|
+
markdown.gsub(/\((.*)\)/, "(http://#{Motor.app_host}\\1)")
|
54
|
+
.gsub(MARKDOWN_LINK_TRANSFORM_REGEXP, '<\2|\1>')
|
55
|
+
.gsub(MARKDOWN_HEADING_TRANSFORM_REGEXP, "\n\n*\\1*")
|
56
|
+
.gsub(MARKDOWN_BOLD_TRANSFORM_REGEXP, '*')
|
57
|
+
end
|
58
|
+
|
59
|
+
def generate_csv(query_result)
|
60
|
+
rows = [query_result.columns.pluck(:name)] + query_result.data
|
61
|
+
|
62
|
+
rows.map(&:to_csv).join
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_data(alert, slack_user = nil)
|
66
|
+
if slack_user
|
67
|
+
Queries::RunQuery.call(alert.query, variables_hash: { current_user_email: slack_user['email'] })
|
68
|
+
else
|
69
|
+
Queries::RunQuery.call(alert.query)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/motor/alerts.rb
CHANGED
@@ -2,9 +2,51 @@
|
|
2
2
|
|
3
3
|
module Motor
|
4
4
|
module Alerts
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def send_alert(alert)
|
8
|
+
if alert.preferences[:adapter] == 'slack'
|
9
|
+
send_slack_alert(alert)
|
10
|
+
else
|
11
|
+
send_email_alert(alert)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_email_alert(alert)
|
16
|
+
alert.to_emails.split(',').each do |email|
|
17
|
+
if email.include?('@')
|
18
|
+
Motor::AlertsMailer.alert_email(alert, email: email).deliver_now!
|
19
|
+
else
|
20
|
+
send_query_email_alerts(alert, email.gsub(/[{}]/, ''))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_query_email_alerts(alert, column_name)
|
26
|
+
result = Queries::RunQuery.call(alert.query)
|
27
|
+
|
28
|
+
column_index = result.columns.find_index { |c| c[:name] == column_name }
|
29
|
+
|
30
|
+
emails = result.data.map { |row| row[column_index] }.uniq
|
31
|
+
|
32
|
+
emails.each do |email|
|
33
|
+
Motor::AlertsMailer.alert_email(alert, email: email).deliver_now!
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def send_slack_alert(alert)
|
38
|
+
slack_users = Slack::Client.load_users(limit: Slack::ITEMS_LIMIT)['members']
|
39
|
+
|
40
|
+
alert.preferences[:slack_conversation_ids].each do |conversation_id|
|
41
|
+
user = slack_users.find { |u| u['id'] == conversation_id }
|
42
|
+
|
43
|
+
Motor::Alerts::SlackSender.call(alert, conversation_id, slack_user: user)
|
44
|
+
end
|
45
|
+
end
|
5
46
|
end
|
6
47
|
end
|
7
48
|
|
8
49
|
require_relative './alerts/scheduler'
|
9
50
|
require_relative './alerts/scheduled_alerts_cache'
|
10
51
|
require_relative './alerts/persistance'
|
52
|
+
require_relative './alerts/slack_sender'
|
@@ -14,6 +14,7 @@ module Motor
|
|
14
14
|
model[:associations] = filter_associations(model[:associations], ability)
|
15
15
|
model[:columns] = filter_columns(klass, model[:columns], ability)
|
16
16
|
model[:actions] = filter_actions(klass, model[:actions], ability)
|
17
|
+
model[:tabs] = filter_tabs(klass, model[:tabs], ability)
|
17
18
|
|
18
19
|
model
|
19
20
|
end.compact
|
@@ -27,6 +28,13 @@ module Motor
|
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
31
|
+
def filter_tabs(_model, tabs, ability)
|
32
|
+
tabs = tabs.reject { |t| t[:name] == 'audits' } unless ability.can?(:read, Motor::Audit)
|
33
|
+
tabs = tabs.reject { |t| t[:name] == 'notes' } unless ability.can?(:read, Motor::Note)
|
34
|
+
|
35
|
+
tabs
|
36
|
+
end
|
37
|
+
|
30
38
|
def filter_columns(model, columns, ability)
|
31
39
|
columns.map do |column|
|
32
40
|
next unless ability.can?(:read, model, column[:name])
|
@@ -34,7 +34,6 @@ module Motor
|
|
34
34
|
}
|
35
35
|
].freeze
|
36
36
|
end
|
37
|
-
# rubocop:enable Metrics/MethodLength
|
38
37
|
|
39
38
|
def tabs
|
40
39
|
[
|
@@ -44,9 +43,24 @@ module Motor
|
|
44
43
|
tab_type: BuildSchema::DEFAULT_TYPE,
|
45
44
|
preferences: {},
|
46
45
|
visible: true
|
46
|
+
},
|
47
|
+
{
|
48
|
+
name: 'audits',
|
49
|
+
display_name: I18n.t('motor.audits'),
|
50
|
+
tab_type: BuildSchema::DEFAULT_TYPE,
|
51
|
+
preferences: {},
|
52
|
+
visible: true
|
53
|
+
},
|
54
|
+
{
|
55
|
+
name: 'notes',
|
56
|
+
display_name: I18n.t('motor.notes'),
|
57
|
+
tab_type: BuildSchema::DEFAULT_TYPE,
|
58
|
+
preferences: {},
|
59
|
+
visible: true
|
47
60
|
}
|
48
61
|
].freeze
|
49
62
|
end
|
63
|
+
# rubocop:enable Metrics/MethodLength
|
50
64
|
end
|
51
65
|
end
|
52
66
|
end
|
@@ -79,7 +79,10 @@ module Motor
|
|
79
79
|
searchable_columns: FindSearchableColumns.call(model),
|
80
80
|
custom_sql: nil,
|
81
81
|
visible: true,
|
82
|
-
display_primary_key: true
|
82
|
+
display_primary_key: true,
|
83
|
+
preferences: {
|
84
|
+
display_as: 'table'
|
85
|
+
}
|
83
86
|
}.with_indifferent_access
|
84
87
|
end
|
85
88
|
# rubocop:enable Metrics/MethodLength
|
data/lib/motor/build_schema.rb
CHANGED
@@ -102,6 +102,13 @@ module Motor
|
|
102
102
|
|
103
103
|
ReorderSchema.call(schema, cache_keys)
|
104
104
|
end
|
105
|
+
|
106
|
+
def for_model(model)
|
107
|
+
schema = Motor::BuildSchema::LoadFromRails.build_model_schema(model)
|
108
|
+
configs = Motor::Resource.find_by(name: schema[:name]).preferences
|
109
|
+
|
110
|
+
MergeSchemaConfigs.merge_model(schema, configs)
|
111
|
+
end
|
105
112
|
end
|
106
113
|
end
|
107
114
|
|
@@ -38,25 +38,25 @@ module Motor
|
|
38
38
|
def build_data(cache_keys = {}, current_user = nil, current_ability = nil)
|
39
39
|
configs_cache_key = cache_keys[:configs]
|
40
40
|
|
41
|
-
{
|
42
|
-
|
43
|
-
current_user: current_user&.as_json(only: %i[id email]),
|
41
|
+
{ version: Motor::VERSION,
|
42
|
+
current_user: serialize_current_user(current_user),
|
44
43
|
current_rules: current_ability.serialized_rules,
|
45
44
|
audits_count: Motor::Audit.count,
|
46
45
|
i18n: i18n_data,
|
47
46
|
base_path: Motor::Admin.routes.url_helpers.motor_path,
|
47
|
+
cabel_path: Motor::Admin.routes.url_helpers.try(:motor_cabel_path),
|
48
48
|
admin_settings_path: Rails.application.routes.url_helpers.try(:admin_settings_general_path),
|
49
49
|
schema: Motor::BuildSchema.call(cache_keys, current_ability),
|
50
|
-
header_links: header_links_data_hash(configs_cache_key),
|
50
|
+
header_links: header_links_data_hash(current_user, current_ability, configs_cache_key),
|
51
51
|
homepage_layout: homepage_layout_data_hash(configs_cache_key),
|
52
|
+
databases: database_names,
|
52
53
|
queries: queries_data_hash(build_cache_key(cache_keys, :queries, current_user, current_ability),
|
53
54
|
current_ability),
|
54
55
|
dashboards: dashboards_data_hash(build_cache_key(cache_keys, :dashboards, current_user, current_ability),
|
55
56
|
current_ability),
|
56
57
|
alerts: alerts_data_hash(build_cache_key(cache_keys, :alerts, current_user, current_ability),
|
57
58
|
current_ability),
|
58
|
-
forms: forms_data_hash(build_cache_key(cache_keys, :forms, current_user, current_ability), current_ability)
|
59
|
-
}
|
59
|
+
forms: forms_data_hash(build_cache_key(cache_keys, :forms, current_user, current_ability), current_ability) }
|
60
60
|
end
|
61
61
|
# rubocop:enable Metrics/AbcSize
|
62
62
|
|
@@ -64,15 +64,68 @@ module Motor
|
|
64
64
|
I18n.t('motor', default: I18n.t('motor', locale: :en))
|
65
65
|
end
|
66
66
|
|
67
|
+
def serialize_current_user(user)
|
68
|
+
return unless user
|
69
|
+
|
70
|
+
attrs = user.as_json(only: %i[id email first_name last_name])
|
71
|
+
|
72
|
+
attrs['role'] = user.role if user.respond_to?(:role)
|
73
|
+
attrs['role_names'] = user.role_names if user.respond_to?(:role_names)
|
74
|
+
|
75
|
+
attrs
|
76
|
+
end
|
77
|
+
|
67
78
|
# @return [String]
|
68
79
|
def build_cache_key(cache_keys, key, current_user, current_ability)
|
69
80
|
"#{cache_keys[key].hash}#{current_user&.id}#{current_ability&.rules_hash}"
|
70
81
|
end
|
71
82
|
|
72
|
-
def header_links_data_hash(cache_key = nil)
|
83
|
+
def header_links_data_hash(current_user, current_ability, cache_key = nil)
|
73
84
|
configs = Motor::Configs::LoadFromCache.load_configs(cache_key: cache_key)
|
74
85
|
|
75
|
-
configs.find { |c| c.key == 'header.links' }&.value || []
|
86
|
+
links = configs.find { |c| c.key == 'header.links' }&.value || []
|
87
|
+
links = add_default_links(links)
|
88
|
+
|
89
|
+
return links unless current_user
|
90
|
+
return links if current_ability.can?(:manage, :all)
|
91
|
+
|
92
|
+
filter_links_for_user(current_user, links)
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_default_links(links)
|
96
|
+
new_links = links.clone
|
97
|
+
|
98
|
+
unless links.find { |l| l['link_type'] == 'forms' }
|
99
|
+
new_links.unshift({ 'name' => I18n.t('motor.forms'), 'link_type' => 'forms' })
|
100
|
+
end
|
101
|
+
|
102
|
+
unless links.find { |l| l['link_type'] == 'reports' }
|
103
|
+
new_links.unshift({ 'name' => I18n.t('motor.reports'), 'link_type' => 'reports' })
|
104
|
+
end
|
105
|
+
|
106
|
+
new_links
|
107
|
+
end
|
108
|
+
|
109
|
+
def filter_links_for_user(current_user, links)
|
110
|
+
links.select do |link|
|
111
|
+
conditions = link['conditions']
|
112
|
+
|
113
|
+
next true if conditions.blank?
|
114
|
+
|
115
|
+
conditions.all? do |cond|
|
116
|
+
field_name =
|
117
|
+
if cond['field'] == 'role' && !current_user.respond_to?(:role)
|
118
|
+
:role_names
|
119
|
+
else
|
120
|
+
cond['field'].to_sym
|
121
|
+
end
|
122
|
+
|
123
|
+
next false unless field_name.in?(%i[email role role_names])
|
124
|
+
next false unless current_user.respond_to?(field_name)
|
125
|
+
|
126
|
+
Array.wrap(current_user.public_send(field_name)).intersection(Array.wrap(cond['value'])).present?
|
127
|
+
end
|
128
|
+
end
|
76
129
|
end
|
77
130
|
|
78
131
|
def homepage_layout_data_hash(cache_key = nil)
|
@@ -81,6 +134,18 @@ module Motor
|
|
81
134
|
configs.find { |c| c.key == 'homepage.layout' }&.value || []
|
82
135
|
end
|
83
136
|
|
137
|
+
def database_names
|
138
|
+
if defined?(Motor::EncryptedConfig)
|
139
|
+
Motor::DatabaseClasses.constants.map { |e| e.to_s.titleize }
|
140
|
+
elsif ActiveRecord::Base.configurations.try(:configurations)
|
141
|
+
ActiveRecord::Base.configurations.configurations
|
142
|
+
.select { |c| c.env_name == Rails.env }
|
143
|
+
.map { |c| c.try(:name) || c.try(:spec_name) }.compact
|
144
|
+
else
|
145
|
+
['primary']
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
84
149
|
def queries_data_hash(cache_key = nil, current_ability = nil)
|
85
150
|
Motor::Configs::LoadFromCache.load_queries(cache_key: cache_key, current_ability: current_ability)
|
86
151
|
.as_json(only: %i[id name updated_at],
|
data/lib/motor/configs.rb
CHANGED
@@ -5,8 +5,7 @@ module Motor
|
|
5
5
|
FILE_PATH = 'config/motor.yml'
|
6
6
|
SYNC_API_PATH = '/motor_configs_sync'
|
7
7
|
SYNC_ACCESS_KEY = ENV.fetch('MOTOR_SYNC_API_KEY', '')
|
8
|
-
|
9
|
-
PWD_FILE_NAME = 'motor-admin.yml'
|
8
|
+
WORKDIR_FILE_NAME = 'motor-admin.yml'
|
10
9
|
|
11
10
|
module_function
|
12
11
|
|
@@ -22,8 +21,8 @@ module Motor
|
|
22
21
|
|
23
22
|
# @return [String]
|
24
23
|
def file_path
|
25
|
-
if
|
26
|
-
[ENV['
|
24
|
+
if defined?(MotorAdmin::Application)
|
25
|
+
[ENV['WORKDIR'], WORKDIR_FILE_NAME].compact.join('/')
|
27
26
|
else
|
28
27
|
Rails.root.join(FILE_PATH).to_s
|
29
28
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Notes
|
5
|
+
module NotifyMentions
|
6
|
+
EMAIL_REGEXP =
|
7
|
+
/[a-z0-9][.']?(?:(?:[a-z0-9_-]++[.'])*[a-z0-9_-]++)*@(?:[a-z0-9]++[.-])*[a-z0-9]++\.[a-z]{2,}/i.freeze
|
8
|
+
NOTIFICATION_DESCRIPTION_LIMIT = 100
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def call(note, current_user)
|
13
|
+
users_class = fetch_users_class(current_user)
|
14
|
+
|
15
|
+
return unless users_class
|
16
|
+
|
17
|
+
emails = note.body.scan(EMAIL_REGEXP)
|
18
|
+
emails -= [current_user&.email]
|
19
|
+
|
20
|
+
users_class.where(email: emails).each do |user|
|
21
|
+
notification = find_or_build_notification(note, user, current_user)
|
22
|
+
|
23
|
+
next if notification.persisted?
|
24
|
+
|
25
|
+
notification.save!
|
26
|
+
|
27
|
+
Motor::NotificationsMailer.notify_mention_email(notification).deliver_later!
|
28
|
+
Motor::NotificationsChannel.broadcast_to(user, ['notify', notification.as_json(include: %i[record])])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_or_build_notification(note, user, current_user)
|
33
|
+
notification = note.notifications.find { |e| e.recipient_id == user.id && e.recipient_type == user.class.name }
|
34
|
+
|
35
|
+
return notification if notification
|
36
|
+
|
37
|
+
Motor::Notification.new(
|
38
|
+
title: build_mention_title(note),
|
39
|
+
description: build_mention_description(note, current_user),
|
40
|
+
recipient: user,
|
41
|
+
record: note
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_mention_title(note)
|
46
|
+
configs = Motor::BuildSchema.for_model(note.record.class)
|
47
|
+
|
48
|
+
display_value = note.record.attributes[configs['display_column']]
|
49
|
+
|
50
|
+
I18n.t('new_mention_for',
|
51
|
+
resource: ["#{configs['display_name'].singularize} ##{note.record[note.record.class.primary_key]}",
|
52
|
+
display_value].join(' '),
|
53
|
+
scope: :motor)
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_mention_description(note, current_user)
|
57
|
+
I18n.t('user_mentioned_you_with_note',
|
58
|
+
user: current_user&.email || 'Anonymous',
|
59
|
+
note: note.body.truncate(NOTIFICATION_DESCRIPTION_LIMIT),
|
60
|
+
scope: :motor)
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_users_class(current_user)
|
64
|
+
return current_user.class if current_user
|
65
|
+
return Motor::AdminUser if defined?(Motor::AdminUser)
|
66
|
+
return AdminUser if defined?(AdminUser)
|
67
|
+
return User if defined?(User)
|
68
|
+
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Notes
|
5
|
+
module NotifyReminder
|
6
|
+
NOTIFICATION_DESCRIPTION_LIMIT = 100
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def call(reminder)
|
11
|
+
notification = find_or_build_notification(reminder)
|
12
|
+
|
13
|
+
return if notification.persisted?
|
14
|
+
|
15
|
+
notification.save!
|
16
|
+
|
17
|
+
Motor::NotificationsMailer.notify_reminder_email(notification).deliver_later!
|
18
|
+
Motor::NotificationsChannel.broadcast_to(reminder.recipient,
|
19
|
+
['notify', notification.as_json(include: %i[record])])
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_or_build_notification(reminder)
|
23
|
+
notification = reminder.notifications.take
|
24
|
+
|
25
|
+
return notification if notification
|
26
|
+
|
27
|
+
note = reminder.record
|
28
|
+
|
29
|
+
Motor::Notification.new(
|
30
|
+
title: build_notification_title(note),
|
31
|
+
description: note.body.truncate(NOTIFICATION_DESCRIPTION_LIMIT),
|
32
|
+
recipient: reminder.recipient,
|
33
|
+
record: reminder
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_notification_title(note)
|
38
|
+
configs = Motor::BuildSchema.for_model(note.record.class)
|
39
|
+
|
40
|
+
display_value = note.record.attributes[configs['display_column']]
|
41
|
+
|
42
|
+
I18n.t('new_reminder_for',
|
43
|
+
resource: ["#{configs['display_name'].singularize} ##{note.record[note.record.class.primary_key]}",
|
44
|
+
display_value].join(' '),
|
45
|
+
scope: :motor)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Notes
|
5
|
+
module Persist
|
6
|
+
TAG_REGEXP = /#\w+?\b/.freeze
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def call(note, current_user)
|
11
|
+
note.author ||= current_user
|
12
|
+
|
13
|
+
tags = parse_tag_names(note)
|
14
|
+
|
15
|
+
Motor::Notes::Tags.assign_tags(note, tags)
|
16
|
+
|
17
|
+
note.save!
|
18
|
+
|
19
|
+
broadcast_note(note)
|
20
|
+
|
21
|
+
note
|
22
|
+
end
|
23
|
+
|
24
|
+
def broadcast_note(note)
|
25
|
+
Motor::NotesChannel.broadcast_to(
|
26
|
+
note.values_at(:record_type, :record_id).join(':'),
|
27
|
+
['update', note.as_json(include: %i[author tags reminders])]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_tag_names(note)
|
32
|
+
note.body.scan(TAG_REGEXP).map { |tag| tag[1..] }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Notes
|
5
|
+
module RemindersScheduler
|
6
|
+
SCHEDULER_INTERVAL = 1.minute
|
7
|
+
CHECK_BEHIND_DURATION = 6.hours
|
8
|
+
|
9
|
+
SCHEDULER_TASK = Concurrent::TimerTask.new(
|
10
|
+
execution_interval: SCHEDULER_INTERVAL
|
11
|
+
) { Motor::Notes::RemindersScheduler.call }
|
12
|
+
|
13
|
+
REMINDER_NOTIFICATIONS_JOIN_SQL = <<~SQL.squish
|
14
|
+
LEFT JOIN motor_notifications
|
15
|
+
ON motor_notifications.record_id = motor_reminders.id
|
16
|
+
AND motor_notifications.record_type = 'Motor::Reminder'
|
17
|
+
SQL
|
18
|
+
|
19
|
+
module_function
|
20
|
+
|
21
|
+
def call
|
22
|
+
ActiveRecord::Base.logger.silence do
|
23
|
+
load_reminders.each do |reminder|
|
24
|
+
::Motor::NotifyReminderJob.perform_later(reminder)
|
25
|
+
rescue StandardError => e
|
26
|
+
Rails.logger.error(e)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_reminders
|
32
|
+
::Motor::Reminder
|
33
|
+
.joins(REMINDER_NOTIFICATIONS_JOIN_SQL)
|
34
|
+
.where(scheduled_at: CHECK_BEHIND_DURATION.ago..Time.current.end_of_minute)
|
35
|
+
.where(motor_notifications: { id: nil })
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Notes
|
5
|
+
module Tags
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def assign_tags(note, tags)
|
9
|
+
return note if tags.blank?
|
10
|
+
|
11
|
+
tags.each do |tag_name|
|
12
|
+
next if note.note_tag_tags.find { |tt| tt.tag.name.casecmp(tag_name).zero? }
|
13
|
+
|
14
|
+
tag = NoteTag.find_or_initialize_by(name: tag_name)
|
15
|
+
|
16
|
+
note.note_tag_tags.new(tag: tag)
|
17
|
+
end
|
18
|
+
|
19
|
+
remove_missing_tags(note, tags) if note.persisted?
|
20
|
+
|
21
|
+
note
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_missing_tags(note, tags)
|
25
|
+
downcase_tags = tags.map(&:downcase)
|
26
|
+
tags_to_remove = note.tags.reject { |tt| tt.name.downcase.in?(downcase_tags) }
|
27
|
+
|
28
|
+
note.tags -= tags_to_remove
|
29
|
+
|
30
|
+
note
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/motor/notes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Notes
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require_relative './notes/tags'
|
9
|
+
require_relative './notes/persist'
|
10
|
+
require_relative './notes/notify_mentions'
|
11
|
+
require_relative './notes/notify_reminder'
|
12
|
+
require_relative './notes/reminders_scheduler'
|