Commit 4d5caa89 authored by Adam Hegyi's avatar Adam Hegyi Committed by Michael Kozono

Query backend for tasks by type chart

Query for getting counts by date for Issue or MergeRequest records
within a group. This query object is going to be used for the tasks
by type chart.
parent bc8c2026
......@@ -9,24 +9,6 @@ class Analytics::TasksByTypeController < Analytics::ApplicationController
before_action :validate_label_ids
before_action :prepare_date_range
# Mocked data, this will be replaced with real implementation
class TasksByType
LabelCountResult = Struct.new(:label, :series)
def counts_by_labels
[
LabelCountResult.new(GroupLabel.new(id: 1, title: 'label 1'), [
["2018-01-01", 23],
["2018-01-02", 5]
]),
LabelCountResult.new(GroupLabel.new(id: 2, title: 'label 3'), [
["2018-01-01", 3],
["2018-01-03", 10]
])
]
end
end
def show
render json: Analytics::TasksByTypeLabelEntity.represent(counts_by_labels)
end
......@@ -34,7 +16,13 @@ class Analytics::TasksByTypeController < Analytics::ApplicationController
private
def counts_by_labels
TasksByType.new.counts_by_labels
Gitlab::Analytics::TypeOfWork::TasksByType.new(group: @group, current_user: current_user, params: {
subject: params[:subject],
label_ids: Array(params[:label_ids]),
project_ids: Array(params[:project_ids]),
created_after: @created_after.to_time.utc.beginning_of_day,
created_before: @created_before.to_time.utc.end_of_day
}).counts_by_labels
end
def validate_label_ids
......
---
title: Data API endpoint for tasks by type chart within the analytics workspace
merge_request: 17944
author:
type: added
# frozen_string_literal: true
module Gitlab
module Analytics
module TypeOfWork
class TasksByType
LabelCountResult = Struct.new(:label, :series)
FINDER_CLASSES = {
MergeRequest.to_s => MergeRequestsFinder,
Issue.to_s => IssuesFinder
}.freeze
def initialize(group:, params:, current_user:)
@group = group
@params = params
@finder = finder_class.new(current_user, finder_params)
end
def counts_by_labels
format_result(query_result)
end
private
attr_reader :group, :params, :finder
def finder_class
FINDER_CLASSES.fetch(params[:subject], FINDER_CLASSES.keys.first)
end
def format_result(result)
result.each_with_object({}) do |((label_id, date), count), hash|
label = labels_by_id.fetch(label_id)
hash[label_id] ||= LabelCountResult.new(label, [])
hash[label_id].series << [date, count]
end.values
end
def finder_params
{ include_subgroups: true, group_id: group.id }.merge(params.slice(:created_after, :created_before))
end
# rubocop: disable CodeReuse/ActiveRecord
def query_result
finder
.execute
.joins(:label_links)
.where(filters)
.group(label_id_column, date_column)
.reorder(nil)
.count(subject_table[:id])
end
def filters
{}.tap do |hash|
hash[:label_links] = { label_id: labels_by_id.keys }
hash[:project_id] = params[:project_ids] unless params[:project_ids].blank?
end
end
def labels
@labels ||= GroupLabel.where(id: params[:label_ids])
end
# rubocop: enable CodeReuse/ActiveRecord
def label_id_column
LabelLink.arel_table[:label_id]
end
# Generating `DATE(created_at)` string
def date_column
Arel::Nodes::NamedFunction.new('DATE', [subject_table[:created_at]]).to_sql
end
def subject_table
finder.klass.arel_table
end
def labels_by_id
@labels_by_id = labels.each_with_object({}) { |label, hash| hash[label.id] = label }
end
end
end
end
end
......@@ -5,23 +5,34 @@ require 'spec_helper'
describe Analytics::TasksByTypeController do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:params) { { group_id: group.full_path, label_ids: [1, 2], created_after: '2018-01-01' } }
let(:subject) { get :show, params: params }
let(:label) { create(:group_label, group: group) }
let(:params) { { group_id: group.full_path, label_ids: [label.id], created_after: 10.days.ago, subject: 'Issue' } }
let!(:issue) { create(:labeled_issue, created_at: 5.days.ago, project: create(:project, group: group), labels: [label]) }
subject { get :show, params: params }
before do
stub_licensed_features(type_of_work_analytics: true)
stub_feature_flags(Gitlab::Analytics::TASKS_BY_TYPE_CHART_FEATURE_FLAG => true)
group.add_reporter(user)
sign_in(user)
end
it 'succeeds' do
subject
context 'when valid parameters are given' do
it 'succeeds' do
subject
expect(response).to be_successful
expect(response).to match_response_schema('analytics/tasks_by_type', dir: 'ee')
end
expect(response).to be_successful
expect(response).to match_response_schema('analytics/tasks_by_type', dir: 'ee')
it 'returns valid count' do
subject
date, count = json_response.first["series"].first
expect(Date.parse(date)).to eq(issue.created_at.to_date)
expect(count).to eq(1)
end
end
context 'when user access level is lower than reporter' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Analytics::TypeOfWork::TasksByType do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:other_group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:label) { create(:group_label, group: group) }
let_it_be(:label_for_subgroup) { create(:group_label, group: group) }
let_it_be(:other_label) { create(:group_label, group: other_group) }
let_it_be(:project) { create(:project, group: group) }
let(:params) do
{
group: group,
params: { label_ids: [label.id, label_for_subgroup.id], created_after: 10.days.ago, created_before: Date.today },
current_user: user
}
end
subject do
described_class.new(params).counts_by_labels
end
around do |example|
Timecop.freeze { example.run }
end
before do
group.add_reporter(user)
end
shared_examples '#counts_by_labels' do
let!(:with_label) do
create(factory_name, {
:created_at => 3.days.ago,
:labels => [label],
project_attribute_name => project
})
end
let!(:with_label_on_other_date) do
create(factory_name, {
:created_at => 2.days.ago,
:labels => [label],
project_attribute_name => create(:project, group: group)
})
end
let!(:with_subgroup) do
create(factory_name, {
:created_at => 3.days.ago,
:labels => [label, label_for_subgroup],
project_attribute_name => create(:project, group: subgroup)
})
end
let!(:outside_group) do
create(factory_name, {
:created_at => 3.days.ago,
:labels => [other_label],
project_attribute_name => create(:project, group: other_group)
})
end
def label_count_for(label, result)
label_count_result = result.find { |r| r.label.id == label.id }
label_count_result.series.sum(&:last) # format: [DATE, COUNT]
end
it 'counts the records by label and date' do
expect(label_count_for(label, subject)).to eq(3)
end
it 'counts should include subgroups' do
expect(label_count_for(label_for_subgroup, subject)).to eq(1)
end
it 'does not include count from outside of the group' do
label_ids = subject.map { |r| r.label.id }
expect(label_ids).to contain_exactly(label.id, label_for_subgroup.id)
end
context 'when group without any record is given' do
before do
params[:group] = create(:group)
end
it { expect(subject).to be_empty }
end
context 'when no labels are given' do
before do
params[:params][:label_ids] = []
end
it { expect(subject).to be_empty }
end
context 'when records are outside of the given time range' do
before do
params[:params][:created_after] = 2.years.ago
params[:params][:created_before] = 1.year.ago
end
it { expect(subject).to be_empty }
end
context 'when filtering by `project_ids`' do
before do
params[:params][:project_ids] = [project.id]
end
it { expect(label_count_for(label, subject)).to eq(1) }
end
end
context 'when subject is `Issue`' do
let(:factory_name) { :labeled_issue }
let(:project_attribute_name) { :project }
before do
params[:params][:subject] = Issue.to_s
end
include_examples '#counts_by_labels'
end
context 'when subject is `MergeRequest`' do
let(:factory_name) { :labeled_merge_request }
let(:project_attribute_name) { :source_project }
before do
params[:params][:subject] = MergeRequest.to_s
end
include_examples '#counts_by_labels'
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment