forest_liana 9.22.2 → 9.22.3
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 277b2499887f0ff82ea53ca55fc79843541cef4709f064c70f5efa55a4735d53
|
|
4
|
+
data.tar.gz: f0fbeb9771dae6c1db93581279e433815f47eca0e257543f24bf012057da51d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e02b44b8348a306b0e7e60b44da087c0effb4ba538947e9165e6db2e156698b4a276a0288335ea8b7768454e0d4309ef4232e37c994abd2adffc04763f66f2b
|
|
7
|
+
data.tar.gz: 2be7bc1df7f51619407f1fe8f2b49a8e7b5629ada6151677e6f1f9a57a5548ffbeec5cfdb33f31cb8ca015a26937fb73e96f7decb9b63e959903e69497791f1b
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
module ForestLiana
|
|
2
2
|
module AggregationHelper
|
|
3
|
-
def resolve_field_path(field_param, default_field = 'id')
|
|
3
|
+
def resolve_field_path(field_param, default_field = 'id', resource = @resource)
|
|
4
4
|
if field_param.blank?
|
|
5
|
-
default_field ||=
|
|
6
|
-
return "#{
|
|
5
|
+
default_field ||= resource.primary_key || 'id'
|
|
6
|
+
return "#{resource.table_name}.#{default_field}"
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
if field_param.include?(':')
|
|
10
10
|
association, field = field_param.split ':'
|
|
11
|
-
associated_resource =
|
|
11
|
+
associated_resource = resource.reflect_on_association(association.to_sym)
|
|
12
12
|
"#{associated_resource.table_name}.#{field}"
|
|
13
13
|
else
|
|
14
|
-
"#{
|
|
14
|
+
"#{resource.table_name}.#{field_param}"
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def aggregation_sql(type, field)
|
|
19
|
-
field_path = resolve_field_path(field)
|
|
18
|
+
def aggregation_sql(type, field, resource = @resource)
|
|
19
|
+
field_path = resolve_field_path(field, 'id', resource)
|
|
20
20
|
|
|
21
21
|
case type
|
|
22
22
|
when 'sum'
|
|
@@ -4,9 +4,10 @@ module ForestLiana
|
|
|
4
4
|
|
|
5
5
|
def initialize(parent_model, params, forest_user)
|
|
6
6
|
@resource = parent_model
|
|
7
|
-
@scoped_parent_model = get_scoped_model(parent_model, forest_user, params[:timezone])
|
|
8
|
-
|
|
9
|
-
@
|
|
7
|
+
@scoped_parent_model = get_scoped_model(parent_model, forest_user, params[:timezone]) || parent_model.unscoped
|
|
8
|
+
@relationship_field_name = params[:relationshipFieldName].to_sym
|
|
9
|
+
@child_model = @scoped_parent_model.reflect_on_association(@relationship_field_name).klass
|
|
10
|
+
@child_scope = get_scoped_model(@child_model, forest_user, params[:timezone])
|
|
10
11
|
@label_field = params[:labelFieldName]
|
|
11
12
|
@aggregate = params[:aggregator].downcase
|
|
12
13
|
@aggregate_field = params[:aggregateFieldName]
|
|
@@ -15,17 +16,25 @@ module ForestLiana
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def perform
|
|
18
|
-
includes = ForestLiana::QueryHelper.get_one_association_names_symbol(@scoped_child_model)
|
|
19
|
-
|
|
20
19
|
alias_name = aggregation_alias(@aggregate, @aggregate_field)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
# NOTICE: The chart groups on the parent collection and aggregates its related records, so
|
|
21
|
+
# the query starts from the parent: querying the related model leaves the parent
|
|
22
|
+
# table out of the FROM clause unless it happens to be one of its belongs_to.
|
|
23
|
+
aggregation = aggregation_sql(@aggregate, @aggregate_field, @child_model)
|
|
24
|
+
records = @scoped_parent_model.joins(@relationship_field_name)
|
|
25
|
+
|
|
26
|
+
# NOTICE: The related collection scope goes through a subquery: merging it would re-root its
|
|
27
|
+
# joins onto the parent, and reorder drops the ordering of the parent default_scope.
|
|
28
|
+
if @child_scope
|
|
29
|
+
child_key = @child_model.primary_key
|
|
30
|
+
records = records.where(@child_model.table_name => { child_key => @child_scope.select(child_key) })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
result = records
|
|
25
34
|
.group(@group_by)
|
|
26
|
-
.
|
|
35
|
+
.reorder(Arel.sql("#{alias_name} DESC"))
|
|
27
36
|
.limit(@limit)
|
|
28
|
-
.pluck(@group_by, Arel.sql("#{
|
|
37
|
+
.pluck(@group_by, Arel.sql("#{aggregation} AS #{alias_name}"))
|
|
29
38
|
.map { |key, value| { key: key, value: value } }
|
|
30
39
|
|
|
31
40
|
@record = Model::Stat.new(value: result)
|
|
@@ -34,7 +43,7 @@ module ForestLiana
|
|
|
34
43
|
def get_scoped_model(model, forest_user, timezone)
|
|
35
44
|
scope_filters = ForestLiana::ScopeManager.get_scope(model.name, forest_user)
|
|
36
45
|
|
|
37
|
-
return
|
|
46
|
+
return if scope_filters.blank?
|
|
38
47
|
|
|
39
48
|
FiltersParser.new(scope_filters, model, timezone, @params).apply_filters
|
|
40
49
|
end
|
data/lib/forest_liana/version.rb
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module ForestLiana
|
|
2
|
+
describe LeaderboardStatGetter do
|
|
3
|
+
let(:rendering_id) { 13 }
|
|
4
|
+
let(:user) { { 'id' => '1', 'rendering_id' => rendering_id } }
|
|
5
|
+
let(:scopes) { {'scopes' => {}, 'team' => {'id' => '1', 'name' => 'Operations'}} }
|
|
6
|
+
|
|
7
|
+
before(:each) do
|
|
8
|
+
ForestLiana::ScopeManager.invalidate_scope_cache(rendering_id)
|
|
9
|
+
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes).and_return(scopes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe 'on a relationship whose model does not belong to the parent collection' do
|
|
13
|
+
let(:params) {
|
|
14
|
+
{
|
|
15
|
+
type: 'Leaderboard',
|
|
16
|
+
timezone: 'Europe/Paris',
|
|
17
|
+
relationshipFieldName: 'addresses',
|
|
18
|
+
labelFieldName: 'name',
|
|
19
|
+
aggregator: 'Count',
|
|
20
|
+
limit: 5
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
before(:each) do
|
|
25
|
+
king = User.create!(name: 'Aegon', title: 'king')
|
|
26
|
+
villager = User.create!(name: 'Davos', title: 'villager')
|
|
27
|
+
|
|
28
|
+
2.times { |index| Address.create!(line1: "#{index} Dragonstone", addressable: king) }
|
|
29
|
+
Address.create!(line1: '1 Flea Bottom', addressable: villager)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'ranks the parent records by number of related records' do
|
|
33
|
+
getter = LeaderboardStatGetter.new(User, params, user)
|
|
34
|
+
getter.perform
|
|
35
|
+
|
|
36
|
+
expect(getter.record.value).to eq([
|
|
37
|
+
{ key: 'Aegon', value: 2 },
|
|
38
|
+
{ key: 'Davos', value: 1 }
|
|
39
|
+
])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe 'on a relationship whose model belongs to the parent collection' do
|
|
44
|
+
let(:aggregator) { 'Count' }
|
|
45
|
+
let(:aggregate_field) { nil }
|
|
46
|
+
let(:params) {
|
|
47
|
+
{
|
|
48
|
+
type: 'Leaderboard',
|
|
49
|
+
timezone: 'Europe/Paris',
|
|
50
|
+
relationshipFieldName: 'trees',
|
|
51
|
+
labelFieldName: 'name',
|
|
52
|
+
aggregator: aggregator,
|
|
53
|
+
aggregateFieldName: aggregate_field,
|
|
54
|
+
limit: 5
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
before(:each) do
|
|
59
|
+
dragonstone = Island.create!(name: 'Dragonstone')
|
|
60
|
+
skagos = Island.create!(name: 'Skagos')
|
|
61
|
+
|
|
62
|
+
Tree.create!(name: 'Old Tree', age: 15, island: dragonstone)
|
|
63
|
+
Tree.create!(name: 'Young Tree', age: 3, island: dragonstone)
|
|
64
|
+
Tree.create!(name: 'Lone Tree', age: 30, island: skagos)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'counts the related records, not the parent ones' do
|
|
68
|
+
getter = LeaderboardStatGetter.new(Island, params, user)
|
|
69
|
+
getter.perform
|
|
70
|
+
|
|
71
|
+
expect(getter.record.value).to eq([
|
|
72
|
+
{ key: 'Dragonstone', value: 2 },
|
|
73
|
+
{ key: 'Skagos', value: 1 }
|
|
74
|
+
])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context 'when the chart sums a field of the related records' do
|
|
78
|
+
let(:aggregator) { 'Sum' }
|
|
79
|
+
let(:aggregate_field) { 'age' }
|
|
80
|
+
|
|
81
|
+
it 'sums that field on the related model' do
|
|
82
|
+
getter = LeaderboardStatGetter.new(Island, params, user)
|
|
83
|
+
getter.perform
|
|
84
|
+
|
|
85
|
+
expect(getter.record.value).to eq([
|
|
86
|
+
{ key: 'Skagos', value: 30 },
|
|
87
|
+
{ key: 'Dragonstone', value: 18 }
|
|
88
|
+
])
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context 'when the related collection is scoped on a field of its own relationship' do
|
|
93
|
+
let(:scopes) {
|
|
94
|
+
{
|
|
95
|
+
'scopes' => {
|
|
96
|
+
'Tree' => { 'aggregator' => 'and', 'conditions' => [{ 'field' => 'owner:name', 'operator' => 'equal', 'value' => 'Aegon' }] }
|
|
97
|
+
},
|
|
98
|
+
'team' => { 'id' => '1', 'name' => 'Operations' }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
before(:each) do
|
|
103
|
+
Tree.find_by!(name: 'Lone Tree').update!(owner: User.create!(name: 'Aegon'))
|
|
104
|
+
Tree.find_by!(name: 'Old Tree').update!(owner: User.create!(name: 'Davos'))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'only aggregates the related records the scope keeps' do
|
|
108
|
+
getter = LeaderboardStatGetter.new(Island, params, user)
|
|
109
|
+
getter.perform
|
|
110
|
+
|
|
111
|
+
expect(getter.record.value).to eq([
|
|
112
|
+
{ key: 'Skagos', value: 1 }
|
|
113
|
+
])
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe 'on a scoped parent collection whose default_scope orders' do
|
|
119
|
+
let(:scopes) {
|
|
120
|
+
{
|
|
121
|
+
'scopes' => {
|
|
122
|
+
'Owner' => { 'aggregator' => 'and', 'conditions' => [{ 'field' => 'name', 'operator' => 'not_equal', 'value' => 'Theon' }] }
|
|
123
|
+
},
|
|
124
|
+
'team' => { 'id' => '1', 'name' => 'Operations' }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let(:params) {
|
|
128
|
+
{
|
|
129
|
+
type: 'Leaderboard',
|
|
130
|
+
timezone: 'Europe/Paris',
|
|
131
|
+
relationshipFieldName: 'trees',
|
|
132
|
+
labelFieldName: 'name',
|
|
133
|
+
aggregator: 'Count',
|
|
134
|
+
limit: 5
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
before(:each) do
|
|
139
|
+
jon = Owner.create!(name: 'Jon', hired_at: 2.years.ago)
|
|
140
|
+
sansa = Owner.create!(name: 'Sansa', hired_at: 1.year.ago)
|
|
141
|
+
|
|
142
|
+
Tree.create!(name: 'Weirwood', owner_id: jon.id)
|
|
143
|
+
2.times { |index| Tree.create!(name: "Pine #{index}", owner_id: sansa.id) }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'ranks by the aggregate, not by the default_scope ordering' do
|
|
147
|
+
getter = LeaderboardStatGetter.new(Owner, params, user)
|
|
148
|
+
getter.perform
|
|
149
|
+
|
|
150
|
+
expect(getter.record.value).to eq([
|
|
151
|
+
{ key: 'Sansa', value: 2 },
|
|
152
|
+
{ key: 'Jon', value: 1 }
|
|
153
|
+
])
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
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.22.
|
|
4
|
+
version: 9.22.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -501,6 +501,7 @@ files:
|
|
|
501
501
|
- spec/services/forest_liana/has_many_getter_pagination_spec.rb
|
|
502
502
|
- spec/services/forest_liana/has_many_getter_spec.rb
|
|
503
503
|
- spec/services/forest_liana/ip_whitelist_checker_spec.rb
|
|
504
|
+
- spec/services/forest_liana/leaderboard_stat_getter_spec.rb
|
|
504
505
|
- spec/services/forest_liana/line_stat_getter_spec.rb
|
|
505
506
|
- spec/services/forest_liana/pie_stat_getter_spec.rb
|
|
506
507
|
- spec/services/forest_liana/projection_parser_spec.rb
|
|
@@ -848,6 +849,7 @@ test_files:
|
|
|
848
849
|
- spec/services/forest_liana/has_many_getter_pagination_spec.rb
|
|
849
850
|
- spec/services/forest_liana/has_many_getter_spec.rb
|
|
850
851
|
- spec/services/forest_liana/ip_whitelist_checker_spec.rb
|
|
852
|
+
- spec/services/forest_liana/leaderboard_stat_getter_spec.rb
|
|
851
853
|
- spec/services/forest_liana/line_stat_getter_spec.rb
|
|
852
854
|
- spec/services/forest_liana/pie_stat_getter_spec.rb
|
|
853
855
|
- spec/services/forest_liana/projection_parser_spec.rb
|