Commit 0f0e168a authored by Nick Thomas's avatar Nick Thomas

Merge branch '196834-confidential-issue' into 'master'

Add GraphQL group timelogs time arguments

See merge request gitlab-org/gitlab!28560
parents 1c6b1427 f879127e
...@@ -16,8 +16,8 @@ class Timelog < ApplicationRecord ...@@ -16,8 +16,8 @@ class Timelog < ApplicationRecord
) )
end end
scope :between_dates, -> (start_date, end_date) do scope :between_times, -> (start_time, end_time) do
where('spent_at BETWEEN ? AND ?', start_date, end_date) where('spent_at BETWEEN ? AND ?', start_time, end_time)
end end
def issuable def issuable
......
...@@ -4097,9 +4097,14 @@ type Group { ...@@ -4097,9 +4097,14 @@ type Group {
before: String before: String
""" """
List time logs within a time range where the logged date is before end_date parameter. List time logs within a date range where the logged date is equal to or before endDate
""" """
endDate: Time! endDate: Time
"""
List time-logs within a time range where the logged time is equal to or before endTime
"""
endTime: Time
""" """
Returns the first _n_ elements from the list. Returns the first _n_ elements from the list.
...@@ -4112,9 +4117,14 @@ type Group { ...@@ -4112,9 +4117,14 @@ type Group {
last: Int last: Int
""" """
List time logs within a time range where the logged date is after start_date parameter. List time logs within a date range where the logged date is equal to or after startDate
"""
startDate: Time
"""
List time-logs within a time range where the logged time is equal to or after startTime
""" """
startDate: Time! startTime: Time
): TimelogConnection! ): TimelogConnection!
""" """
......
...@@ -11530,29 +11530,41 @@ ...@@ -11530,29 +11530,41 @@
"args": [ "args": [
{ {
"name": "startDate", "name": "startDate",
"description": "List time logs within a time range where the logged date is after start_date parameter.", "description": "List time logs within a date range where the logged date is equal to or after startDate",
"type": { "type": {
"kind": "NON_NULL", "kind": "SCALAR",
"name": null, "name": "Time",
"ofType": { "ofType": null
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
}, },
"defaultValue": null "defaultValue": null
}, },
{ {
"name": "endDate", "name": "endDate",
"description": "List time logs within a time range where the logged date is before end_date parameter.", "description": "List time logs within a date range where the logged date is equal to or before endDate",
"type": { "type": {
"kind": "NON_NULL", "kind": "SCALAR",
"name": null, "name": "Time",
"ofType": { "ofType": null
"kind": "SCALAR", },
"name": "Time", "defaultValue": null
"ofType": null },
} {
"name": "startTime",
"description": "List time-logs within a time range where the logged time is equal to or after startTime",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "endTime",
"description": "List time-logs within a time range where the logged time is equal to or before endTime",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}, },
"defaultValue": null "defaultValue": null
}, },
......
...@@ -3,61 +3,93 @@ ...@@ -3,61 +3,93 @@
module Resolvers module Resolvers
class TimelogResolver < BaseResolver class TimelogResolver < BaseResolver
argument :start_date, Types::TimeType, argument :start_date, Types::TimeType,
required: true, required: false,
description: 'List time logs within a time range where the logged date is after start_date parameter.' description: 'List time logs within a date range where the logged date is equal to or after startDate'
argument :end_date, Types::TimeType, argument :end_date, Types::TimeType,
required: true, required: false,
description: 'List time logs within a time range where the logged date is before end_date parameter.' description: 'List time logs within a date range where the logged date is equal to or before endDate'
argument :start_time, Types::TimeType,
required: false,
description: 'List time-logs within a time range where the logged time is equal to or after startTime'
argument :end_time, Types::TimeType,
required: false,
description: 'List time-logs within a time range where the logged time is equal to or before endTime'
def resolve(**args) def resolve(**args)
validate_date_params!(args) return Timelog.none unless timelogs_available_for_user?
authorize_group_timelogs!
validate_params_presence!(args)
transformed_args = transform_args(args)
validate_time_difference!(transformed_args)
find_timelogs(args) find_timelogs(transformed_args)
end end
private private
def find_timelogs(args) def find_timelogs(args)
group.timelogs(args[:start_date], args[:end_date]) group.timelogs(args[:start_time], args[:end_time])
end end
def validate_date_params!(args) def timelogs_available_for_user?
validate_dates_present!(args[:start_date], args[:end_date]) group&.feature_available?(:group_timelogs) &&
validate_dates_difference!(args[:start_date], args[:end_date])
validate_date_range!(args[:start_date], args[:end_date])
end
def valid_object?
group.present? &&
group&.feature_available?(:group_timelogs) &&
group&.user_can_access_group_timelogs?(context[:current_user]) group&.user_can_access_group_timelogs?(context[:current_user])
end end
def authorize_group_timelogs! def validate_params_presence!(args)
unless valid_object? message = case time_params_count(args)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, when 0
"The resource is not available or you don't have permission to perform this action" 'Start and End arguments must be present'
end when 1
'Both Start and End arguments must be present'
when 2
validate_duplicated_args(args)
when 3 || 4
'Only Time or Date arguments must be present'
end
raise_argument_error(message) if message
end end
def validate_dates_present!(start_date, end_date) def validate_time_difference!(args)
return if start_date.present? && end_date.present? message = if args[:end_time] < args[:start_time]
'Start argument must be before End argument'
elsif args[:end_time] - args[:start_time] > 60.days
'The time range period cannot contain more than 60 days'
end
raise_argument_error('Both start_date and end_date must be present.') raise_argument_error(message) if message
end end
def validate_dates_difference!(start_date, end_date) def transform_args(args)
return if end_date > start_date return args if args.keys == [:start_time, :end_time]
raise_argument_error('start_date must be earlier than end_date.') time_args = args.except(:start_date, :end_date)
if time_args.empty?
time_args[:start_time] = args[:start_date].beginning_of_day
time_args[:end_time] = args[:end_date].end_of_day
elsif time_args.key?(:start_time)
time_args[:end_time] = args[:end_date].end_of_day
elsif time_args.key?(:end_time)
time_args[:start_time] = args[:start_date].beginning_of_day
end
time_args
end end
def validate_date_range!(start_date, end_date) def time_params_count(args)
return if end_date - start_date <= 60.days [:start_time, :end_time, :start_date, :end_date].count { |param| args.key?(param) }
end
raise_argument_error('The date range period cannot contain more than 60 days') def validate_duplicated_args(args)
if args.key?(:start_time) && args.key?(:start_date) ||
args.key?(:end_time) && args.key?(:end_date)
'Both Start and End arguments must be present'
end
end end
def raise_argument_error(message) def raise_argument_error(message)
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
module HasTimelogsReport module HasTimelogsReport
extend ActiveSupport::Concern extend ActiveSupport::Concern
def timelogs(start_date, end_date) def timelogs(start_time, end_time)
@timelogs ||= timelogs_for(start_date, end_date) @timelogs ||= timelogs_for(start_time, end_time)
end end
def user_can_access_group_timelogs?(current_user) def user_can_access_group_timelogs?(current_user)
...@@ -15,7 +15,7 @@ module HasTimelogsReport ...@@ -15,7 +15,7 @@ module HasTimelogsReport
private private
def timelogs_for(start_date, end_date) def timelogs_for(start_time, end_time)
Timelog.between_dates(start_date, end_date).for_issues_in_group(self) Timelog.between_times(start_time, end_time).for_issues_in_group(self)
end end
end end
---
title: Change GraphQL arguments in group.timelogs query to use startTime and endTime
merge_request: 28560
author:
type: changed
...@@ -16,8 +16,8 @@ describe GitlabSchema.types['Group'] do ...@@ -16,8 +16,8 @@ describe GitlabSchema.types['Group'] do
describe 'timelogs field' do describe 'timelogs field' do
subject { described_class.fields['timelogs'] } subject { described_class.fields['timelogs'] }
it 'finds timelogs between start date and end date' do it 'finds timelogs between start time and end time' do
is_expected.to have_graphql_arguments(:start_date, :end_date, :after, :before, :first, :last) is_expected.to have_graphql_arguments(:start_time, :end_time, :start_date, :end_date, :after, :before, :first, :last)
is_expected.to have_graphql_resolver(Resolvers::TimelogResolver) is_expected.to have_graphql_resolver(Resolvers::TimelogResolver)
is_expected.to have_non_null_graphql_type(Types::TimelogType.connection_type) is_expected.to have_non_null_graphql_type(Types::TimelogType.connection_type)
end end
......
...@@ -6,93 +6,153 @@ describe Resolvers::TimelogResolver do ...@@ -6,93 +6,153 @@ describe Resolvers::TimelogResolver do
include GraphqlHelpers include GraphqlHelpers
context "within a group" do context "within a group" do
let(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let(:user) { create(:user) } let(:group) { create(:group) }
let(:group) { create(:group) } let(:project) { create(:project, :public, group: group) }
let(:project) { create(:project, :public, group: group) }
before do before do
group.add_users([current_user, user], :developer) group.add_developer(current_user)
project.add_developer(user) project.add_developer(current_user)
stub_licensed_features(group_timelogs: true) stub_licensed_features(group_timelogs: true)
end end
describe '#resolve' do describe '#resolve' do
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project) } let(:issue2) { create(:issue, project: project) }
let!(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: 5.days.ago) } let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
let!(:timelog2) { create(:timelog, issue: issue2, user: user, spent_at: 10.days.ago) } let!(:timelog1) { create(:timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
let(:start_date) { 6.days.ago } let!(:timelog2) { create(:timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
let(:end_date) { 2.days.ago } let!(:timelog3) { create(:timelog, issue: issue2, spent_at: 10.days.ago) }
shared_examples 'validation fails with error' do
it 'raises error with correct message' do
expect { resolve_timelogs(start_date: start_date, end_date: end_date) }
.to raise_error(
error_type,
message
)
end
end
it 'finds all timelogs within given dates' do it 'finds all timelogs within given dates' do
timelogs = resolve_timelogs(start_date: start_date, end_date: end_date) timelogs = resolve_timelogs(args)
expect(timelogs).to contain_exactly(timelog1) expect(timelogs).to contain_exactly(timelog1)
end end
it 'return nothing when user has insufficient permissions' do
group.add_guest(current_user)
expect(resolve_timelogs(args)).to be_empty
end
it 'returns nothing when feature is disabled' do
stub_licensed_features(group_timelogs: false)
expect(resolve_timelogs(args)).to be_empty
end
context 'when start_time and end_date are present' do
let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
it 'finds timelogs until the end of day of end_date' do
timelogs = resolve_timelogs(args)
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
end
context 'finds timelogs until the time specified on end_time' do
let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
it 'finds all timelogs within start_date and end_time' do
timelogs = resolve_timelogs(args)
expect(timelogs).to contain_exactly(timelog1)
end
end
context 'when arguments are invalid' do context 'when arguments are invalid' do
let(:error_type) { Gitlab::Graphql::Errors::ArgumentError } let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError }
context 'when no time or date arguments are present' do
let(:args) { {} }
it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Start and End arguments must be present/)
end
end
context 'when only start_time is present' do
let(:args) { { start_time: 6.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
context 'when only end_time is present' do
let(:args) { { end_time: 2.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end
context 'when only start_date is present' do context 'when only start_date is present' do
let(:end_date) { nil } let(:args) { { start_date: 6.days.ago } }
let(:message) { 'Both start_date and end_date must be present.' }
it_behaves_like 'validation fails with error' it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end end
context 'when only end_date is present' do context 'when only end_date is present' do
let(:start_date) { nil } let(:args) { { end_date: 2.days.ago } }
let(:message) { 'Both start_date and end_date must be present.' }
it_behaves_like 'validation fails with error' it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end end
context 'when start_date is later than end_date' do context 'when start_time and start_date are present' do
let(:start_date) { 3.days.ago } let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
let(:end_date) { 5.days.ago }
let(:message) { 'start_date must be earlier than end_date.' }
it_behaves_like 'validation fails with error' it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end end
context 'when time range is more than 60 days' do context 'when end_time and end_date are present' do
let(:start_date) { 3.months.ago } let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
let(:end_date) { 1.day.ago }
let(:message) { 'The date range period cannot contain more than 60 days' }
it_behaves_like 'validation fails with error' it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Both Start and End arguments must be present/)
end
end end
end
context 'when resource is not available' do context 'when three arguments are present' do
let(:error_type) { Gitlab::Graphql::Errors::ResourceNotAvailable } let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
let(:message) { "The resource is not available or you don't have permission to perform this action" }
context 'when feature is disabled' do it 'returns correct error' do
before do expect {resolve_timelogs(args)}
stub_licensed_features(group_timelogs: false) .to raise_error(error_class, /Only Time or Date arguments must be present/)
end end
it_behaves_like 'validation fails with error'
end end
context "when user has insufficient permissions" do context 'when start argument is after end argument' do
before do let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
group.add_guest(current_user)
it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /Start argument must be before End argument/)
end end
end
it_behaves_like 'validation fails with error' context 'when time range is more than 60 days' do
let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
it 'returns correct error' do
expect {resolve_timelogs(args)}
.to raise_error(error_class, /The time range period cannot contain more than 60 days/)
end
end end
end end
end end
......
...@@ -11,23 +11,23 @@ describe HasTimelogsReport do ...@@ -11,23 +11,23 @@ describe HasTimelogsReport do
let!(:timelog1) { create_timelog(15.days.ago) } let!(:timelog1) { create_timelog(15.days.ago) }
let!(:timelog2) { create_timelog(10.days.ago) } let!(:timelog2) { create_timelog(10.days.ago) }
let!(:timelog3) { create_timelog(5.days.ago) } let!(:timelog3) { create_timelog(5.days.ago) }
let(:start_date) { 20.days.ago } let(:start_time) { 20.days.ago }
let(:end_date) { 8.days.ago } let(:end_time) { 8.days.ago }
before do before do
group.add_developer(user) group.add_developer(user)
end end
it 'returns collection of timelogs between given dates' do it 'returns collection of timelogs between given times' do
expect(group.timelogs(start_date, end_date).to_a).to match_array([timelog1, timelog2]) expect(group.timelogs(start_time, end_time).to_a).to match_array([timelog1, timelog2])
end end
it 'returns empty collection if dates are not present' do it 'returns empty collection if times are not present' do
expect(group.timelogs(nil, nil)).to be_empty expect(group.timelogs(nil, nil)).to be_empty
end end
it 'returns empty collection if date range is invalid' do it 'returns empty collection if time range is invalid' do
expect(group.timelogs(end_date, start_date)).to be_empty expect(group.timelogs(end_time, start_time)).to be_empty
end end
end end
...@@ -54,7 +54,7 @@ describe HasTimelogsReport do ...@@ -54,7 +54,7 @@ describe HasTimelogsReport do
end end
end end
def create_timelog(date) def create_timelog(time)
create(:timelog, issue: issue, user: user, spent_at: date) create(:timelog, issue: issue, user: user, spent_at: time)
end end
end end
...@@ -6,44 +6,16 @@ describe 'Timelogs through GroupQuery' do ...@@ -6,44 +6,16 @@ describe 'Timelogs through GroupQuery' do
include GraphqlHelpers include GraphqlHelpers
describe 'Get list of timelogs from a group issues' do describe 'Get list of timelogs from a group issues' do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:group) { create(:group) } let_it_be(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) } let_it_be(:project) { create(:project, :public, group: group) }
let(:milestone) { create(:milestone, group: group) } let_it_be(:milestone) { create(:milestone, group: group) }
let(:epic) { create(:epic, group: group) } let_it_be(:epic) { create(:epic, group: group) }
let(:issue) { create(:issue, project: project, milestone: milestone, epic: epic) } let_it_be(:issue) { create(:issue, project: project, milestone: milestone, epic: epic) }
let!(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: 10.days.ago) } let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') }
let!(:timelog2) { create(:timelog, spent_at: 15.days.ago) } let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') }
let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] } let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } }
let(:query) do let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
timelog_nodes = <<~NODE
nodes {
date
spentAt
timeSpent
user {
username
}
issue {
title
milestone {
title
}
epic {
title
}
}
}
NODE
graphql_query_for("group", { "fullPath" => group.full_path },
['groupTimelogsEnabled', query_graphql_field(
"timelogs",
{ startDate: "#{13.days.ago.to_date}", endDate: "#{2.days.ago.to_date}" },
timelog_nodes
)]
)
end
before do before do
group.add_developer(user) group.add_developer(user)
...@@ -81,13 +53,20 @@ describe 'Timelogs through GroupQuery' do ...@@ -81,13 +53,20 @@ describe 'Timelogs through GroupQuery' do
expect(milestone_title).to eq([milestone.title]) expect(milestone_title).to eq([milestone.title])
expect(epic_title).to eq([epic.title]) expect(epic_title).to eq([epic.title])
end end
end
context 'when requests has errors' do context 'when arguments with no time are present' do
let(:error_message) do let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') }
"The resource is not available or you don't have permission to perform this action" let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') }
let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' }}
it 'sets times as start of day and end of day' do
expect(response).to have_gitlab_http_status(:ok)
expect(timelog_array.size).to eq 2
end
end end
end
context 'when requests has errors' do
context 'when group_timelogs feature is disabled' do context 'when group_timelogs feature is disabled' do
before do before do
stub_licensed_features(group_timelogs: false) stub_licensed_features(group_timelogs: false)
...@@ -97,8 +76,9 @@ describe 'Timelogs through GroupQuery' do ...@@ -97,8 +76,9 @@ describe 'Timelogs through GroupQuery' do
post_graphql(query, current_user: user) post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to include(a_hash_including('message' => error_message)) expect(graphql_errors).to be_nil
expect(graphql_data['group']).to be_nil expect(timelogs_data).to be_empty
expect(graphql_data['group']['groupTimelogsEnabled']).to be_falsey
end end
end end
...@@ -124,8 +104,9 @@ describe 'Timelogs through GroupQuery' do ...@@ -124,8 +104,9 @@ describe 'Timelogs through GroupQuery' do
post_graphql(query, current_user: guest) post_graphql(query, current_user: guest)
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to include(a_hash_including('message' => error_message)) expect(graphql_errors).to be_nil
expect(graphql_data['group']).to be_nil expect(timelogs_data).to be_empty
expect(graphql_data['group']['groupTimelogsEnabled']).to be_truthy
end end
end end
end end
...@@ -136,4 +117,34 @@ describe 'Timelogs through GroupQuery' do ...@@ -136,4 +117,34 @@ describe 'Timelogs through GroupQuery' do
extract_attribute ? item[extract_attribute] : item extract_attribute ? item[extract_attribute] : item
end end
end end
def query(timelog_params = params)
timelog_nodes = <<~NODE
nodes {
date
spentAt
timeSpent
user {
username
}
issue {
title
milestone {
title
}
epic {
title
}
}
}
NODE
graphql_query_for("group", { "fullPath" => group.full_path },
['groupTimelogsEnabled', query_graphql_field(
"timelogs",
timelog_params,
timelog_nodes
)]
)
end
end end
...@@ -56,12 +56,12 @@ RSpec.describe Timelog do ...@@ -56,12 +56,12 @@ RSpec.describe Timelog do
end end
end end
describe 'between_dates' do describe 'between_times' do
it 'returns collection of timelogs within given dates' do it 'returns collection of timelogs within given times' do
create(:timelog, spent_at: 65.days.ago) create(:timelog, spent_at: 65.days.ago)
timelog1 = create(:timelog, spent_at: 15.days.ago) timelog1 = create(:timelog, spent_at: 15.days.ago)
timelog2 = create(:timelog, spent_at: 5.days.ago) timelog2 = create(:timelog, spent_at: 5.days.ago)
timelogs = described_class.between_dates(20.days.ago, 1.day.ago) timelogs = described_class.between_times(20.days.ago, 1.day.ago)
expect(timelogs).to contain_exactly(timelog1, timelog2) expect(timelogs).to contain_exactly(timelog1, timelog2)
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