motor-admin 0.3.16 → 0.4.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.
- 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/sessions_controller.rb +3 -0
- 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-99bab2664944ee03d10f.css.gz} +0 -0
- data/ui/dist/main-99bab2664944ee03d10f.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'
|