motor-admin-cstham8 0.4.35
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 +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
|