forest_admin_datasource_customizer 1.0.0.pre.beta.21 → 1.0.0.pre.beta.58
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/forest_admin_datasource_customizer.gemspec +3 -2
- data/lib/forest_admin_datasource_customizer/collection_customizer.rb +292 -5
- data/lib/forest_admin_datasource_customizer/context/agent_customization_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/context/collection_customization_context.rb +15 -0
- data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb +50 -0
- data/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_data_source.rb +18 -0
- data/lib/forest_admin_datasource_customizer/datasource_customizer.rb +43 -13
- data/lib/forest_admin_datasource_customizer/decorators/action/action_collection_decorator.rb +137 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/base_action.rb +67 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context.rb +56 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/context/action_context_single.rb +26 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/dynamic_field.rb +50 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/result_builder.rb +68 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/types/action_scope.rb +15 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/types/field_type.rb +35 -0
- data/lib/forest_admin_datasource_customizer/decorators/action/widget_field.rb +357 -0
- data/lib/forest_admin_datasource_customizer/decorators/binary/binary_collection_decorator.rb +215 -0
- data/lib/forest_admin_datasource_customizer/decorators/binary/binary_helper.rb +17 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_collection_decorator.rb +41 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_context.rb +33 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/chart_datasource_decorator.rb +46 -0
- data/lib/forest_admin_datasource_customizer/decorators/chart/result_builder.rb +148 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb +115 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/computed_definition.rb +21 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/utils/computed_field.rb +74 -0
- data/lib/forest_admin_datasource_customizer/decorators/computed/utils/flattener.rb +49 -0
- data/lib/forest_admin_datasource_customizer/decorators/decorators_stack.rb +33 -4
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_aggregate_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_create_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_delete_context.rb +12 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_list_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/after/hook_after_update_context.rb +12 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_aggregate_context.rb +20 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_create_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_delete_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_list_context.rb +19 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/before/hook_before_update_context.rb +19 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/context/hook_context.rb +22 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/hook_collection_decorator.rb +95 -0
- data/lib/forest_admin_datasource_customizer/decorators/hook/hooks.rb +26 -0
- data/lib/forest_admin_datasource_customizer/decorators/operators_emulate/operators_emulate_collection_decorator.rb +118 -0
- data/lib/forest_admin_datasource_customizer/decorators/operators_equivalence/operators_equivalence_collection_decorator.rb +50 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/create_override_customization_context.rb +16 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/delete_override_customization_context.rb +16 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/context/update_override_customization_context.rb +17 -0
- data/lib/forest_admin_datasource_customizer/decorators/override/override_collection_decorator.rb +49 -0
- data/lib/forest_admin_datasource_customizer/decorators/publication/publication_collection_decorator.rb +95 -0
- data/lib/forest_admin_datasource_customizer/decorators/publication/publication_datasource_decorator.rb +57 -0
- data/lib/forest_admin_datasource_customizer/decorators/relation/relation_collection_decorator.rb +268 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_datasource_decorator.rb +70 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_collection/rename_collection_decorator.rb +37 -0
- data/lib/forest_admin_datasource_customizer/decorators/rename_field/rename_field_collection_decorator.rb +190 -0
- data/lib/forest_admin_datasource_customizer/decorators/schema/schema_collection_decorator.rb +21 -0
- data/lib/forest_admin_datasource_customizer/decorators/search/search_collection_decorator.rb +135 -0
- data/lib/forest_admin_datasource_customizer/decorators/segment/segment_collection_decorator.rb +60 -0
- data/lib/forest_admin_datasource_customizer/decorators/sort/sort_collection_decorator.rb +127 -0
- data/lib/forest_admin_datasource_customizer/decorators/validation/validation_collection_decorator.rb +82 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/create_relations/create_relations_collection_decorator.rb +75 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/update_relations/update_relations_collection_decorator.rb +96 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_datasource_decorator.rb +14 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_customization_context.rb +18 -0
- data/lib/forest_admin_datasource_customizer/decorators/write/write_replace/write_replace_collection_decorator.rb +125 -0
- data/lib/forest_admin_datasource_customizer/plugins/add_external_relation.rb +27 -0
- data/lib/forest_admin_datasource_customizer/plugins/import_field.rb +74 -0
- data/lib/forest_admin_datasource_customizer/version.rb +1 -1
- metadata +84 -5
- data/README.md +0 -31
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'marcel'
|
3
|
+
|
4
|
+
module ForestAdminDatasourceCustomizer
|
5
|
+
module Decorators
|
6
|
+
module Binary
|
7
|
+
class BinaryCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
8
|
+
include ForestAdminDatasourceToolkit
|
9
|
+
include ForestAdminDatasourceToolkit::Decorators
|
10
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
11
|
+
|
12
|
+
OPERATORS_WITH_REPLACEMENT = [Operators::AFTER, Operators::BEFORE, Operators::CONTAINS,
|
13
|
+
Operators::ENDS_WITH, Operators::EQUAL, Operators::GREATER_THAN,
|
14
|
+
Operators::I_CONTAINS, Operators::NOT_IN, Operators::I_ENDS_WITH,
|
15
|
+
Operators::I_STARTS_WITH, Operators::LESS_THAN, Operators::NOT_CONTAINS,
|
16
|
+
Operators::NOT_EQUAL, Operators::STARTS_WITH, Operators::IN].freeze
|
17
|
+
|
18
|
+
def initialize(child_collection, datasource)
|
19
|
+
super
|
20
|
+
@use_hex_conversion = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_binary_mode(name, type)
|
24
|
+
field = @child_collection.schema[:fields][name]
|
25
|
+
|
26
|
+
raise Exceptions::ForestException, 'Invalid binary mode' unless %w[datauri hex].include?(type)
|
27
|
+
|
28
|
+
unless field&.type == 'Column' && field&.column_type == 'Binary'
|
29
|
+
raise Exceptions::ForestException, 'Expected a binary field'
|
30
|
+
end
|
31
|
+
|
32
|
+
@use_hex_conversion[name] = (type == 'hex')
|
33
|
+
mark_schema_as_dirty
|
34
|
+
end
|
35
|
+
|
36
|
+
def refine_schema(sub_schema)
|
37
|
+
fields = {}
|
38
|
+
|
39
|
+
sub_schema[:fields].each do |name, schema|
|
40
|
+
if schema.type == 'Column'
|
41
|
+
new_schema = schema.dup
|
42
|
+
new_schema.column_type = replace_column_type(schema.column_type)
|
43
|
+
new_schema.validations = replace_validation(name, schema)
|
44
|
+
fields[name] = new_schema
|
45
|
+
else
|
46
|
+
fields[name] = schema
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
sub_schema[:fields] = fields
|
51
|
+
sub_schema
|
52
|
+
end
|
53
|
+
|
54
|
+
def refine_filter(_caller, filter = nil)
|
55
|
+
filter&.override(
|
56
|
+
condition_tree: filter&.condition_tree&.replace_leafs do |leaf|
|
57
|
+
convert_condition_tree_leaf(leaf)
|
58
|
+
end
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
def create(caller, data)
|
63
|
+
data_with_binary = convert_record(true, data)
|
64
|
+
record = super(caller, data_with_binary)
|
65
|
+
|
66
|
+
convert_record(false, record)
|
67
|
+
end
|
68
|
+
|
69
|
+
def list(caller, filter, projection)
|
70
|
+
records = super
|
71
|
+
records.map! { |record| convert_record(false, record) }
|
72
|
+
|
73
|
+
records
|
74
|
+
end
|
75
|
+
|
76
|
+
def update(caller, filter, patch)
|
77
|
+
super(caller, filter, convert_record(true, patch))
|
78
|
+
end
|
79
|
+
|
80
|
+
def aggregate(caller, filter, aggregation, limit = nil)
|
81
|
+
rows = super
|
82
|
+
rows.map! do |row|
|
83
|
+
{
|
84
|
+
'value' => row['value'],
|
85
|
+
'group' => row['group'].to_h { |path, value| [path, convert_value(false, path, value)] }
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def convert_condition_tree_leaf(leaf)
|
91
|
+
prefix, suffix = leaf.field.split(':')
|
92
|
+
schema = @child_collection.schema[:fields][prefix]
|
93
|
+
|
94
|
+
if schema.type != 'Column'
|
95
|
+
condition_tree = @datasource.get_collection(schema.foreign_collection).convert_condition_tree_leaf(
|
96
|
+
leaf.override(field: suffix)
|
97
|
+
)
|
98
|
+
|
99
|
+
return condition_tree.nest(prefix)
|
100
|
+
end
|
101
|
+
|
102
|
+
if OPERATORS_WITH_REPLACEMENT.include?(leaf.operator)
|
103
|
+
column_type = if [Operators::IN, Operators::NOT_IN].include?(leaf.operator)
|
104
|
+
[schema.column_type]
|
105
|
+
else
|
106
|
+
schema.column_type
|
107
|
+
end
|
108
|
+
|
109
|
+
return leaf.override(
|
110
|
+
value: convert_value_helper(true, column_type, should_use_hex(prefix), leaf.value)
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
leaf
|
115
|
+
end
|
116
|
+
|
117
|
+
def should_use_hex(name)
|
118
|
+
return @use_hex_conversion[name] if @use_hex_conversion.key?(name)
|
119
|
+
|
120
|
+
Utils::Schema.primary_key?(@child_collection, name) || Utils::Schema.foreign_key?(@child_collection, name)
|
121
|
+
end
|
122
|
+
|
123
|
+
def convert_record(to_backend, record)
|
124
|
+
if record
|
125
|
+
record = record.to_h do |path, value|
|
126
|
+
[path, convert_value(to_backend, path, value)]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
record
|
131
|
+
end
|
132
|
+
|
133
|
+
def convert_value(to_backend, path, value)
|
134
|
+
prefix, suffix = path.split(':')
|
135
|
+
field = @child_collection.schema[:fields][prefix]
|
136
|
+
|
137
|
+
if field.type != 'Column'
|
138
|
+
foreign_collection = @datasource.get_collection(field.foreign_collection)
|
139
|
+
|
140
|
+
return suffix ? foreign_collection.convert_value(to_backend, suffix,
|
141
|
+
value) : foreign_collection.convert_record(to_backend,
|
142
|
+
value)
|
143
|
+
end
|
144
|
+
|
145
|
+
binary_mode = should_use_hex(path)
|
146
|
+
|
147
|
+
convert_value_helper(to_backend, field.column_type, binary_mode, value)
|
148
|
+
end
|
149
|
+
|
150
|
+
def convert_value_helper(to_backend, column_type, use_hex, value)
|
151
|
+
if value
|
152
|
+
return convert_scalar(to_backend, use_hex, value) if column_type == 'Binary'
|
153
|
+
|
154
|
+
if column_type.is_a? Array
|
155
|
+
return value.map { |v| convert_value_helper(to_backend, column_type[0], use_hex, v) }
|
156
|
+
end
|
157
|
+
|
158
|
+
unless column_type.is_a? String
|
159
|
+
return column_type.to_h { |key, type| [key, convert_value_helper(to_backend, type, use_hex, value[key])] }
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
value
|
164
|
+
end
|
165
|
+
|
166
|
+
def convert_scalar(to_backend, use_hex, value)
|
167
|
+
if to_backend
|
168
|
+
return use_hex ? BinaryHelper.hex_to_bin(value) : Base64.strict_decode64(value.partition(',')[2])
|
169
|
+
end
|
170
|
+
|
171
|
+
return BinaryHelper.bin_to_hex(value) if use_hex
|
172
|
+
|
173
|
+
data = Base64.strict_encode64(value)
|
174
|
+
mime = Marcel::MimeType.for StringIO.new(value)
|
175
|
+
|
176
|
+
"data:#{mime};base64,#{data}"
|
177
|
+
end
|
178
|
+
|
179
|
+
def replace_column_type(column_type)
|
180
|
+
if column_type.is_a? String
|
181
|
+
return column_type == 'Binary' ? 'String' : column_type
|
182
|
+
end
|
183
|
+
|
184
|
+
return [replace_column_type(column_type[0])] if column_type.is_a? Array
|
185
|
+
|
186
|
+
column_type.transform_values { |type| replace_column_type(type) }
|
187
|
+
end
|
188
|
+
|
189
|
+
def replace_validation(name, column_schema)
|
190
|
+
if column_schema.column_type == 'Binary'
|
191
|
+
validations = []
|
192
|
+
min_length = (column_schema.validations.find { |v| v[:operator] == Operators::LONGER_THAN } || {})[:value]
|
193
|
+
max_length = (column_schema.validations.find { |v| v[:operator] == Operators::SHORTER_THAN } || {})[:value]
|
194
|
+
|
195
|
+
if should_use_hex(name)
|
196
|
+
validations << { operator: Operators::MATCH, value: '/^[0-9a-f]+$/' }
|
197
|
+
validations << { operator: Operators::LONGER_THAN, value: (min_length * 2) + 1 } if min_length
|
198
|
+
validations << { operator: Operators::SHORTER_THAN, value: (max_length * 2) - 1 } if max_length
|
199
|
+
else
|
200
|
+
validations << { operator: Operators::MATCH, value: '/^data:.*;base64,.*/' }
|
201
|
+
end
|
202
|
+
|
203
|
+
if column_schema.validations.find { |v| v[:operator] == Operators::PRESENT }
|
204
|
+
validations << { operator: Operators::PRESENT }
|
205
|
+
end
|
206
|
+
|
207
|
+
return validations
|
208
|
+
end
|
209
|
+
|
210
|
+
column_schema.validations
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module ForestAdminDatasourceCustomizer
|
4
|
+
module Decorators
|
5
|
+
module Binary
|
6
|
+
class BinaryHelper
|
7
|
+
def self.bin_to_hex(data)
|
8
|
+
data.unpack1('H*')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.hex_to_bin(data)
|
12
|
+
data.scan(/../).map(&:hex).pack('c*')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Chart
|
4
|
+
class ChartCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
include ForestAdminDatasourceToolkit
|
6
|
+
include ForestAdminDatasourceToolkit::Decorators
|
7
|
+
|
8
|
+
attr_reader :charts
|
9
|
+
|
10
|
+
def initialize(child_collection, datasource)
|
11
|
+
@charts = {}
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_chart(name, &definition)
|
16
|
+
raise(Exceptions::ForestException, "Chart #{name} already exists.") if schema[:charts].include?(name)
|
17
|
+
|
18
|
+
@charts[name] = definition
|
19
|
+
mark_schema_as_dirty
|
20
|
+
end
|
21
|
+
|
22
|
+
def render_chart(caller, name, record_id)
|
23
|
+
if @charts.key?(name)
|
24
|
+
context = ChartContext.new(self, caller, record_id)
|
25
|
+
result_builder = ResultBuilder.new
|
26
|
+
|
27
|
+
return @charts[name].call(context, result_builder)
|
28
|
+
end
|
29
|
+
|
30
|
+
@child_collection.render_chart(caller, name, record_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def refine_schema(sub_schema)
|
34
|
+
sub_schema[:charts] = sub_schema[:charts].union(@charts.keys)
|
35
|
+
|
36
|
+
sub_schema
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Chart
|
4
|
+
class ChartContext < ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext
|
5
|
+
include ForestAdminDatasourceToolkit
|
6
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
7
|
+
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
|
8
|
+
|
9
|
+
attr_reader :composite_record_id
|
10
|
+
|
11
|
+
def initialize(collection, caller, record_id)
|
12
|
+
super(collection, caller)
|
13
|
+
@composite_record_id = record_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def record_id
|
17
|
+
if @composite_record_id.size > 1
|
18
|
+
raise Exceptions::ForestException,
|
19
|
+
"Collection is using a composite pk: use 'context.composite_record_id'."
|
20
|
+
end
|
21
|
+
|
22
|
+
@composite_record_id[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_record(fields)
|
26
|
+
condition_tree = ConditionTreeFactory.match_ids(@real_collection, [@composite_record_id])
|
27
|
+
|
28
|
+
collection.list(Filter.new(condition_tree: condition_tree), Projection.new(fields))[0]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Chart
|
4
|
+
class ChartDatasourceDecorator < ForestAdminDatasourceToolkit::Decorators::DatasourceDecorator
|
5
|
+
include ForestAdminDatasourceToolkit
|
6
|
+
include ForestAdminDatasourceToolkit::Decorators
|
7
|
+
|
8
|
+
def initialize(child_datasource)
|
9
|
+
@charts = {}
|
10
|
+
super(child_datasource, ChartCollectionDecorator)
|
11
|
+
end
|
12
|
+
|
13
|
+
def schema
|
14
|
+
child_schema = @child_datasource.schema.dup
|
15
|
+
|
16
|
+
duplicate = @charts.keys.find { |name| child_schema[:charts].include?(name) }
|
17
|
+
|
18
|
+
raise(Exceptions::ForestException, "Chart #{duplicate} is defined twice.") if duplicate
|
19
|
+
|
20
|
+
child_schema[:charts] = child_schema[:charts].union(@charts.keys)
|
21
|
+
|
22
|
+
child_schema
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_chart(name, &definition)
|
26
|
+
raise(Exceptions::ForestException, "Chart #{name} already exists.") if schema[:charts].include?(name)
|
27
|
+
|
28
|
+
@charts[name] = definition
|
29
|
+
end
|
30
|
+
|
31
|
+
def render_chart(caller, name)
|
32
|
+
chart_definition = @charts[name]
|
33
|
+
|
34
|
+
if chart_definition
|
35
|
+
return chart_definition.call(
|
36
|
+
Context::AgentCustomizationContext.new(self, caller),
|
37
|
+
ResultBuilder.new
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
super
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
|
3
|
+
module ForestAdminDatasourceCustomizer
|
4
|
+
module Decorators
|
5
|
+
module Chart
|
6
|
+
class ResultBuilder
|
7
|
+
include ForestAdminDatasourceToolkit::Components::Charts
|
8
|
+
include ForestAdminDatasourceToolkit::Utils
|
9
|
+
|
10
|
+
TIME_FORMAT = {
|
11
|
+
Day: '%d/%m/%Y',
|
12
|
+
Week: 'W%V-%G',
|
13
|
+
Month: '%b %y',
|
14
|
+
Year: '%Y'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def value(value, previous_value = nil)
|
18
|
+
ValueChart.new(value, previous_value).serialize
|
19
|
+
end
|
20
|
+
|
21
|
+
def distribution(data)
|
22
|
+
data = HashHelper.convert_keys(data, :to_s).map do |key, value|
|
23
|
+
{ key: key, value: value }
|
24
|
+
end
|
25
|
+
|
26
|
+
PieChart.new(data).serialize
|
27
|
+
end
|
28
|
+
|
29
|
+
def percentage(value)
|
30
|
+
PercentageChart.new(value).serialize
|
31
|
+
end
|
32
|
+
|
33
|
+
def objective(value, objective)
|
34
|
+
ObjectiveChart.new(value, objective).serialize
|
35
|
+
end
|
36
|
+
|
37
|
+
def leaderboard(value)
|
38
|
+
data = distribution(value).sort { |a, b| b[:value] - a[:value] }
|
39
|
+
|
40
|
+
LeaderboardChart.new(data).serialize
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add a TimeBasedChart based on a time range and a set of values.
|
44
|
+
# @param time_range - The time range for the chart, specified as "Year", "Month", "Week" or "Day".
|
45
|
+
# @param values - This is an array of objects with 'date' and 'value' properties
|
46
|
+
# @returns {TimeBasedChart} Returns a TimeBasedChart representing the data within the specified
|
47
|
+
# time range.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# time_based(
|
51
|
+
# 'Day',
|
52
|
+
# [
|
53
|
+
# { date: '2023-01-01', value: 42 },
|
54
|
+
# { date: '2023-01-02', value: 55 },
|
55
|
+
# { date: '2023-01-03', value: null },
|
56
|
+
# ]
|
57
|
+
# );
|
58
|
+
def time_based(time_range, values)
|
59
|
+
return [] if values.nil?
|
60
|
+
|
61
|
+
values = HashHelper.convert_keys(values, :to_sym)
|
62
|
+
values = values.map { |date, value| { date: date, value: value } } unless values.is_a? Array
|
63
|
+
data = build_time_based_chart_result(time_range, values)
|
64
|
+
|
65
|
+
LineChart.new(data).serialize
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add a MultipleTimeBasedChart based on a time range,
|
69
|
+
# an array of dates, and multiple lines of data.
|
70
|
+
#
|
71
|
+
# @param time_range - The time range for the chart, specified as "Year", "Month", "Week" or "Day".
|
72
|
+
# @param dates - An array of dates that define the x-axis values for the chart.
|
73
|
+
# @param lines - An array of lines, each containing a label and an array of numeric data values (or null)
|
74
|
+
# corresponding to the dates.
|
75
|
+
# @returns {MultipleTimeBasedChart} Returns a MultipleTimeBasedChart representing multiple
|
76
|
+
# lines of data within the specified time range.
|
77
|
+
#
|
78
|
+
# @example
|
79
|
+
# multiple_time_based(
|
80
|
+
# 'Day',
|
81
|
+
# [
|
82
|
+
# Date.new('1985-10-26'),
|
83
|
+
# Date.new('2011-10-05T14:48:00.000Z'),
|
84
|
+
# Date.new()
|
85
|
+
# ],
|
86
|
+
# [
|
87
|
+
# { label: 'line1', values: [1, 2, 3] },
|
88
|
+
# { label: 'line2', values: [3, 4, null] }
|
89
|
+
# ],
|
90
|
+
# );
|
91
|
+
def multiple_time_based(time_range, dates, lines)
|
92
|
+
return { labels: nil, values: nil } if dates.nil? || lines.nil?
|
93
|
+
|
94
|
+
formatted_times = nil
|
95
|
+
formatted_lines = lines.map do |line|
|
96
|
+
values = dates.each_with_index.with_object([]) do |(date, index), memo|
|
97
|
+
memo.push({ date: date, value: line[:values][index] })
|
98
|
+
end
|
99
|
+
|
100
|
+
build_time_based = build_time_based_chart_result(time_range, values)
|
101
|
+
formatted_times = build_time_based.map { |time_based| time_based[:label] } if formatted_times.nil?
|
102
|
+
|
103
|
+
{ key: line[:label], values: build_time_based.map { |time_based| time_based[:values][:value] } }
|
104
|
+
end
|
105
|
+
|
106
|
+
{
|
107
|
+
labels: formatted_times,
|
108
|
+
values: formatted_times&.length&.positive? ? formatted_lines : nil
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def smart(data)
|
113
|
+
data
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def build_time_based_chart_result(time_range, points)
|
119
|
+
return [] if points.empty?
|
120
|
+
|
121
|
+
format = TIME_FORMAT[time_range.to_sym]
|
122
|
+
formatted = {}
|
123
|
+
points.each do |point|
|
124
|
+
point[:date] = DateTime.parse(point[:date].to_s) if point[:date].is_a?(String) || point[:date].is_a?(Symbol)
|
125
|
+
label = point[:date].strftime(format)
|
126
|
+
formatted[label] = (formatted[label] || 0) + point[:value] if point[:value].is_a? Numeric
|
127
|
+
end
|
128
|
+
|
129
|
+
data_points = []
|
130
|
+
dates = points.map { |point| point[:date] }
|
131
|
+
.sort { |date_a, date_b| date_a - date_b }
|
132
|
+
|
133
|
+
# first date
|
134
|
+
current = dates.first.send(:"beginning_of_#{time_range.to_s.downcase}")
|
135
|
+
last = dates.last
|
136
|
+
|
137
|
+
while current <= last
|
138
|
+
label = current.strftime(format)
|
139
|
+
data_points << { label: label, values: { value: formatted[label] || 0 } }
|
140
|
+
current += 1.send(time_range.to_s.downcase.to_sym)
|
141
|
+
end
|
142
|
+
|
143
|
+
data_points
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/lib/forest_admin_datasource_customizer/decorators/computed/compute_collection_decorator.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Computed
|
4
|
+
class ComputeCollectionDecorator < ForestAdminDatasourceToolkit::Decorators::CollectionDecorator
|
5
|
+
include ForestAdminDatasourceToolkit::Components::Query
|
6
|
+
include ForestAdminDatasourceToolkit::Validations
|
7
|
+
include ForestAdminDatasourceCustomizer::Decorators::Computed::Utils
|
8
|
+
include ForestAdminDatasourceToolkit::Exceptions
|
9
|
+
|
10
|
+
def initialize(child_collection, datasource)
|
11
|
+
super
|
12
|
+
@computeds = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_computed(path)
|
16
|
+
index = path.index(':')
|
17
|
+
return @computeds[path] if index.nil?
|
18
|
+
|
19
|
+
foreign_collection = schema[:fields][path[0, index]].foreign_collection
|
20
|
+
association = @datasource.get_collection(foreign_collection)
|
21
|
+
|
22
|
+
association.get_computed(path[index + 1, path.length - index - 1])
|
23
|
+
end
|
24
|
+
|
25
|
+
def register_computed(name, computed)
|
26
|
+
FieldValidator.validate_name(@name, name)
|
27
|
+
|
28
|
+
# Check that all dependencies exist and are columns
|
29
|
+
computed.dependencies.each do |field|
|
30
|
+
FieldValidator.validate(self, field)
|
31
|
+
end
|
32
|
+
|
33
|
+
if computed.dependencies.length <= 0
|
34
|
+
raise ForestException,
|
35
|
+
"Computed field '#{name}' must have at least one dependency."
|
36
|
+
end
|
37
|
+
|
38
|
+
@computeds[name] = computed
|
39
|
+
mark_schema_as_dirty
|
40
|
+
end
|
41
|
+
|
42
|
+
def list(caller, filter, projection)
|
43
|
+
child_projection = projection.replace { |path| rewrite_field(self, path) }
|
44
|
+
records = @child_collection.list(caller, filter, child_projection)
|
45
|
+
return records if child_projection.equals(projection)
|
46
|
+
|
47
|
+
context = ForestAdminDatasourceCustomizer::Context::CollectionCustomizationContext.new(self, caller)
|
48
|
+
|
49
|
+
ComputedField.compute_from_records(context, self, child_projection, projection, records)
|
50
|
+
end
|
51
|
+
|
52
|
+
def aggregate(caller, filter, aggregation, limit = nil)
|
53
|
+
# No computed are used in the aggregation => just delegate to the underlying collection.
|
54
|
+
unless aggregation.projection.any? do |field|
|
55
|
+
get_computed(field)
|
56
|
+
end
|
57
|
+
return @child_collection.aggregate(caller, filter, aggregation,
|
58
|
+
limit)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Fallback to full emulation.
|
62
|
+
aggregation.apply(
|
63
|
+
list(caller, filter, aggregation.projection),
|
64
|
+
caller.timezone,
|
65
|
+
limit
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def refine_schema(child_schema)
|
70
|
+
schema = child_schema.clone
|
71
|
+
schema[:fields] = child_schema[:fields].clone
|
72
|
+
|
73
|
+
@computeds.each do |name, computed|
|
74
|
+
schema[:fields][name] = ForestAdminDatasourceToolkit::Schema::ColumnSchema.new(
|
75
|
+
column_type: computed.column_type,
|
76
|
+
default_value: computed.default_value,
|
77
|
+
enum_values: computed.enum_values || [],
|
78
|
+
filter_operators: [],
|
79
|
+
is_primary_key: false,
|
80
|
+
is_read_only: true,
|
81
|
+
is_sortable: false
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
schema
|
86
|
+
end
|
87
|
+
|
88
|
+
def rewrite_field(collection, path)
|
89
|
+
# Projection is targeting a field on another collection => recurse.
|
90
|
+
if path.include?(':')
|
91
|
+
prefix = path.split(':')[0]
|
92
|
+
schema = collection.schema[:fields][prefix]
|
93
|
+
association = collection.datasource.get_collection(schema.foreign_collection)
|
94
|
+
|
95
|
+
return Projection.new([path])
|
96
|
+
.unnest
|
97
|
+
.replace { |sub_path| rewrite_field(association, sub_path) }
|
98
|
+
.nest(prefix: prefix)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Computed field that we own: recursively replace by dependencies
|
102
|
+
computed = collection.get_computed(path)
|
103
|
+
|
104
|
+
if computed
|
105
|
+
Projection.new(computed.dependencies.flatten).replace do |dep_path|
|
106
|
+
rewrite_field(collection, dep_path)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
Projection.new([path])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ForestAdminDatasourceCustomizer
|
2
|
+
module Decorators
|
3
|
+
module Computed
|
4
|
+
class ComputedDefinition
|
5
|
+
attr_reader :column_type, :dependencies, :default_value, :enum_values
|
6
|
+
|
7
|
+
def initialize(column_type:, dependencies:, values:, default_value: nil, enum_values: nil)
|
8
|
+
@column_type = column_type
|
9
|
+
@dependencies = dependencies
|
10
|
+
@values = values
|
11
|
+
@default_value = default_value
|
12
|
+
@enum_values = enum_values
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_values(*args)
|
16
|
+
@values.call(*args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|