motor-admin-cstham8 0.4.35

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +661 -0
  3. data/README.md +230 -0
  4. data/Rakefile +11 -0
  5. data/app/channels/motor/application_cable/channel.rb +14 -0
  6. data/app/channels/motor/application_cable/connection.rb +27 -0
  7. data/app/channels/motor/notes_channel.rb +9 -0
  8. data/app/channels/motor/notifications_channel.rb +9 -0
  9. data/app/controllers/concerns/motor/current_ability.rb +21 -0
  10. data/app/controllers/concerns/motor/current_user_method.rb +18 -0
  11. data/app/controllers/concerns/motor/load_and_authorize_dynamic_resource.rb +73 -0
  12. data/app/controllers/concerns/motor/wrap_io_params.rb +25 -0
  13. data/app/controllers/motor/active_storage_attachments_controller.rb +64 -0
  14. data/app/controllers/motor/alerts_controller.rb +82 -0
  15. data/app/controllers/motor/api_base_controller.rb +33 -0
  16. data/app/controllers/motor/api_configs_controller.rb +54 -0
  17. data/app/controllers/motor/application_controller.rb +8 -0
  18. data/app/controllers/motor/assets_controller.rb +43 -0
  19. data/app/controllers/motor/audits_controller.rb +16 -0
  20. data/app/controllers/motor/auth_tokens_controller.rb +36 -0
  21. data/app/controllers/motor/configs_controller.rb +33 -0
  22. data/app/controllers/motor/dashboards_controller.rb +64 -0
  23. data/app/controllers/motor/data_controller.rb +88 -0
  24. data/app/controllers/motor/forms_controller.rb +61 -0
  25. data/app/controllers/motor/icons_controller.rb +22 -0
  26. data/app/controllers/motor/note_tags_controller.rb +13 -0
  27. data/app/controllers/motor/notes_controller.rb +58 -0
  28. data/app/controllers/motor/notifications_controller.rb +33 -0
  29. data/app/controllers/motor/queries_controller.rb +64 -0
  30. data/app/controllers/motor/reminders_controller.rb +38 -0
  31. data/app/controllers/motor/resource_default_queries_controller.rb +23 -0
  32. data/app/controllers/motor/resource_methods_controller.rb +23 -0
  33. data/app/controllers/motor/resources_controller.rb +26 -0
  34. data/app/controllers/motor/run_api_requests_controller.rb +56 -0
  35. data/app/controllers/motor/run_graphql_requests_controller.rb +48 -0
  36. data/app/controllers/motor/run_queries_controller.rb +77 -0
  37. data/app/controllers/motor/schema_controller.rb +31 -0
  38. data/app/controllers/motor/send_alerts_controller.rb +26 -0
  39. data/app/controllers/motor/sessions_controller.rb +23 -0
  40. data/app/controllers/motor/slack_conversations_controller.rb +11 -0
  41. data/app/controllers/motor/tags_controller.rb +11 -0
  42. data/app/controllers/motor/ui_controller.rb +51 -0
  43. data/app/controllers/motor/users_for_autocomplete_controller.rb +23 -0
  44. data/app/jobs/motor/alert_sending_job.rb +13 -0
  45. data/app/jobs/motor/application_job.rb +6 -0
  46. data/app/jobs/motor/notify_note_mentions_job.rb +9 -0
  47. data/app/jobs/motor/notify_reminder_job.rb +9 -0
  48. data/app/mailers/motor/alerts_mailer.rb +39 -0
  49. data/app/mailers/motor/application_mailer.rb +33 -0
  50. data/app/mailers/motor/notifications_mailer.rb +33 -0
  51. data/app/models/motor/alert.rb +30 -0
  52. data/app/models/motor/alert_lock.rb +7 -0
  53. data/app/models/motor/api_config.rb +28 -0
  54. data/app/models/motor/application_record.rb +18 -0
  55. data/app/models/motor/audit.rb +13 -0
  56. data/app/models/motor/config.rb +13 -0
  57. data/app/models/motor/dashboard.rb +26 -0
  58. data/app/models/motor/form.rb +23 -0
  59. data/app/models/motor/note.rb +18 -0
  60. data/app/models/motor/note_tag.rb +7 -0
  61. data/app/models/motor/note_tag_tag.rb +8 -0
  62. data/app/models/motor/notification.rb +14 -0
  63. data/app/models/motor/query.rb +33 -0
  64. data/app/models/motor/reminder.rb +13 -0
  65. data/app/models/motor/resource.rb +15 -0
  66. data/app/models/motor/tag.rb +7 -0
  67. data/app/models/motor/taggable_tag.rb +8 -0
  68. data/app/views/layouts/motor/application.html.erb +17 -0
  69. data/app/views/layouts/motor/mailer.html.erb +72 -0
  70. data/app/views/motor/alerts_mailer/alert_email.html.erb +54 -0
  71. data/app/views/motor/notifications_mailer/notify_mention_email.html.erb +28 -0
  72. data/app/views/motor/notifications_mailer/notify_reminder_email.html.erb +28 -0
  73. data/app/views/motor/ui/show.html.erb +1 -0
  74. data/config/locales/el.yml +420 -0
  75. data/config/locales/en.yml +340 -0
  76. data/config/locales/es.yml +420 -0
  77. data/config/locales/ja.yml +340 -0
  78. data/config/locales/pt.yml +416 -0
  79. data/config/routes.rb +65 -0
  80. data/lib/generators/motor/install_generator.rb +24 -0
  81. data/lib/generators/motor/install_notes_generator.rb +22 -0
  82. data/lib/generators/motor/migration.rb +17 -0
  83. data/lib/generators/motor/templates/install.rb +271 -0
  84. data/lib/generators/motor/templates/install_api_configs.rb +86 -0
  85. data/lib/generators/motor/templates/install_notes.rb +83 -0
  86. data/lib/generators/motor/templates/upgrade_motor_api_actions.rb +71 -0
  87. data/lib/generators/motor/upgrade_generator.rb +43 -0
  88. data/lib/motor/active_record_utils/action_text_attribute_patch.rb +19 -0
  89. data/lib/motor/active_record_utils/active_record_connection_column_patch.rb +14 -0
  90. data/lib/motor/active_record_utils/active_record_filter.rb +405 -0
  91. data/lib/motor/active_record_utils/active_storage_blob_patch.rb +30 -0
  92. data/lib/motor/active_record_utils/active_storage_links_extension.rb +11 -0
  93. data/lib/motor/active_record_utils/defined_scopes_extension.rb +25 -0
  94. data/lib/motor/active_record_utils/fetch_methods.rb +24 -0
  95. data/lib/motor/active_record_utils/types.rb +64 -0
  96. data/lib/motor/active_record_utils.rb +45 -0
  97. data/lib/motor/admin.rb +141 -0
  98. data/lib/motor/alerts/persistance.rb +97 -0
  99. data/lib/motor/alerts/scheduled_alerts_cache.rb +29 -0
  100. data/lib/motor/alerts/scheduler.rb +30 -0
  101. data/lib/motor/alerts/slack_sender.rb +74 -0
  102. data/lib/motor/alerts.rb +52 -0
  103. data/lib/motor/api_configs.rb +41 -0
  104. data/lib/motor/api_query/apply_scope.rb +44 -0
  105. data/lib/motor/api_query/build_json.rb +171 -0
  106. data/lib/motor/api_query/build_meta.rb +20 -0
  107. data/lib/motor/api_query/filter.rb +125 -0
  108. data/lib/motor/api_query/paginate.rb +19 -0
  109. data/lib/motor/api_query/search.rb +60 -0
  110. data/lib/motor/api_query/sort.rb +64 -0
  111. data/lib/motor/api_query.rb +24 -0
  112. data/lib/motor/assets.rb +62 -0
  113. data/lib/motor/build_schema/active_storage_attachment_schema.rb +125 -0
  114. data/lib/motor/build_schema/adjust_devise_model_schema.rb +60 -0
  115. data/lib/motor/build_schema/apply_permissions.rb +64 -0
  116. data/lib/motor/build_schema/defaults.rb +66 -0
  117. data/lib/motor/build_schema/find_display_column.rb +65 -0
  118. data/lib/motor/build_schema/find_icon.rb +135 -0
  119. data/lib/motor/build_schema/find_searchable_columns.rb +33 -0
  120. data/lib/motor/build_schema/load_from_rails.rb +361 -0
  121. data/lib/motor/build_schema/merge_schema_configs.rb +157 -0
  122. data/lib/motor/build_schema/reorder_schema.rb +88 -0
  123. data/lib/motor/build_schema/utils.rb +31 -0
  124. data/lib/motor/build_schema.rb +125 -0
  125. data/lib/motor/cancan_utils/ability_patch.rb +31 -0
  126. data/lib/motor/cancan_utils/can_manage_all.rb +14 -0
  127. data/lib/motor/cancan_utils.rb +9 -0
  128. data/lib/motor/configs/build_configs_hash.rb +90 -0
  129. data/lib/motor/configs/build_ui_app_tag.rb +177 -0
  130. data/lib/motor/configs/load_from_cache.rb +110 -0
  131. data/lib/motor/configs/sync_from_file.rb +35 -0
  132. data/lib/motor/configs/sync_from_hash.rb +159 -0
  133. data/lib/motor/configs/sync_middleware.rb +72 -0
  134. data/lib/motor/configs/sync_with_remote.rb +47 -0
  135. data/lib/motor/configs/write_to_file.rb +36 -0
  136. data/lib/motor/configs.rb +39 -0
  137. data/lib/motor/dashboards/persistance.rb +73 -0
  138. data/lib/motor/dashboards.rb +8 -0
  139. data/lib/motor/forms/persistance.rb +93 -0
  140. data/lib/motor/forms.rb +8 -0
  141. data/lib/motor/hash_serializer.rb +21 -0
  142. data/lib/motor/net_http_utils.rb +50 -0
  143. data/lib/motor/notes/notify_mentions.rb +71 -0
  144. data/lib/motor/notes/notify_reminder.rb +48 -0
  145. data/lib/motor/notes/persist.rb +36 -0
  146. data/lib/motor/notes/reminders_scheduler.rb +39 -0
  147. data/lib/motor/notes/tags.rb +34 -0
  148. data/lib/motor/notes.rb +12 -0
  149. data/lib/motor/queries/persistance.rb +90 -0
  150. data/lib/motor/queries/postgresql_exec_query.rb +28 -0
  151. data/lib/motor/queries/render_sql_template.rb +61 -0
  152. data/lib/motor/queries/run_query.rb +289 -0
  153. data/lib/motor/queries.rb +11 -0
  154. data/lib/motor/railtie.rb +11 -0
  155. data/lib/motor/resources/custom_sql_columns_cache.rb +17 -0
  156. data/lib/motor/resources/fetch_configured_model.rb +269 -0
  157. data/lib/motor/resources/persist_configs.rb +232 -0
  158. data/lib/motor/resources.rb +19 -0
  159. data/lib/motor/slack/client.rb +62 -0
  160. data/lib/motor/slack.rb +16 -0
  161. data/lib/motor/tags.rb +32 -0
  162. data/lib/motor/tasks/motor.rake +54 -0
  163. data/lib/motor/version.rb +5 -0
  164. data/lib/motor-admin-cstham8.rb +3 -0
  165. data/lib/motor.rb +87 -0
  166. data/ui/dist/manifest.json +1990 -0
  167. metadata +303 -0
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Filter
6
+ LIKE_FILTER_VALUE_REGEXP = /\A%?(.*?)%?\z/.freeze
7
+ DISTINCT_RESTRICTED_COLUMN_TYPES = %i[json point].freeze
8
+
9
+ module_function
10
+
11
+ def call(rel, params)
12
+ return rel if params.blank?
13
+
14
+ normalized_params = normalize_params(Array.wrap(params))
15
+
16
+ rel = apply_filters(rel, normalized_params)
17
+ rel = rel.distinct if can_apply_distinct?(rel)
18
+
19
+ rel
20
+ end
21
+
22
+ def clean_filters(value)
23
+ if value.class.name == 'ActionController::Parameters'
24
+ value.to_unsafe_h
25
+ elsif value.is_a?(Array)
26
+ value.map { |v| clean_filters(v) }
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ def apply_predicates(rel, filters)
33
+ joins = ActiveRecord::PredicateBuilder.filter_joins(rel.klass, filters)
34
+
35
+ joins.flatten.reduce(rel) do |acc, j|
36
+ if j.is_a?(String) || j.is_a?(Arel::Nodes::Join)
37
+ acc.joins(j)
38
+ elsif j.present?
39
+ acc.left_outer_joins(j)
40
+ else
41
+ acc
42
+ end
43
+ end
44
+ end
45
+
46
+ def apply_filters(rel, filters)
47
+ filters = clean_filters(filters)
48
+
49
+ rel = apply_predicates(rel, filters)
50
+
51
+ alias_tracker = ActiveRecord::Associations::AliasTracker.create(rel.connection, rel.table.name, [])
52
+ filter_clause_factory = ActiveRecord::Relation::FilterClauseFactory.new(rel.klass, rel.predicate_builder)
53
+
54
+ where_clause = filter_clause_factory.build(filters, alias_tracker)
55
+
56
+ rel_values = rel.instance_variable_get(:@values)
57
+
58
+ if rel_values[:where]
59
+ rel_values[:where] += where_clause
60
+ else
61
+ rel_values[:where] = where_clause
62
+ end
63
+
64
+ rel
65
+ end
66
+
67
+ def normalize_params(params)
68
+ params.map do |item|
69
+ next item if item.is_a?(String)
70
+ next normalize_params(item) if item.is_a?(Array)
71
+
72
+ item = item.to_unsafe_h if item.respond_to?(:to_unsafe_h)
73
+
74
+ item.transform_values do |filter|
75
+ if filter.is_a?(Hash)
76
+ normalize_filter_hash(filter)
77
+ else
78
+ filter
79
+ end
80
+ end
81
+ end.split('OR').product(['OR']).flatten(1)[0...-1]
82
+ end
83
+
84
+ def normalize_filter_hash(hash)
85
+ hash.each_with_object({}) do |(action, value), acc|
86
+ new_action, new_value =
87
+ if value.is_a?(Hash)
88
+ [action, normalize_filter_hash(value)]
89
+ else
90
+ normalize_action(action, value)
91
+ end
92
+
93
+ acc[new_action] = new_value
94
+
95
+ acc
96
+ end
97
+ end
98
+
99
+ def can_apply_distinct?(rel)
100
+ rel.columns.none? do |column|
101
+ DISTINCT_RESTRICTED_COLUMN_TYPES.include?(column.type)
102
+ end
103
+ end
104
+
105
+ def normalize_action(action, value)
106
+ case action
107
+ when 'includes'
108
+ ['contains', value]
109
+ when 'contains'
110
+ ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1%')]
111
+ when 'starts_with'
112
+ ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '\1%')]
113
+ when 'ends_with'
114
+ ['ilike', value.sub(LIKE_FILTER_VALUE_REGEXP, '%\1')]
115
+ when 'eqnull'
116
+ ['eq', nil]
117
+ when 'neqnull'
118
+ ['neq', nil]
119
+ else
120
+ [action, value]
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Paginate
6
+ MAX_PER_PAGE = 500
7
+
8
+ module_function
9
+
10
+ def call(rel, params)
11
+ params ||= {}
12
+
13
+ rel = rel.limit([MAX_PER_PAGE, (params[:limit] || MAX_PER_PAGE).to_i].min)
14
+
15
+ rel.offset(params[:offset].to_i)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Search
6
+ STRING_COLUMN_TYPES = %i[text string citext].freeze
7
+ NUMBER_COLUMN_TYPES = %i[integer float].freeze
8
+
9
+ module_function
10
+
11
+ def call(rel, keyword)
12
+ return rel if keyword.blank?
13
+
14
+ filters = fetch_filters(rel, keyword)
15
+
16
+ arel_where = build_arel_or_query(filters)
17
+
18
+ rel.where(arel_where)
19
+ end
20
+
21
+ def fetch_filters(rel, keyword)
22
+ klass = rel.klass
23
+
24
+ selected_columns = find_searchable_columns(klass)
25
+
26
+ build_arel_filters(klass, selected_columns, keyword)
27
+ end
28
+
29
+ def build_arel_filters(model, column_names, keyword)
30
+ arel_table = model.arel_table
31
+
32
+ column_names.map do |name|
33
+ column_type = model.columns_hash[name]&.type
34
+
35
+ next unless column_type
36
+
37
+ if STRING_COLUMN_TYPES.include?(column_type)
38
+ arel_table[name].matches("%#{keyword}%")
39
+ elsif NUMBER_COLUMN_TYPES.include?(column_type)
40
+ arel_table[name].eq(keyword.to_f) if keyword.match?(/\A\d/)
41
+ else
42
+ arel_table[name].eq(keyword)
43
+ end
44
+ end.compact
45
+ end
46
+
47
+ def build_arel_or_query(filter_array)
48
+ filter_array.reduce(nil) do |acc, filter|
49
+ next acc = filter unless acc
50
+
51
+ acc.or(filter)
52
+ end
53
+ end
54
+
55
+ def find_searchable_columns(model)
56
+ model.try(:motor_searchable_columns) || Motor::BuildSchema::FindSearchableColumns.call(model)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module Sort
6
+ FIELD_PARSE_REGEXP = /\A(-)?(.*)\z/.freeze
7
+
8
+ module_function
9
+
10
+ def call(rel, param)
11
+ return rel if param.blank?
12
+
13
+ arel_order = build_arel_order(rel.klass, param)
14
+ join_params = build_join_params(rel.klass, param)
15
+
16
+ rel = rel.left_joins(join_params) if join_params.present?
17
+
18
+ rel.reorder(arel_order)
19
+ end
20
+
21
+ def build_join_params(_model, param)
22
+ param.split(',').each_with_object({}) do |field, result|
23
+ key = field[FIELD_PARSE_REGEXP, 2]
24
+ *path, _ = key.split('.')
25
+
26
+ path.reduce(result) do |acc, fragment|
27
+ hash = {}
28
+
29
+ acc[fragment] = hash
30
+
31
+ hash
32
+ end
33
+ end
34
+ end
35
+
36
+ def build_arel_order(model, param)
37
+ param.split(',').map do |field|
38
+ direction, key = field.match(FIELD_PARSE_REGEXP).captures
39
+ *path, field = key.split('.')
40
+
41
+ reflection_model =
42
+ path.reduce(model) do |acc, fragment|
43
+ acc.reflections[fragment].klass
44
+ end
45
+
46
+ arel_column = reflection_model.arel_table[field]
47
+
48
+ arel_direction = direction.present? ? arel_column.desc : arel_column.asc
49
+
50
+ maybe_add_null_last(model, arel_direction)
51
+ end
52
+ end
53
+
54
+ def maybe_add_null_last(model, arel_direction)
55
+ if arel_direction.respond_to?(:nulls_last) &&
56
+ model.connection.class.name == 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter'
57
+ arel_direction.nulls_last
58
+ else
59
+ arel_direction
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module ApiQuery
5
+ module_function
6
+
7
+ def call(rel, params)
8
+ rel = ApiQuery::Sort.call(rel, params[:sort] || params[:order])
9
+ rel = ApiQuery::Paginate.call(rel, params[:page])
10
+ rel = ApiQuery::Filter.call(rel, params[:filter] || params[:filters])
11
+ rel = ApiQuery::ApplyScope.call(rel, params[:scope])
12
+
13
+ ApiQuery::Search.call(rel, params[:q] || params[:search] || params[:query])
14
+ end
15
+ end
16
+ end
17
+
18
+ require_relative 'api_query/sort'
19
+ require_relative 'api_query/paginate'
20
+ require_relative 'api_query/filter'
21
+ require_relative 'api_query/search'
22
+ require_relative 'api_query/apply_scope'
23
+ require_relative 'api_query/build_meta'
24
+ require_relative 'api_query/build_json'
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module Assets
5
+ InvalidPathError = Class.new(StandardError)
6
+
7
+ ASSETS_PATH = Pathname.new(__dir__).join('../../ui/dist')
8
+ MANIFEST_PATH = ASSETS_PATH.join('manifest.json')
9
+ DEV_SERVER_URL = 'http://localhost:9090/'
10
+
11
+ CACHE_STORE =
12
+ if Rails.env.production?
13
+ ActiveSupport::Cache::MemoryStore.new(size: 5.megabytes)
14
+ else
15
+ ActiveSupport::Cache::NullStore.new
16
+ end
17
+
18
+ module_function
19
+
20
+ def manifest
21
+ CACHE_STORE.fetch('manifest') do
22
+ JSON.parse(MANIFEST_PATH.read)
23
+ end
24
+ end
25
+
26
+ def icons
27
+ manifest.select do |k, v|
28
+ !k.ends_with?('.gz') && v.starts_with?('icons/') && v.exclude?('DS_Store')
29
+ end.keys
30
+ end
31
+
32
+ def asset_path(path)
33
+ Motor::Admin.routes.url_helpers.motor_asset_path(manifest[path])
34
+ end
35
+
36
+ def load_asset(filename, gzip: false)
37
+ if Motor.development?
38
+ load_from_dev_server(filename)
39
+ else
40
+ load_from_disk(filename, gzip: gzip)
41
+ end
42
+ end
43
+
44
+ def load_from_disk(filename, gzip:)
45
+ filename += '.gz' if gzip
46
+
47
+ raise InvalidPathError if filename.include?('..')
48
+
49
+ path = ASSETS_PATH.join(filename)
50
+
51
+ raise InvalidPathError unless path.to_s.starts_with?(ASSETS_PATH.to_s)
52
+
53
+ path.read
54
+ end
55
+
56
+ def load_from_dev_server(filename)
57
+ uri = URI(DEV_SERVER_URL + filename)
58
+
59
+ Net::HTTP.get_response(uri).body
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module ActiveStorageAttachmentSchema
6
+ module_function
7
+
8
+ # rubocop:disable Metrics/MethodLength
9
+ def call
10
+ model = ActiveStorage::Attachment
11
+
12
+ {
13
+ name: model.name.underscore,
14
+ slug: Utils.slugify(model),
15
+ class_name: model.name,
16
+ table_name: model.table_name,
17
+ primary_key: model.primary_key,
18
+ display_name: model.model_name.human(count: 2, default: 'Attachments'),
19
+ display_column: 'id',
20
+ icon: 'paperclip',
21
+ columns: [
22
+ {
23
+ name: 'id',
24
+ display_name: 'ID',
25
+ column_type: 'integer',
26
+ access_type: 'read_only',
27
+ default_value: nil,
28
+ validators: [],
29
+ format: {},
30
+ is_array: false,
31
+ reference: nil,
32
+ virtual: false
33
+ },
34
+ {
35
+ name: 'path',
36
+ display_name: model.human_attribute_name(:path),
37
+ column_type: 'string',
38
+ access_type: 'read_only',
39
+ default_value: nil,
40
+ validators: [],
41
+ format: {},
42
+ is_array: false,
43
+ reference: nil,
44
+ virtual: true
45
+ },
46
+ {
47
+ name: 'name',
48
+ display_name: model.human_attribute_name(:name),
49
+ column_type: 'string',
50
+ access_type: 'read_write',
51
+ default_value: nil,
52
+ validators: [],
53
+ format: {},
54
+ is_array: false,
55
+ reference: nil,
56
+ virtual: false
57
+ },
58
+ {
59
+ name: 'record_type',
60
+ display_name: model.human_attribute_name(:record_type),
61
+ column_type: 'string',
62
+ access_type: 'read_write',
63
+ default_value: nil,
64
+ validators: [],
65
+ format: {},
66
+ is_array: false,
67
+ reference: nil,
68
+ virtual: false
69
+ },
70
+ {
71
+ name: 'record_id',
72
+ display_name: model.human_attribute_name(:record),
73
+ column_type: 'integer',
74
+ access_type: 'read_write',
75
+ default_value: nil,
76
+ validators: [],
77
+ format: {},
78
+ is_array: false,
79
+ reference: {
80
+ name: 'record',
81
+ display_name: model.human_attribute_name(:record),
82
+ model_name: nil,
83
+ reference_type: 'belongs_to',
84
+ foreign_key: 'record_id',
85
+ polymorphic: true
86
+ },
87
+ virtual: false
88
+ },
89
+ {
90
+ name: 'file',
91
+ display_name: model.human_attribute_name(:file),
92
+ column_type: 'file',
93
+ access_type: 'write_only',
94
+ default_value: nil,
95
+ validators: [],
96
+ format: {},
97
+ is_array: false,
98
+ reference: nil,
99
+ virtual: false
100
+ },
101
+ {
102
+ name: 'created_at',
103
+ display_name: model.human_attribute_name(:created_at),
104
+ column_type: 'datetime',
105
+ access_type: 'read_only',
106
+ default_value: nil,
107
+ validators: [],
108
+ format: {},
109
+ is_array: false,
110
+ reference: nil,
111
+ virtual: false
112
+ }
113
+ ],
114
+ associations: [],
115
+ scopes: [],
116
+ preferences: {},
117
+ actions: Motor::BuildSchema::Defaults.actions.reject { |e| e[:name] == 'edit' },
118
+ tabs: Motor::BuildSchema::Defaults.tabs,
119
+ visible: false
120
+ }.with_indifferent_access
121
+ end
122
+ # rubocop:enable Metrics/MethodLength
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module AdjustDeviseModelSchema
6
+ HIDDEN_COLUMNS = %w[
7
+ encrypted_password
8
+ reset_password_token
9
+ confirmation_token
10
+ ].freeze
11
+
12
+ READ_ONLY_COLUMNS = %w[
13
+ reset_password_sent_at
14
+ remember_created_at
15
+ sign_in_count
16
+ current_sign_in_at
17
+ last_sign_in_at
18
+ current_sign_in_ip
19
+ last_sign_in_ip
20
+ confirmed_at
21
+ confirmation_sent_at
22
+ ].freeze
23
+
24
+ module_function
25
+
26
+ def call(schema, devise_modules)
27
+ modify_column_access_types!(schema[:columns])
28
+ add_password_column!(schema[:columns]) if devise_modules.include?(:database_authenticatable)
29
+
30
+ schema
31
+ end
32
+
33
+ def modify_column_access_types!(columns)
34
+ columns.each do |column|
35
+ column[:access_type] =
36
+ case column[:name]
37
+ when *HIDDEN_COLUMNS
38
+ ColumnAccessTypes::HIDDEN
39
+ when *READ_ONLY_COLUMNS
40
+ ColumnAccessTypes::READ_ONLY
41
+ else
42
+ column[:access_type]
43
+ end
44
+ end
45
+ end
46
+
47
+ def add_password_column!(columns)
48
+ columns << {
49
+ name: 'password',
50
+ display_name: I18n.t('motor.password'),
51
+ column_type: 'string',
52
+ access_type: 'write_only',
53
+ default_value: nil,
54
+ validators: [],
55
+ virtual: true
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module ApplyPermissions
6
+ module_function
7
+
8
+ def call(schema, ability)
9
+ schema.map do |model|
10
+ klass = model[:class_name].constantize
11
+
12
+ next unless ability.can?(:read, klass)
13
+
14
+ model[:associations] = filter_associations(model[:associations], ability)
15
+ model[:columns] = filter_columns(klass, model[:columns], ability)
16
+ model[:actions] = filter_actions(klass, model[:actions], ability)
17
+ model[:tabs] = filter_tabs(klass, model[:tabs], ability)
18
+
19
+ model
20
+ end.compact
21
+ end
22
+
23
+ def filter_associations(associations, ability)
24
+ associations.select do |assoc|
25
+ model_class = assoc[:model_name].classify.safe_constantize
26
+
27
+ model_class && ability.can?(:read, model_class)
28
+ end
29
+ end
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
+
38
+ def filter_columns(model, columns, ability)
39
+ columns.map do |column|
40
+ next unless ability.can?(:read, model, column[:name])
41
+
42
+ reference_model_name = column.dig(:reference, :model_name)
43
+ model_class = reference_model_name&.classify&.safe_constantize
44
+
45
+ next if reference_model_name &&
46
+ (model_class.nil? || !ability.can?(:read, model_class))
47
+
48
+ if !ability.can?(:update, model, column[:name]) &&
49
+ column[:access_type] != BuildSchema::ColumnAccessTypes::HIDDEN
50
+ column = column.merge(access_type: BuildSchema::ColumnAccessTypes::READ_ONLY)
51
+ end
52
+
53
+ column
54
+ end.compact
55
+ end
56
+
57
+ def filter_actions(model, actions, ability)
58
+ actions.select do |action|
59
+ ability.can?(action[:name].to_sym, model)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Motor
4
+ module BuildSchema
5
+ module Defaults
6
+ module_function
7
+
8
+ # rubocop:disable Metrics/MethodLength
9
+ def actions
10
+ [
11
+ {
12
+ name: 'create',
13
+ display_name: I18n.t('motor.create'),
14
+ action_type: BuildSchema::DEFAULT_TYPE,
15
+ preferences: {},
16
+ apply_on: 'collection',
17
+ visible: true
18
+ },
19
+ {
20
+ name: 'edit',
21
+ display_name: I18n.t('motor.edit'),
22
+ action_type: BuildSchema::DEFAULT_TYPE,
23
+ preferences: {},
24
+ apply_on: 'member',
25
+ visible: true
26
+ },
27
+ {
28
+ name: 'remove',
29
+ display_name: I18n.t('motor.remove'),
30
+ action_type: BuildSchema::DEFAULT_TYPE,
31
+ preferences: {},
32
+ apply_on: 'member',
33
+ visible: true
34
+ }
35
+ ].freeze
36
+ end
37
+
38
+ def tabs
39
+ [
40
+ {
41
+ name: 'details',
42
+ display_name: I18n.t('motor.details'),
43
+ tab_type: BuildSchema::DEFAULT_TYPE,
44
+ preferences: {},
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
60
+ }
61
+ ].freeze
62
+ end
63
+ # rubocop:enable Metrics/MethodLength
64
+ end
65
+ end
66
+ end