Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Léo-Paul Géneau
gitlab-ce
Commits
cc1e43da
Commit
cc1e43da
authored
Jan 18, 2017
by
Sean McGivern
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'time-tracking-api' into 'master'
Time tracking API Closes #25861 See merge request !8483
parents
c7390058
0f3c9355
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
608 additions
and
45 deletions
+608
-45
app/models/concerns/time_trackable.rb
app/models/concerns/time_trackable.rb
+23
-9
app/services/issuable_base_service.rb
app/services/issuable_base_service.rb
+1
-9
app/services/slash_commands/interpret_service.rb
app/services/slash_commands/interpret_service.rb
+2
-5
changelogs/unreleased/time-tracking-api.yml
changelogs/unreleased/time-tracking-api.yml
+4
-0
doc/api/issues.md
doc/api/issues.md
+140
-0
doc/api/merge_requests.md
doc/api/merge_requests.md
+139
-0
lib/api/entities.rb
lib/api/entities.rb
+7
-0
lib/api/helpers.rb
lib/api/helpers.rb
+4
-0
lib/api/issues.rb
lib/api/issues.rb
+2
-0
lib/api/merge_requests.rb
lib/api/merge_requests.rb
+12
-10
lib/api/time_tracking_endpoints.rb
lib/api/time_tracking_endpoints.rb
+114
-0
lib/gitlab/time_tracking_formatter.rb
lib/gitlab/time_tracking_formatter.rb
+5
-1
spec/models/concerns/issuable_spec.rb
spec/models/concerns/issuable_spec.rb
+5
-5
spec/requests/api/issues_spec.rb
spec/requests/api/issues_spec.rb
+6
-0
spec/requests/api/merge_requests_spec.rb
spec/requests/api/merge_requests_spec.rb
+7
-1
spec/services/slash_commands/interpret_service_spec.rb
spec/services/slash_commands/interpret_service_spec.rb
+3
-3
spec/services/system_note_service_spec.rb
spec/services/system_note_service_spec.rb
+2
-2
spec/support/api/time_tracking_shared_examples.rb
spec/support/api/time_tracking_shared_examples.rb
+132
-0
No files found.
app/models/concerns/time_trackable.rb
View file @
cc1e43da
...
...
@@ -9,27 +9,32 @@ module TimeTrackable
extend
ActiveSupport
::
Concern
included
do
attr_reader
:time_spent
attr_reader
:time_spent
,
:time_spent_user
alias_method
:time_spent?
,
:time_spent
default_value_for
:time_estimate
,
value:
0
,
allows_nil:
false
validates
:time_estimate
,
numericality:
{
message:
'has an invalid format'
},
allow_nil:
false
validate
:check_negative_time_spent
has_many
:timelogs
,
as: :trackable
,
dependent: :destroy
end
def
spend_time
(
seconds
,
user
)
return
if
seconds
==
0
def
spend_time
(
options
)
@time_spent
=
options
[
:duration
]
@time_spent_user
=
options
[
:user
]
@original_total_time_spent
=
nil
@time_spent
=
seconds
@time_spent_user
=
user
return
if
@time_spent
==
0
if
seconds
==
:reset
if
@time_spent
==
:reset
reset_spent_time
else
add_or_subtract_spent_time
end
end
alias_method
:spend_time
=
,
:spend_time
def
total_time_spent
timelogs
.
sum
(
:time_spent
)
...
...
@@ -50,9 +55,18 @@ module TimeTrackable
end
def
add_or_subtract_spent_time
# Exit if time to subtract exceeds the total time spent.
return
if
time_spent
<
0
&&
(
time_spent
.
abs
>
total_time_spent
)
timelogs
.
new
(
time_spent:
time_spent
,
user:
@time_spent_user
)
end
def
check_negative_time_spent
return
if
time_spent
.
nil?
||
time_spent
==
:reset
# we need to cache the total time spent so multiple calls to #valid?
# doesn't give a false error
@original_total_time_spent
||=
total_time_spent
if
time_spent
<
0
&&
(
time_spent
.
abs
>
@original_total_time_spent
)
errors
.
add
(
:time_spent
,
'Time to subtract exceeds the total time spent'
)
end
end
end
app/services/issuable_base_service.rb
View file @
cc1e43da
...
...
@@ -164,7 +164,6 @@ class IssuableBaseService < BaseService
def
create
(
issuable
)
merge_slash_commands_into_params!
(
issuable
)
filter_params
(
issuable
)
change_time_spent
(
issuable
)
params
.
delete
(
:state_event
)
params
[
:author
]
||=
current_user
...
...
@@ -207,14 +206,13 @@ class IssuableBaseService < BaseService
change_subscription
(
issuable
)
change_todo
(
issuable
)
filter_params
(
issuable
)
time_spent
=
change_time_spent
(
issuable
)
old_labels
=
issuable
.
labels
.
to_a
old_mentioned_users
=
issuable
.
mentioned_users
.
to_a
label_ids
=
process_label_ids
(
params
,
existing_label_ids:
issuable
.
label_ids
)
params
[
:label_ids
]
=
label_ids
if
labels_changing?
(
issuable
.
label_ids
,
label_ids
)
if
(
params
.
present?
||
time_spent
)
&&
update_issuable
(
issuable
,
params
)
if
params
.
present?
&&
update_issuable
(
issuable
,
params
)
# We do not touch as it will affect a update on updated_at field
ActiveRecord
::
Base
.
no_touching
do
handle_common_system_notes
(
issuable
,
old_labels:
old_labels
)
...
...
@@ -261,12 +259,6 @@ class IssuableBaseService < BaseService
end
end
def
change_time_spent
(
issuable
)
time_spent
=
params
.
delete
(
:spend_time
)
issuable
.
spend_time
(
time_spent
,
current_user
)
if
time_spent
end
def
has_changes?
(
issuable
,
old_labels:
[])
valid_attrs
=
[
:title
,
:description
,
:assignee_id
,
:milestone_id
,
:target_branch
]
...
...
app/services/slash_commands/interpret_service.rb
View file @
cc1e43da
...
...
@@ -274,13 +274,10 @@ module SlashCommands
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
issuable
)
end
command
:spend
do
|
raw_duration
|
reduce_time
=
raw_duration
.
sub!
(
/\A-/
,
''
)
time_spent
=
Gitlab
::
TimeTrackingFormatter
.
parse
(
raw_duration
)
if
time_spent
time_spent
*=
-
1
if
reduce_time
@updates
[
:spend_time
]
=
time_spent
@updates
[
:spend_time
]
=
{
duration:
time_spent
,
user:
current_user
}
end
end
...
...
@@ -299,7 +296,7 @@ module SlashCommands
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
project
)
end
command
:remove_time_spent
do
@updates
[
:spend_time
]
=
:reset
@updates
[
:spend_time
]
=
{
duration: :reset
,
user:
current_user
}
end
# This is a dummy command, so that it appears in the autocomplete commands
...
...
changelogs/unreleased/time-tracking-api.yml
0 → 100644
View file @
cc1e43da
---
title
:
Add new endpoints for Time Tracking.
merge_request
:
8483
author
:
doc/api/issues.md
View file @
cc1e43da
...
...
@@ -712,6 +712,146 @@ Example response:
}
```
## Set a time estimate for an issue
Sets an estimated time of work for this issue.
```
POST /projects/:id/issues/:issue_id/time_estimate
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`issue_id`
| integer | yes | The ID of a project's issue |
|
`duration`
| string | yes | The duration in human format. e.g: 3h30m |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration
=
3h30m
```
Example response:
```
json
{
"human_time_estimate"
:
"3h 30m"
,
"human_total_time_spent"
:
null
,
"time_estimate"
:
12600
,
"total_time_spent"
:
0
}
```
## Reset the time estimate for an issue
Resets the estimated time for this issue to 0 seconds.
```
POST /projects/:id/issues/:issue_id/reset_time_estimate
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`issue_id`
| integer | yes | The ID of a project's issue |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate
```
Example response:
```
json
{
"human_time_estimate"
:
null
,
"human_total_time_spent"
:
null
,
"time_estimate"
:
0
,
"total_time_spent"
:
0
}
```
## Add spent time for an issue
Adds spent time for this issue
```
POST /projects/:id/issues/:issue_id/add_spent_time
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`issue_id`
| integer | yes | The ID of a project's issue |
|
`duration`
| string | yes | The duration in human format. e.g: 3h30m |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration
=
1h
```
Example response:
```
json
{
"human_time_estimate"
:
null
,
"human_total_time_spent"
:
"1h"
,
"time_estimate"
:
0
,
"total_time_spent"
:
3600
}
```
## Reset spent time for an issue
Resets the total spent time for this issue to 0 seconds.
```
POST /projects/:id/issues/:issue_id/reset_spent_time
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`issue_id`
| integer | yes | The ID of a project's issue |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time
```
Example response:
```
json
{
"human_time_estimate"
:
null
,
"human_total_time_spent"
:
null
,
"time_estimate"
:
0
,
"total_time_spent"
:
0
}
```
## Get time tracking stats
```
GET /projects/:id/issues/:issue_id/time_stats
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`issue_id`
| integer | yes | The ID of a project's issue |
```
bash
curl
--request
GET
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats
```
Example response:
```
json
{
"human_time_estimate"
:
"2h"
,
"human_total_time_spent"
:
"1h"
,
"time_estimate"
:
7200
,
"total_time_spent"
:
3600
}
```
## Comments on issues
Comments are done via the
[
notes
](
notes.md
)
resource.
doc/api/merge_requests.md
View file @
cc1e43da
...
...
@@ -1018,3 +1018,142 @@ Example response:
}]
}
```
## Set a time estimate for a merge request
Sets an estimated time of work for this merge request.
```
POST /projects/:id/merge_requests/:merge_request_id/time_estimate
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`merge_request_id`
| integer | yes | The ID of a project's merge request |
|
`duration`
| string | yes | The duration in human format. e.g: 3h30m |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration
=
3h30m
```
Example response:
```
json
{
"human_time_estimate"
:
"3h 30m"
,
"human_total_time_spent"
:
null
,
"time_estimate"
:
12600
,
"total_time_spent"
:
0
}
```
## Reset the time estimate for a merge request
Resets the estimated time for this merge request to 0 seconds.
```
POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`merge_request_id`
| integer | yes | The ID of a project's merge_request |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate
```
Example response:
```
json
{
"human_time_estimate"
:
null
,
"human_total_time_spent"
:
null
,
"time_estimate"
:
0
,
"total_time_spent"
:
0
}
```
## Add spent time for a merge request
Adds spent time for this merge request
```
POST /projects/:id/merge_requests/:merge_request_id/add_spent_time
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`merge_request_id`
| integer | yes | The ID of a project's merge request |
|
`duration`
| string | yes | The duration in human format. e.g: 3h30m |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration
=
1h
```
Example response:
```
json
{
"human_time_estimate"
:
null
,
"human_total_time_spent"
:
"1h"
,
"time_estimate"
:
0
,
"total_time_spent"
:
3600
}
```
## Reset spent time for a merge request
Resets the total spent time for this merge request to 0 seconds.
```
POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`merge_request_id`
| integer | yes | The ID of a project's merge_request |
```
bash
curl
--request
POST
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time
```
Example response:
```
json
{
"human_time_estimate"
:
null
,
"human_total_time_spent"
:
null
,
"time_estimate"
:
0
,
"total_time_spent"
:
0
}
```
## Get time tracking stats
```
GET /projects/:id/merge_requests/:merge_request_id/time_stats
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
|
`id`
| integer | yes | The ID of a project |
|
`merge_request_id`
| integer | yes | The ID of a project's merge request |
```
bash
curl
--request
GET
--header
"PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"
https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats
```
Example response:
```
json
{
"human_time_estimate"
:
"2h"
,
"human_total_time_spent"
:
"1h"
,
"time_estimate"
:
7200
,
"total_time_spent"
:
3600
}
```
lib/api/entities.rb
View file @
cc1e43da
...
...
@@ -268,6 +268,13 @@ module API
end
end
class
IssuableTimeStats
<
Grape
::
Entity
expose
:time_estimate
expose
:total_time_spent
expose
:human_time_estimate
expose
:human_total_time_spent
end
class
ExternalIssue
<
Grape
::
Entity
expose
:title
expose
:id
...
...
lib/api/helpers.rb
View file @
cc1e43da
...
...
@@ -86,6 +86,10 @@ module API
IssuesFinder
.
new
(
current_user
,
project_id:
user_project
.
id
).
find
(
id
)
end
def
find_project_merge_request
(
id
)
MergeRequestsFinder
.
new
(
current_user
,
project_id:
user_project
.
id
).
find
(
id
)
end
def
authenticate!
unauthorized!
unless
current_user
end
...
...
lib/api/issues.rb
View file @
cc1e43da
...
...
@@ -89,6 +89,8 @@ module API
requires
:id
,
type:
String
,
desc:
'The ID of a project'
end
resource
:projects
do
include
TimeTrackingEndpoints
desc
'Get a list of project issues'
do
success
Entities
::
Issue
end
...
...
lib/api/merge_requests.rb
View file @
cc1e43da
...
...
@@ -10,6 +10,8 @@ module API
requires
:id
,
type:
String
,
desc:
'The ID of a project'
end
resource
:projects
do
include
TimeTrackingEndpoints
helpers
do
def
handle_merge_request_errors!
(
errors
)
if
errors
[
:project_access
].
any?
...
...
@@ -96,7 +98,7 @@ module API
requires
:merge_request_id
,
type:
Integer
,
desc:
'The ID of a merge request'
end
delete
":id/merge_requests/:merge_request_id"
do
merge_request
=
user_project
.
merge_requests
.
find_by
(
id:
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
authorize!
(
:destroy_merge_request
,
merge_request
)
merge_request
.
destroy
...
...
@@ -116,7 +118,7 @@ module API
success
Entities
::
MergeRequest
end
get
path
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
authorize!
:read_merge_request
,
merge_request
present
merge_request
,
with:
Entities
::
MergeRequest
,
current_user:
current_user
,
project:
user_project
end
...
...
@@ -125,7 +127,7 @@ module API
success
Entities
::
RepoCommit
end
get
"
#{
path
}
/commits"
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
authorize!
:read_merge_request
,
merge_request
present
merge_request
.
commits
,
with:
Entities
::
RepoCommit
end
...
...
@@ -134,7 +136,7 @@ module API
success
Entities
::
MergeRequestChanges
end
get
"
#{
path
}
/changes"
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
authorize!
:read_merge_request
,
merge_request
present
merge_request
,
with:
Entities
::
MergeRequestChanges
,
current_user:
current_user
end
...
...
@@ -153,7 +155,7 @@ module API
:remove_source_branch
end
put
path
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
.
delete
(
:merge_request_id
))
merge_request
=
find_project_merge_request
(
params
.
delete
(
:merge_request_id
))
authorize!
:update_merge_request
,
merge_request
mr_params
=
declared_params
(
include_missing:
false
)
...
...
@@ -180,7 +182,7 @@ module API
optional
:sha
,
type:
String
,
desc:
'When present, must have the HEAD SHA of the source branch'
end
put
"
#{
path
}
/merge"
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
# Merge request can not be merged
# because user dont have permissions to push into target branch
...
...
@@ -216,7 +218,7 @@ module API
success
Entities
::
MergeRequest
end
post
"
#{
path
}
/cancel_merge_when_build_succeeds"
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
unauthorized!
unless
merge_request
.
can_cancel_merge_when_build_succeeds?
(
current_user
)
...
...
@@ -233,7 +235,7 @@ module API
use
:pagination
end
get
"
#{
path
}
/comments"
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
authorize!
:read_merge_request
,
merge_request
...
...
@@ -248,7 +250,7 @@ module API
requires
:note
,
type:
String
,
desc:
'The text of the comment'
end
post
"
#{
path
}
/comments"
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
authorize!
:create_note
,
merge_request
opts
=
{
...
...
@@ -273,7 +275,7 @@ module API
use
:pagination
end
get
"
#{
path
}
/closes_issues"
do
merge_request
=
user_project
.
merge_requests
.
find
(
params
[
:merge_request_id
])
merge_request
=
find_project_merge_request
(
params
[
:merge_request_id
])
issues
=
::
Kaminari
.
paginate_array
(
merge_request
.
closes_issues
(
current_user
))
present
paginate
(
issues
),
with:
issue_entity
(
user_project
),
current_user:
current_user
end
...
...
lib/api/time_tracking_endpoints.rb
0 → 100644
View file @
cc1e43da
module
API
module
TimeTrackingEndpoints
extend
ActiveSupport
::
Concern
included
do
helpers
do
def
issuable_name
declared_params
.
has_key?
(
:issue_id
)
?
'issue'
:
'merge_request'
end
def
issuable_key
"
#{
issuable_name
}
_id"
.
to_sym
end
def
update_issuable_key
"update_
#{
issuable_name
}
"
.
to_sym
end
def
read_issuable_key
"read_
#{
issuable_name
}
"
.
to_sym
end
def
load_issuable
@issuable
||=
begin
case
issuable_name
when
'issue'
find_project_issue
(
params
.
delete
(
issuable_key
))
when
'merge_request'
find_project_merge_request
(
params
.
delete
(
issuable_key
))
end
end
end
def
update_issuable
(
attrs
)
custom_params
=
declared_params
(
include_missing:
false
)
custom_params
.
merge!
(
attrs
)
issuable
=
update_service
.
new
(
user_project
,
current_user
,
custom_params
).
execute
(
load_issuable
)
if
issuable
.
valid?
present
issuable
,
with:
Entities
::
IssuableTimeStats
else
render_validation_error!
(
issuable
)
end
end
def
update_service
issuable_name
==
'issue'
?
::
Issues
::
UpdateService
:
::
MergeRequests
::
UpdateService
end
end
issuable_name
=
name
.
end_with?
(
'Issues'
)
?
'issue'
:
'merge_request'
issuable_collection_name
=
issuable_name
.
pluralize
issuable_key
=
"
#{
issuable_name
}
_id"
.
to_sym
desc
"Set a time estimate for a project
#{
issuable_name
}
"
params
do
requires
issuable_key
,
type:
Integer
,
desc:
"The ID of a project
#{
issuable_name
}
"
requires
:duration
,
type:
String
,
desc:
'The duration to be parsed'
end
post
":id/
#{
issuable_collection_name
}
/:
#{
issuable_key
}
/time_estimate"
do
authorize!
update_issuable_key
,
load_issuable
status
:ok
update_issuable
(
time_estimate:
Gitlab
::
TimeTrackingFormatter
.
parse
(
params
.
delete
(
:duration
)))
end
desc
"Reset the time estimate for a project
#{
issuable_name
}
"
params
do
requires
issuable_key
,
type:
Integer
,
desc:
"The ID of a project
#{
issuable_name
}
"
end
post
":id/
#{
issuable_collection_name
}
/:
#{
issuable_key
}
/reset_time_estimate"
do
authorize!
update_issuable_key
,
load_issuable
status
:ok
update_issuable
(
time_estimate:
0
)
end
desc
"Add spent time for a project
#{
issuable_name
}
"
params
do
requires
issuable_key
,
type:
Integer
,
desc:
"The ID of a project
#{
issuable_name
}
"
requires
:duration
,
type:
String
,
desc:
'The duration to be parsed'
end
post
":id/
#{
issuable_collection_name
}
/:
#{
issuable_key
}
/add_spent_time"
do
authorize!
update_issuable_key
,
load_issuable
update_issuable
(
spend_time:
{
duration:
Gitlab
::
TimeTrackingFormatter
.
parse
(
params
.
delete
(
:duration
)),
user:
current_user
})
end
desc
"Reset spent time for a project
#{
issuable_name
}
"
params
do
requires
issuable_key
,
type:
Integer
,
desc:
"The ID of a project
#{
issuable_name
}
"
end
post
":id/
#{
issuable_collection_name
}
/:
#{
issuable_key
}
/reset_spent_time"
do
authorize!
update_issuable_key
,
load_issuable
status
:ok
update_issuable
(
spend_time:
{
duration: :reset
,
user:
current_user
})
end
desc
"Show time stats for a project
#{
issuable_name
}
"
params
do
requires
issuable_key
,
type:
Integer
,
desc:
"The ID of a project
#{
issuable_name
}
"
end
get
":id/
#{
issuable_collection_name
}
/:
#{
issuable_key
}
/time_stats"
do
authorize!
read_issuable_key
,
load_issuable
present
load_issuable
,
with:
Entities
::
IssuableTimeStats
end
end
end
end
lib/gitlab/time_tracking_formatter.rb
View file @
cc1e43da
...
...
@@ -4,7 +4,11 @@ module Gitlab
def
parse
(
string
)
with_custom_config
do
ChronicDuration
.
parse
(
string
,
default_unit:
'hours'
)
rescue
nil
string
.
sub!
(
/\A-/
,
''
)
seconds
=
ChronicDuration
.
parse
(
string
,
default_unit:
'hours'
)
rescue
nil
seconds
*=
-
1
if
seconds
&&
Regexp
.
last_match
seconds
end
end
...
...
spec/models/concerns/issuable_spec.rb
View file @
cc1e43da
...
...
@@ -414,7 +414,7 @@ describe Issue, "Issuable" do
let
(
:issue
)
{
create
(
:issue
)
}
def
spend_time
(
seconds
)
issue
.
spend_time
(
seconds
,
user
)
issue
.
spend_time
(
duration:
seconds
,
user:
user
)
issue
.
save!
end
...
...
@@ -438,10 +438,10 @@ describe Issue, "Issuable" do
end
context
'when time to substract exceeds the total time spent'
do
it
'should not alter the total time spent'
do
it
'raise a validation error'
do
expect
do
spend_time
(
-
3600
)
expect
(
issue
.
total_time_spent
).
to
eq
(
1800
)
end
.
to
raise_error
(
ActiveRecord
::
RecordInvalid
)
end
end
end
...
...
spec/requests/api/issues_spec.rb
View file @
cc1e43da
...
...
@@ -1193,4 +1193,10 @@ describe API::Issues, api: true do
expect
(
response
).
to
have_http_status
(
404
)
end
end
describe
'time tracking endpoints'
do
let
(
:issuable
)
{
issue
}
include_examples
'time tracking endpoints'
,
'issue'
end
end
spec/requests/api/merge_requests_spec.rb
View file @
cc1e43da
...
...
@@ -6,7 +6,7 @@ describe API::MergeRequests, api: true do
let
(
:user
)
{
create
(
:user
)
}
let
(
:admin
)
{
create
(
:user
,
:admin
)
}
let
(
:non_member
)
{
create
(
:user
)
}
let!
(
:project
)
{
create
(
:project
,
creator_id:
user
.
id
,
namespace:
user
.
namespace
)
}
let!
(
:project
)
{
create
(
:project
,
:public
,
creator_id:
user
.
id
,
namespace:
user
.
namespace
)
}
let!
(
:merge_request
)
{
create
(
:merge_request
,
:simple
,
author:
user
,
assignee:
user
,
source_project:
project
,
target_project:
project
,
title:
"Test"
,
created_at:
base_time
)
}
let!
(
:merge_request_closed
)
{
create
(
:merge_request
,
state:
"closed"
,
author:
user
,
assignee:
user
,
source_project:
project
,
target_project:
project
,
title:
"Closed test"
,
created_at:
base_time
+
1
.
second
)
}
let!
(
:merge_request_merged
)
{
create
(
:merge_request
,
state:
"merged"
,
author:
user
,
assignee:
user
,
source_project:
project
,
target_project:
project
,
title:
"Merged test"
,
created_at:
base_time
+
2
.
seconds
,
merge_commit_sha:
'9999999999999999999999999999999999999999'
)
}
...
...
@@ -671,6 +671,12 @@ describe API::MergeRequests, api: true do
end
end
describe
'Time tracking'
do
let
(
:issuable
)
{
merge_request
}
include_examples
'time tracking endpoints'
,
'merge_request'
end
def
mr_with_later_created_and_updated_at_time
merge_request
merge_request
.
created_at
+=
1
.
hour
...
...
spec/services/slash_commands/interpret_service_spec.rb
View file @
cc1e43da
...
...
@@ -223,7 +223,7 @@ describe SlashCommands::InterpretService, services: true do
it
'populates spend_time: 3600 if content contains /spend 1h'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
3600
)
expect
(
updates
).
to
eq
(
spend_time:
{
duration:
3600
,
user:
developer
}
)
end
end
...
...
@@ -231,7 +231,7 @@ describe SlashCommands::InterpretService, services: true do
it
'populates spend_time: -1800 if content contains /spend -30m'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
-
1800
)
expect
(
updates
).
to
eq
(
spend_time:
{
duration:
-
1800
,
user:
developer
}
)
end
end
...
...
@@ -247,7 +247,7 @@ describe SlashCommands::InterpretService, services: true do
it
'populates spend_time: :reset if content contains /remove_time_spent'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
:reset
)
expect
(
updates
).
to
eq
(
spend_time:
{
duration: :reset
,
user:
developer
}
)
end
end
...
...
spec/services/system_note_service_spec.rb
View file @
cc1e43da
...
...
@@ -765,7 +765,7 @@ describe SystemNoteService, services: true do
# We need a custom noteable in order to the shared examples to be green.
let
(
:noteable
)
do
mr
=
create
(
:merge_request
,
source_project:
project
)
mr
.
spend_time
(
1
,
author
)
mr
.
spend_time
(
duration:
360000
,
user:
author
)
mr
.
save!
mr
end
...
...
@@ -801,7 +801,7 @@ describe SystemNoteService, services: true do
end
def
spend_time!
(
seconds
)
noteable
.
spend_time
(
seconds
,
author
)
noteable
.
spend_time
(
duration:
seconds
,
user:
author
)
noteable
.
save!
end
end
...
...
spec/support/api/time_tracking_shared_examples.rb
0 → 100644
View file @
cc1e43da
shared_examples
'an unauthorized API user'
do
it
{
is_expected
.
to
eq
(
403
)
}
end
shared_examples
'time tracking endpoints'
do
|
issuable_name
|
issuable_collection_name
=
issuable_name
.
pluralize
describe
"POST /projects/:id/
#{
issuable_collection_name
}
/:
#{
issuable_name
}
_id/time_estimate"
do
context
'with an unauthorized user'
do
subject
{
post
(
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/time_estimate"
,
non_member
),
duration:
'1w'
)
}
it_behaves_like
'an unauthorized API user'
end
it
"sets the time estimate for
#{
issuable_name
}
"
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/time_estimate"
,
user
),
duration:
'1w'
expect
(
response
).
to
have_http_status
(
200
)
expect
(
json_response
[
'human_time_estimate'
]).
to
eq
(
'1w'
)
end
describe
'updating the current estimate'
do
before
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/time_estimate"
,
user
),
duration:
'1w'
end
context
'when duration has a bad format'
do
it
'does not modify the original estimate'
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/time_estimate"
,
user
),
duration:
'foo'
expect
(
response
).
to
have_http_status
(
400
)
expect
(
issuable
.
reload
.
human_time_estimate
).
to
eq
(
'1w'
)
end
end
context
'with a valid duration'
do
it
'updates the estimate'
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/time_estimate"
,
user
),
duration:
'3w1h'
expect
(
response
).
to
have_http_status
(
200
)
expect
(
issuable
.
reload
.
human_time_estimate
).
to
eq
(
'3w 1h'
)
end
end
end
end
describe
"POST /projects/:id/
#{
issuable_collection_name
}
/:
#{
issuable_name
}
_id/reset_time_estimate"
do
context
'with an unauthorized user'
do
subject
{
post
(
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/reset_time_estimate"
,
non_member
))
}
it_behaves_like
'an unauthorized API user'
end
it
"resets the time estimate for
#{
issuable_name
}
"
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/reset_time_estimate"
,
user
)
expect
(
response
).
to
have_http_status
(
200
)
expect
(
json_response
[
'time_estimate'
]).
to
eq
(
0
)
end
end
describe
"POST /projects/:id/
#{
issuable_collection_name
}
/:
#{
issuable_name
}
_id/add_spent_time"
do
context
'with an unauthorized user'
do
subject
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/add_spent_time"
,
non_member
),
duration:
'2h'
end
it_behaves_like
'an unauthorized API user'
end
it
"add spent time for
#{
issuable_name
}
"
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/add_spent_time"
,
user
),
duration:
'2h'
expect
(
response
).
to
have_http_status
(
201
)
expect
(
json_response
[
'human_total_time_spent'
]).
to
eq
(
'2h'
)
end
context
'when subtracting time'
do
it
'subtracts time of the total spent time'
do
issuable
.
update_attributes!
(
spend_time:
{
duration:
7200
,
user:
user
})
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/add_spent_time"
,
user
),
duration:
'-1h'
expect
(
response
).
to
have_http_status
(
201
)
expect
(
json_response
[
'total_time_spent'
]).
to
eq
(
3600
)
end
end
context
'when time to subtract is greater than the total spent time'
do
it
'does not modify the total time spent'
do
issuable
.
update_attributes!
(
spend_time:
{
duration:
7200
,
user:
user
})
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/add_spent_time"
,
user
),
duration:
'-1w'
expect
(
response
).
to
have_http_status
(
400
)
expect
(
json_response
[
'message'
][
'time_spent'
].
first
).
to
match
(
/exceeds the total time spent/
)
end
end
end
describe
"POST /projects/:id/
#{
issuable_collection_name
}
/:
#{
issuable_name
}
_id/reset_spent_time"
do
context
'with an unauthorized user'
do
subject
{
post
(
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/reset_spent_time"
,
non_member
))
}
it_behaves_like
'an unauthorized API user'
end
it
"resets spent time for
#{
issuable_name
}
"
do
post
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/reset_spent_time"
,
user
)
expect
(
response
).
to
have_http_status
(
200
)
expect
(
json_response
[
'total_time_spent'
]).
to
eq
(
0
)
end
end
describe
"GET /projects/:id/
#{
issuable_collection_name
}
/:
#{
issuable_name
}
_id/time_stats"
do
it
"returns the time stats for
#{
issuable_name
}
"
do
issuable
.
update_attributes!
(
spend_time:
{
duration:
1800
,
user:
user
},
time_estimate:
3600
)
get
api
(
"/projects/
#{
project
.
id
}
/
#{
issuable_collection_name
}
/
#{
issuable
.
id
}
/time_stats"
,
user
)
expect
(
response
).
to
have_http_status
(
200
)
expect
(
json_response
[
'total_time_spent'
]).
to
eq
(
1800
)
expect
(
json_response
[
'time_estimate'
]).
to
eq
(
3600
)
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment