active_element 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +8 -9
  5. data/active_element.gemspec +1 -1
  6. data/app/assets/javascripts/active_element/application.js +3 -1
  7. data/app/assets/javascripts/active_element/popover.js +6 -0
  8. data/app/assets/javascripts/active_element/setup.js +12 -0
  9. data/app/assets/javascripts/active_element/{search_field.js → text_search_field.js} +2 -2
  10. data/app/assets/javascripts/active_element/toast.js +8 -0
  11. data/app/assets/stylesheets/active_element/application.scss +65 -1
  12. data/app/controllers/active_element/application_controller.rb +13 -19
  13. data/app/views/active_element/components/form/_label.html.erb +2 -2
  14. data/app/views/active_element/components/form/_templates.html.erb +8 -8
  15. data/app/views/active_element/components/form.html.erb +3 -3
  16. data/app/views/active_element/components/table/_collection_row.html.erb +3 -3
  17. data/app/views/active_element/components/table/collection.html.erb +1 -1
  18. data/app/views/active_element/components/table/item.html.erb +3 -3
  19. data/app/views/active_element/navbar/_menu.html.erb +2 -2
  20. data/app/views/layouts/active_element.html.erb +21 -23
  21. data/app/views/layouts/active_element_error.html.erb +1 -1
  22. data/config/routes.rb +10 -1
  23. data/lib/active_element/components/button.rb +6 -41
  24. data/lib/active_element/components/form.rb +12 -3
  25. data/lib/active_element/components/text_search/active_record_authorization.rb +13 -0
  26. data/lib/active_element/components/text_search/authorization.rb +117 -0
  27. data/lib/active_element/components/text_search/component.rb +118 -0
  28. data/lib/active_element/components/text_search/sql.rb +107 -0
  29. data/lib/active_element/components/text_search.rb +23 -0
  30. data/lib/active_element/components/util/decorator.rb +2 -2
  31. data/lib/active_element/components/util/record_path.rb +84 -0
  32. data/lib/active_element/components/util.rb +7 -2
  33. data/lib/active_element/components.rb +1 -0
  34. data/lib/active_element/controller_action.rb +9 -10
  35. data/lib/active_element/controller_interface.rb +78 -0
  36. data/lib/active_element/permissions_check.rb +33 -29
  37. data/lib/active_element/permissions_report.rb +57 -0
  38. data/lib/active_element/route.rb +1 -1
  39. data/lib/active_element/routes.rb +1 -1
  40. data/lib/active_element/version.rb +1 -1
  41. data/lib/active_element.rb +24 -10
  42. data/lib/tasks/active_element.rake +1 -16
  43. data/rspec-documentation/pages/Components/Tables.md +2 -2
  44. data/rspec-documentation/spec_helper.rb +1 -1
  45. metadata +19 -12
  46. data/app/controllers/active_element/text_searches_controller.rb +0 -189
  47. data/lib/active_element/active_record_text_search_authorization.rb +0 -12
  48. data/lib/active_element/colorized_string.rb +0 -33
@@ -1,189 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveElement
4
- # Used by auto-complete search field for executing a text search on the provided model and
5
- # attributes.
6
- #
7
- # The user must have the permission `can_create_<application_name>_active_element_text_searches`.
8
- #
9
- # A model must call `authorize_active_element_text_search_for` to enable text search. e.g.:
10
- #
11
- # class MyModel < ApplicationRecord
12
- # authorize_active_element_text_search_for :name, exposes: [:email]
13
- # end
14
- #
15
- # This will allow searching on the `name` column and permits returning each matching record's
16
- # `:email` column's value.
17
- #
18
- # TODO: Refactor logic into separate classes.
19
- # rubocop:disable Metrics/ClassLength
20
- class TextSearchesController < ApplicationController
21
- DEFAULT_LIMIT = 50
22
-
23
- before_action :verify_parameters
24
- before_action :verify_model
25
-
26
- def create
27
- render json: { results: results, request_id: params[:request_id] }
28
- end
29
-
30
- private
31
-
32
- def verify_parameters
33
- return if %i[model attributes value query].all? { |parameter| params[parameter].present? }
34
-
35
- render json: { message: 'Must provide parameters: [model, attributes, value, query] for text search.' },
36
- status: :unprocessable_entity
37
- end
38
-
39
- def verify_model
40
- return if [model, search_columns, value_column].all?(&:present?) && authorized?
41
-
42
- render json: { message: verify_fail_message }, status: :unprocessable_entity
43
- end
44
-
45
- def verify_fail_message
46
- requested = %i[model attributes value].index_with { |key| params[key] }
47
- .compact_blank
48
- .map { |key, value| "#{key}: #{value}" }
49
- "Unpermitted or unavailable search for: { #{requested.join(', ')} }"
50
- end
51
-
52
- def results
53
- @results ||= model.where(*whereclause)
54
- .limit(limit)
55
- .pluck(value_column.name, *search_columns.map(&:name))
56
- .map { |value, *attributes| result(value, attributes) }
57
- end
58
-
59
- def result(value, attributes)
60
- { value: value, attributes: attributes.reject { |attribute| attribute == value } }
61
- end
62
-
63
- def model
64
- @model ||= params[:model].camelize(:upper).safe_constantize
65
- end
66
-
67
- def query
68
- params[:query]
69
- end
70
-
71
- def value_column
72
- return nil if params[:value].blank?
73
-
74
- @value_column ||= model&.columns&.find { |column| column.name == params[:value] }
75
- end
76
-
77
- def search_columns
78
- return nil if params[:attributes].blank?
79
-
80
- @search_columns ||= params[:attributes].map { |attribute| column_for(attribute) }.compact
81
- end
82
-
83
- def column_for(attribute)
84
- matched_column = model&.columns&.find { |column| column.name == attribute }
85
- return nil if matched_column.blank?
86
-
87
- compatible_column?(matched_column) ? matched_column : nil
88
- end
89
-
90
- def authorized?
91
- model.authorized_active_element_text_search_fields&.any? do |field, exposed|
92
- exposed = [exposed] unless exposed.is_a?(Array)
93
- authorized_field?(field, exposed)
94
- end
95
- end
96
-
97
- def authorized_field?(field, exposed)
98
- return false unless search_columns.map { |column| column.name.to_sym }.include?(field.to_sym)
99
- return false unless exposed&.map(&:to_sym)&.include?(value_column.name.to_sym)
100
-
101
- true
102
- end
103
-
104
- def whereclause
105
- clauses = search_columns.map { |column| "#{column.name} #{operator(column)} ?" }
106
- [clauses.join(' OR '), search_columns.map { |column| search_param(column) }].flatten
107
- end
108
-
109
- def operator(column)
110
- case column.type
111
- when :string
112
- model.connection.adapter_name == 'SQLite' ? 'LIKE' : 'ILIKE'
113
- else
114
- '='
115
- end
116
- end
117
-
118
- def compatible_column?(column) # rubocop:disable Metrics/MethodLength
119
- case column.type
120
- when :string
121
- true
122
- when :integer
123
- integer?
124
- when :float
125
- float?
126
- when :decimal
127
- decimal?
128
- else
129
- Rails.logger.info("Skipping query `#{query}` for incompatible column: #{column.name}")
130
- false
131
- end
132
- end
133
-
134
- def integer?
135
- Integer(query)
136
- true
137
- rescue ArgumentError
138
- false
139
- end
140
-
141
- def float?
142
- Float(query)
143
- true
144
- rescue ArgumentError
145
- false
146
- end
147
-
148
- def decimal?
149
- BigDecimal(query)
150
- true
151
- rescue ArgumentError
152
- false
153
- end
154
-
155
- def search_param(column)
156
- case column.type
157
- when :string
158
- "#{query}%"
159
- else
160
- query
161
- end
162
- end
163
-
164
- def limit
165
- DEFAULT_LIMIT
166
- end
167
-
168
- def permissions_check
169
- @permissions_check ||= PermissionsCheck.new(
170
- required: [],
171
- actual: current_user.permissions,
172
- controller_path: controller_path,
173
- action_name: action_name,
174
- rails_component: rails_component
175
- )
176
- end
177
-
178
- def required_permissions
179
- application_name = rails_component.application_name
180
- permission = "can_text_search_#{application_name}_#{params[:model]&.pluralize}"
181
- [[permission, { only: :create }]]
182
- end
183
-
184
- def rails_component
185
- @rails_component ||= RailsComponent.new(Rails)
186
- end
187
- end
188
- # rubocop:enable Metrics/ClassLength
189
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ActiveRecord::Base.class_eval do
4
- class << self
5
- attr_reader :authorized_active_element_text_search_fields
6
-
7
- def authorize_active_element_text_search_for(field, exposes:)
8
- @authorized_active_element_text_search_fields ||= []
9
- @authorized_active_element_text_search_fields << [field, exposes]
10
- end
11
- end
12
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveElement
4
- # Wraps strings in terminal escape codes to provide colourisation in (e.g.) logging output.
5
- class ColorizedString
6
- COLOR_CODES = {
7
- cyan: '36',
8
- red: '31',
9
- green: '32',
10
- blue: '34',
11
- purple: '35',
12
- yellow: '33',
13
- light_gray: '37',
14
- light_blue: '1;34',
15
- white: '1;37'
16
- }.freeze
17
-
18
- def initialize(string, color:)
19
- @string = string
20
- @color = color
21
- end
22
-
23
- def value
24
- return string unless Rails.env.development? || Rails.env.test?
25
-
26
- "\e[#{COLOR_CODES.fetch(color)}m#{string}\e[0m"
27
- end
28
-
29
- private
30
-
31
- attr_reader :string, :color
32
- end
33
- end