From 4cf042cea1a0ac39d0d886438be0f815689f9172 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 24 Oct 2023 18:41:02 +0200 Subject: [PATCH 01/18] [TAN-460] Move users_by_birthyear and users_by_gender to analytics API --- ...pdate_dimension_users_view_v3.analytics.rb | 5 ++++ back/db/structure.sql | 7 +++-- .../views/analytics_dimension_users_v03.sql | 7 +++++ .../app/models/analytics/dimension_user.rb | 2 ++ ...24154935_update_dimension_users_view_v3.rb | 5 ++++ .../views/analytics_dimension_users_v03.sql | 7 +++++ .../analytics_participations_spec.rb | 29 ++++++++++++++++--- 7 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb create mode 100644 back/db/views/analytics_dimension_users_v03.sql create mode 100644 back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb create mode 100644 back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql diff --git a/back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb b/back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb new file mode 100644 index 000000000000..492020d8b581 --- /dev/null +++ b/back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb @@ -0,0 +1,5 @@ +class UpdateDimensionUsersViewV3 < ActiveRecord::Migration[7.0] + def change + update_view :analytics_dimension_users, version: 3, revert_to_version: 2 + end +end diff --git a/back/db/structure.sql b/back/db/structure.sql index 4a334afb6e46..a92457791a29 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -1401,7 +1401,9 @@ CREATE TABLE public.users ( CREATE VIEW public.analytics_dimension_users AS SELECT users.id, COALESCE(((users.roles -> 0) ->> 'type'::text), 'citizen'::text) AS role, - users.invite_status + users.invite_status, + (users.custom_field_values ->> 'gender'::text) AS gender, + (users.custom_field_values ->> 'birthyear'::text) AS birthyear FROM public.users; @@ -7969,6 +7971,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230915391649'), ('20230927135924'), ('20231003095622'), -('20231024082513'); +('20231024082513'), +('20231024154935'); diff --git a/back/db/views/analytics_dimension_users_v03.sql b/back/db/views/analytics_dimension_users_v03.sql new file mode 100644 index 000000000000..7cb8f906b7e5 --- /dev/null +++ b/back/db/views/analytics_dimension_users_v03.sql @@ -0,0 +1,7 @@ +SELECT + id, + COALESCE(roles->0->>'type','citizen') AS role, + invite_status, + custom_field_values->>'gender' as gender, + custom_field_values->>'birthyear' as birthyear +FROM users; diff --git a/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb b/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb index 730e8ee81caa..fa1b82d37a92 100644 --- a/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb +++ b/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb @@ -7,6 +7,8 @@ # id :uuid primary key # role :text # invite_status :string +# gender :text +# birthyear :text # module Analytics class DimensionUser < Analytics::ApplicationRecordView diff --git a/back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb b/back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb new file mode 100644 index 000000000000..492020d8b581 --- /dev/null +++ b/back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb @@ -0,0 +1,5 @@ +class UpdateDimensionUsersViewV3 < ActiveRecord::Migration[7.0] + def change + update_view :analytics_dimension_users, version: 3, revert_to_version: 2 + end +end diff --git a/back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql b/back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql new file mode 100644 index 000000000000..7cb8f906b7e5 --- /dev/null +++ b/back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql @@ -0,0 +1,7 @@ +SELECT + id, + COALESCE(roles->0->>'type','citizen') AS role, + invite_status, + custom_field_values->>'gender' as gender, + custom_field_values->>'birthyear' as birthyear +FROM users; diff --git a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb index 10d083f319a3..75c2c494682a 100644 --- a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb +++ b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb @@ -29,11 +29,15 @@ create(:dimension_type, name: type[:name], parent: type[:parent]) end + male = create(:user, gender: 'male') + female = create(:user, gender: 'female') + unspecified = create(:user, gender: 'unspecified') + # Create participations (3 by citizens, 1 by admin) - idea = create(:idea, created_at: dates[0]) - create(:comment, created_at: dates[2], post: idea) - create(:reaction, created_at: dates[3], user: create(:admin), reactable: idea) - create(:initiative, created_at: dates[1]) + idea = create(:idea, created_at: dates[0], author: male) + create(:comment, created_at: dates[2], post: idea, author: female) + create(:reaction, created_at: dates[3], user: create(:admin, gender: 'female'), reactable: idea) + create(:initiative, created_at: dates[1], author: unspecified) end example 'group participations by month' do @@ -70,6 +74,23 @@ expect(response_data[:attributes]).to match_array([{ count: 1 }]) end + example 'group by gender' do + do_request({ + query: { + fact: 'participation', + groups: 'dimension_user.gender', + aggregations: { + all: 'count' + } + } + }) + assert_status 200 + expect(response_data[:attributes]).to match_array([ + { 'dimension_user.gender': 'female', count: 2 }, + { 'dimension_user.gender': 'unspecified', count: 1 }, + { 'dimension_user.gender': 'male', count: 1 }]) + end + example 'filter participations by project' do do_request({ query: { From 61c4273b940e6162ce67710bd6cfb141b8aa1aec Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 31 Oct 2023 19:18:35 +0100 Subject: [PATCH 02/18] [TAN-460] Add dimension_user_custom_fields --- ...eate_dimension_user_custom_field.analytics.rb | 6 ++++++ back/db/structure.sql | 16 +++++++++++++++- ...nalytics_dimension_user_custom_fields_v01.sql | 7 +++++++ .../analytics/dimension_user_custom_field.rb | 13 +++++++++++++ .../app/models/analytics/fact_participation.rb | 1 + ...1174154_create_dimension_user_custom_field.rb | 5 +++++ ...nalytics_dimension_user_custom_fields_v01.sql | 7 +++++++ back/engines/commercial/analytics/lib/query.rb | 13 +++++++------ .../acceptance/analytics_participations_spec.rb | 15 ++++++++++----- 9 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb create mode 100644 back/db/views/analytics_dimension_user_custom_fields_v01.sql create mode 100644 back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb create mode 100644 back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb create mode 100644 back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql diff --git a/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb b/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb new file mode 100644 index 000000000000..42a77d2c505f --- /dev/null +++ b/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb @@ -0,0 +1,6 @@ +# This migration comes from analytics (originally 20231031174154) +class CreateDimensionUserCustomField < ActiveRecord::Migration[7.0] + def change + create_view :analytics_dimension_user_custom_fields + end +end diff --git a/back/db/structure.sql b/back/db/structure.sql index a92457791a29..35bbf2dceda5 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -668,6 +668,7 @@ DROP VIEW IF EXISTS public.analytics_fact_email_deliveries; DROP TABLE IF EXISTS public.email_campaigns_deliveries; DROP TABLE IF EXISTS public.email_campaigns_campaigns; DROP VIEW IF EXISTS public.analytics_dimension_users; +DROP VIEW IF EXISTS public.analytics_dimension_user_custom_fields; DROP TABLE IF EXISTS public.users; DROP TABLE IF EXISTS public.analytics_dimension_types; DROP VIEW IF EXISTS public.analytics_dimension_statuses; @@ -1394,6 +1395,18 @@ CREATE TABLE public.users ( ); +-- +-- Name: analytics_dimension_user_custom_fields; Type: VIEW; Schema: public; Owner: - +-- + +CREATE VIEW public.analytics_dimension_user_custom_fields AS + SELECT users.id, + custom_field_values.key, + custom_field_values.value + FROM (public.users + JOIN LATERAL jsonb_each_text(users.custom_field_values) custom_field_values(key, value) ON (true)); + + -- -- Name: analytics_dimension_users; Type: VIEW; Schema: public; Owner: - -- @@ -7972,6 +7985,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230927135924'), ('20231003095622'), ('20231024082513'), -('20231024154935'); +('20231024154935'), +('20231031175023'); diff --git a/back/db/views/analytics_dimension_user_custom_fields_v01.sql b/back/db/views/analytics_dimension_user_custom_fields_v01.sql new file mode 100644 index 000000000000..898c423d1f6e --- /dev/null +++ b/back/db/views/analytics_dimension_user_custom_fields_v01.sql @@ -0,0 +1,7 @@ +SELECT + users.id, + custom_field_values.key, + custom_field_values.value +FROM + users + JOIN jsonb_each_text(users.custom_field_values) custom_field_values ON true; diff --git a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb new file mode 100644 index 000000000000..64697c7f5be2 --- /dev/null +++ b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb @@ -0,0 +1,13 @@ +# == Schema Information +# +# Table name: analytics_dimension_user_custom_fields +# +# id :uuid primary key +# key :text +# value :text +# +module Analytics + class DimensionUserCustomField < Analytics::ApplicationRecordView + self.primary_key = :id + end +end diff --git a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb index 1bf1f5fbebd0..8700fe6518c9 100644 --- a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb +++ b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb @@ -17,6 +17,7 @@ module Analytics class FactParticipation < Analytics::ApplicationRecordView self.primary_key = :id belongs_to :dimension_user, class_name: 'Analytics::DimensionUser' + belongs_to :dimension_user_custom_fields, class_name: 'Analytics::DimensionUserCustomField', foreign_key: :dimension_user_id belongs_to :dimension_type, class_name: 'Analytics::DimensionType' belongs_to :dimension_date_created, class_name: 'Analytics::DimensionDate', primary_key: 'date' belongs_to :dimension_project, class_name: 'Analytics::DimensionProject', optional: true diff --git a/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb b/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb new file mode 100644 index 000000000000..ea49b37d9b8f --- /dev/null +++ b/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb @@ -0,0 +1,5 @@ +class CreateDimensionUserCustomField < ActiveRecord::Migration[7.0] + def change + create_view :analytics_dimension_user_custom_fields + end +end diff --git a/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql b/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql new file mode 100644 index 000000000000..898c423d1f6e --- /dev/null +++ b/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql @@ -0,0 +1,7 @@ +SELECT + users.id, + custom_field_values.key, + custom_field_values.value +FROM + users + JOIN jsonb_each_text(users.custom_field_values) custom_field_values ON true; diff --git a/back/engines/commercial/analytics/lib/query.rb b/back/engines/commercial/analytics/lib/query.rb index 14454f8432da..70ea62745672 100644 --- a/back/engines/commercial/analytics/lib/query.rb +++ b/back/engines/commercial/analytics/lib/query.rb @@ -149,14 +149,15 @@ def aggregations_names def calculate_fact_attributes model_attributes = model.columns_hash.transform_values(&:type) + associations_attributes.merge(model_attributes) + end - associations_attributes = model.reflect_on_all_associations.map do |assoc| - assoc.klass.columns_hash.to_h do |column_name, column| - ["#{assoc.name}.#{column_name}", column.type] + def associations_attributes + model.reflect_on_all_associations.each_with_object({}) do |assoc, result| + assoc.klass.columns_hash.each do |column_name, column| + result["#{assoc.name}.#{column_name}"] = column.type end - end.reduce(:merge) - - associations_attributes.merge(model_attributes) + end end end end diff --git a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb index 75c2c494682a..7dac99cb0030 100644 --- a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb +++ b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb @@ -78,17 +78,22 @@ do_request({ query: { fact: 'participation', - groups: 'dimension_user.gender', + groups: 'dimension_user_custom_fields.value', + filters: { + 'dimension_user_custom_fields.key': 'gender' + }, aggregations: { all: 'count' } } }) - assert_status 200 + expect(json_response_body).to have_key(:data) expect(response_data[:attributes]).to match_array([ - { 'dimension_user.gender': 'female', count: 2 }, - { 'dimension_user.gender': 'unspecified', count: 1 }, - { 'dimension_user.gender': 'male', count: 1 }]) + { 'dimension_user_custom_fields.value': 'female', count: 2 }, + { 'dimension_user_custom_fields.value': 'unspecified', count: 1 }, + { 'dimension_user_custom_fields.value': 'male', count: 1 } + ]) + assert_status 200 end example 'filter participations by project' do From 0d7e1f1f3f8e7f626cb637cb908d4cdc408583cf Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 31 Oct 2023 19:36:35 +0100 Subject: [PATCH 03/18] Revert "[TAN-460] Move users_by_birthyear and users_by_gender to analytics API" This reverts commit 4cf042cea1a0ac39d0d886438be0f815689f9172. --- ...54935_update_dimension_users_view_v3.analytics.rb | 5 ----- back/db/structure.sql | 5 +---- back/db/views/analytics_dimension_users_v03.sql | 7 ------- .../analytics/app/models/analytics/dimension_user.rb | 2 -- .../20231024154935_update_dimension_users_view_v3.rb | 5 ----- .../db/views/analytics_dimension_users_v03.sql | 7 ------- .../spec/acceptance/analytics_participations_spec.rb | 12 ++++-------- 7 files changed, 5 insertions(+), 38 deletions(-) delete mode 100644 back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb delete mode 100644 back/db/views/analytics_dimension_users_v03.sql delete mode 100644 back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb delete mode 100644 back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql diff --git a/back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb b/back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb deleted file mode 100644 index 492020d8b581..000000000000 --- a/back/db/migrate/20231024154935_update_dimension_users_view_v3.analytics.rb +++ /dev/null @@ -1,5 +0,0 @@ -class UpdateDimensionUsersViewV3 < ActiveRecord::Migration[7.0] - def change - update_view :analytics_dimension_users, version: 3, revert_to_version: 2 - end -end diff --git a/back/db/structure.sql b/back/db/structure.sql index 35bbf2dceda5..6ac32ad7a37e 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -1414,9 +1414,7 @@ CREATE VIEW public.analytics_dimension_user_custom_fields AS CREATE VIEW public.analytics_dimension_users AS SELECT users.id, COALESCE(((users.roles -> 0) ->> 'type'::text), 'citizen'::text) AS role, - users.invite_status, - (users.custom_field_values ->> 'gender'::text) AS gender, - (users.custom_field_values ->> 'birthyear'::text) AS birthyear + users.invite_status FROM public.users; @@ -7985,7 +7983,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230927135924'), ('20231003095622'), ('20231024082513'), -('20231024154935'), ('20231031175023'); diff --git a/back/db/views/analytics_dimension_users_v03.sql b/back/db/views/analytics_dimension_users_v03.sql deleted file mode 100644 index 7cb8f906b7e5..000000000000 --- a/back/db/views/analytics_dimension_users_v03.sql +++ /dev/null @@ -1,7 +0,0 @@ -SELECT - id, - COALESCE(roles->0->>'type','citizen') AS role, - invite_status, - custom_field_values->>'gender' as gender, - custom_field_values->>'birthyear' as birthyear -FROM users; diff --git a/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb b/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb index fa1b82d37a92..730e8ee81caa 100644 --- a/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb +++ b/back/engines/commercial/analytics/app/models/analytics/dimension_user.rb @@ -7,8 +7,6 @@ # id :uuid primary key # role :text # invite_status :string -# gender :text -# birthyear :text # module Analytics class DimensionUser < Analytics::ApplicationRecordView diff --git a/back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb b/back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb deleted file mode 100644 index 492020d8b581..000000000000 --- a/back/engines/commercial/analytics/db/migrate/20231024154935_update_dimension_users_view_v3.rb +++ /dev/null @@ -1,5 +0,0 @@ -class UpdateDimensionUsersViewV3 < ActiveRecord::Migration[7.0] - def change - update_view :analytics_dimension_users, version: 3, revert_to_version: 2 - end -end diff --git a/back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql b/back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql deleted file mode 100644 index 7cb8f906b7e5..000000000000 --- a/back/engines/commercial/analytics/db/views/analytics_dimension_users_v03.sql +++ /dev/null @@ -1,7 +0,0 @@ -SELECT - id, - COALESCE(roles->0->>'type','citizen') AS role, - invite_status, - custom_field_values->>'gender' as gender, - custom_field_values->>'birthyear' as birthyear -FROM users; diff --git a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb index 7dac99cb0030..fe7be57f2cef 100644 --- a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb +++ b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb @@ -29,15 +29,11 @@ create(:dimension_type, name: type[:name], parent: type[:parent]) end - male = create(:user, gender: 'male') - female = create(:user, gender: 'female') - unspecified = create(:user, gender: 'unspecified') - # Create participations (3 by citizens, 1 by admin) - idea = create(:idea, created_at: dates[0], author: male) - create(:comment, created_at: dates[2], post: idea, author: female) - create(:reaction, created_at: dates[3], user: create(:admin, gender: 'female'), reactable: idea) - create(:initiative, created_at: dates[1], author: unspecified) + idea = create(:idea, created_at: dates[0]) + create(:comment, created_at: dates[2], post: idea) + create(:reaction, created_at: dates[3], user: create(:admin), reactable: idea) + create(:initiative, created_at: dates[1]) end example 'group participations by month' do From 9414af36994a939ffd09115920933dc261f86881 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 31 Oct 2023 19:40:59 +0100 Subject: [PATCH 04/18] [TAN-460] Add specs removed in reverted commit --- .../spec/acceptance/analytics_participations_spec.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb index fe7be57f2cef..7dac99cb0030 100644 --- a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb +++ b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb @@ -29,11 +29,15 @@ create(:dimension_type, name: type[:name], parent: type[:parent]) end + male = create(:user, gender: 'male') + female = create(:user, gender: 'female') + unspecified = create(:user, gender: 'unspecified') + # Create participations (3 by citizens, 1 by admin) - idea = create(:idea, created_at: dates[0]) - create(:comment, created_at: dates[2], post: idea) - create(:reaction, created_at: dates[3], user: create(:admin), reactable: idea) - create(:initiative, created_at: dates[1]) + idea = create(:idea, created_at: dates[0], author: male) + create(:comment, created_at: dates[2], post: idea, author: female) + create(:reaction, created_at: dates[3], user: create(:admin, gender: 'female'), reactable: idea) + create(:initiative, created_at: dates[1], author: unspecified) end example 'group participations by month' do From ce35f6fa11d50e3b19f4e305c8a70ef1fd457b04 Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 31 Oct 2023 19:53:11 +0100 Subject: [PATCH 05/18] [TAN-460] Fix rubocop offences --- ...231031175023_create_dimension_user_custom_field.analytics.rb | 2 ++ .../app/models/analytics/dimension_user_custom_field.rb | 2 ++ .../20231031174154_create_dimension_user_custom_field.rb | 2 ++ 3 files changed, 6 insertions(+) diff --git a/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb b/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb index 42a77d2c505f..bfa58eb98bc7 100644 --- a/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb +++ b/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This migration comes from analytics (originally 20231031174154) class CreateDimensionUserCustomField < ActiveRecord::Migration[7.0] def change diff --git a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb index 64697c7f5be2..dbdf686d89e9 100644 --- a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb +++ b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == Schema Information # # Table name: analytics_dimension_user_custom_fields diff --git a/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb b/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb index ea49b37d9b8f..8d260a72fd6a 100644 --- a/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb +++ b/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class CreateDimensionUserCustomField < ActiveRecord::Migration[7.0] def change create_view :analytics_dimension_user_custom_fields From fa7b88bc8872720959dfaa44c761168de35e5f26 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 1 Nov 2023 09:52:23 +0100 Subject: [PATCH 06/18] [TAN-460] Improve participations->custom_fields association --- back/db/structure.sql | 2 +- .../views/analytics_dimension_user_custom_fields_v01.sql | 2 +- .../app/models/analytics/dimension_user_custom_field.rb | 7 +++---- .../analytics/app/models/analytics/fact_participation.rb | 2 +- .../views/analytics_dimension_user_custom_fields_v01.sql | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/back/db/structure.sql b/back/db/structure.sql index 6ac32ad7a37e..36f7d0dc5b85 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -1400,7 +1400,7 @@ CREATE TABLE public.users ( -- CREATE VIEW public.analytics_dimension_user_custom_fields AS - SELECT users.id, + SELECT users.id AS user_id, custom_field_values.key, custom_field_values.value FROM (public.users diff --git a/back/db/views/analytics_dimension_user_custom_fields_v01.sql b/back/db/views/analytics_dimension_user_custom_fields_v01.sql index 898c423d1f6e..8e88617f12bf 100644 --- a/back/db/views/analytics_dimension_user_custom_fields_v01.sql +++ b/back/db/views/analytics_dimension_user_custom_fields_v01.sql @@ -1,5 +1,5 @@ SELECT - users.id, + users.id as user_id, custom_field_values.key, custom_field_values.value FROM diff --git a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb index dbdf686d89e9..c18f6323bece 100644 --- a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb +++ b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb @@ -4,12 +4,11 @@ # # Table name: analytics_dimension_user_custom_fields # -# id :uuid primary key -# key :text -# value :text +# user_id :uuid +# key :text +# value :text # module Analytics class DimensionUserCustomField < Analytics::ApplicationRecordView - self.primary_key = :id end end diff --git a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb index 8700fe6518c9..54d6881a9fb2 100644 --- a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb +++ b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb @@ -17,7 +17,7 @@ module Analytics class FactParticipation < Analytics::ApplicationRecordView self.primary_key = :id belongs_to :dimension_user, class_name: 'Analytics::DimensionUser' - belongs_to :dimension_user_custom_fields, class_name: 'Analytics::DimensionUserCustomField', foreign_key: :dimension_user_id + has_many :dimension_user_custom_fields, class_name: 'Analytics::DimensionUserCustomField', foreign_key: :user_id, primary_key: :dimension_user_id belongs_to :dimension_type, class_name: 'Analytics::DimensionType' belongs_to :dimension_date_created, class_name: 'Analytics::DimensionDate', primary_key: 'date' belongs_to :dimension_project, class_name: 'Analytics::DimensionProject', optional: true diff --git a/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql b/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql index 898c423d1f6e..8e88617f12bf 100644 --- a/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql +++ b/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql @@ -1,5 +1,5 @@ SELECT - users.id, + users.id as user_id, custom_field_values.key, custom_field_values.value FROM From 477b4536aa83c851e1320573f3923fd0372eb1b2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 1 Nov 2023 09:52:57 +0100 Subject: [PATCH 07/18] [TAN-460] Test complicated participation query with groups and filters --- .../analytics_participations_spec.rb | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb index 7dac99cb0030..8cccdeb4ed38 100644 --- a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb +++ b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb @@ -74,7 +74,33 @@ expect(response_data[:attributes]).to match_array([{ count: 1 }]) end - example 'group by gender' do + example 'filter participants by gender and group by month' do + do_request({ + query: { + fact: 'participation', + groups: 'dimension_date_created.month', + filters: { + 'dimension_user.role': ['citizen', 'admin', nil], + 'dimension_user_custom_fields.key': 'gender', + 'dimension_user_custom_fields.value': 'female' + }, + aggregations: { + dimension_user_id: 'count', # we count participants, not participations + 'dimension_date_created.date': 'first' + } + } + }) + assert_status 200 + expect(response_data[:attributes]).to match_array([ + { + count_dimension_user_id: 2, + 'dimension_date_created.month': '2022-10', + first_dimension_date_created_date: '2022-10-01' + } + ]) + end + + example 'group participations by gender' do do_request({ query: { fact: 'participation', From 302f31c49ab66138f9e4e6d43c7a12d556e8b242 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 1 Nov 2023 10:22:44 +0100 Subject: [PATCH 08/18] [TAN-460] Rename DimensionUserCustomField to DimensionUserCustomFieldValue --- ...create_dimension_user_custom_field.analytics.rb | 8 -------- ...dimension_user_custom_field_values.analytics.rb | 8 ++++++++ back/db/structure.sql | 6 +++--- ...ics_dimension_user_custom_field_values_v01.sql} | 0 ...eld.rb => dimension_user_custom_field_value.rb} | 4 ++-- .../app/models/analytics/fact_participation.rb | 2 +- ...031174154_create_dimension_user_custom_field.rb | 7 ------- ...54_create_dimension_user_custom_field_values.rb | 7 +++++++ ...ics_dimension_user_custom_field_values_v01.sql} | 0 .../acceptance/analytics_participations_spec.rb | 14 +++++++------- 10 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb create mode 100644 back/db/migrate/20231031175023_create_dimension_user_custom_field_values.analytics.rb rename back/db/views/{analytics_dimension_user_custom_fields_v01.sql => analytics_dimension_user_custom_field_values_v01.sql} (100%) rename back/engines/commercial/analytics/app/models/analytics/{dimension_user_custom_field.rb => dimension_user_custom_field_value.rb} (51%) delete mode 100644 back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb create mode 100644 back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field_values.rb rename back/engines/commercial/analytics/db/views/{analytics_dimension_user_custom_fields_v01.sql => analytics_dimension_user_custom_field_values_v01.sql} (100%) diff --git a/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb b/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb deleted file mode 100644 index bfa58eb98bc7..000000000000 --- a/back/db/migrate/20231031175023_create_dimension_user_custom_field.analytics.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# This migration comes from analytics (originally 20231031174154) -class CreateDimensionUserCustomField < ActiveRecord::Migration[7.0] - def change - create_view :analytics_dimension_user_custom_fields - end -end diff --git a/back/db/migrate/20231031175023_create_dimension_user_custom_field_values.analytics.rb b/back/db/migrate/20231031175023_create_dimension_user_custom_field_values.analytics.rb new file mode 100644 index 000000000000..edcbc4635a73 --- /dev/null +++ b/back/db/migrate/20231031175023_create_dimension_user_custom_field_values.analytics.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# This migration comes from analytics (originally 20231031174154) +class CreateDimensionUserCustomFieldValues < ActiveRecord::Migration[7.0] + def change + create_view :analytics_dimension_user_custom_field_values + end +end diff --git a/back/db/structure.sql b/back/db/structure.sql index 36f7d0dc5b85..b953847b4235 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -668,7 +668,7 @@ DROP VIEW IF EXISTS public.analytics_fact_email_deliveries; DROP TABLE IF EXISTS public.email_campaigns_deliveries; DROP TABLE IF EXISTS public.email_campaigns_campaigns; DROP VIEW IF EXISTS public.analytics_dimension_users; -DROP VIEW IF EXISTS public.analytics_dimension_user_custom_fields; +DROP VIEW IF EXISTS public.analytics_dimension_user_custom_field_values; DROP TABLE IF EXISTS public.users; DROP TABLE IF EXISTS public.analytics_dimension_types; DROP VIEW IF EXISTS public.analytics_dimension_statuses; @@ -1396,10 +1396,10 @@ CREATE TABLE public.users ( -- --- Name: analytics_dimension_user_custom_fields; Type: VIEW; Schema: public; Owner: - +-- Name: analytics_dimension_user_custom_field_values; Type: VIEW; Schema: public; Owner: - -- -CREATE VIEW public.analytics_dimension_user_custom_fields AS +CREATE VIEW public.analytics_dimension_user_custom_field_values AS SELECT users.id AS user_id, custom_field_values.key, custom_field_values.value diff --git a/back/db/views/analytics_dimension_user_custom_fields_v01.sql b/back/db/views/analytics_dimension_user_custom_field_values_v01.sql similarity index 100% rename from back/db/views/analytics_dimension_user_custom_fields_v01.sql rename to back/db/views/analytics_dimension_user_custom_field_values_v01.sql diff --git a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field_value.rb similarity index 51% rename from back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb rename to back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field_value.rb index c18f6323bece..ccd9333db16a 100644 --- a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field.rb +++ b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field_value.rb @@ -2,13 +2,13 @@ # == Schema Information # -# Table name: analytics_dimension_user_custom_fields +# Table name: analytics_dimension_user_custom_field_values # # user_id :uuid # key :text # value :text # module Analytics - class DimensionUserCustomField < Analytics::ApplicationRecordView + class DimensionUserCustomFieldValue < Analytics::ApplicationRecordView end end diff --git a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb index 54d6881a9fb2..92e1b67d3279 100644 --- a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb +++ b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb @@ -17,7 +17,7 @@ module Analytics class FactParticipation < Analytics::ApplicationRecordView self.primary_key = :id belongs_to :dimension_user, class_name: 'Analytics::DimensionUser' - has_many :dimension_user_custom_fields, class_name: 'Analytics::DimensionUserCustomField', foreign_key: :user_id, primary_key: :dimension_user_id + has_many :dimension_user_custom_field_values, class_name: 'Analytics::DimensionUserCustomFieldValue', foreign_key: :user_id, primary_key: :dimension_user_id belongs_to :dimension_type, class_name: 'Analytics::DimensionType' belongs_to :dimension_date_created, class_name: 'Analytics::DimensionDate', primary_key: 'date' belongs_to :dimension_project, class_name: 'Analytics::DimensionProject', optional: true diff --git a/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb b/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb deleted file mode 100644 index 8d260a72fd6a..000000000000 --- a/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class CreateDimensionUserCustomField < ActiveRecord::Migration[7.0] - def change - create_view :analytics_dimension_user_custom_fields - end -end diff --git a/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field_values.rb b/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field_values.rb new file mode 100644 index 000000000000..6479f042967c --- /dev/null +++ b/back/engines/commercial/analytics/db/migrate/20231031174154_create_dimension_user_custom_field_values.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class CreateDimensionUserCustomFieldValues < ActiveRecord::Migration[7.0] + def change + create_view :analytics_dimension_user_custom_field_values + end +end diff --git a/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql b/back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_field_values_v01.sql similarity index 100% rename from back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_fields_v01.sql rename to back/engines/commercial/analytics/db/views/analytics_dimension_user_custom_field_values_v01.sql diff --git a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb index 8cccdeb4ed38..12b47604ee11 100644 --- a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb +++ b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb @@ -81,8 +81,8 @@ groups: 'dimension_date_created.month', filters: { 'dimension_user.role': ['citizen', 'admin', nil], - 'dimension_user_custom_fields.key': 'gender', - 'dimension_user_custom_fields.value': 'female' + 'dimension_user_custom_field_values.key': 'gender', + 'dimension_user_custom_field_values.value': 'female' }, aggregations: { dimension_user_id: 'count', # we count participants, not participations @@ -104,9 +104,9 @@ do_request({ query: { fact: 'participation', - groups: 'dimension_user_custom_fields.value', + groups: 'dimension_user_custom_field_values.value', filters: { - 'dimension_user_custom_fields.key': 'gender' + 'dimension_user_custom_field_values.key': 'gender' }, aggregations: { all: 'count' @@ -115,9 +115,9 @@ }) expect(json_response_body).to have_key(:data) expect(response_data[:attributes]).to match_array([ - { 'dimension_user_custom_fields.value': 'female', count: 2 }, - { 'dimension_user_custom_fields.value': 'unspecified', count: 1 }, - { 'dimension_user_custom_fields.value': 'male', count: 1 } + { 'dimension_user_custom_field_values.value': 'female', count: 2 }, + { 'dimension_user_custom_field_values.value': 'unspecified', count: 1 }, + { 'dimension_user_custom_field_values.value': 'male', count: 1 } ]) assert_status 200 end From cb7576e0c36bae9b1b1a614f37169fc463f9b4d4 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 1 Nov 2023 18:14:13 +0100 Subject: [PATCH 09/18] [TAN-460] Add support of groupping by null values --- back/db/structure.sql | 76 ++++++++------- ...dimension_user_custom_field_values_v01.sql | 22 ++++- back/engines/commercial/analytics/README.md | 33 ++++++- .../dimension_user_custom_field_value.rb | 6 +- .../models/analytics/fact_participation.rb | 2 +- .../analytics_participations_spec.rb | 94 ++++++++++--------- 6 files changed, 143 insertions(+), 90 deletions(-) diff --git a/back/db/structure.sql b/back/db/structure.sql index b953847b4235..f5ca913d27d0 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -630,7 +630,6 @@ DROP TABLE IF EXISTS public.email_campaigns_consents; DROP TABLE IF EXISTS public.email_campaigns_campaigns_groups; DROP TABLE IF EXISTS public.email_campaigns_campaign_email_commands; DROP TABLE IF EXISTS public.custom_forms; -DROP TABLE IF EXISTS public.custom_fields; DROP TABLE IF EXISTS public.custom_field_options; DROP TABLE IF EXISTS public.cosponsors_initiatives; DROP TABLE IF EXISTS public.content_builder_layouts; @@ -670,6 +669,7 @@ DROP TABLE IF EXISTS public.email_campaigns_campaigns; DROP VIEW IF EXISTS public.analytics_dimension_users; DROP VIEW IF EXISTS public.analytics_dimension_user_custom_field_values; DROP TABLE IF EXISTS public.users; +DROP TABLE IF EXISTS public.custom_fields; DROP TABLE IF EXISTS public.analytics_dimension_types; DROP VIEW IF EXISTS public.analytics_dimension_statuses; DROP TABLE IF EXISTS public.initiative_statuses; @@ -1356,6 +1356,36 @@ CREATE TABLE public.analytics_dimension_types ( ); +-- +-- Name: custom_fields; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.custom_fields ( + id uuid DEFAULT shared_extensions.gen_random_uuid() NOT NULL, + resource_type character varying, + key character varying, + input_type character varying, + title_multiloc jsonb DEFAULT '{}'::jsonb, + description_multiloc jsonb DEFAULT '{}'::jsonb, + required boolean DEFAULT false, + ordering integer, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + enabled boolean DEFAULT true NOT NULL, + code character varying, + resource_id uuid, + hidden boolean DEFAULT false NOT NULL, + maximum integer, + minimum_label_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, + maximum_label_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, + logic jsonb DEFAULT '{}'::jsonb NOT NULL, + answer_visible_to character varying, + select_count_enabled boolean DEFAULT false NOT NULL, + maximum_select_count integer, + minimum_select_count integer +); + + -- -- Name: users; Type: TABLE; Schema: public; Owner: - -- @@ -1400,11 +1430,15 @@ CREATE TABLE public.users ( -- CREATE VIEW public.analytics_dimension_user_custom_field_values AS - SELECT users.id AS user_id, - custom_field_values.key, - custom_field_values.value - FROM (public.users - JOIN LATERAL jsonb_each_text(users.custom_field_values) custom_field_values(key, value) ON (true)); + SELECT DISTINCT u.id AS dimension_user_id, + cf.key, + cf.value + FROM ((public.users u + LEFT JOIN LATERAL ( SELECT custom_fields.key, + (u.custom_field_values ->> (custom_fields.key)::text) AS value + FROM public.custom_fields + WHERE ((custom_fields.resource_type)::text = 'User'::text)) cf ON (true)) + LEFT JOIN LATERAL ( SELECT jsonb_object_keys(u.custom_field_values) AS key) cfv ON (true)); -- @@ -2163,36 +2197,6 @@ CREATE TABLE public.custom_field_options ( ); --- --- Name: custom_fields; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.custom_fields ( - id uuid DEFAULT shared_extensions.gen_random_uuid() NOT NULL, - resource_type character varying, - key character varying, - input_type character varying, - title_multiloc jsonb DEFAULT '{}'::jsonb, - description_multiloc jsonb DEFAULT '{}'::jsonb, - required boolean DEFAULT false, - ordering integer, - created_at timestamp without time zone NOT NULL, - updated_at timestamp without time zone NOT NULL, - enabled boolean DEFAULT true NOT NULL, - code character varying, - resource_id uuid, - hidden boolean DEFAULT false NOT NULL, - maximum integer, - minimum_label_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, - maximum_label_multiloc jsonb DEFAULT '{}'::jsonb NOT NULL, - logic jsonb DEFAULT '{}'::jsonb NOT NULL, - answer_visible_to character varying, - select_count_enabled boolean DEFAULT false NOT NULL, - maximum_select_count integer, - minimum_select_count integer -); - - -- -- Name: custom_forms; Type: TABLE; Schema: public; Owner: - -- diff --git a/back/db/views/analytics_dimension_user_custom_field_values_v01.sql b/back/db/views/analytics_dimension_user_custom_field_values_v01.sql index 8e88617f12bf..2606a15751e1 100644 --- a/back/db/views/analytics_dimension_user_custom_field_values_v01.sql +++ b/back/db/views/analytics_dimension_user_custom_field_values_v01.sql @@ -1,7 +1,19 @@ SELECT - users.id as user_id, - custom_field_values.key, - custom_field_values.value + DISTINCT u.id as dimension_user_id, + cf.key, + cf.value FROM - users - JOIN jsonb_each_text(users.custom_field_values) custom_field_values ON true; + users u + LEFT JOIN LATERAL ( + SELECT + key, + custom_field_values ->> key AS value + FROM + custom_fields + where + custom_fields.resource_type = 'User' + ) cf ON true + LEFT JOIN LATERAL ( + SELECT + jsonb_object_keys(u.custom_field_values) AS key + ) cfv ON true; diff --git a/back/engines/commercial/analytics/README.md b/back/engines/commercial/analytics/README.md index 1e34d437f8da..8179acd8ebf4 100644 --- a/back/engines/commercial/analytics/README.md +++ b/back/engines/commercial/analytics/README.md @@ -1,6 +1,6 @@ # Analytics This is a separate analytics engine, which separates out the data for dashboard from the main operational data -using both database views on the existing data tables and data copied from other sources. +using both database views on the existing data tables and data copied from other sources. The data is modelled following the conventions of a **star schema**. These views and tables sit in the same tenant schema. However, in the future it is intended @@ -21,6 +21,35 @@ Views are copied across when the migration is run. Views and tables should be named as follows: * analytics_dimension_* - holds dimensions by which the facts can be filtered -* analytics_fact_* - holds 'facts' - the actual data we're interested in +* analytics_fact_* - holds 'facts' - the actual data we're interested in such as posts, participations, visits +### Testing queries in your dev env + +You can use this request in the browser developer console. +```js +fetch(window.location.origin + "/web_api/v1/analytics", { + headers: { + authorization: `Bearer ${document.cookie.split("; ").find((x) => x.startsWith("cl2_jwt")).replace("cl2_jwt=", "")}`, + "content-type": "application/json", + }, + body: `{ + "query": { + "fact": "participation", + "groups": "dimension_user_custom_field_values.value", + "filters": { + "dimension_user_custom_field_values.key": "gender" + }, + "aggregations": { + "all": "count" + } + } + }`, + method: "POST", +}) + .then((response) => response.json()) + .then((data) => { + console.log(JSON.stringify(data.data, null, 2)); + console.log(data); + }); +``` diff --git a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field_value.rb b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field_value.rb index ccd9333db16a..a2c56135a88e 100644 --- a/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field_value.rb +++ b/back/engines/commercial/analytics/app/models/analytics/dimension_user_custom_field_value.rb @@ -4,9 +4,9 @@ # # Table name: analytics_dimension_user_custom_field_values # -# user_id :uuid -# key :text -# value :text +# dimension_user_id :uuid +# key :string +# value :text # module Analytics class DimensionUserCustomFieldValue < Analytics::ApplicationRecordView diff --git a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb index 92e1b67d3279..e3e940a642f0 100644 --- a/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb +++ b/back/engines/commercial/analytics/app/models/analytics/fact_participation.rb @@ -17,7 +17,7 @@ module Analytics class FactParticipation < Analytics::ApplicationRecordView self.primary_key = :id belongs_to :dimension_user, class_name: 'Analytics::DimensionUser' - has_many :dimension_user_custom_field_values, class_name: 'Analytics::DimensionUserCustomFieldValue', foreign_key: :user_id, primary_key: :dimension_user_id + has_many :dimension_user_custom_field_values, class_name: 'Analytics::DimensionUserCustomFieldValue', foreign_key: :dimension_user_id, primary_key: :dimension_user_id belongs_to :dimension_type, class_name: 'Analytics::DimensionType' belongs_to :dimension_date_created, class_name: 'Analytics::DimensionDate', primary_key: 'date' belongs_to :dimension_project, class_name: 'Analytics::DimensionProject', optional: true diff --git a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb index 12b47604ee11..fbe1a3331cfb 100644 --- a/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb +++ b/back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb @@ -74,52 +74,60 @@ expect(response_data[:attributes]).to match_array([{ count: 1 }]) end - example 'filter participants by gender and group by month' do - do_request({ - query: { - fact: 'participation', - groups: 'dimension_date_created.month', - filters: { - 'dimension_user.role': ['citizen', 'admin', nil], - 'dimension_user_custom_field_values.key': 'gender', - 'dimension_user_custom_field_values.value': 'female' - }, - aggregations: { - dimension_user_id: 'count', # we count participants, not participations - 'dimension_date_created.date': 'first' + context 'when querying custom fields' do + before { create(:custom_field, key: :gender, resource_type: 'User') } + + example 'filter participants by gender and group by month' do + do_request({ + query: { + fact: 'participation', + groups: 'dimension_date_created.month', + filters: { + 'dimension_user.role': ['citizen', 'admin', nil], + 'dimension_user_custom_field_values.key': 'gender', + 'dimension_user_custom_field_values.value': 'female' + }, + aggregations: { + 'dimension_user_custom_field_values.dimension_user_id': 'count', # we count participants, not participations + 'dimension_date_created.date': 'first' + } } - } - }) - assert_status 200 - expect(response_data[:attributes]).to match_array([ - { - count_dimension_user_id: 2, - 'dimension_date_created.month': '2022-10', - first_dimension_date_created_date: '2022-10-01' - } - ]) - end + }) + assert_status 200 + expect(response_data[:attributes]).to match_array([ + { + count_dimension_user_custom_field_values_dimension_user_id: 2, + 'dimension_date_created.month': '2022-10', + first_dimension_date_created_date: '2022-10-01' + } + ]) + end - example 'group participations by gender' do - do_request({ - query: { - fact: 'participation', - groups: 'dimension_user_custom_field_values.value', - filters: { - 'dimension_user_custom_field_values.key': 'gender' - }, - aggregations: { - all: 'count' + example 'group participations by gender' do + nil_gender_user = create(:user) + create(:initiative, created_at: Date.new(2023, 11, 1), author: nil_gender_user) + + do_request({ + query: { + fact: 'participation', + groups: 'dimension_user_custom_field_values.value', + filters: { + 'dimension_user_custom_field_values.key': 'gender' + }, + aggregations: { + all: 'count' + } } - } - }) - expect(json_response_body).to have_key(:data) - expect(response_data[:attributes]).to match_array([ - { 'dimension_user_custom_field_values.value': 'female', count: 2 }, - { 'dimension_user_custom_field_values.value': 'unspecified', count: 1 }, - { 'dimension_user_custom_field_values.value': 'male', count: 1 } - ]) - assert_status 200 + }) + expect(json_response_body).to have_key(:data) + assert_status 200 + expect(response_data[:attributes]).to match_array([ + { 'dimension_user_custom_field_values.value': nil, count: 1 }, + { 'dimension_user_custom_field_values.value': 'female', count: 2 }, + { 'dimension_user_custom_field_values.value': 'unspecified', count: 1 }, + { 'dimension_user_custom_field_values.value': 'male', count: 1 } + ]) + end end example 'filter participations by project' do From abf25810c8a33eb6e92589ad960cf06b67f59d89 Mon Sep 17 00:00:00 2001 From: Alexander Date: Fri, 3 Nov 2023 13:11:45 +0100 Subject: [PATCH 10/18] [TAN-449] Add endpoints to fetch dynamic and snapshotted data by report graphs --- ...31103094549_create_published_data_units.rb | 11 +++++++ back/engines/commercial/analytics/README.md | 2 +- .../web_api/v1/analytics_controller.rb | 10 +++--- .../web_api/v1/data_units_controller.rb | 19 ++++++++++++ .../v1/published_data_units_controller.rb | 19 ++++++++++++ .../web_api/v1/reports_controller.rb | 8 +++++ .../report_builder/published_data_unit.rb | 6 ++++ .../report_builder/query_repository.rb | 31 +++++++++++++++++++ 8 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 back/db/migrate/20231103094549_create_published_data_units.rb create mode 100644 back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb create mode 100644 back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb create mode 100644 back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb create mode 100644 back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb diff --git a/back/db/migrate/20231103094549_create_published_data_units.rb b/back/db/migrate/20231103094549_create_published_data_units.rb new file mode 100644 index 000000000000..0cfbc289fb40 --- /dev/null +++ b/back/db/migrate/20231103094549_create_published_data_units.rb @@ -0,0 +1,11 @@ +class CreatePublishedDataUnits < ActiveRecord::Migration[7.0] + def change + create_table :published_data_units, id: :uuid do |t| + t.references :report_id, null: false, foreign_key: true, type: :uuid + t.string :graph_id + t.jsonb :data + + t.timestamps + end + end +end diff --git a/back/engines/commercial/analytics/README.md b/back/engines/commercial/analytics/README.md index 8179acd8ebf4..0b3e60508f16 100644 --- a/back/engines/commercial/analytics/README.md +++ b/back/engines/commercial/analytics/README.md @@ -41,7 +41,7 @@ fetch(window.location.origin + "/web_api/v1/analytics", { "dimension_user_custom_field_values.key": "gender" }, "aggregations": { - "all": "count" + "dimension_user_custom_field_values.dimension_user_id": "count" } } }`, diff --git a/back/engines/commercial/analytics/app/controllers/analytics/web_api/v1/analytics_controller.rb b/back/engines/commercial/analytics/app/controllers/analytics/web_api/v1/analytics_controller.rb index e589520ae1ad..5b508500c680 100644 --- a/back/engines/commercial/analytics/app/controllers/analytics/web_api/v1/analytics_controller.rb +++ b/back/engines/commercial/analytics/app/controllers/analytics/web_api/v1/analytics_controller.rb @@ -8,11 +8,11 @@ class AnalyticsController < ::ApplicationController skip_after_action :verify_policy_scoped, only: :index after_action :verify_authorized, only: :index def index - handle_request + handle_request(params[:query]) end def create - handle_request + handle_request(params[:query]) end def schema @@ -27,12 +27,12 @@ def schema private - def handle_request + def handle_request(query) authorize :analytics, policy_class: AnalyticsPolicy - results, errors, paginations = handle_multiple(Array.wrap(params[:query])) + results, errors, paginations = handle_multiple(Array.wrap(query)) - unless params[:query].instance_of?(Array) + unless query.instance_of?(Array) results = results.empty? ? results : results[0] paginations = paginations.empty? ? paginations : paginations[0] errors = errors.key?(0) ? errors[0] : errors diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb new file mode 100644 index 000000000000..7445bf5acdf0 --- /dev/null +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ReportBuilder + module WebApi + module V1 + class DataUnitsController < ApplicationController + def users_by_gender + results = ReportBuilder::QueryRepository.new.users_by_gender + render json: { + data: { type: 'report_builder_data_units', attributes: results }, + links: 'paginations' + } + end + + def users_by_birthyear; end + end + end + end +end diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb new file mode 100644 index 000000000000..6e7930357cf0 --- /dev/null +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ReportBuilder + module WebApi + module V1 + class PublishedDataUnitsController < ApplicationController + def users_by_gender + results = PublishedDataUnit.find_by(report.id, graph_id) + render json: { + data: { type: 'report_builder_data_units', attributes: results }, + links: 'paginations' + } + end + + def users_by_birthyear; end + end + end + end +end diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb index 9c6f30e0ffa4..f374e89847a6 100644 --- a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb @@ -6,6 +6,14 @@ module V1 class ReportsController < ::ApplicationController skip_before_action :authenticate_user + def publish + report.craftjs_jsonmultiloc.each_graph do + method = ReportBuilder::QueryRepository::MAPPING[graph[:resolvedName]] + snapshotted_data = method.call + PublishedDataUnit.create!(data: snapshotted_data, graph_id, report.id) + end + end + def index reports = policy_scope(ReportBuilder::Report.with_platform_context) reports = paginate(reports) diff --git a/back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb b/back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb new file mode 100644 index 000000000000..56e0bb94bb43 --- /dev/null +++ b/back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +module ReportBuilder + class PublishedDataUnit < ::ApplicationRecord + + end +end diff --git a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb new file mode 100644 index 000000000000..987c815ed261 --- /dev/null +++ b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ReportBuilder::QueryRepository + MAPPING = { + 'GenderWidget' => :users_by_gender, + } + + def users_by_gender + query(fact: 'participation', + groups: 'dimension_date_created.month', + filters: { + 'dimension_user.role': ['citizen', 'admin', nil], + 'dimension_user_custom_field_values.key': 'gender', + 'dimension_user_custom_field_values.value': 'female' + }, + aggregations: { + 'dimension_user_custom_field_values.dimension_user_id': 'count', # we count participants, not participations + 'dimension_date_created.date': 'first' + }) + end + + def users_by_birthyear; end + + private + + def query(json_query) + query = Analytics::Query.new(json_query) + query.run + query.results + end +end From 11d19367d63ab1e2cdc71a4f3e33fa10bda021ce Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 13 Nov 2023 22:39:57 +0100 Subject: [PATCH 11/18] [TAN-449] Graph data units API MVP with integration tests --- back/Gemfile.lock | 1 + ...31103094549_create_published_data_units.rb | 11 --- ...94549_create_published_graph_data_units.rb | 11 +++ back/db/structure.sql | 42 ++++++++++ .../analytics/query_runner_service.rb | 2 +- .../analytics/query_validator_service.rb | 3 +- .../web_api/v1/data_units_controller.rb | 19 ----- .../web_api/v1/graph_data_units_controller.rb | 30 ++++++++ .../v1/published_data_units_controller.rb | 19 ----- .../web_api/v1/reports_controller.rb | 13 ++-- .../report_builder/published_data_unit.rb | 6 -- .../published_graph_data_unit.rb | 27 +++++++ .../report_builder/graph_data_unit_policy.rb | 13 ++++ .../policies/report_builder/report_policy.rb | 1 + .../report_builder/query_repository.rb | 30 +++++--- .../report_builder/report_publisher.rb | 28 +++++++ .../report_builder/config/routes.rb | 10 +++ .../report_builder/report_builder.gemspec | 1 + .../requests/data_units_publishing_spec.rb | 77 +++++++++++++++++++ .../spec/support/api_authentication_helper.rb | 6 +- 20 files changed, 275 insertions(+), 75 deletions(-) delete mode 100644 back/db/migrate/20231103094549_create_published_data_units.rb create mode 100644 back/db/migrate/20231103094549_create_published_graph_data_units.rb delete mode 100644 back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb create mode 100644 back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb delete mode 100644 back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb delete mode 100644 back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb create mode 100644 back/engines/commercial/report_builder/app/models/report_builder/published_graph_data_unit.rb create mode 100644 back/engines/commercial/report_builder/app/policies/report_builder/graph_data_unit_policy.rb create mode 100644 back/engines/commercial/report_builder/app/services/report_builder/report_publisher.rb create mode 100644 back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb diff --git a/back/Gemfile.lock b/back/Gemfile.lock index 524b3d99aa79..53bb17550a92 100644 --- a/back/Gemfile.lock +++ b/back/Gemfile.lock @@ -282,6 +282,7 @@ PATH remote: engines/commercial/report_builder specs: report_builder (0.1.0) + analytics content_builder rails (~> 7.0) diff --git a/back/db/migrate/20231103094549_create_published_data_units.rb b/back/db/migrate/20231103094549_create_published_data_units.rb deleted file mode 100644 index 0cfbc289fb40..000000000000 --- a/back/db/migrate/20231103094549_create_published_data_units.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreatePublishedDataUnits < ActiveRecord::Migration[7.0] - def change - create_table :published_data_units, id: :uuid do |t| - t.references :report_id, null: false, foreign_key: true, type: :uuid - t.string :graph_id - t.jsonb :data - - t.timestamps - end - end -end diff --git a/back/db/migrate/20231103094549_create_published_graph_data_units.rb b/back/db/migrate/20231103094549_create_published_graph_data_units.rb new file mode 100644 index 000000000000..81828fe6b9e0 --- /dev/null +++ b/back/db/migrate/20231103094549_create_published_graph_data_units.rb @@ -0,0 +1,11 @@ +class CreatePublishedGraphDataUnits < ActiveRecord::Migration[7.0] + def change + create_table :report_builder_published_graph_data_units, id: :uuid do |t| + t.references :report_builder_report, null: false, foreign_key: true, index: { name: :report_builder_published_data_units_report_id_idx }, type: :uuid + t.string :graph_id, null: false + t.jsonb :data, null: false + + t.timestamps + end + end +end diff --git a/back/db/structure.sql b/back/db/structure.sql index 963b3ee26b63..03192d042563 100644 --- a/back/db/structure.sql +++ b/back/db/structure.sql @@ -110,6 +110,7 @@ ALTER TABLE IF EXISTS ONLY public.permissions_custom_fields DROP CONSTRAINT IF E ALTER TABLE IF EXISTS ONLY public.analytics_dimension_projects_fact_visits DROP CONSTRAINT IF EXISTS fk_rails_4ecebb6e8a; ALTER TABLE IF EXISTS ONLY public.initiative_images DROP CONSTRAINT IF EXISTS fk_rails_4df6f76970; ALTER TABLE IF EXISTS ONLY public.notifications DROP CONSTRAINT IF EXISTS fk_rails_4aea6afa11; +ALTER TABLE IF EXISTS ONLY public.report_builder_published_graph_data_units DROP CONSTRAINT IF EXISTS fk_rails_4846b9a405; ALTER TABLE IF EXISTS ONLY public.notifications DROP CONSTRAINT IF EXISTS fk_rails_46dd2ccfd1; ALTER TABLE IF EXISTS ONLY public.email_campaigns_examples DROP CONSTRAINT IF EXISTS fk_rails_465d6356b2; ALTER TABLE IF EXISTS ONLY public.insights_text_network_analysis_tasks_views DROP CONSTRAINT IF EXISTS fk_rails_3e0e58a177; @@ -141,6 +142,7 @@ DROP TRIGGER IF EXISTS que_state_notify ON public.que_jobs; DROP TRIGGER IF EXISTS que_job_notify ON public.que_jobs; DROP INDEX IF EXISTS public.users_unique_lower_email_idx; DROP INDEX IF EXISTS public.spam_reportable_index; +DROP INDEX IF EXISTS public.report_builder_published_data_units_report_id_idx; DROP INDEX IF EXISTS public.que_poll_idx_with_job_schema_version; DROP INDEX IF EXISTS public.que_poll_idx; DROP INDEX IF EXISTS public.que_jobs_data_gin_idx; @@ -431,6 +433,7 @@ ALTER TABLE IF EXISTS ONLY public.static_pages_topics DROP CONSTRAINT IF EXISTS ALTER TABLE IF EXISTS ONLY public.spam_reports DROP CONSTRAINT IF EXISTS spam_reports_pkey; ALTER TABLE IF EXISTS ONLY public.schema_migrations DROP CONSTRAINT IF EXISTS schema_migrations_pkey; ALTER TABLE IF EXISTS ONLY public.report_builder_reports DROP CONSTRAINT IF EXISTS report_builder_reports_pkey; +ALTER TABLE IF EXISTS ONLY public.report_builder_published_graph_data_units DROP CONSTRAINT IF EXISTS report_builder_published_graph_data_units_pkey; ALTER TABLE IF EXISTS ONLY public.que_values DROP CONSTRAINT IF EXISTS que_values_pkey; ALTER TABLE IF EXISTS ONLY public.que_lockers DROP CONSTRAINT IF EXISTS que_lockers_pkey; ALTER TABLE IF EXISTS ONLY public.que_jobs DROP CONSTRAINT IF EXISTS que_jobs_pkey; @@ -560,6 +563,7 @@ DROP TABLE IF EXISTS public.static_page_files; DROP TABLE IF EXISTS public.spam_reports; DROP TABLE IF EXISTS public.schema_migrations; DROP TABLE IF EXISTS public.report_builder_reports; +DROP TABLE IF EXISTS public.report_builder_published_graph_data_units; DROP TABLE IF EXISTS public.que_values; DROP TABLE IF EXISTS public.que_lockers; DROP SEQUENCE IF EXISTS public.que_jobs_id_seq; @@ -3332,6 +3336,20 @@ CREATE TABLE public.que_values ( WITH (fillfactor='90'); +-- +-- Name: report_builder_published_graph_data_units; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.report_builder_published_graph_data_units ( + id uuid DEFAULT shared_extensions.gen_random_uuid() NOT NULL, + report_builder_report_id uuid NOT NULL, + graph_id character varying NOT NULL, + data jsonb NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + -- -- Name: report_builder_reports; Type: TABLE; Schema: public; Owner: - -- @@ -4505,6 +4523,14 @@ ALTER TABLE ONLY public.que_values ADD CONSTRAINT que_values_pkey PRIMARY KEY (key); +-- +-- Name: report_builder_published_graph_data_units report_builder_published_graph_data_units_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.report_builder_published_graph_data_units + ADD CONSTRAINT report_builder_published_graph_data_units_pkey PRIMARY KEY (id); + + -- -- Name: report_builder_reports report_builder_reports_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -6551,6 +6577,13 @@ CREATE INDEX que_poll_idx ON public.que_jobs USING btree (queue, priority, run_a CREATE INDEX que_poll_idx_with_job_schema_version ON public.que_jobs USING btree (job_schema_version, queue, priority, run_at, id) WHERE ((finished_at IS NULL) AND (expired_at IS NULL)); +-- +-- Name: report_builder_published_data_units_report_id_idx; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX report_builder_published_data_units_report_id_idx ON public.report_builder_published_graph_data_units USING btree (report_builder_report_id); + + -- -- Name: spam_reportable_index; Type: INDEX; Schema: public; Owner: - -- @@ -6795,6 +6828,14 @@ ALTER TABLE ONLY public.notifications ADD CONSTRAINT fk_rails_46dd2ccfd1 FOREIGN KEY (phase_id) REFERENCES public.phases(id); +-- +-- Name: report_builder_published_graph_data_units fk_rails_4846b9a405; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.report_builder_published_graph_data_units + ADD CONSTRAINT fk_rails_4846b9a405 FOREIGN KEY (report_builder_report_id) REFERENCES public.report_builder_reports(id); + + -- -- Name: notifications fk_rails_4aea6afa11; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -7998,6 +8039,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20231018083110'), ('20231024082513'), ('20231031175023'), +('20231103094549'), ('20231109101517'); diff --git a/back/engines/commercial/analytics/app/services/analytics/query_runner_service.rb b/back/engines/commercial/analytics/app/services/analytics/query_runner_service.rb index 8f1883953cf0..8a5055af685f 100644 --- a/back/engines/commercial/analytics/app/services/analytics/query_runner_service.rb +++ b/back/engines/commercial/analytics/app/services/analytics/query_runner_service.rb @@ -159,7 +159,7 @@ def page(results) def pagination_query_params(number) return if number.nil? - json_query = @json_query.to_unsafe_hash + json_query = @json_query.try(:to_unsafe_hash) || @json_query if @json_query.key?(:page) json_query[:page][:number] = number else diff --git a/back/engines/commercial/analytics/app/services/analytics/query_validator_service.rb b/back/engines/commercial/analytics/app/services/analytics/query_validator_service.rb index 94c1e54d40aa..6bfb677d5124 100644 --- a/back/engines/commercial/analytics/app/services/analytics/query_validator_service.rb +++ b/back/engines/commercial/analytics/app/services/analytics/query_validator_service.rb @@ -45,7 +45,8 @@ def add_error(messages) end def validate_json - json_errors = JSON::Validator.fully_validate(self.class.schema, @json_query.to_unsafe_hash) + unsafe_hash = @json_query.try(:to_unsafe_hash) || @json_query + json_errors = JSON::Validator.fully_validate(self.class.schema, unsafe_hash) return true if json_errors.empty? add_error(json_errors) diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb deleted file mode 100644 index 7445bf5acdf0..000000000000 --- a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/data_units_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module ReportBuilder - module WebApi - module V1 - class DataUnitsController < ApplicationController - def users_by_gender - results = ReportBuilder::QueryRepository.new.users_by_gender - render json: { - data: { type: 'report_builder_data_units', attributes: results }, - links: 'paginations' - } - end - - def users_by_birthyear; end - end - end - end -end diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb new file mode 100644 index 000000000000..c9367ac2c608 --- /dev/null +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module ReportBuilder + module WebApi + module V1 + class GraphDataUnitsController < ApplicationController + def live + authorize :live?, policy_class: GraphDataUnitPolicy + results = ReportBuilder::QueryRepository.new.data_by_graph(params[:resolved_name], params[:props]) + render_results(results) + end + + def published + data_unit = PublishedGraphDataUnit.find_by!(report_builder_report_id: params[:report_id], graph_id: params[:graph_id]) + authorize data_unit, policy_class: GraphDataUnitPolicy + render_results(data_unit.data) + end + + private + + def render_results(results) + render json: { + data: { type: 'report_builder_data_units', attributes: results }, + links: 'paginations' + } + end + end + end + end +end diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb deleted file mode 100644 index 6e7930357cf0..000000000000 --- a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/published_data_units_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module ReportBuilder - module WebApi - module V1 - class PublishedDataUnitsController < ApplicationController - def users_by_gender - results = PublishedDataUnit.find_by(report.id, graph_id) - render json: { - data: { type: 'report_builder_data_units', attributes: results }, - links: 'paginations' - } - end - - def users_by_birthyear; end - end - end - end -end diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb index f374e89847a6..b5588957d4cd 100644 --- a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/reports_controller.rb @@ -6,14 +6,6 @@ module V1 class ReportsController < ::ApplicationController skip_before_action :authenticate_user - def publish - report.craftjs_jsonmultiloc.each_graph do - method = ReportBuilder::QueryRepository::MAPPING[graph[:resolvedName]] - snapshotted_data = method.call - PublishedDataUnit.create!(data: snapshotted_data, graph_id, report.id) - end - end - def index reports = policy_scope(ReportBuilder::Report.with_platform_context) reports = paginate(reports) @@ -43,6 +35,11 @@ def update render json: serialize_report(report), status: :ok end + def publish + ReportPublisher.new(report).publish + head :ok + end + def destroy side_fx_service.before_destroy(report, current_user) if report.destroy diff --git a/back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb b/back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb deleted file mode 100644 index 56e0bb94bb43..000000000000 --- a/back/engines/commercial/report_builder/app/models/report_builder/published_data_unit.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true -module ReportBuilder - class PublishedDataUnit < ::ApplicationRecord - - end -end diff --git a/back/engines/commercial/report_builder/app/models/report_builder/published_graph_data_unit.rb b/back/engines/commercial/report_builder/app/models/report_builder/published_graph_data_unit.rb new file mode 100644 index 000000000000..5d3a290f8442 --- /dev/null +++ b/back/engines/commercial/report_builder/app/models/report_builder/published_graph_data_unit.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: report_builder_published_graph_data_units +# +# id :uuid not null, primary key +# report_builder_report_id :uuid not null +# graph_id :string not null +# data :jsonb not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# report_builder_published_data_units_report_id_idx (report_builder_report_id) +# +# Foreign Keys +# +# fk_rails_... (report_builder_report_id => report_builder_reports.id) +# +module ReportBuilder + class PublishedGraphDataUnit < ::ApplicationRecord + # TODO: rename report_builder_report_id to report_id + belongs_to :report, class_name: 'ReportBuilder::Report', foreign_key: 'report_builder_report_id' + end +end diff --git a/back/engines/commercial/report_builder/app/policies/report_builder/graph_data_unit_policy.rb b/back/engines/commercial/report_builder/app/policies/report_builder/graph_data_unit_policy.rb new file mode 100644 index 000000000000..2eaff3a89848 --- /dev/null +++ b/back/engines/commercial/report_builder/app/policies/report_builder/graph_data_unit_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ReportBuilder + class GraphDataUnitPolicy < ::ApplicationPolicy + def live? + admin? && active? + end + + def published? + PhasePolicy::Scope.new(user, Phase).resolve.exists?(id: record.report.phase_id) + end + end +end diff --git a/back/engines/commercial/report_builder/app/policies/report_builder/report_policy.rb b/back/engines/commercial/report_builder/app/policies/report_builder/report_policy.rb index 03fe8f90aedd..c080fdcd22ec 100644 --- a/back/engines/commercial/report_builder/app/policies/report_builder/report_policy.rb +++ b/back/engines/commercial/report_builder/app/policies/report_builder/report_policy.rb @@ -24,6 +24,7 @@ def allowed? alias show? allowed? alias create? allowed? alias update? allowed? + alias publish? allowed? alias destroy? allowed? alias layout? allowed? end diff --git a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb index 987c815ed261..5ec577818790 100644 --- a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb +++ b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb @@ -1,22 +1,33 @@ # frozen_string_literal: true +require 'query' + class ReportBuilder::QueryRepository - MAPPING = { - 'GenderWidget' => :users_by_gender, + GRAPH_RESOLVED_NAMES_METHODS = { + 'GenderWidget' => :users_by_gender } - def users_by_gender - query(fact: 'participation', + def data_by_graph(graph_resolved_name, props) + method = GRAPH_RESOLVED_NAMES_METHODS[graph_resolved_name] + return unless method + + send(method, props) + end + + protected + + def users_by_gender(_props = nil) + query( + fact: 'participation', groups: 'dimension_date_created.month', filters: { - 'dimension_user.role': ['citizen', 'admin', nil], - 'dimension_user_custom_field_values.key': 'gender', - 'dimension_user_custom_field_values.value': 'female' + 'dimension_user.role': ['citizen', 'admin', nil] }, aggregations: { 'dimension_user_custom_field_values.dimension_user_id': 'count', # we count participants, not participations 'dimension_date_created.date': 'first' - }) + } + ) end def users_by_birthyear; end @@ -24,7 +35,8 @@ def users_by_birthyear; end private def query(json_query) - query = Analytics::Query.new(json_query) + query = Analytics::Query.new(json_query.with_indifferent_access) + query.validate query.run query.results end diff --git a/back/engines/commercial/report_builder/app/services/report_builder/report_publisher.rb b/back/engines/commercial/report_builder/app/services/report_builder/report_publisher.rb new file mode 100644 index 000000000000..62c5e7d51eca --- /dev/null +++ b/back/engines/commercial/report_builder/app/services/report_builder/report_publisher.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class ReportBuilder::ReportPublisher + def initialize(report) + @report = report + end + + # TODO: move all CraftJS related logic in one class + def publish + ReportBuilder::PublishedGraphDataUnit.where(report_builder_report_id: @report.id).destroy_all + + nodes = @report.layout.craftjs_jsonmultiloc['en'] + nodes.each do |node_id, node_obj| + type = node_obj['type'] + # TODO: is_a? is ugly! fix it + resolved_name = type.is_a?(Hash) ? type['resolvedName'] : next + + data = ReportBuilder::QueryRepository.new.data_by_graph(resolved_name, node_obj['props']) + next unless data + + ReportBuilder::PublishedGraphDataUnit.create!( + report_builder_report_id: @report.id, + graph_id: node_id, + data: data + ) + end + end +end diff --git a/back/engines/commercial/report_builder/config/routes.rb b/back/engines/commercial/report_builder/config/routes.rb index e4edaf46e91f..448f9d2b124d 100644 --- a/back/engines/commercial/report_builder/config/routes.rb +++ b/back/engines/commercial/report_builder/config/routes.rb @@ -5,6 +5,16 @@ namespace :v1 do resources :reports, only: %i[index show create destroy update] do get :layout, on: :member + put :publish, on: :member + + collection do + resources :graph_data_units, only: %i[] do + collection do + get :live + get :published + end + end + end end end end diff --git a/back/engines/commercial/report_builder/report_builder.gemspec b/back/engines/commercial/report_builder/report_builder.gemspec index b71a6b76f197..7708cd5bcb2d 100644 --- a/back/engines/commercial/report_builder/report_builder.gemspec +++ b/back/engines/commercial/report_builder/report_builder.gemspec @@ -17,5 +17,6 @@ Gem::Specification.new do |spec| spec.files = Dir['{app,config,db,lib}/**/*', 'Rakefile'] spec.add_dependency 'content_builder' + spec.add_dependency 'analytics' spec.add_dependency 'rails', '~> 7.0' end diff --git a/back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb b/back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb new file mode 100644 index 000000000000..10dd1afc870c --- /dev/null +++ b/back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ReportBuilder::WebApi::V1::GraphDataUnitsController do + let(:craftjs_jsonmultiloc) do + { + 'en' => { + 'ROOT' => { 'type' => 'div', 'nodes' => ['gJxirq8X7m'], 'props' => { 'id' => 'e2e-content-builder-frame' }, 'custom' => {}, 'hidden' => false, 'isCanvas' => true, 'displayName' => 'div', 'linkedNodes' => {} }, + 'gJxirq8X7m' => { + 'type' => { 'resolvedName' => 'GenderWidget' }, + 'nodes' => [], + 'props' => { + 'endAt' => '2023-11-13T18:41:15.1515', + 'title' => 'Users by gender', + 'startAt' => '2023-11-12T00:00:00.000', + 'projectId' => 'ef411c75-69c4-47cc-8043-39329e10ad16' + }, + 'custom' => { + 'title' => { 'id' => 'app.containers.admin.ReportBuilder.charts.usersByGender', 'defaultMessage' => 'Users by gender' }, + 'noPointerEvents' => true + }, + 'hidden' => false, + 'parent' => 'ROOT', + 'isCanvas' => false, + 'displayName' => 'GenderWidget', + 'linkedNodes' => {} + } + } + } + end + let(:admin_headers) { { Authorization: authorization_header(create(:admin)) } } + let(:user_headers) { { Authorization: authorization_header(create(:user)) } } + let(:report) do + create(:report, layout: build(:layout, craftjs_jsonmultiloc: craftjs_jsonmultiloc), phase_id: create(:phase).id) + end + + # see back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb + def build_analytics_data + date = Date.new(2022, 9, 1) + create(:dimension_date, date: date) + create(:dimension_type, name: 'idea', parent: 'post') + create(:idea, created_at: date) + end + + before do + host! 'example.org' + build_analytics_data + end + + it 'previews live data, publishes it, and returns published data' do + expected_attrs = [{ + count_dimension_user_custom_field_values_dimension_user_id: 1, + 'dimension_date_created.month': '2022-09', + first_dimension_date_created_date: '2022-09-01' + }] + + get '/web_api/v1/reports/graph_data_units/live', + params: { resolved_name: 'GenderWidget' }, + headers: admin_headers + expect(json_parse(response.body).dig(:data, :attributes)).to eq(expected_attrs) + + put "/web_api/v1/reports/#{report.id}/publish", headers: admin_headers + expect(ReportBuilder::PublishedGraphDataUnit.count).to eq(1) + data_unit = ReportBuilder::PublishedGraphDataUnit.first + expect(data_unit).to have_attributes( + report_builder_report_id: report.id, + graph_id: 'gJxirq8X7m', + data: expected_attrs.map(&:deep_stringify_keys) + ) + + get '/web_api/v1/reports/graph_data_units/published', + params: { report_id: report.id, graph_id: 'gJxirq8X7m' }, + headers: user_headers + expect(json_parse(response.body).dig(:data, :attributes)).to eq(expected_attrs) + end +end diff --git a/back/spec/support/api_authentication_helper.rb b/back/spec/support/api_authentication_helper.rb index 37f1574a1f65..71d1b9ab7315 100644 --- a/back/spec/support/api_authentication_helper.rb +++ b/back/spec/support/api_authentication_helper.rb @@ -10,7 +10,11 @@ def resident_header_token end def header_token_for(user) + header 'Authorization', authorization_header(user) + end + + def authorization_header(user) token = AuthToken::AuthToken.new(payload: user.to_token_payload).token - header 'Authorization', "Bearer #{token}" + "Bearer #{token}" end end From 4018cd32f4fa3a7d58bc75389fcc6739b8fbde3c Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 14 Nov 2023 12:19:56 +0100 Subject: [PATCH 12/18] [TAN-449] Add props to users_by_gender query --- .../web_api/v1/graph_data_units_controller.rb | 2 +- .../report_builder/query_repository.rb | 24 +++++++++++++------ .../requests/data_units_publishing_spec.rb | 8 ++++--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb index c9367ac2c608..6aa42f6ff7a9 100644 --- a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb @@ -6,7 +6,7 @@ module V1 class GraphDataUnitsController < ApplicationController def live authorize :live?, policy_class: GraphDataUnitPolicy - results = ReportBuilder::QueryRepository.new.data_by_graph(params[:resolved_name], params[:props]) + results = ReportBuilder::QueryRepository.new.data_by_graph(params[:resolved_name], params[:props] || {}) render_results(results) end diff --git a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb index 5ec577818790..b901fe9ac684 100644 --- a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb +++ b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb @@ -1,31 +1,39 @@ # frozen_string_literal: true +# rubocop:disable Naming/VariableName require 'query' class ReportBuilder::QueryRepository GRAPH_RESOLVED_NAMES_METHODS = { 'GenderWidget' => :users_by_gender - } + }.freeze def data_by_graph(graph_resolved_name, props) method = GRAPH_RESOLVED_NAMES_METHODS[graph_resolved_name] return unless method - send(method, props) + send(method, **props) end protected - def users_by_gender(_props = nil) + def users_by_gender(startAt: nil, endAt: nil, projectId: nil, **_other_props) query( fact: 'participation', - groups: 'dimension_date_created.month', + groups: 'dimension_user_custom_field_values.value', filters: { - 'dimension_user.role': ['citizen', 'admin', nil] + 'dimension_user.role': ['citizen', 'admin', nil], + 'dimension_user_custom_field_values.key': 'gender', + # TODO: try to move `compact.presence` to Analytics::Query + **{ + 'dimension_date_created.date' => + { from: startAt, to: endAt }.compact.presence + }.compact, + # TODO: use dimension_project_id + **{ 'dimension_projects.id': projectId }.compact }, aggregations: { - 'dimension_user_custom_field_values.dimension_user_id': 'count', # we count participants, not participations - 'dimension_date_created.date': 'first' + 'dimension_user_custom_field_values.dimension_user_id': 'count' # we count participants, not participations } ) end @@ -36,8 +44,10 @@ def users_by_birthyear; end def query(json_query) query = Analytics::Query.new(json_query.with_indifferent_access) + # TODO: it's weird to validate and do not check the result. Fix this. query.validate query.run query.results end end +# rubocop:enable Naming/VariableName diff --git a/back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb b/back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb index 10dd1afc870c..2656ad64a724 100644 --- a/back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb +++ b/back/engines/commercial/report_builder/spec/requests/data_units_publishing_spec.rb @@ -35,12 +35,15 @@ create(:report, layout: build(:layout, craftjs_jsonmultiloc: craftjs_jsonmultiloc), phase_id: create(:phase).id) end + let(:gender) { 'female' } + # see back/engines/commercial/analytics/spec/acceptance/analytics_participations_spec.rb def build_analytics_data date = Date.new(2022, 9, 1) create(:dimension_date, date: date) create(:dimension_type, name: 'idea', parent: 'post') - create(:idea, created_at: date) + create(:custom_field, key: :gender, resource_type: 'User') + create(:idea, created_at: date, author: create(:user, gender: gender)) end before do @@ -51,8 +54,7 @@ def build_analytics_data it 'previews live data, publishes it, and returns published data' do expected_attrs = [{ count_dimension_user_custom_field_values_dimension_user_id: 1, - 'dimension_date_created.month': '2022-09', - first_dimension_date_created_date: '2022-09-01' + 'dimension_user_custom_field_values.value': gender }] get '/web_api/v1/reports/graph_data_units/live', From f1c40c8c13dd81a57d8a0bf03b5278502225333c Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 15 Nov 2023 18:14:04 +0100 Subject: [PATCH 13/18] [TAN-449] Move queries to separate classes --- .../services/report_builder/queries/base.rb | 29 ++++++++ .../queries/reactions_by_time.rb | 40 +++++++++++ .../report_builder/queries/users_by_gender.rb | 21 ++++++ .../report_builder/query_repository.rb | 71 +++++++------------ 4 files changed, 114 insertions(+), 47 deletions(-) create mode 100644 back/engines/commercial/report_builder/app/services/report_builder/queries/base.rb create mode 100644 back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb create mode 100644 back/engines/commercial/report_builder/app/services/report_builder/queries/users_by_gender.rb diff --git a/back/engines/commercial/report_builder/app/services/report_builder/queries/base.rb b/back/engines/commercial/report_builder/app/services/report_builder/queries/base.rb new file mode 100644 index 000000000000..2f4f804837b9 --- /dev/null +++ b/back/engines/commercial/report_builder/app/services/report_builder/queries/base.rb @@ -0,0 +1,29 @@ +class ReportBuilder::Queries::Base + RESOLUTION_TO_INTERVAL = { + 'month' => 'month', + 'week' => 'week', + 'day' => 'date' + }.freeze + + def query(**_props) + raise NotImplementedError + end + + private + + def date_filter(dimension, start_at, end_at) + # TODO: try to move `compact.presence` to Analytics::Query + { + "#{dimension}.date" => { from: start_at, to: end_at }.compact.presence + }.compact + end + + def project_filter(dimension, project_id) + # TODO: use dimension_project_id + { "#{dimension}.id" => project_id }.compact + end + + def interval(resolution) + RESOLUTION_TO_INTERVAL[resolution] + end +end diff --git a/back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb b/back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb new file mode 100644 index 000000000000..b79a1cc34c40 --- /dev/null +++ b/back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb @@ -0,0 +1,40 @@ +# rubocop:disable Naming/VariableName +module ReportBuilder + class Queries::ReactionsByTime < Queries::Base + def query(startAt: nil, endAt: nil, projectId: nil, **_other_props) + startAt ||= '2017-01-01' + endAt ||= Time.now + time_series_query = { + fact: 'participation', + filters: { + **date_filter('dimension_date_created', startAt, endAt), + **project_filter('dimension_project', projectId), + 'dimension_type.name': 'reaction', + 'dimension_type.parent': 'idea' + }, + groups: "dimension_date_created.#{interval(resolution)}", + aggregations: { + 'dimension_date_created.date' => 'first', + likes_count: 'sum', + dislikes_count: 'sum' + } + } + + posts_by_time_total = { + fact: 'participation', + filters: { + **date_filter('dimension_date_created', nil, endAt), + **project_filter('dimension_project', projectId), + 'dimension_type.name' => 'reaction', + 'dimension_type.parent' => 'idea' + }, + aggregations: { + reactions_count: 'sum' + } + } + + [time_series_query, posts_by_time_total] + end + end +end +# rubocop:enable Naming/VariableName diff --git a/back/engines/commercial/report_builder/app/services/report_builder/queries/users_by_gender.rb b/back/engines/commercial/report_builder/app/services/report_builder/queries/users_by_gender.rb new file mode 100644 index 000000000000..50d2d971fd5b --- /dev/null +++ b/back/engines/commercial/report_builder/app/services/report_builder/queries/users_by_gender.rb @@ -0,0 +1,21 @@ +# rubocop:disable Naming/VariableName +module ReportBuilder + class Queries::UsersByGender < Queries::Base + def query(startAt: nil, endAt: nil, projectId: nil, **_other_props) + { + fact: 'participation', + groups: 'dimension_user_custom_field_values.value', + filters: { + 'dimension_user.role': ['citizen', 'admin', nil], + 'dimension_user_custom_field_values.key': 'gender', + **date_filter('dimension_date_created', startAt, endAt), + **project_filter('dimension_projects', projectId) + }, + aggregations: { + 'dimension_user_custom_field_values.dimension_user_id': 'count' + } + } + end + end +end +# rubocop:enable Naming/VariableName diff --git a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb index b901fe9ac684..0296cf7302e3 100644 --- a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb +++ b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb @@ -1,53 +1,30 @@ # frozen_string_literal: true -# rubocop:disable Naming/VariableName require 'query' -class ReportBuilder::QueryRepository - GRAPH_RESOLVED_NAMES_METHODS = { - 'GenderWidget' => :users_by_gender - }.freeze - - def data_by_graph(graph_resolved_name, props) - method = GRAPH_RESOLVED_NAMES_METHODS[graph_resolved_name] - return unless method - - send(method, **props) - end - - protected - - def users_by_gender(startAt: nil, endAt: nil, projectId: nil, **_other_props) - query( - fact: 'participation', - groups: 'dimension_user_custom_field_values.value', - filters: { - 'dimension_user.role': ['citizen', 'admin', nil], - 'dimension_user_custom_field_values.key': 'gender', - # TODO: try to move `compact.presence` to Analytics::Query - **{ - 'dimension_date_created.date' => - { from: startAt, to: endAt }.compact.presence - }.compact, - # TODO: use dimension_project_id - **{ 'dimension_projects.id': projectId }.compact - }, - aggregations: { - 'dimension_user_custom_field_values.dimension_user_id': 'count' # we count participants, not participations - } - ) - end - - def users_by_birthyear; end - - private - - def query(json_query) - query = Analytics::Query.new(json_query.with_indifferent_access) - # TODO: it's weird to validate and do not check the result. Fix this. - query.validate - query.run - query.results +module ReportBuilder + # TODO: rename to sth else + class QueryRepository + GRAPH_RESOLVED_NAMES_METHODS = { + 'GenderWidget' => Queries::UsersByGender, + 'ReactionsByTime' => Queries::ReactionsByTime + }.freeze + + def data_by_graph(graph_resolved_name, props) + klass = GRAPH_RESOLVED_NAMES_METHODS[graph_resolved_name] + return unless klass + + run_query(klass.new.query(**props)) + end + + protected + + def run_query(json_query) + query = Analytics::Query.new(json_query.with_indifferent_access) + # TODO: it's weird to validate and do not check the result. Fix this. + query.validate + query.run + query.results + end end end -# rubocop:enable Naming/VariableName From 74bfb17f0106f5ed9d2eb96db4be830dfb13b042 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 15 Nov 2023 18:53:05 +0100 Subject: [PATCH 14/18] [TAN-449] Make names same as on FE --- .../app/services/report_builder/queries/reactions_by_time.rb | 2 +- .../app/services/report_builder/query_repository.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb b/back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb index b79a1cc34c40..0c067c3463db 100644 --- a/back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb +++ b/back/engines/commercial/report_builder/app/services/report_builder/queries/reactions_by_time.rb @@ -1,7 +1,7 @@ # rubocop:disable Naming/VariableName module ReportBuilder class Queries::ReactionsByTime < Queries::Base - def query(startAt: nil, endAt: nil, projectId: nil, **_other_props) + def query(startAt: nil, endAt: nil, projectId: nil, resolution: nil, **_other_props) startAt ||= '2017-01-01' endAt ||= Time.now time_series_query = { diff --git a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb index 0296cf7302e3..3335fbbd63c4 100644 --- a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb +++ b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb @@ -7,7 +7,7 @@ module ReportBuilder class QueryRepository GRAPH_RESOLVED_NAMES_METHODS = { 'GenderWidget' => Queries::UsersByGender, - 'ReactionsByTime' => Queries::ReactionsByTime + 'ReactionsByTimeWidget' => Queries::ReactionsByTime }.freeze def data_by_graph(graph_resolved_name, props) From 0c74e3a8c2e77d78fc0ebcb6667a22f9f496a88e Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 15 Nov 2023 19:00:22 +0100 Subject: [PATCH 15/18] [TAN-449] Permit all props --- .../report_builder/web_api/v1/graph_data_units_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb index 6aa42f6ff7a9..1fec773fbfa9 100644 --- a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb @@ -6,7 +6,7 @@ module V1 class GraphDataUnitsController < ApplicationController def live authorize :live?, policy_class: GraphDataUnitPolicy - results = ReportBuilder::QueryRepository.new.data_by_graph(params[:resolved_name], params[:props] || {}) + results = ReportBuilder::QueryRepository.new.data_by_graph(params[:resolved_name], params[:props].permit! || {}) render_results(results) end From 2a60e3bcb2a0b08d7d051a487b3ccc88fcbce095 Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Wed, 15 Nov 2023 17:54:30 +0000 Subject: [PATCH 16/18] _ --- .../useReactionsByTime/index.ts | 33 +++++++++------ .../useReactionsByTime/keys.ts | 13 ++++++ .../useReactionsByTime/useWhatever.ts | 29 ++++++++++++++ .../ReactionsByTimeWidget/keys.ts | 13 ++++++ .../ReactionsByTimeWidget/types.ts | 40 +++++++++++++++++++ 5 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/keys.ts create mode 100644 front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/useWhatever.ts create mode 100644 front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/keys.ts create mode 100644 front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/types.ts diff --git a/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/index.ts b/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/index.ts index 1ddb442e0893..e886cb4281ac 100644 --- a/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/index.ts +++ b/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/index.ts @@ -3,7 +3,7 @@ import { useIntl } from 'utils/cl-intl'; import { getTranslations } from './translations'; // query -import { query } from './query'; +// import { query } from './query'; // parse import { parseTimeSeries, parseExcelData } from './parse'; @@ -12,8 +12,9 @@ import { parseTimeSeries, parseExcelData } from './parse'; import { getFormattedNumbers } from 'components/admin/GraphCards/_utils/parse'; // typings -import { QueryParameters, Response } from './typings'; -import useAnalytics from 'api/analytics/useAnalytics'; +import { QueryParameters } from './typings'; +// import useAnalytics from 'api/analytics/useAnalytics'; +import useWhatever from './useWhatever'; import { useMemo, useState } from 'react'; export default function useReactionsByTime({ @@ -23,16 +24,22 @@ export default function useReactionsByTime({ resolution, }: QueryParameters) { const { formatMessage } = useIntl(); - const [currentResolution, setCurrentResolution] = useState(resolution); - const { data: analytics } = useAnalytics( - query({ - projectId, - startAtMoment, - endAtMoment, - resolution, - }), - () => setCurrentResolution(resolution) - ); + const [currentResolution, _setCurrentResolution] = useState(resolution); + // const { data: analytics } = useAnalytics( + // query({ + // projectId, + // startAtMoment, + // endAtMoment, + // resolution, + // }), + // () => setCurrentResolution(resolution) + // ); + const { data: analytics } = useWhatever({ + projectId, + startAtMoment, + endAtMoment, + resolution, + }); const timeSeries = useMemo( () => diff --git a/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/keys.ts b/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/keys.ts new file mode 100644 index 000000000000..9eba13a2f531 --- /dev/null +++ b/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/keys.ts @@ -0,0 +1,13 @@ +import { QueryKeys } from 'utils/cl-react-query/types'; +import { QueryParameters } from './typings'; + +const baseKey = { type: 'report_builder_data_units' }; + +const usersByAgeKeys = { + all: () => [baseKey], + item: (params: QueryParameters) => [ + { ...baseKey, operation: 'item', parameters: params }, + ], +} satisfies QueryKeys; + +export default usersByAgeKeys; diff --git a/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/useWhatever.ts b/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/useWhatever.ts new file mode 100644 index 000000000000..3d34634c139e --- /dev/null +++ b/front/app/components/admin/GraphCards/ReactionsByTimeCard/useReactionsByTime/useWhatever.ts @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query'; +import { CLErrors } from 'typings'; +import fetcher from 'utils/cl-react-query/fetcher'; +import { Response, QueryParameters } from './typings'; +import reactionsByTimeKeys from './keys'; + +const fetchReactionsByTime = (props: QueryParameters) => + fetcher({ + path: `/reports/graph_data_units/live`, + action: 'get', + queryParams: { + resolved_name: 'ReactionsByTimeWidget', + props: { + projectId: props.projectId, + resolution: props.resolution, + startAt: props.startAtMoment?.format('yyyy-MM-DD'), + endAt: props.endAtMoment?.format('yyyy-MM-DD'), + }, + }, + }); + +const useReactionsByTime = ({ ...queryParameters }: QueryParameters) => { + return useQuery({ + queryKey: reactionsByTimeKeys.item(queryParameters), + queryFn: () => fetchReactionsByTime(queryParameters), + }); +}; + +export default useReactionsByTime; diff --git a/front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/keys.ts b/front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/keys.ts new file mode 100644 index 000000000000..e52dbdd96394 --- /dev/null +++ b/front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/keys.ts @@ -0,0 +1,13 @@ +import { QueryKeys } from 'utils/cl-react-query/types'; +import { QueryParameters } from './types'; + +const baseKey = { type: 'report_builder_data_units' }; + +const usersByAgeKeys = { + all: () => [baseKey], + item: (params: QueryParameters) => [ + { ...baseKey, operation: 'item', parameters: params }, + ], +} satisfies QueryKeys; + +export default usersByAgeKeys; diff --git a/front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/types.ts b/front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/types.ts new file mode 100644 index 000000000000..d7223bbde394 --- /dev/null +++ b/front/app/containers/Admin/reporting/components/ReportBuilder/Widgets/ChartWidgets/ReactionsByTimeWidget/types.ts @@ -0,0 +1,40 @@ +import { + ProjectId, + Dates, + Resolution, +} from 'components/admin/GraphCards/typings'; + +export type QueryParameters = ProjectId & Dates & Resolution; + +export type ReactionsByTime = { + data: ReactionsByTimeData; +}; + +export interface ReactionsByTimeData { + id: ''; + type: 'report_builder_data_units'; + attributes: [TimeSeriesResponse | [], [ReactionsCountRow] | []]; +} + +type TimeSeriesResponse = TimeSeriesResponseRow[]; + +export interface TimeSeriesResponseRow { + first_dimension_date_created_date: string; + sum_dislikes_count: number; + sum_likes_count: number; +} + +interface ReactionsCountRow { + sum_reactions_count: number; +} + +// Hook return value +export interface TimeSeriesRow { + /* Date format: YYYY-MM-DD */ + date: string; + likes: number; + dislikes: number; + total: number; +} + +export type TimeSeries = TimeSeriesRow[]; From 6035eb5d015aabed01046af49e814ff41d7be5d6 Mon Sep 17 00:00:00 2001 From: Luuc van der Zee Date: Wed, 15 Nov 2023 18:10:03 +0000 Subject: [PATCH 17/18] _ --- .../report_builder/web_api/v1/graph_data_units_controller.rb | 2 +- .../app/services/report_builder/query_repository.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb index 1fec773fbfa9..d352bba21b29 100644 --- a/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb +++ b/back/engines/commercial/report_builder/app/controllers/report_builder/web_api/v1/graph_data_units_controller.rb @@ -6,7 +6,7 @@ module V1 class GraphDataUnitsController < ApplicationController def live authorize :live?, policy_class: GraphDataUnitPolicy - results = ReportBuilder::QueryRepository.new.data_by_graph(params[:resolved_name], params[:props].permit! || {}) + results = ReportBuilder::QueryRepository.new.data_by_graph(params[:resolved_name], params.permit![:props] || {}) render_results(results) end diff --git a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb index 3335fbbd63c4..56aec0742e6a 100644 --- a/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb +++ b/back/engines/commercial/report_builder/app/services/report_builder/query_repository.rb @@ -20,7 +20,7 @@ def data_by_graph(graph_resolved_name, props) protected def run_query(json_query) - query = Analytics::Query.new(json_query.with_indifferent_access) + query = Analytics::Query.new({ query: json_query }.with_indifferent_access[:query]) # TODO: it's weird to validate and do not check the result. Fix this. query.validate query.run From 3ca7ef586266de95cfc744df520c24e9c63b6b4e Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 16 Nov 2023 18:14:10 +0100 Subject: [PATCH 18/18] [TAN-449] Allow nil report name --- .../report_builder/app/models/report_builder/report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/back/engines/commercial/report_builder/app/models/report_builder/report.rb b/back/engines/commercial/report_builder/app/models/report_builder/report.rb index 2644530767e5..64166221ac73 100644 --- a/back/engines/commercial/report_builder/app/models/report_builder/report.rb +++ b/back/engines/commercial/report_builder/app/models/report_builder/report.rb @@ -37,6 +37,6 @@ class Report < ::ApplicationRecord scope :global, -> { where(phase_id: nil) } - validates :name, uniqueness: true, if: :present? + validates :name, uniqueness: true, allow_nil: true end end