Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e6f66f3
feat(charts): add charts components & validator
matthv Nov 17, 2023
d281a3f
fix: typo charts components
matthv Nov 17, 2023
09ffbe4
fix: value chart class
matthv Nov 17, 2023
99caa83
fix: objective chart
matthv Nov 17, 2023
32b834c
chore: add tests on charts components
matthv Nov 17, 2023
f53709c
chore: add test on chart validator class
matthv Nov 20, 2023
0ddad11
chore: remove semantic-release-rubygem package
matthv Nov 21, 2023
b58b6da
feat(aggregation): add methods on aggregation class to handle charts
matthv Nov 21, 2023
26736b1
feat: add query aggregate
nicolasalexandre9 Nov 21, 2023
c75b6e7
chore: add test on aggregation
matthv Nov 22, 2023
ba3087c
feat: add charts route
nicolasalexandre9 Nov 28, 2023
cea6e51
test: add test on charts route
nicolasalexandre9 Nov 30, 2023
0a33f43
test: add test on get_through_origin of collection utils class
nicolasalexandre9 Dec 1, 2023
39bf84a
fix(permission): fix permission_system function
nicolasalexandre9 Dec 8, 2023
f212922
feat: update chart permissions
nicolasalexandre9 Dec 8, 2023
92bd36f
fix: agent factory set cache
matthv Dec 12, 2023
7f864d5
fix(permissions): add deepsort package + sort recursively params for …
matthv Dec 12, 2023
ac217e7
fix: lint
matthv Dec 12, 2023
8d140e4
fix: requirement for query_string_parser
matthv Dec 13, 2023
d16429c
feat(chart): inject context_variables
matthv Dec 13, 2023
11643b2
feat(permissions): remove useless attributes
matthv Dec 13, 2023
6b9da53
chore: remove attr_reader team user for permissions
matthv Dec 13, 2023
4b9a0cd
chore: remove comment
nicolasalexandre9 Dec 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 36 additions & 37 deletions .releaserc.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,42 +46,41 @@ module.exports = {
},
],
'@semantic-release/github',
// 'semantic-release-rubygem',
// [
// 'semantic-release-slack-bot',
// {
// markdownReleaseNotes: true,
// notifyOnSuccess: true,
// notifyOnFail: false,
// onSuccessTemplate: {
// text: "📦 $package_name@$npm_package_version has been released!",
// blocks: [{
// type: 'section',
// text: {
// type: 'mrkdwn',
// text: '*New `$package_name` package released!*'
// }
// }, {
// type: 'context',
// elements: [{
// type: 'mrkdwn',
// text: "📦 *Version:* <$repo_url/releases/tag/v$npm_package_version|$npm_package_version>"
// }]
// }, {
// type: 'divider',
// }],
// attachments: [{
// blocks: [{
// type: 'section',
// text: {
// type: 'mrkdwn',
// text: '*Changes* of version $release_notes',
// },
// }],
// }],
// },
// packageName: 'agent_ruby',
// }
// ],
[
'semantic-release-slack-bot',
{
markdownReleaseNotes: true,
notifyOnSuccess: true,
notifyOnFail: false,
onSuccessTemplate: {
text: "📦 $package_name@$npm_package_version has been released!",
blocks: [{
type: 'section',
text: {
type: 'mrkdwn',
text: '*New `$package_name` package released!*'
}
}, {
type: 'context',
elements: [{
type: 'mrkdwn',
text: "📦 *Version:* <$repo_url/releases/tag/v$npm_package_version|$npm_package_version>"
}]
}, {
type: 'divider',
}],
attachments: [{
blocks: [{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Changes* of version $release_notes',
},
}],
}],
},
packageName: 'agent_ruby',
}
],
],
}
9 changes: 6 additions & 3 deletions .rubocop.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -187,12 +187,12 @@ Metrics/MethodLength:
Max: 20
Exclude:
- 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb'
- 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/parser/validation.rb'
- 'packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb'
- 'packages/forest_admin_datasource_active_record/spec/dummy/**/*'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/collection.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/auth/oauth2/forest_provider.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/serializer/json_api_serializer.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_leaf.rb'
Expand All@@ -207,7 +207,9 @@ Metrics/ClassLength:
Exclude:
- 'packages/forest_admin_agent/lib/forest_admin_agent/utils/schema/generator_field.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/charts/charts.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/aggregation.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/nodes/condition_tree_leaf.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/transforms/comparisons.rb'
Expand All@@ -232,8 +234,9 @@ Layout/LineLength:
- 'packages/forest_admin_agent/spec/lib/forest_admin_agent/http/forest_admin_api_requester_spec.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/routes/resources/list.rb'
- 'packages/forest_admin_agent/lib/forest_admin_agent/services/permissions.rb'
- 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb'
- 'packages/forest_admin_agent/spec/lib/forest_admin_agent/routes/resources/charts/charts_spec.rb'
- 'packages/forest_admin_agent/spec/lib/forest_admin_agent/services/smart_action_checker_spec.rb'
- 'packages/forest_admin_agent/spec/lib/forest_admin_agent/utils/context_variables_injector_spec.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/condition_tree/condition_tree_factory.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/utils/collection.rb'
- 'packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/components/query/filter_factory.rb'
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,7 +19,6 @@
"@semantic-release/exec": "6.0.3",
"@semantic-release/git": "10.0.1",
"semantic-release": "21.1.1",
"semantic-release-rubygem": "^1.2.0",
"semantic-release-slack-bot": "4.0.2"
}
}
1 change: 1 addition & 0 deletions packages/forest_admin_agent/forest_admin_agent.gemspec
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,6 +34,7 @@ admin work on any Ruby application."
spec.require_paths = ["lib"]

spec.add_dependency "activesupport", ">= 6.1"
spec.add_dependency "deepsort", "~> 0.4.5"
spec.add_dependency "dry-container", "~> 0.11"
spec.add_dependency "faraday", "~> 2.7"
spec.add_dependency "filecache", "~> 1.0"
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,9 +63,7 @@ def build_cache
return unless @has_env_secret

cache = @container.resolve(:cache)
cache.get_or_set 'config' do
@options.to_h
end
cache.set('config', @options.to_h)
end

def build_logger
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def self.routes
# api_charts_routes,
System::HealthCheck.new.routes,
Security::Authentication.new.routes,
Charts::Charts.new.routes,
Resources::Count.new.routes,
Resources::Delete.new.routes,
Resources::List.new.routes,
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
require 'jsonapi-serializers'
require 'active_support/inflector'

module ForestAdminAgent
module Routes
module Charts
class Charts < AbstractAuthenticatedRoute
include ForestAdminAgent::Builder
include ForestAdminAgent::Utils
include ForestAdminDatasourceToolkit::Components::Query
include ForestAdminDatasourceToolkit::Components::Query::ConditionTree
include ForestAdminDatasourceToolkit::Components::Charts

attr_reader :filter

FORMAT = {
Day: '%d/%m/%Y',
Week: 'W%W-%Y',
Month: '%b %y',
Year: '%Y'
}.freeze

def setup_routes
add_route('forest_chart', 'post', '/stats/:collection_name', lambda { |args|
handle_request(args)
})
self
end

def handle_request(args = {})
build(args)
@permissions.can_chart?(args[:params])
@args = args
self.type = args[:params][:type]
@filter = Filter.new(
condition_tree: ConditionTreeFactory.intersect(
[
@permissions.get_scope(@collection),
ForestAdminAgent::Utils::QueryStringParser.parse_condition_tree(
@collection, args
)
]
)
)

inject_context_variables

{ content: Serializer::ForestChartSerializer.serialize(send(:"make_#{@type}")) }
end

private

def type=(type)
chart_types = %w[Value Objective Pie Line Leaderboard]
unless chart_types.include?(type)
raise ForestAdminDatasourceToolkit::Exceptions::ForestException, "Invalid Chart type #{type}"
end

@type = type.downcase
end

def inject_context_variables
user = @permissions.get_user_data(@caller.id)
team = @permissions.get_team(@caller.rendering_id)

context_variables = ForestAdminAgent::Utils::ContextVariables.new(team, user,
@args[:params][:contextVariables])
return unless @args[:params][:filter]

@filter = @filter.override(condition_tree: ContextVariablesInjector.inject_context_in_filter(
@filter.condition_tree, context_variables
))
end

def make_value
value = compute_value(@filter)
previous_value = nil
is_and_aggregator = @filter.condition_tree&.try(:aggregator) == 'And'
with_count_previous = @filter.condition_tree&.some_leaf(&:use_interval_operator)

if with_count_previous && !is_and_aggregator
previous_value = compute_value(FilterFactory.get_previous_period_filter(@filter, @caller.timezone))
end

ValueChart.new(value, previous_value)
end

def make_objective
ObjectiveChart.new(compute_value(@filter))
end

def make_pie
group_field = @args[:params][:groupByFieldName]
aggregation = Aggregation.new(
operation: @args[:params][:aggregator],
field: @args[:params][:aggregateFieldName],
groups: group_field ? [{ field: group_field }] : []
)

result = @collection.aggregate(@caller, @filter, aggregation)

PieChart.new(result.map { |row| { key: row[:group][group_field], value: row[:value] } })
end

def make_line
group_by_field_name = @args[:params][:groupByFieldName]
time_range = @args[:params][:timeRange]
filter_only_with_values = @filter.override(
condition_tree: ConditionTree::ConditionTreeFactory.intersect(
[
@filter.condition_tree,
ConditionTree::Nodes::ConditionTreeLeaf.new(group_by_field_name, ConditionTree::Operators::PRESENT)
]
)
)
rows = @collection.aggregate(
@caller,
filter_only_with_values,
Aggregation.new(
operation: @args[:params][:aggregator],
field: @args[:params][:aggregateField],
groups: [{ field: group_by_field_name, operation: time_range }]
)
)

values = {}
rows.each { |row| values[row[:group][group_by_field_name]] = row[:value] }
dates = values.keys.sort
current = dates[0]
last = dates.last
result = []
while current <= last
result << {
label: current.strftime(FORMAT[time_range.to_sym]),
values: { value: values[current] || 0 }
}
current += 1.send(time_range.downcase.pluralize.to_sym)
end

LineChart.new(result)
end

def make_leaderboard
field = @collection.fields[@args[:params][:relationshipFieldName]]

if field && field.type == 'OneToMany'
inverse = ForestAdminDatasourceToolkit::Utils::Collection.get_inverse_relation(
@collection,
@args[:params][:relationshipFieldName]
)
if inverse
collection = field.foreign_collection
filter = @filter.nest(inverse)
aggregation = Aggregation.new(
operation: @args[:params][:aggregator],
field: @args[:params][:aggregateFieldName],
groups: [{ field: "#{inverse}:#{@args[:params][:labelFieldName]}" }]
)
end
end

if field && field.type == 'ManyToMany'
origin = ForestAdminDatasourceToolkit::Utils::Collection.get_through_origin(
@collection,
@args[:params][:relationshipFieldName]
)
target = ForestAdminDatasourceToolkit::Utils::Collection.get_through_target(
@collection,
@args[:params][:relationshipFieldName]
)
if origin && target
collection = field.through_collection
filter = @filter.nest(origin)
aggregation = Aggregation.new(
operation: @args[:params][:aggregator],
field: @args[:params][:aggregateFieldName] ? "#{target}:#{@args[:params][:aggregateFieldName]}" : nil,
groups: [{ field: "#{origin}:#{@args[:params][:labelFieldName]}" }]
)
end
end

if collection && filter && aggregation
rows = @datasource.collection(collection).aggregate(
@caller,
filter,
aggregation,
@args[:params][:limit]
)

result = rows.map do |row|
{
key: row[:group][aggregation.groups[0][:field]],
value: row[:value]
}
end

return LeaderboardChart.new(result)
end

raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
'Failed to generate leaderboard chart: parameters do not match pre-requisites'
end

def compute_value(filter)
aggregation = Aggregation.new(operation: @args[:params][:aggregator],
field: @args[:params][:aggregateFieldName])
result = @collection.aggregate(@caller, filter, aggregation)

result[0][:value] || 0
end
end
end
end
end
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,17 +33,17 @@ def handle_request(args = {})
def link_one_to_one_relations(args, record)
args[:params][:data][:relationships]&.map do |field, value|
schema = @collection.fields[field]
if schema.type == 'OneToOne'
id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true)
foreign_collection = @datasource.collection(schema.foreign_collection)
# Load the value that will be used as origin_key
origin_value = record[schema.origin_key_target]
next unless schema.type == 'OneToOne'

# update new relation (may update zero or one records).
condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id])
filter = Filter.new(condition_tree: condition_tree)
foreign_collection.update(@caller, filter, { schema.origin_key => origin_value })
end
id = Utils::Id.unpack_id(@collection, value['data']['id'], with_key: true)
foreign_collection = @datasource.collection(schema.foreign_collection)
# Load the value that will be used as origin_key
origin_value = record[schema.origin_key_target]

# update new relation (may update zero or one records).
condition_tree = ConditionTree::ConditionTreeFactory.match_records(foreign_collection, [id])
filter = Filter.new(condition_tree: condition_tree)
foreign_collection.update(@caller, filter, { schema.origin_key => origin_value })
end
end
end
Expand Down
Loading