From d0ec241b46942326cbf14cac70f98f823b35ee6b Mon Sep 17 00:00:00 2001 From: yuenmichelle1 Date: Wed, 6 Sep 2023 15:00:27 -0500 Subject: [PATCH] Classification user groups individual stats (#25) * individual stats breakdown per group. add session time to user group projects breakdown. * update sort of group member stats breakdown * Update user_group_member_stats_breakdown_serializer.rb * add specs user group classification count controller for individual stats breakdown stats * add specs for group member breakdown query stats * add spec for serializer * update session time to be included with top contributors --- ...r_group_classification_count_controller.rb | 5 +- ...count_group_active_user_classifications.rb | 2 +- app/queries/count_group_member_breakdown.rb | 30 ++++++++ .../count_group_project_contributions.rb | 2 +- ...group_member_stats_breakdown_serializer.rb | 31 ++++++++ ...up_classification_count_controller_spec.rb | 49 +++++++----- ..._group_active_user_classifications_spec.rb | 2 +- .../count_group_member_breakdown_spec.rb | 73 ++++++++++++++++++ .../count_group_project_contributions_spec.rb | 2 +- ..._member_stats_breakdown_serializer_spec.rb | 75 +++++++++++++++++++ 10 files changed, 246 insertions(+), 25 deletions(-) create mode 100644 app/queries/count_group_member_breakdown.rb create mode 100644 app/serializers/user_group_member_stats_breakdown_serializer.rb create mode 100644 spec/queries/count_group_member_breakdown_spec.rb create mode 100644 spec/serializers/user_group_member_stats_breakdown_serializer_spec.rb diff --git a/app/controllers/user_group_classification_count_controller.rb b/app/controllers/user_group_classification_count_controller.rb index 3521640..fa15afc 100644 --- a/app/controllers/user_group_classification_count_controller.rb +++ b/app/controllers/user_group_classification_count_controller.rb @@ -12,8 +12,9 @@ def query # authorize :queried_user_group_context, :show? skip_authorization if params[:individual_stats_breakdown] - # TODO: in a separate PR - # Plan is to query from DailyGroupClassificationCountAndTimePerUserPerProject + group_member_classification_counts = CountGroupMemberBreakdown.new.call(group_classification_count_params) + + render json: UserGroupMemberStatsBreakdownSerializer.new(group_member_classification_counts) else group_classification_counts = CountGroupClassifications.new(group_classification_count_params).call(group_classification_count_params) group_active_user_classification_counts = CountGroupActiveUserClassifications.new(group_classification_count_params).call(group_classification_count_params) diff --git a/app/queries/count_group_active_user_classifications.rb b/app/queries/count_group_active_user_classifications.rb index c25765d..27ccae3 100644 --- a/app/queries/count_group_active_user_classifications.rb +++ b/app/queries/count_group_active_user_classifications.rb @@ -23,7 +23,7 @@ def initial_scope(relation) end def select_clause - 'user_id, SUM(classification_count)::integer AS count' + 'user_id, SUM(classification_count)::integer AS count, SUM(total_session_time)::float AS session_time' end def relation(params) diff --git a/app/queries/count_group_member_breakdown.rb b/app/queries/count_group_member_breakdown.rb new file mode 100644 index 0000000..c130a90 --- /dev/null +++ b/app/queries/count_group_member_breakdown.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class CountGroupMemberBreakdown + include Filterable + attr_reader :counts + + def initialize + @counts = initial_scope(relation) + end + + def call(params={}) + scoped = @counts + scoped = filter_by_user_group_id(scoped, params[:id]) + filter_by_date_range(scoped, params[:start_date], params[:end_date]) + end + + private + + def initial_scope(relation) + relation.select(select_clause).group('user_id, project_id') + end + + def select_clause + 'user_id, project_id, SUM(classification_count)::integer AS count, SUM(total_session_time)::float AS session_time' + end + + def relation + UserGroupClassificationCounts::DailyGroupUserProjectClassificationCount + end +end diff --git a/app/queries/count_group_project_contributions.rb b/app/queries/count_group_project_contributions.rb index c5401c7..75844aa 100644 --- a/app/queries/count_group_project_contributions.rb +++ b/app/queries/count_group_project_contributions.rb @@ -21,7 +21,7 @@ def initial_scope(relation) end def select_clause - 'project_id, SUM(classification_count)::integer AS count' + 'project_id, SUM(classification_count)::integer AS count, SUM(total_session_time)::float AS session_time' end def relation diff --git a/app/serializers/user_group_member_stats_breakdown_serializer.rb b/app/serializers/user_group_member_stats_breakdown_serializer.rb new file mode 100644 index 0000000..f8a556e --- /dev/null +++ b/app/serializers/user_group_member_stats_breakdown_serializer.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class UserGroupMemberStatsBreakdownSerializer + attr_reader :group_member_classification_counts + + def initialize(counts_scope) + @group_member_classification_counts = counts_scope + end + + def as_json(_options) + { + group_member_stats_breakdown: counts_grouped_by_user.sort_by { |member_stat| member_stat[:count] }.reverse + } + end + + private + + def counts_grouped_by_user + counts_grouped_by_member = group_member_classification_counts.group_by { |member_proj_contribution| member_proj_contribution[:user_id] }.transform_values do |member_counts_per_project| + total_per_member = { count: member_counts_per_project.sum(&:count) } + total_per_member[:session_time] = member_counts_per_project.sum(&:session_time) + total_per_member[:project_contributions] = individual_member_project_contributions(member_counts_per_project) + total_per_member + end + counts_grouped_by_member.map { |user_id, totals| { user_id: }.merge(totals) } + end + + def individual_member_project_contributions(member_counts_per_project) + member_counts_per_project.sort_by(&:count).reverse.map { |member_count| member_count.as_json.except('user_id') } + end +end diff --git a/spec/controllers/user_group_classification_count_controller_spec.rb b/spec/controllers/user_group_classification_count_controller_spec.rb index 5e29ca4..8a0ad13 100644 --- a/spec/controllers/user_group_classification_count_controller_spec.rb +++ b/spec/controllers/user_group_classification_count_controller_spec.rb @@ -6,28 +6,39 @@ describe 'GET query' do let!(:classification_user_group) { create(:classification_user_group) } - it 'returns total_count, time_spent, active_users, and project_contributions of user group' do - get :query, params: { id: classification_user_group.user_group_id.to_s } - expect(response.status).to eq(200) - response_body = JSON.parse(response.body) - expect(response_body['total_count']).to eq(1) - expect(response_body['time_spent']).to eq(classification_user_group.session_time) - expect(response_body['active_users']).to eq(1) - expect(response_body['project_contributions'].length).to eq(1) - end + context 'individual_stats_breakdown is false/not a param' do + it 'returns total_count, time_spent, active_users, and project_contributions of user group' do + get :query, params: { id: classification_user_group.user_group_id.to_s } + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + expect(response_body['total_count']).to eq(1) + expect(response_body['time_spent']).to eq(classification_user_group.session_time) + expect(response_body['active_users']).to eq(1) + expect(response_body['project_contributions'].length).to eq(1) + end + + it 'does not compute project_contributions when params[:project_id] given' do + get :query, params: { id: classification_user_group.user_group_id.to_s, project_id: 2 } + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + expect(response_body).not_to have_key('project_contributions') + end - it 'does not compute project_contributions when params[:project_id] given' do - get :query, params: { id: classification_user_group.user_group_id.to_s, project_id: 2 } - expect(response.status).to eq(200) - response_body = JSON.parse(response.body) - expect(response_body).not_to have_key(:project_contributions) + it 'does not compute project_contributions when params[:workflow_id] given' do + get :query, params: { id: classification_user_group.user_group_id.to_s, workflow_id: 2 } + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + expect(response_body).not_to have_key('project_contributions') + end end - it 'does not compute project_contributions when params[:workflow_id] given' do - get :query, params: { id: classification_user_group.user_group_id.to_s, workflow_id: 2 } - expect(response.status).to eq(200) - response_body = JSON.parse(response.body) - expect(response_body).not_to have_key(:project_contributions) + context 'individual_stats_breakdown is true' do + it 'returns group_member_stats_breakdown' do + get :query, params: { id: classification_user_group.user_group_id.to_s, individual_stats_breakdown: true } + expect(response.status).to eq(200) + response_body = JSON.parse(response.body) + expect(response_body).to have_key('group_member_stats_breakdown') + end end context 'param validations' do diff --git a/spec/queries/count_group_active_user_classifications_spec.rb b/spec/queries/count_group_active_user_classifications_spec.rb index 93b856e..fce64b6 100644 --- a/spec/queries/count_group_active_user_classifications_spec.rb +++ b/spec/queries/count_group_active_user_classifications_spec.rb @@ -24,7 +24,7 @@ describe 'select_clause' do it 'selects user_id and orders users by count' do counts = group_active_users_query.call(params) - expected_select_query = 'SELECT user_id, SUM(classification_count)::integer AS count FROM "daily_group_classification_count_and_time_per_user" ' + expected_select_query = 'SELECT user_id, SUM(classification_count)::integer AS count, SUM(total_session_time)::float AS session_time FROM "daily_group_classification_count_and_time_per_user" ' expected_select_query += 'GROUP BY "daily_group_classification_count_and_time_per_user"."user_id" ORDER BY count DESC' expect(counts.to_sql).to eq(expected_select_query) end diff --git a/spec/queries/count_group_member_breakdown_spec.rb b/spec/queries/count_group_member_breakdown_spec.rb new file mode 100644 index 0000000..c1b1d52 --- /dev/null +++ b/spec/queries/count_group_member_breakdown_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe CountGroupMemberBreakdown do + let(:params) { {} } + let(:group_member_breakdown_query) { described_class.new } + describe 'relation' do + it 'returns DailyGroupUserProjectClassificationCount' do + expect(group_member_breakdown_query.counts.model).to be UserGroupClassificationCounts::DailyGroupUserProjectClassificationCount + end + end + + describe 'select_clause' do + it 'selects user_id, project_id, sum of counts and time' do + counts = group_member_breakdown_query.call(params) + expected_select_query = 'SELECT user_id, project_id, SUM(classification_count)::integer AS count, SUM(total_session_time)::float AS session_time ' + expected_select_query += 'FROM "daily_group_classification_count_and_time_per_user_per_project" ' + expected_select_query += 'GROUP BY user_id, project_id' + expect(counts.to_sql).to eq(expected_select_query) + end + end + + describe '#call' do + let!(:classification_user_group) { create(:classification_user_group) } + let!(:diff_workflow_event) { create(:cug_with_diff_workflow) } + let!(:diff_project_event) { create(:cug_with_diff_project) } + let!(:diff_time_event) { create(:cug_created_yesterday) } + let!(:diff_user_classification) { create(:cug_with_diff_user) } + let!(:diff_user_group_classification) { create(:cug_with_diff_user_group) } + + before(:each) do + params[:id] = classification_user_group.user_group_id.to_s + end + + it_behaves_like 'is filterable by date range' do + let(:counts_query) { described_class.new } + end + + it 'filters by given user_group_id' do + counts = group_member_breakdown_query.call(params) + expect(counts.to_sql).to include(".\"user_group_id\" = #{classification_user_group.user_id}") + end + + it 'returns classification counts of given user group grouped by user and project' do + counts = group_member_breakdown_query.call(params) + # because default is grouped by project_id and user_id, we expect results to look something like: + # [ + # , + # , + # + # ] + expect(counts.length).to eq(3) + # the 3 for user_id: 1, project_id:1 being + # [classification_user_group, diff_workflow_event, diff_time_event] + expect(counts[0].count).to eq(3) + expect(counts[1].count).to eq(1) + expect(counts[1].count).to eq(1) + end + + it 'returns counts of events within given date range' do + last_week = Date.today - 7 + yesterday = Date.today - 1 + params[:start_date] = last_week.to_s + params[:end_date] = yesterday.to_s + counts = group_member_breakdown_query.call(params) + expect(counts.length).to eq(1) + expect(counts[0].count).to eq(1) + expect(counts[0].project_id).to eq(diff_time_event.project_id) + expect(counts[0].user_id).to eq(diff_time_event.user_id) + end + end +end diff --git a/spec/queries/count_group_project_contributions_spec.rb b/spec/queries/count_group_project_contributions_spec.rb index 8c205dd..ae493c0 100644 --- a/spec/queries/count_group_project_contributions_spec.rb +++ b/spec/queries/count_group_project_contributions_spec.rb @@ -14,7 +14,7 @@ describe 'select_clause' do it 'selects project_id and orders by count' do counts = group_classifications_query.call(params) - expected_select_query = 'SELECT project_id, SUM(classification_count)::integer AS count FROM "daily_group_classification_count_and_time_per_project" ' + expected_select_query = 'SELECT project_id, SUM(classification_count)::integer AS count, SUM(total_session_time)::float AS session_time FROM "daily_group_classification_count_and_time_per_project" ' expected_select_query += 'GROUP BY "daily_group_classification_count_and_time_per_project"."project_id" ORDER BY count DESC' expect(counts.to_sql).to eq(expected_select_query) end diff --git a/spec/serializers/user_group_member_stats_breakdown_serializer_spec.rb b/spec/serializers/user_group_member_stats_breakdown_serializer_spec.rb new file mode 100644 index 0000000..c964d5a --- /dev/null +++ b/spec/serializers/user_group_member_stats_breakdown_serializer_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UserGroupMemberStatsBreakdownSerializer do + let(:user_project_classification_count) { build(:daily_user_project_classification_count) } + + let(:count_serializer) { described_class.new([user_project_classification_count]) } + + it 'returns group_member_stats as array' do + serialized = count_serializer.as_json({}) + expect(serialized).to have_key(:group_member_stats_breakdown) + expect(serialized[:group_member_stats_breakdown].length).to eq(1) + expect(serialized[:group_member_stats_breakdown][0]).to have_key(:user_id) + expect(serialized[:group_member_stats_breakdown][0]).to have_key(:count) + expect(serialized[:group_member_stats_breakdown][0]).to have_key(:session_time) + expect(serialized[:group_member_stats_breakdown][0]).to have_key(:project_contributions) + end + + it 'sums up total_count per user correctly' do + count2 = build(:user_diff_proj_classification_count) + classification_counts = [user_project_classification_count, count2] + serializer = described_class.new(classification_counts) + serialized = serializer.as_json({}) + expect(serialized[:group_member_stats_breakdown][0][:count]).to eq(classification_counts.sum(&:count)) + end + + it 'sums up time_spent per user correctly' do + count2 = build(:user_diff_proj_classification_count) + classification_counts = [user_project_classification_count, count2] + serializer = described_class.new(classification_counts) + serialized = serializer.as_json({}) + expect(serialized[:group_member_stats_breakdown][0][:session_time]).to eq(classification_counts.sum(&:session_time)) + end + + it 'shows project contributions per user correctly' do + count2 = build(:user_diff_proj_classification_count) + classification_counts = [user_project_classification_count, count2] + serializer = described_class.new(classification_counts) + serialized = serializer.as_json({}) + member_project_contributions = serialized[:group_member_stats_breakdown][0][:project_contributions] + expect(member_project_contributions.length).to eq(2) + expect(member_project_contributions[0]).not_to have_key('user_id') + expect(member_project_contributions[0]).to have_key('project_id') + expect(member_project_contributions[0]).to have_key('count') + expect(member_project_contributions[0]).to have_key('session_time') + end + + it 'shows user project contributions in order by count desc' do + count2 = build(:user_diff_proj_classification_count) + count2.count = user_project_classification_count.count + 100 + classification_counts = [user_project_classification_count, count2] + serializer = described_class.new(classification_counts) + serialized = serializer.as_json({}) + member_project_contributions = serialized[:group_member_stats_breakdown][0][:project_contributions] + expect(member_project_contributions[0]['project_id']).to eq(count2.project_id) + expect(member_project_contributions[0]['count']).to eq(count2.count) + expect(member_project_contributions[0]['session_time']).to eq(count2.session_time) + expect(member_project_contributions[1]['project_id']).to eq(user_project_classification_count.project_id) + expect(member_project_contributions[1]['count']).to eq(user_project_classification_count.count) + expect(member_project_contributions[1]['session_time']).to eq(user_project_classification_count.session_time) + end + + it 'shows group_memer_stats_breakdown in order by top contributors' do + diff_group_member_stats = build(:daily_user_project_classification_count) + diff_group_member_stats.user_id = 2 + diff_group_member_stats.count = user_project_classification_count.count + 100 + classification_counts = [user_project_classification_count, diff_group_member_stats] + serializer = described_class.new(classification_counts) + serialized = serializer.as_json({}) + expect(serialized[:group_member_stats_breakdown].length).to eq(2) + expect(serialized[:group_member_stats_breakdown][0][:user_id]).to eq(diff_group_member_stats.user_id) + expect(serialized[:group_member_stats_breakdown][1][:user_id]).to eq(user_project_classification_count.user_id) + end +end