motor-admin-cstham8 0.4.35
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +230 -0
- data/Rakefile +11 -0
- 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_ability.rb +21 -0
- data/app/controllers/concerns/motor/current_user_method.rb +18 -0
- data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +73 -0
- data/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
- data/app/controllers/motor/active_storage_attachments_controller.rb +64 -0
- data/app/controllers/motor/alerts_controller.rb +82 -0
- data/app/controllers/motor/api_base_controller.rb +33 -0
- data/app/controllers/motor/api_configs_controller.rb +54 -0
- data/app/controllers/motor/application_controller.rb +8 -0
- data/app/controllers/motor/assets_controller.rb +43 -0
- data/app/controllers/motor/audits_controller.rb +16 -0
- data/app/controllers/motor/auth_tokens_controller.rb +36 -0
- data/app/controllers/motor/configs_controller.rb +33 -0
- data/app/controllers/motor/dashboards_controller.rb +64 -0
- data/app/controllers/motor/data_controller.rb +88 -0
- data/app/controllers/motor/forms_controller.rb +61 -0
- data/app/controllers/motor/icons_controller.rb +22 -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/queries_controller.rb +64 -0
- data/app/controllers/motor/reminders_controller.rb +38 -0
- data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
- data/app/controllers/motor/resource_methods_controller.rb +23 -0
- data/app/controllers/motor/resources_controller.rb +26 -0
- data/app/controllers/motor/run_api_requests_controller.rb +56 -0
- data/app/controllers/motor/run_graphql_requests_controller.rb +48 -0
- data/app/controllers/motor/run_queries_controller.rb +77 -0
- data/app/controllers/motor/schema_controller.rb +31 -0
- data/app/controllers/motor/send_alerts_controller.rb +26 -0
- data/app/controllers/motor/sessions_controller.rb +23 -0
- data/app/controllers/motor/slack_conversations_controller.rb +11 -0
- data/app/controllers/motor/tags_controller.rb +11 -0
- data/app/controllers/motor/ui_controller.rb +51 -0
- data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
- data/app/jobs/motor/alert_sending_job.rb +13 -0
- data/app/jobs/motor/application_job.rb +6 -0
- 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 +39 -0
- data/app/mailers/motor/application_mailer.rb +33 -0
- data/app/mailers/motor/notifications_mailer.rb +33 -0
- data/app/models/motor/alert.rb +30 -0
- data/app/models/motor/alert_lock.rb +7 -0
- data/app/models/motor/api_config.rb +28 -0
- data/app/models/motor/application_record.rb +18 -0
- data/app/models/motor/audit.rb +13 -0
- data/app/models/motor/config.rb +13 -0
- data/app/models/motor/dashboard.rb +26 -0
- data/app/models/motor/form.rb +23 -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/query.rb +33 -0
- data/app/models/motor/reminder.rb +13 -0
- data/app/models/motor/resource.rb +15 -0
- data/app/models/motor/tag.rb +7 -0
- data/app/models/motor/taggable_tag.rb +8 -0
- data/app/views/layouts/motor/application.html.erb +17 -0
- data/app/views/layouts/motor/mailer.html.erb +72 -0
- data/app/views/motor/alerts_mailer/alert_email.html.erb +54 -0
- 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/app/views/motor/ui/show.html.erb +1 -0
- data/config/locales/el.yml +420 -0
- data/config/locales/en.yml +340 -0
- data/config/locales/es.yml +420 -0
- data/config/locales/ja.yml +340 -0
- data/config/locales/pt.yml +416 -0
- data/config/routes.rb +65 -0
- data/lib/generators/motor/install_generator.rb +24 -0
- data/lib/generators/motor/install_notes_generator.rb +22 -0
- data/lib/generators/motor/migration.rb +17 -0
- data/lib/generators/motor/templates/install.rb +271 -0
- data/lib/generators/motor/templates/install_api_configs.rb +86 -0
- data/lib/generators/motor/templates/install_notes.rb +83 -0
- data/lib/generators/motor/templates/upgrade_motor_api_actions.rb +71 -0
- data/lib/generators/motor/upgrade_generator.rb +43 -0
- data/lib/motor/active_record_utils/action_text_attribute_patch.rb +19 -0
- data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +14 -0
- data/lib/motor/active_record_utils/active_record_filter.rb +405 -0
- data/lib/motor/active_record_utils/active_storage_blob_patch.rb +30 -0
- data/lib/motor/active_record_utils/active_storage_links_extension.rb +11 -0
- data/lib/motor/active_record_utils/defined_scopes_extension.rb +25 -0
- data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
- data/lib/motor/active_record_utils/types.rb +64 -0
- data/lib/motor/active_record_utils.rb +45 -0
- data/lib/motor/admin.rb +141 -0
- data/lib/motor/alerts/persistance.rb +97 -0
- data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
- data/lib/motor/alerts/scheduler.rb +30 -0
- data/lib/motor/alerts/slack_sender.rb +74 -0
- data/lib/motor/alerts.rb +52 -0
- data/lib/motor/api_configs.rb +41 -0
- data/lib/motor/api_query/apply_scope.rb +44 -0
- data/lib/motor/api_query/build_json.rb +171 -0
- data/lib/motor/api_query/build_meta.rb +20 -0
- data/lib/motor/api_query/filter.rb +125 -0
- data/lib/motor/api_query/paginate.rb +19 -0
- data/lib/motor/api_query/search.rb +60 -0
- data/lib/motor/api_query/sort.rb +64 -0
- data/lib/motor/api_query.rb +24 -0
- data/lib/motor/assets.rb +62 -0
- data/lib/motor/build_schema/active_storage_attachment_schema.rb +125 -0
- data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
- data/lib/motor/build_schema/apply_permissions.rb +64 -0
- data/lib/motor/build_schema/defaults.rb +66 -0
- data/lib/motor/build_schema/find_display_column.rb +65 -0
- data/lib/motor/build_schema/find_icon.rb +135 -0
- data/lib/motor/build_schema/find_searchable_columns.rb +33 -0
- data/lib/motor/build_schema/load_from_rails.rb +361 -0
- data/lib/motor/build_schema/merge_schema_configs.rb +157 -0
- data/lib/motor/build_schema/reorder_schema.rb +88 -0
- data/lib/motor/build_schema/utils.rb +31 -0
- data/lib/motor/build_schema.rb +125 -0
- data/lib/motor/cancan_utils/ability_patch.rb +31 -0
- data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
- data/lib/motor/cancan_utils.rb +9 -0
- data/lib/motor/configs/build_configs_hash.rb +90 -0
- data/lib/motor/configs/build_ui_app_tag.rb +177 -0
- data/lib/motor/configs/load_from_cache.rb +110 -0
- data/lib/motor/configs/sync_from_file.rb +35 -0
- data/lib/motor/configs/sync_from_hash.rb +159 -0
- data/lib/motor/configs/sync_middleware.rb +72 -0
- data/lib/motor/configs/sync_with_remote.rb +47 -0
- data/lib/motor/configs/write_to_file.rb +36 -0
- data/lib/motor/configs.rb +39 -0
- data/lib/motor/dashboards/persistance.rb +73 -0
- data/lib/motor/dashboards.rb +8 -0
- data/lib/motor/forms/persistance.rb +93 -0
- data/lib/motor/forms.rb +8 -0
- data/lib/motor/hash_serializer.rb +21 -0
- data/lib/motor/net_http_utils.rb +50 -0
- data/lib/motor/notes/notify_mentions.rb +71 -0
- data/lib/motor/notes/notify_reminder.rb +48 -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/persistance.rb +90 -0
- data/lib/motor/queries/postgresql_exec_query.rb +28 -0
- data/lib/motor/queries/render_sql_template.rb +61 -0
- data/lib/motor/queries/run_query.rb +289 -0
- data/lib/motor/queries.rb +11 -0
- data/lib/motor/railtie.rb +11 -0
- data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
- data/lib/motor/resources/fetch_configured_model.rb +269 -0
- data/lib/motor/resources/persist_configs.rb +232 -0
- data/lib/motor/resources.rb +19 -0
- data/lib/motor/slack/client.rb +62 -0
- data/lib/motor/slack.rb +16 -0
- data/lib/motor/tags.rb +32 -0
- data/lib/motor/tasks/motor.rake +54 -0
- data/lib/motor/version.rb +5 -0
- data/lib/motor-admin-cstham8.rb +3 -0
- data/lib/motor.rb +87 -0
- data/ui/dist/manifest.json +1990 -0
- metadata +303 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Dashboards
|
5
|
+
module Persistance
|
6
|
+
TitleAlreadyExists = Class.new(StandardError)
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def build_from_params(params, current_user = nil)
|
11
|
+
dashboard = assign_attributes(Dashboard.new, params)
|
12
|
+
|
13
|
+
dashboard.author = current_user
|
14
|
+
|
15
|
+
dashboard
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_from_params!(params, current_user = nil)
|
19
|
+
raise TitleAlreadyExists if Dashboard.exists?(title: params[:title])
|
20
|
+
|
21
|
+
dashboard = build_from_params(params, current_user)
|
22
|
+
|
23
|
+
ApplicationRecord.transaction do
|
24
|
+
dashboard.save!
|
25
|
+
end
|
26
|
+
|
27
|
+
dashboard
|
28
|
+
rescue ActiveRecord::RecordNotUnique
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_from_params!(dashboard, params, force_replace: false)
|
33
|
+
tag_ids = dashboard.tags.ids
|
34
|
+
|
35
|
+
dashboard = assign_attributes(dashboard, params)
|
36
|
+
|
37
|
+
raise TitleAlreadyExists if !force_replace && title_already_exists?(dashboard)
|
38
|
+
|
39
|
+
ApplicationRecord.transaction do
|
40
|
+
archive_with_existing_name(dashboard) if force_replace
|
41
|
+
|
42
|
+
dashboard.save!
|
43
|
+
end
|
44
|
+
|
45
|
+
dashboard.touch if tag_ids.sort != dashboard.tags.reload.ids.sort && params[:updated_at].blank?
|
46
|
+
|
47
|
+
dashboard
|
48
|
+
rescue ActiveRecord::RecordNotUnique
|
49
|
+
retry
|
50
|
+
end
|
51
|
+
|
52
|
+
def assign_attributes(dashboard, params)
|
53
|
+
dashboard.assign_attributes(params.slice(:title, :description, :preferences))
|
54
|
+
dashboard.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
|
55
|
+
|
56
|
+
Motor::Tags.assign_tags(dashboard, params[:tags])
|
57
|
+
end
|
58
|
+
|
59
|
+
def archive_with_existing_name(dashboard)
|
60
|
+
Motor::Dashboard.where(['title = ? AND id != ?', dashboard.title, dashboard.id])
|
61
|
+
.update_all(deleted_at: Time.current)
|
62
|
+
end
|
63
|
+
|
64
|
+
def title_already_exists?(dashboard)
|
65
|
+
if dashboard.new_record?
|
66
|
+
Motor::Dashboard.exists?(title: dashboard.title, deleted_at: nil)
|
67
|
+
else
|
68
|
+
Motor::Dashboard.exists?(['title = ? AND id != ? AND deleted_at IS NULL', dashboard.title, dashboard.id])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Forms
|
5
|
+
module Persistance
|
6
|
+
NameAlreadyExists = Class.new(StandardError)
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def build_from_params(params, current_user = nil)
|
11
|
+
form = assign_attributes(Form.new, params)
|
12
|
+
|
13
|
+
form.author = current_user
|
14
|
+
|
15
|
+
form
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_from_params!(params, current_user = nil)
|
19
|
+
raise NameAlreadyExists if Form.exists?(name: params[:name])
|
20
|
+
|
21
|
+
form = build_from_params(params, current_user)
|
22
|
+
|
23
|
+
ApplicationRecord.transaction do
|
24
|
+
form.save!
|
25
|
+
end
|
26
|
+
|
27
|
+
form
|
28
|
+
rescue ActiveRecord::RecordNotUnique
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_from_params!(form, params, force_replace: false)
|
33
|
+
tag_ids = form.tags.ids
|
34
|
+
|
35
|
+
form = assign_attributes(form, params)
|
36
|
+
|
37
|
+
raise NameAlreadyExists if !force_replace && name_already_exists?(form)
|
38
|
+
|
39
|
+
ApplicationRecord.transaction do
|
40
|
+
archive_with_existing_name(form) if force_replace
|
41
|
+
find_or_assign_api_config(form)
|
42
|
+
|
43
|
+
form.save!
|
44
|
+
end
|
45
|
+
|
46
|
+
form.touch if tag_ids.sort != form.tags.reload.ids.sort && params[:updated_at].blank?
|
47
|
+
|
48
|
+
form
|
49
|
+
rescue ActiveRecord::RecordNotUnique
|
50
|
+
retry
|
51
|
+
end
|
52
|
+
|
53
|
+
def assign_attributes(form, params)
|
54
|
+
form.assign_attributes(params.slice(:name, :description, :api_path, :http_method, :preferences,
|
55
|
+
:api_config_name))
|
56
|
+
form.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
|
57
|
+
|
58
|
+
find_or_assign_api_config(form)
|
59
|
+
|
60
|
+
Motor::Tags.assign_tags(form, params[:tags])
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_or_assign_api_config(form)
|
64
|
+
return if form.api_config.present?
|
65
|
+
|
66
|
+
config = Motor::ApiConfig.find_by(url: [form.api_config_name, form.api_config_name.delete_suffix('/')])
|
67
|
+
|
68
|
+
if config
|
69
|
+
config.update!(deleted_at: nil)
|
70
|
+
|
71
|
+
form.api_config_name = config.name
|
72
|
+
else
|
73
|
+
form.api_config = Motor::ApiConfig.new(url: form.api_config_name.delete_suffix('/')).tap do |c|
|
74
|
+
c.name = c.url.sub(%r{\Ahttps?://}, '').delete_suffix('/')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def archive_with_existing_name(form)
|
80
|
+
Motor::Form.where(['name = ? AND id != ?', form.name, form.id])
|
81
|
+
.update_all(deleted_at: Time.current)
|
82
|
+
end
|
83
|
+
|
84
|
+
def name_already_exists?(form)
|
85
|
+
if form.new_record?
|
86
|
+
Motor::Form.exists?(['name = ? AND deleted_at IS NULL', form.name])
|
87
|
+
else
|
88
|
+
Motor::Form.exists?(['name = ? AND id != ? AND deleted_at IS NULL', form.name, form.id])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/motor/forms.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
class HashSerializer
|
5
|
+
def self.dump(hash)
|
6
|
+
hash.to_json
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.load(hash)
|
10
|
+
return hash unless hash
|
11
|
+
|
12
|
+
hash = JSON.parse(hash.presence || '{}') if hash.is_a?(String)
|
13
|
+
|
14
|
+
if hash.is_a?(Hash)
|
15
|
+
hash.with_indifferent_access
|
16
|
+
else
|
17
|
+
hash.is_a?(FalseClass) ? hash : hash || {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module NetHttpUtils
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def get(url, params = {}, headers = {}, _body = nil)
|
8
|
+
request = build_request(Net::HTTP::Get, url, params, headers, nil)
|
9
|
+
|
10
|
+
execute_request(request)
|
11
|
+
end
|
12
|
+
|
13
|
+
def post(url, params = {}, headers = {}, body = '')
|
14
|
+
request = build_request(Net::HTTP::Post, url, params, headers, body)
|
15
|
+
|
16
|
+
execute_request(request)
|
17
|
+
end
|
18
|
+
|
19
|
+
def put(url, params = {}, headers = {}, body = '')
|
20
|
+
request = build_request(Net::HTTP::Put, url, params, headers, body)
|
21
|
+
|
22
|
+
execute_request(request)
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(url, params = {}, headers = {}, body = '')
|
26
|
+
request = build_request(Net::HTTP::Delete, url, params, headers, body)
|
27
|
+
|
28
|
+
execute_request(request)
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_request(method_class, url, params, headers, body)
|
32
|
+
uri = URI(url)
|
33
|
+
uri.query = params.to_query if params.present?
|
34
|
+
|
35
|
+
request = method_class.new(uri)
|
36
|
+
request.body = body if body.present?
|
37
|
+
headers.each { |key, value| request[key] = value }
|
38
|
+
|
39
|
+
request
|
40
|
+
end
|
41
|
+
|
42
|
+
def execute_request(request)
|
43
|
+
uri = request.uri
|
44
|
+
|
45
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.port == 443) do |http|
|
46
|
+
http.request(request)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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).find_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('motor.new_mention_for',
|
51
|
+
resource: ["#{configs['display_name'].singularize} ##{note.record[note.record.class.primary_key]}",
|
52
|
+
display_value].join(' '))
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_mention_description(note, current_user)
|
56
|
+
I18n.t('motor.user_mentioned_you_with_note',
|
57
|
+
user: current_user&.email || 'Anonymous',
|
58
|
+
note: note.body.truncate(NOTIFICATION_DESCRIPTION_LIMIT))
|
59
|
+
end
|
60
|
+
|
61
|
+
def fetch_users_class(current_user)
|
62
|
+
return current_user.class if current_user
|
63
|
+
return Motor::AdminUser if defined?(Motor::AdminUser)
|
64
|
+
return AdminUser if defined?(AdminUser)
|
65
|
+
return User if defined?(User)
|
66
|
+
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,48 @@
|
|
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('motor.new_reminder_for',
|
43
|
+
resource: ["#{configs['display_name'].singularize} ##{note.record[note.record.class.primary_key]}",
|
44
|
+
display_value].join(' '))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
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).pluck(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 cast(motor_notifications.record_id as int) = 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'
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Queries
|
5
|
+
module Persistance
|
6
|
+
NameAlreadyExists = Class.new(StandardError)
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def build_from_params(params, current_user = nil)
|
11
|
+
query = assign_attributes(Query.new, params)
|
12
|
+
|
13
|
+
query.author = current_user
|
14
|
+
|
15
|
+
query
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_from_params!(params, current_user = nil)
|
19
|
+
raise NameAlreadyExists if Query.exists?(name: params[:name])
|
20
|
+
|
21
|
+
query = build_from_params(params, current_user)
|
22
|
+
|
23
|
+
ApplicationRecord.transaction do
|
24
|
+
query.save!
|
25
|
+
end
|
26
|
+
|
27
|
+
query
|
28
|
+
rescue ActiveRecord::RecordNotUnique
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_from_params!(query, params, force_replace: false)
|
33
|
+
tag_ids = query.tags.ids
|
34
|
+
|
35
|
+
query = assign_attributes(query, params)
|
36
|
+
|
37
|
+
raise NameAlreadyExists if !force_replace && name_already_exists?(query)
|
38
|
+
|
39
|
+
ApplicationRecord.transaction do
|
40
|
+
archive_with_existing_name(query) if force_replace
|
41
|
+
assign_or_create_api_config!(query)
|
42
|
+
|
43
|
+
query.save!
|
44
|
+
end
|
45
|
+
|
46
|
+
query.touch if tag_ids.sort != query.tags.reload.ids.sort && params[:updated_at].blank?
|
47
|
+
|
48
|
+
query
|
49
|
+
rescue ActiveRecord::RecordNotUnique
|
50
|
+
retry
|
51
|
+
end
|
52
|
+
|
53
|
+
def assign_attributes(query, params)
|
54
|
+
query.assign_attributes(params.slice(:name, :description, :sql_body, :preferences))
|
55
|
+
query.updated_at = [params[:updated_at], Time.current].min if params[:updated_at].present?
|
56
|
+
|
57
|
+
Motor::Tags.assign_tags(query, params[:tags])
|
58
|
+
end
|
59
|
+
|
60
|
+
def archive_with_existing_name(query)
|
61
|
+
Motor::Query.where(['name = ? AND id != ?', query.name, query.id])
|
62
|
+
.update_all(deleted_at: Time.current)
|
63
|
+
end
|
64
|
+
|
65
|
+
def assign_or_create_api_config!(query)
|
66
|
+
api_config_name = query.preferences[:api_config_name]
|
67
|
+
|
68
|
+
return if api_config_name.blank?
|
69
|
+
return if Motor::ApiConfig.find_by(name: api_config_name)
|
70
|
+
|
71
|
+
config = Motor::ApiConfig.find_by(url: [api_config_name, api_config_name.delete_suffix('/')])
|
72
|
+
|
73
|
+
config&.update!(deleted_at: nil)
|
74
|
+
|
75
|
+
config ||= Motor::ApiConfig.create!(name: api_config_name.sub(%r{\Ahttps?://}, '').delete_suffix('/'),
|
76
|
+
url: api_config_name.delete_suffix('/'))
|
77
|
+
|
78
|
+
query.preferences[:api_config_name] = config.name
|
79
|
+
end
|
80
|
+
|
81
|
+
def name_already_exists?(query)
|
82
|
+
if query.new_record?
|
83
|
+
Query.exists?(name: query.name, deleted_at: nil)
|
84
|
+
else
|
85
|
+
Query.exists?(['name = ? AND id != ? AND deleted_at IS NULL', query.name, query.id])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Queries
|
5
|
+
module PostgresqlExecQuery
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(conn, statement)
|
9
|
+
conn.send(:execute_and_clear, *statement) do |result|
|
10
|
+
types = {}
|
11
|
+
fields = result.fields
|
12
|
+
|
13
|
+
fields.each_with_index do |fname, i|
|
14
|
+
ftype = result.ftype i
|
15
|
+
fmod = result.fmod i
|
16
|
+
types[fname] = conn.send(:get_oid_type, ftype, fmod, fname)
|
17
|
+
end
|
18
|
+
|
19
|
+
if conn.respond_to?(:build_result, true)
|
20
|
+
conn.send(:build_result, columns: fields, rows: result.values, column_types: types)
|
21
|
+
else
|
22
|
+
ActiveRecord::Result.new(fields, result.values, types)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motor
|
4
|
+
module Queries
|
5
|
+
module RenderSqlTemplate
|
6
|
+
SECTION_OPEN_REGEXP = /{{([#^])\s*(\w+)}}.*\z/m.freeze
|
7
|
+
VARIABLE_REGEXP = /{{\s*(\w+)\s*}}/m.freeze
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(sql, variables)
|
12
|
+
result = render_sections(sql, variables)
|
13
|
+
|
14
|
+
interpolate_variables(result, variables)
|
15
|
+
end
|
16
|
+
|
17
|
+
def interpolate_variables(sql, variables)
|
18
|
+
selected_variables = []
|
19
|
+
|
20
|
+
rendered =
|
21
|
+
sql.gsub(VARIABLE_REGEXP) do
|
22
|
+
variable_name = Regexp.last_match[1]
|
23
|
+
|
24
|
+
index = selected_variables.index { |name, _| name == variable_name }
|
25
|
+
variable_values = variables[variable_name]
|
26
|
+
|
27
|
+
if variable_values.is_a?(Array)
|
28
|
+
first_variable_index = selected_variables.size + 1
|
29
|
+
|
30
|
+
variable_values.each { |value| selected_variables << [variable_name, value] } unless index
|
31
|
+
|
32
|
+
(first_variable_index..selected_variables.size).map { |i| "$#{i}" }.join(', ')
|
33
|
+
else
|
34
|
+
selected_variables << [variable_name, variables[variable_name]] unless index
|
35
|
+
|
36
|
+
"$#{selected_variables.size}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
[rendered, selected_variables]
|
41
|
+
end
|
42
|
+
|
43
|
+
def render_sections(sql, variables)
|
44
|
+
sql.sub(SECTION_OPEN_REGEXP) do |e|
|
45
|
+
variable_name = Regexp.last_match[2]
|
46
|
+
is_negative = Regexp.last_match[1] == '^'
|
47
|
+
|
48
|
+
_, content, rest = e.split(build_section_close_regexp(variable_name), 3)
|
49
|
+
|
50
|
+
is_present = variables[variable_name].present?
|
51
|
+
|
52
|
+
render_sections(is_present ^ is_negative ? content + rest.to_s : rest, variables)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_section_close_regexp(variable_name)
|
57
|
+
%r{{{[#^/]s*#{Regexp.escape(variable_name)}\s*}}}m
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|