active_element 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +8 -9
- data/active_element.gemspec +1 -1
- data/app/assets/javascripts/active_element/application.js +3 -1
- data/app/assets/javascripts/active_element/popover.js +6 -0
- data/app/assets/javascripts/active_element/setup.js +12 -0
- data/app/assets/javascripts/active_element/{search_field.js → text_search_field.js} +2 -2
- data/app/assets/javascripts/active_element/toast.js +8 -0
- data/app/assets/stylesheets/active_element/application.scss +65 -1
- data/app/controllers/active_element/application_controller.rb +13 -19
- data/app/views/active_element/components/form/_label.html.erb +2 -2
- data/app/views/active_element/components/form/_templates.html.erb +8 -8
- data/app/views/active_element/components/form.html.erb +3 -3
- data/app/views/active_element/components/table/_collection_row.html.erb +3 -3
- data/app/views/active_element/components/table/collection.html.erb +1 -1
- data/app/views/active_element/components/table/item.html.erb +3 -3
- data/app/views/active_element/navbar/_menu.html.erb +2 -2
- data/app/views/layouts/active_element.html.erb +21 -23
- data/app/views/layouts/active_element_error.html.erb +1 -1
- data/config/routes.rb +10 -1
- data/lib/active_element/components/button.rb +6 -41
- data/lib/active_element/components/form.rb +12 -3
- data/lib/active_element/components/text_search/active_record_authorization.rb +13 -0
- data/lib/active_element/components/text_search/authorization.rb +117 -0
- data/lib/active_element/components/text_search/component.rb +118 -0
- data/lib/active_element/components/text_search/sql.rb +107 -0
- data/lib/active_element/components/text_search.rb +23 -0
- data/lib/active_element/components/util/decorator.rb +2 -2
- data/lib/active_element/components/util/record_path.rb +84 -0
- data/lib/active_element/components/util.rb +7 -2
- data/lib/active_element/components.rb +1 -0
- data/lib/active_element/controller_action.rb +9 -10
- data/lib/active_element/controller_interface.rb +78 -0
- data/lib/active_element/permissions_check.rb +33 -29
- data/lib/active_element/permissions_report.rb +57 -0
- data/lib/active_element/route.rb +1 -1
- data/lib/active_element/routes.rb +1 -1
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +24 -10
- data/lib/tasks/active_element.rake +1 -16
- data/rspec-documentation/pages/Components/Tables.md +2 -2
- data/rspec-documentation/spec_helper.rb +1 -1
- metadata +19 -12
- data/app/controllers/active_element/text_searches_controller.rb +0 -189
- data/lib/active_element/active_record_text_search_authorization.rb +0 -12
- 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
|