forest_liana 9.14.3 → 9.14.5
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/app/serializers/forest_liana/serializer_factory.rb +4 -1
- data/app/services/forest_liana/aggregation_helper.rb +42 -0
- data/app/services/forest_liana/leaderboard_stat_getter.rb +7 -15
- data/app/services/forest_liana/pie_stat_getter.rb +12 -27
- data/lib/forest_liana/version.rb +1 -1
- data/spec/dummy/app/models/tree.rb +1 -1
- data/spec/services/forest_liana/pie_stat_getter_spec.rb +182 -0
- data/spec/services/forest_liana/serializer_factory_spec.rb +53 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ba28199210707c78a20dc527719d5a3b71e68079f6ca96f18602f978cb87f09
|
4
|
+
data.tar.gz: 60d8e40c7d69086c8c627f61e99bcd6612a5425285d29f3520596e01c0752103
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93ab4cf8bd8f375d38dae4ab1a337bd8a87d6fea98aff86f930468f4a293d60c2776a538916ce6816976f4dc8bf4e5823c809a44e36d684394859546be87adfc
|
7
|
+
data.tar.gz: 91471a98c2418ea67acdcac5e5d6539a611c7d6e717c35ee76a98189aeb71d94534c4535ca4e7c387eaa7d2997216594d4087e936c6bacb4561bd54cd7a0dbb1
|
@@ -146,7 +146,10 @@ module ForestLiana
|
|
146
146
|
@options[:fields][relation_class_name]&.size == 1 &&
|
147
147
|
@options[:fields][relation_class_name]&.include?(relation.klass.primary_key.to_sym)
|
148
148
|
|
149
|
-
attr_data[:attr_or_block] = proc {
|
149
|
+
attr_data[:attr_or_block] = proc {
|
150
|
+
foreign_key_value = object.send(relation.foreign_key.to_sym)
|
151
|
+
foreign_key_value.nil? ? nil : relation.klass.new(relation.klass.primary_key => foreign_key_value)
|
152
|
+
}
|
150
153
|
end
|
151
154
|
end
|
152
155
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ForestLiana
|
2
|
+
module AggregationHelper
|
3
|
+
def resolve_field_path(field_param, default_field = 'id')
|
4
|
+
if field_param.blank?
|
5
|
+
default_field ||= @resource.primary_key || 'id'
|
6
|
+
return "#{@resource.table_name}.#{default_field}"
|
7
|
+
end
|
8
|
+
|
9
|
+
if field_param.include?(':')
|
10
|
+
association, field = field_param.split ':'
|
11
|
+
associated_resource = @resource.reflect_on_association(association.to_sym)
|
12
|
+
"#{associated_resource.table_name}.#{field}"
|
13
|
+
else
|
14
|
+
"#{@resource.table_name}.#{field_param}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def aggregation_sql(type, field)
|
19
|
+
field_path = resolve_field_path(field)
|
20
|
+
|
21
|
+
case type
|
22
|
+
when 'sum'
|
23
|
+
"SUM(#{field_path})"
|
24
|
+
when 'count'
|
25
|
+
"COUNT(DISTINCT #{field_path})"
|
26
|
+
else
|
27
|
+
raise "Unsupported aggregator : #{type}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def aggregation_alias(type, field)
|
32
|
+
case type
|
33
|
+
when 'sum'
|
34
|
+
"sum_#{field.downcase}"
|
35
|
+
when 'count'
|
36
|
+
'count_id'
|
37
|
+
else
|
38
|
+
raise "Unsupported aggregator : #{type}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module ForestLiana
|
2
2
|
class LeaderboardStatGetter < StatGetter
|
3
|
+
include AggregationHelper
|
4
|
+
|
3
5
|
def initialize(parent_model, params, forest_user)
|
6
|
+
@resource = parent_model
|
4
7
|
@scoped_parent_model = get_scoped_model(parent_model, forest_user, params[:timezone])
|
5
8
|
child_model = @scoped_parent_model.reflect_on_association(params[:relationshipFieldName]).klass
|
6
9
|
@scoped_child_model = get_scoped_model(child_model, forest_user, params[:timezone])
|
@@ -14,13 +17,15 @@ module ForestLiana
|
|
14
17
|
def perform
|
15
18
|
includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@scoped_child_model)
|
16
19
|
|
20
|
+
alias_name = aggregation_alias(@aggregate, @aggregate_field)
|
21
|
+
|
17
22
|
result = @scoped_child_model
|
18
23
|
.joins(includes)
|
19
24
|
.where({ @scoped_parent_model.name.downcase.to_sym => @scoped_parent_model })
|
20
25
|
.group(@group_by)
|
21
|
-
.order(
|
26
|
+
.order(Arel.sql("#{alias_name} DESC"))
|
22
27
|
.limit(@limit)
|
23
|
-
.
|
28
|
+
.pluck(@group_by, Arel.sql("#{aggregation_sql(@aggregate, @aggregate_field)} AS #{alias_name}"))
|
24
29
|
.map { |key, value| { key: key, value: value } }
|
25
30
|
|
26
31
|
@record = Model::Stat.new(value: result)
|
@@ -33,18 +38,5 @@ module ForestLiana
|
|
33
38
|
|
34
39
|
FiltersParser.new(scope_filters, model, timezone, @params).apply_filters
|
35
40
|
end
|
36
|
-
|
37
|
-
def order
|
38
|
-
order = 'DESC'
|
39
|
-
|
40
|
-
# NOTICE: The generated alias for a count is "count_all", for a sum the
|
41
|
-
# alias looks like "sum_#{aggregate_field}"
|
42
|
-
if @aggregate == 'sum'
|
43
|
-
field = @aggregate_field.downcase
|
44
|
-
else
|
45
|
-
field = 'all'
|
46
|
-
end
|
47
|
-
"#{@aggregate}_#{field} #{order}"
|
48
|
-
end
|
49
41
|
end
|
50
42
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module ForestLiana
|
2
2
|
class PieStatGetter < StatGetter
|
3
|
+
include AggregationHelper
|
3
4
|
attr_accessor :record
|
4
5
|
|
5
6
|
def perform
|
@@ -13,11 +14,16 @@ module ForestLiana
|
|
13
14
|
resource = FiltersParser.new(filters, resource, @params[:timezone], @params).apply_filters
|
14
15
|
end
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
aggregation_type = @params[:aggregator].downcase
|
18
|
+
aggregation_field = @params[:aggregateFieldName]
|
19
|
+
alias_name = aggregation_alias(aggregation_type, aggregation_field)
|
20
|
+
|
21
|
+
resource = resource
|
22
|
+
.group(groupByFieldName)
|
23
|
+
.order(Arel.sql("#{alias_name} DESC"))
|
24
|
+
.pluck(groupByFieldName, Arel.sql("#{aggregation_sql(aggregation_type, aggregation_field)} AS #{alias_name}"))
|
25
|
+
|
26
|
+
result = resource.map do |key, value|
|
21
27
|
# NOTICE: Display the enum name instead of an integer if it is an
|
22
28
|
# "Enum" field type on old Rails version (before Rails
|
23
29
|
# 5.1.3).
|
@@ -38,28 +44,7 @@ module ForestLiana
|
|
38
44
|
end
|
39
45
|
|
40
46
|
def groupByFieldName
|
41
|
-
|
42
|
-
association, field = @params[:groupByFieldName].split ':'
|
43
|
-
resource = @resource.reflect_on_association(association.to_sym)
|
44
|
-
"#{resource.table_name}.#{field}"
|
45
|
-
else
|
46
|
-
"#{@resource.table_name}.#{@params[:groupByFieldName]}"
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def order
|
51
|
-
order = 'DESC'
|
52
|
-
|
53
|
-
# NOTICE: The generated alias for a count is "count_all", for a sum the
|
54
|
-
# alias looks like "sum_#{aggregateFieldName}"
|
55
|
-
if @params[:aggregator].downcase == 'sum'
|
56
|
-
field = @params[:aggregateFieldName].downcase
|
57
|
-
else
|
58
|
-
# `count_id` is required only for rails v5
|
59
|
-
field = Rails::VERSION::MAJOR == 5 || @includes.size > 0 ? 'id' : 'all'
|
60
|
-
end
|
61
|
-
"#{@params[:aggregator].downcase}_#{field} #{order}"
|
47
|
+
resolve_field_path(@params[:groupByFieldName])
|
62
48
|
end
|
63
|
-
|
64
49
|
end
|
65
50
|
end
|
data/lib/forest_liana/version.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class Tree < ActiveRecord::Base
|
2
2
|
belongs_to :owner, class_name: 'User', inverse_of: :trees_owned
|
3
3
|
belongs_to :cutter, class_name: 'User', inverse_of: :trees_cut
|
4
|
-
belongs_to :island
|
4
|
+
belongs_to :island, optional: true
|
5
5
|
belongs_to :eponymous_island,
|
6
6
|
->(record) { where(name: record.name) },
|
7
7
|
class_name: 'Island',
|
@@ -133,5 +133,187 @@ module ForestLiana
|
|
133
133
|
end
|
134
134
|
end
|
135
135
|
end
|
136
|
+
|
137
|
+
describe 'aggregation methods behavior' do
|
138
|
+
let(:scopes) { {'scopes' => {}, 'team' => {'id' => '1', 'name' => 'Operations'}} }
|
139
|
+
let(:model) { Tree }
|
140
|
+
let(:collection) { 'trees' }
|
141
|
+
let(:groupByFieldName) { 'age' }
|
142
|
+
|
143
|
+
describe 'aggregation_sql method' do
|
144
|
+
subject { PieStatGetter.new(model, params, user) }
|
145
|
+
|
146
|
+
context 'with COUNT aggregator' do
|
147
|
+
let(:params) {
|
148
|
+
{
|
149
|
+
type: 'Pie',
|
150
|
+
sourceCollectionName: collection,
|
151
|
+
timezone: 'Europe/Paris',
|
152
|
+
aggregator: 'Count',
|
153
|
+
groupByFieldName: groupByFieldName
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
it 'should generate correct COUNT SQL' do
|
158
|
+
sql = subject.send(:aggregation_sql, 'count', nil)
|
159
|
+
expect(sql).to eq 'COUNT(DISTINCT trees.id)'
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'should generate correct COUNT SQL with specific field' do
|
163
|
+
sql = subject.send(:aggregation_sql, 'count', 'age')
|
164
|
+
expect(sql).to eq 'COUNT(DISTINCT trees.age)'
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context 'with SUM aggregator' do
|
169
|
+
let(:params) {
|
170
|
+
{
|
171
|
+
type: 'Pie',
|
172
|
+
sourceCollectionName: collection,
|
173
|
+
timezone: 'Europe/Paris',
|
174
|
+
aggregator: 'Sum',
|
175
|
+
aggregateFieldName: 'age',
|
176
|
+
groupByFieldName: groupByFieldName
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
it 'should generate correct SUM SQL' do
|
181
|
+
sql = subject.send(:aggregation_sql, 'sum', 'age')
|
182
|
+
expect(sql).to eq 'SUM(trees.age)'
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context 'with association field' do
|
187
|
+
let(:params) {
|
188
|
+
{
|
189
|
+
type: 'Pie',
|
190
|
+
sourceCollectionName: collection,
|
191
|
+
timezone: 'Europe/Paris',
|
192
|
+
aggregator: 'Count',
|
193
|
+
groupByFieldName: 'owner:name'
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
it 'should handle association fields correctly' do
|
198
|
+
# Assuming Tree belongs_to :owner
|
199
|
+
allow(model).to receive(:reflect_on_association).with(:owner).and_return(
|
200
|
+
double(table_name: 'owners')
|
201
|
+
)
|
202
|
+
|
203
|
+
sql = subject.send(:aggregation_sql, 'count', 'owner:id')
|
204
|
+
expect(sql).to eq 'COUNT(DISTINCT owners.id)'
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
context 'with unsupported aggregator' do
|
209
|
+
let(:params) {
|
210
|
+
{
|
211
|
+
type: 'Pie',
|
212
|
+
sourceCollectionName: collection,
|
213
|
+
timezone: 'Europe/Paris',
|
214
|
+
aggregator: 'Invalid',
|
215
|
+
groupByFieldName: groupByFieldName
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
it 'should raise an error for unsupported aggregator' do
|
220
|
+
expect {
|
221
|
+
subject.send(:aggregation_sql, 'invalid', 'age')
|
222
|
+
}.to raise_error(ForestLiana::Errors::HTTP422Error)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe 'aggregation_alias method' do
|
228
|
+
subject { PieStatGetter.new(model, params, user) }
|
229
|
+
|
230
|
+
context 'with COUNT aggregator' do
|
231
|
+
let(:params) {
|
232
|
+
{
|
233
|
+
type: 'Pie',
|
234
|
+
sourceCollectionName: collection,
|
235
|
+
timezone: 'Europe/Paris',
|
236
|
+
aggregator: 'Count',
|
237
|
+
groupByFieldName: groupByFieldName
|
238
|
+
}
|
239
|
+
}
|
240
|
+
|
241
|
+
it 'should return correct alias for count' do
|
242
|
+
alias_name = subject.send(:aggregation_alias, 'count', nil)
|
243
|
+
expect(alias_name).to eq 'count_id'
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
context 'with SUM aggregator' do
|
248
|
+
let(:params) {
|
249
|
+
{
|
250
|
+
type: 'Pie',
|
251
|
+
sourceCollectionName: collection,
|
252
|
+
timezone: 'Europe/Paris',
|
253
|
+
aggregator: 'Sum',
|
254
|
+
aggregateFieldName: 'age',
|
255
|
+
groupByFieldName: groupByFieldName
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
it 'should return correct alias for sum' do
|
260
|
+
alias_name = subject.send(:aggregation_alias, 'sum', 'age')
|
261
|
+
expect(alias_name).to eq 'sum_age'
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'should handle field names with mixed case' do
|
265
|
+
alias_name = subject.send(:aggregation_alias, 'sum', 'TreeAge')
|
266
|
+
expect(alias_name).to eq 'sum_treeage'
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe 'results ordering' do
|
272
|
+
subject { PieStatGetter.new(model, params, user) }
|
273
|
+
|
274
|
+
context 'with COUNT aggregator' do
|
275
|
+
let(:params) {
|
276
|
+
{
|
277
|
+
type: 'Pie',
|
278
|
+
sourceCollectionName: collection,
|
279
|
+
timezone: 'Europe/Paris',
|
280
|
+
aggregator: 'Count',
|
281
|
+
groupByFieldName: groupByFieldName
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
it 'should return results ordered by count descending' do
|
286
|
+
subject.perform
|
287
|
+
|
288
|
+
expect(subject.record.value).to eq [
|
289
|
+
{ :key => 3, :value => 5},
|
290
|
+
{ :key => 15, :value => 4 }
|
291
|
+
]
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
context 'with SUM aggregator' do
|
296
|
+
let(:params) {
|
297
|
+
{
|
298
|
+
type: 'Pie',
|
299
|
+
sourceCollectionName: collection,
|
300
|
+
timezone: 'Europe/Paris',
|
301
|
+
aggregator: 'Sum',
|
302
|
+
aggregateFieldName: 'age',
|
303
|
+
groupByFieldName: groupByFieldName
|
304
|
+
}
|
305
|
+
}
|
306
|
+
|
307
|
+
it 'should return results ordered by sum descending' do
|
308
|
+
subject.perform
|
309
|
+
|
310
|
+
expect(subject.record.value).to eq [
|
311
|
+
{ :key => 15, :value => 60 },
|
312
|
+
{ :key => 3, :value => 15 }
|
313
|
+
]
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
136
318
|
end
|
137
319
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ForestLiana
|
2
|
+
describe SerializerFactory do
|
3
|
+
describe '#serializer_for has_one_relationships patch' do
|
4
|
+
let(:user) { User.create!(name: 'PatchTest') }
|
5
|
+
let(:island) { Island.create!(name: 'TestIsland') }
|
6
|
+
let(:tree) { Tree.create!(name: 'TestTree', island: island, owner: user) }
|
7
|
+
|
8
|
+
it 'simulates has_one relation with only primary key' do
|
9
|
+
factory = described_class.new
|
10
|
+
serializer_class = factory.serializer_for(Tree)
|
11
|
+
|
12
|
+
serializer_class.send(:has_one, :island) { }
|
13
|
+
|
14
|
+
instance = serializer_class.new(tree, fields: {
|
15
|
+
'Island' => [:id],
|
16
|
+
'Tree' => [:island]
|
17
|
+
})
|
18
|
+
|
19
|
+
relationships = instance.send(:has_one_relationships)
|
20
|
+
expect(relationships).to have_key(:island)
|
21
|
+
relation_data = relationships[:island]
|
22
|
+
expect(relation_data[:attr_or_block]).to be_a(Proc)
|
23
|
+
model = relation_data[:attr_or_block].call
|
24
|
+
|
25
|
+
expect(model).to be_a(Island)
|
26
|
+
expect(model.id).to eq(island.id)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns nil if foreign key is nil' do
|
30
|
+
tree_without_island = Tree.create!(name: 'NoIslandTree', island_id: nil, owner: user)
|
31
|
+
|
32
|
+
factory = described_class.new
|
33
|
+
serializer_class = factory.serializer_for(Tree)
|
34
|
+
|
35
|
+
serializer_class.send(:has_one, :island) { }
|
36
|
+
|
37
|
+
instance = serializer_class.new(tree_without_island, fields: {
|
38
|
+
'Island' => [:id],
|
39
|
+
'Tree' => [:island]
|
40
|
+
})
|
41
|
+
|
42
|
+
relationships = instance.send(:has_one_relationships)
|
43
|
+
expect(relationships).to have_key(:island)
|
44
|
+
relation_data = relationships[:island]
|
45
|
+
expect(relation_data[:attr_or_block]).to be_a(Proc)
|
46
|
+
model = relation_data[:attr_or_block].call
|
47
|
+
|
48
|
+
expect(model).to be_nil
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forest_liana
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 9.14.
|
4
|
+
version: 9.14.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sandro Munda
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -254,6 +254,7 @@ files:
|
|
254
254
|
- app/services/forest_liana/ability/permission.rb
|
255
255
|
- app/services/forest_liana/ability/permission/request_permission.rb
|
256
256
|
- app/services/forest_liana/ability/permission/smart_action_checker.rb
|
257
|
+
- app/services/forest_liana/aggregation_helper.rb
|
257
258
|
- app/services/forest_liana/apimap_sorter.rb
|
258
259
|
- app/services/forest_liana/authentication.rb
|
259
260
|
- app/services/forest_liana/authorization_getter.rb
|
@@ -431,6 +432,7 @@ files:
|
|
431
432
|
- spec/services/forest_liana/resources_getter_spec.rb
|
432
433
|
- spec/services/forest_liana/schema_adapter_spec.rb
|
433
434
|
- spec/services/forest_liana/scope_manager_spec.rb
|
435
|
+
- spec/services/forest_liana/serializer_factory_spec.rb
|
434
436
|
- spec/services/forest_liana/smart_action_field_validator_spec.rb
|
435
437
|
- spec/services/forest_liana/smart_action_form_parser_spec.rb
|
436
438
|
- spec/services/forest_liana/utils/context_variables_injector_spec.rb
|
@@ -736,6 +738,7 @@ test_files:
|
|
736
738
|
- spec/services/forest_liana/resources_getter_spec.rb
|
737
739
|
- spec/services/forest_liana/schema_adapter_spec.rb
|
738
740
|
- spec/services/forest_liana/scope_manager_spec.rb
|
741
|
+
- spec/services/forest_liana/serializer_factory_spec.rb
|
739
742
|
- spec/services/forest_liana/smart_action_field_validator_spec.rb
|
740
743
|
- spec/services/forest_liana/smart_action_form_parser_spec.rb
|
741
744
|
- spec/services/forest_liana/utils/context_variables_injector_spec.rb
|