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
Boxiang Sun
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
...
@@ -9,27 +9,32 @@ module TimeTrackable
extend
ActiveSupport
::
Concern
extend
ActiveSupport
::
Concern
included
do
included
do
attr_reader
:time_spent
attr_reader
:time_spent
,
:time_spent_user
alias_method
:time_spent?
,
:time_spent
alias_method
:time_spent?
,
:time_spent
default_value_for
:time_estimate
,
value:
0
,
allows_nil:
false
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
has_many
:timelogs
,
as: :trackable
,
dependent: :destroy
end
end
def
spend_time
(
seconds
,
user
)
def
spend_time
(
options
)
return
if
seconds
==
0
@time_spent
=
options
[
:duration
]
@time_spent_user
=
options
[
:user
]
@original_total_time_spent
=
nil
@time_spent
=
seconds
return
if
@time_spent
==
0
@time_spent_user
=
user
if
seconds
==
:reset
if
@time_spent
==
:reset
reset_spent_time
reset_spent_time
else
else
add_or_subtract_spent_time
add_or_subtract_spent_time
end
end
end
end
alias_method
:spend_time
=
,
:spend_time
def
total_time_spent
def
total_time_spent
timelogs
.
sum
(
:time_spent
)
timelogs
.
sum
(
:time_spent
)
...
@@ -50,9 +55,18 @@ module TimeTrackable
...
@@ -50,9 +55,18 @@ module TimeTrackable
end
end
def
add_or_subtract_spent_time
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
)
timelogs
.
new
(
time_spent:
time_spent
,
user:
@time_spent_user
)
end
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
end
app/services/issuable_base_service.rb
View file @
cc1e43da
...
@@ -164,7 +164,6 @@ class IssuableBaseService < BaseService
...
@@ -164,7 +164,6 @@ class IssuableBaseService < BaseService
def
create
(
issuable
)
def
create
(
issuable
)
merge_slash_commands_into_params!
(
issuable
)
merge_slash_commands_into_params!
(
issuable
)
filter_params
(
issuable
)
filter_params
(
issuable
)
change_time_spent
(
issuable
)
params
.
delete
(
:state_event
)
params
.
delete
(
:state_event
)
params
[
:author
]
||=
current_user
params
[
:author
]
||=
current_user
...
@@ -207,14 +206,13 @@ class IssuableBaseService < BaseService
...
@@ -207,14 +206,13 @@ class IssuableBaseService < BaseService
change_subscription
(
issuable
)
change_subscription
(
issuable
)
change_todo
(
issuable
)
change_todo
(
issuable
)
filter_params
(
issuable
)
filter_params
(
issuable
)
time_spent
=
change_time_spent
(
issuable
)
old_labels
=
issuable
.
labels
.
to_a
old_labels
=
issuable
.
labels
.
to_a
old_mentioned_users
=
issuable
.
mentioned_users
.
to_a
old_mentioned_users
=
issuable
.
mentioned_users
.
to_a
label_ids
=
process_label_ids
(
params
,
existing_label_ids:
issuable
.
label_ids
)
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
)
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
# We do not touch as it will affect a update on updated_at field
ActiveRecord
::
Base
.
no_touching
do
ActiveRecord
::
Base
.
no_touching
do
handle_common_system_notes
(
issuable
,
old_labels:
old_labels
)
handle_common_system_notes
(
issuable
,
old_labels:
old_labels
)
...
@@ -261,12 +259,6 @@ class IssuableBaseService < BaseService
...
@@ -261,12 +259,6 @@ class IssuableBaseService < BaseService
end
end
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:
[])
def
has_changes?
(
issuable
,
old_labels:
[])
valid_attrs
=
[
:title
,
:description
,
:assignee_id
,
:milestone_id
,
:target_branch
]
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
...
@@ -274,13 +274,10 @@ module SlashCommands
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
issuable
)
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
issuable
)
end
end
command
:spend
do
|
raw_duration
|
command
:spend
do
|
raw_duration
|
reduce_time
=
raw_duration
.
sub!
(
/\A-/
,
''
)
time_spent
=
Gitlab
::
TimeTrackingFormatter
.
parse
(
raw_duration
)
time_spent
=
Gitlab
::
TimeTrackingFormatter
.
parse
(
raw_duration
)
if
time_spent
if
time_spent
time_spent
*=
-
1
if
reduce_time
@updates
[
:spend_time
]
=
{
duration:
time_spent
,
user:
current_user
}
@updates
[
:spend_time
]
=
time_spent
end
end
end
end
...
@@ -299,7 +296,7 @@ module SlashCommands
...
@@ -299,7 +296,7 @@ module SlashCommands
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
project
)
current_user
.
can?
(
:"admin_
#{
issuable
.
to_ability_name
}
"
,
project
)
end
end
command
:remove_time_spent
do
command
:remove_time_spent
do
@updates
[
:spend_time
]
=
:reset
@updates
[
:spend_time
]
=
{
duration: :reset
,
user:
current_user
}
end
end
# This is a dummy command, so that it appears in the autocomplete commands
# 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:
...
@@ -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 on issues
Comments are done via the
[
notes
](
notes.md
)
resource.
Comments are done via the
[
notes
](
notes.md
)
resource.
doc/api/merge_requests.md
View file @
cc1e43da
...
@@ -1018,3 +1018,142 @@ Example response:
...
@@ -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
...
@@ -268,6 +268,13 @@ module API
end
end
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
class
ExternalIssue
<
Grape
::
Entity
expose
:title
expose
:title
expose
:id
expose
:id
...
...
lib/api/helpers.rb
View file @
cc1e43da
...
@@ -86,6 +86,10 @@ module API
...
@@ -86,6 +86,10 @@ module API
IssuesFinder
.
new
(
current_user
,
project_id:
user_project
.
id
).
find
(
id
)
IssuesFinder
.
new
(
current_user
,
project_id:
user_project
.
id
).
find
(
id
)
end
end
def
find_project_merge_request
(
id
)
MergeRequestsFinder
.
new
(
current_user
,
project_id:
user_project
.
id
).
find
(
id
)
end
def
authenticate!
def
authenticate!
unauthorized!
unless
current_user
unauthorized!
unless
current_user
end
end
...
...
lib/api/issues.rb
View file @
cc1e43da
...
@@ -89,6 +89,8 @@ module API
...
@@ -89,6 +89,8 @@ module API
requires
:id
,
type:
String
,
desc:
'The ID of a project'
requires
:id
,
type:
String
,
desc:
'The ID of a project'
end
end
resource
:projects
do
resource
:projects
do
include
TimeTrackingEndpoints
desc
'Get a list of project issues'
do
desc
'Get a list of project issues'
do
success
Entities
::
Issue
success
Entities
::
Issue
end
end
...
...
lib/api/merge_requests.rb
View file @
cc1e43da
...
@@ -10,6 +10,8 @@ module API
...
@@ -10,6 +10,8 @@ module API
requires
:id
,
type:
String
,
desc:
'The ID of a project'
requires
:id
,
type:
String
,
desc:
'The ID of a project'
end
end
resource
:projects
do
resource
:projects
do
include
TimeTrackingEndpoints
helpers
do
helpers
do
def
handle_merge_request_errors!
(
errors
)
def
handle_merge_request_errors!
(
errors
)
if
errors
[
:project_access
].
any?
if
errors
[
:project_access
].
any?
...
@@ -96,7 +98,7 @@ module API
...
@@ -96,7 +98,7 @@ module API
requires
:merge_request_id
,
type:
Integer
,
desc:
'The ID of a merge request'
requires
:merge_request_id
,
type:
Integer
,
desc:
'The ID of a merge request'
end
end
delete
":id/merge_requests/:merge_request_id"
do
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
)
authorize!
(
:destroy_merge_request
,
merge_request
)
merge_request
.
destroy
merge_request
.
destroy
...
@@ -116,7 +118,7 @@ module API
...
@@ -116,7 +118,7 @@ module API
success
Entities
::
MergeRequest
success
Entities
::
MergeRequest
end
end
get
path
do
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
authorize!
:read_merge_request
,
merge_request
present
merge_request
,
with:
Entities
::
MergeRequest
,
current_user:
current_user
,
project:
user_project
present
merge_request
,
with:
Entities
::
MergeRequest
,
current_user:
current_user
,
project:
user_project
end
end
...
@@ -125,7 +127,7 @@ module API
...
@@ -125,7 +127,7 @@ module API
success
Entities
::
RepoCommit
success
Entities
::
RepoCommit
end
end
get
"
#{
path
}
/commits"
do
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
authorize!
:read_merge_request
,
merge_request
present
merge_request
.
commits
,
with:
Entities
::
RepoCommit
present
merge_request
.
commits
,
with:
Entities
::
RepoCommit
end
end
...
@@ -134,7 +136,7 @@ module API
...
@@ -134,7 +136,7 @@ module API
success
Entities
::
MergeRequestChanges
success
Entities
::
MergeRequestChanges
end
end
get
"
#{
path
}
/changes"
do
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
authorize!
:read_merge_request
,
merge_request
present
merge_request
,
with:
Entities
::
MergeRequestChanges
,
current_user:
current_user
present
merge_request
,
with:
Entities
::
MergeRequestChanges
,
current_user:
current_user
end
end
...
@@ -153,7 +155,7 @@ module API
...
@@ -153,7 +155,7 @@ module API
:remove_source_branch
:remove_source_branch
end
end
put
path
do
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
authorize!
:update_merge_request
,
merge_request
mr_params
=
declared_params
(
include_missing:
false
)
mr_params
=
declared_params
(
include_missing:
false
)
...
@@ -180,7 +182,7 @@ module API
...
@@ -180,7 +182,7 @@ module API
optional
:sha
,
type:
String
,
desc:
'When present, must have the HEAD SHA of the source branch'
optional
:sha
,
type:
String
,
desc:
'When present, must have the HEAD SHA of the source branch'
end
end
put
"
#{
path
}
/merge"
do
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
# Merge request can not be merged
# because user dont have permissions to push into target branch
# because user dont have permissions to push into target branch
...
@@ -216,7 +218,7 @@ module API
...
@@ -216,7 +218,7 @@ module API
success
Entities
::
MergeRequest
success
Entities
::
MergeRequest
end
end
post
"
#{
path
}
/cancel_merge_when_build_succeeds"
do
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
)
unauthorized!
unless
merge_request
.
can_cancel_merge_when_build_succeeds?
(
current_user
)
...
@@ -233,7 +235,7 @@ module API
...
@@ -233,7 +235,7 @@ module API
use
:pagination
use
:pagination
end
end
get
"
#{
path
}
/comments"
do
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
authorize!
:read_merge_request
,
merge_request
...
@@ -248,7 +250,7 @@ module API
...
@@ -248,7 +250,7 @@ module API
requires
:note
,
type:
String
,
desc:
'The text of the comment'
requires
:note
,
type:
String
,
desc:
'The text of the comment'
end
end
post
"
#{
path
}
/comments"
do
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
authorize!
:create_note
,
merge_request
opts
=
{
opts
=
{
...
@@ -273,7 +275,7 @@ module API
...
@@ -273,7 +275,7 @@ module API
use
:pagination
use
:pagination
end
end
get
"
#{
path
}
/closes_issues"
do
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
))
issues
=
::
Kaminari
.
paginate_array
(
merge_request
.
closes_issues
(
current_user
))
present
paginate
(
issues
),
with:
issue_entity
(
user_project
),
current_user:
current_user
present
paginate
(
issues
),
with:
issue_entity
(
user_project
),
current_user:
current_user
end
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
...
@@ -4,7 +4,11 @@ module Gitlab
def
parse
(
string
)
def
parse
(
string
)
with_custom_config
do
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
end
end
...
...
spec/models/concerns/issuable_spec.rb
View file @
cc1e43da
...
@@ -414,7 +414,7 @@ describe Issue, "Issuable" do
...
@@ -414,7 +414,7 @@ describe Issue, "Issuable" do
let
(
:issue
)
{
create
(
:issue
)
}
let
(
:issue
)
{
create
(
:issue
)
}
def
spend_time
(
seconds
)
def
spend_time
(
seconds
)
issue
.
spend_time
(
seconds
,
user
)
issue
.
spend_time
(
duration:
seconds
,
user:
user
)
issue
.
save!
issue
.
save!
end
end
...
@@ -438,10 +438,10 @@ describe Issue, "Issuable" do
...
@@ -438,10 +438,10 @@ describe Issue, "Issuable" do
end
end
context
'when time to substract exceeds the total time spent'
do
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
)
spend_time
(
-
3600
)
end
.
to
raise_error
(
ActiveRecord
::
RecordInvalid
)
expect
(
issue
.
total_time_spent
).
to
eq
(
1800
)
end
end
end
end
end
end
...
...
spec/requests/api/issues_spec.rb
View file @
cc1e43da
...
@@ -1193,4 +1193,10 @@ describe API::Issues, api: true do
...
@@ -1193,4 +1193,10 @@ describe API::Issues, api: true do
expect
(
response
).
to
have_http_status
(
404
)
expect
(
response
).
to
have_http_status
(
404
)
end
end
end
end
describe
'time tracking endpoints'
do
let
(
:issuable
)
{
issue
}
include_examples
'time tracking endpoints'
,
'issue'
end
end
end
spec/requests/api/merge_requests_spec.rb
View file @
cc1e43da
...
@@ -6,7 +6,7 @@ describe API::MergeRequests, api: true do
...
@@ -6,7 +6,7 @@ describe API::MergeRequests, api: true do
let
(
:user
)
{
create
(
:user
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:admin
)
{
create
(
:user
,
:admin
)
}
let
(
:admin
)
{
create
(
:user
,
:admin
)
}
let
(
:non_member
)
{
create
(
:user
)
}
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
)
{
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_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'
)
}
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
...
@@ -671,6 +671,12 @@ describe API::MergeRequests, api: true do
end
end
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
def
mr_with_later_created_and_updated_at_time
merge_request
merge_request
merge_request
.
created_at
+=
1
.
hour
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
...
@@ -223,7 +223,7 @@ describe SlashCommands::InterpretService, services: true do
it
'populates spend_time: 3600 if content contains /spend 1h'
do
it
'populates spend_time: 3600 if content contains /spend 1h'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
3600
)
expect
(
updates
).
to
eq
(
spend_time:
{
duration:
3600
,
user:
developer
}
)
end
end
end
end
...
@@ -231,7 +231,7 @@ describe SlashCommands::InterpretService, services: true do
...
@@ -231,7 +231,7 @@ describe SlashCommands::InterpretService, services: true do
it
'populates spend_time: -1800 if content contains /spend -30m'
do
it
'populates spend_time: -1800 if content contains /spend -30m'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
-
1800
)
expect
(
updates
).
to
eq
(
spend_time:
{
duration:
-
1800
,
user:
developer
}
)
end
end
end
end
...
@@ -247,7 +247,7 @@ describe SlashCommands::InterpretService, services: true do
...
@@ -247,7 +247,7 @@ describe SlashCommands::InterpretService, services: true do
it
'populates spend_time: :reset if content contains /remove_time_spent'
do
it
'populates spend_time: :reset if content contains /remove_time_spent'
do
_
,
updates
=
service
.
execute
(
content
,
issuable
)
_
,
updates
=
service
.
execute
(
content
,
issuable
)
expect
(
updates
).
to
eq
(
spend_time:
:reset
)
expect
(
updates
).
to
eq
(
spend_time:
{
duration: :reset
,
user:
developer
}
)
end
end
end
end
...
...
spec/services/system_note_service_spec.rb
View file @
cc1e43da
...
@@ -765,7 +765,7 @@ describe SystemNoteService, services: true do
...
@@ -765,7 +765,7 @@ describe SystemNoteService, services: true do
# We need a custom noteable in order to the shared examples to be green.
# We need a custom noteable in order to the shared examples to be green.
let
(
:noteable
)
do
let
(
:noteable
)
do
mr
=
create
(
:merge_request
,
source_project:
project
)
mr
=
create
(
:merge_request
,
source_project:
project
)
mr
.
spend_time
(
1
,
author
)
mr
.
spend_time
(
duration:
360000
,
user:
author
)
mr
.
save!
mr
.
save!
mr
mr
end
end
...
@@ -801,7 +801,7 @@ describe SystemNoteService, services: true do
...
@@ -801,7 +801,7 @@ describe SystemNoteService, services: true do
end
end
def
spend_time!
(
seconds
)
def
spend_time!
(
seconds
)
noteable
.
spend_time
(
seconds
,
author
)
noteable
.
spend_time
(
duration:
seconds
,
user:
author
)
noteable
.
save!
noteable
.
save!
end
end
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