Skip to content

Commit 0f3c935

Browse files
committed
Add some API endpoints for time tracking.
New endpoints are: POST :project_id/(issues|merge_requests)/(:issue_id|:merge_request_id)/time_estimate" POST :project_id/(issues|merge_requests)/(:issue_id|:merge_request_id)/reset_time_estimate" POST :project_id/(issues|merge_requests)/(:issue_id|:merge_request_id)/add_spent_time" POST :project_id/(issues|merge_requests)/(:issue_id|:merge_request_id)/reset_spent_time" GET :project_id/(issues|merge_requests)/(:issue_id|:merge_request_id)/time_stats"
1 parent 63b3624 commit 0f3c935

18 files changed

+608
-45
lines changed

app/models/concerns/time_trackable.rb

+23-9
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,32 @@ module TimeTrackable
99
extend ActiveSupport::Concern
1010

1111
included do
12-
attr_reader :time_spent
12+
attr_reader :time_spent, :time_spent_user
1313

1414
alias_method :time_spent?, :time_spent
1515

1616
default_value_for :time_estimate, value: 0, allows_nil: false
1717

18+
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
19+
validate :check_negative_time_spent
20+
1821
has_many :timelogs, as: :trackable, dependent: :destroy
1922
end
2023

21-
def spend_time(seconds, user)
22-
return if seconds == 0
24+
def spend_time(options)
25+
@time_spent = options[:duration]
26+
@time_spent_user = options[:user]
27+
@original_total_time_spent = nil
2328

24-
@time_spent = seconds
25-
@time_spent_user = user
29+
return if @time_spent == 0
2630

27-
if seconds == :reset
31+
if @time_spent == :reset
2832
reset_spent_time
2933
else
3034
add_or_subtract_spent_time
3135
end
3236
end
37+
alias_method :spend_time=, :spend_time
3338

3439
def total_time_spent
3540
timelogs.sum(:time_spent)
@@ -50,9 +55,18 @@ def reset_spent_time
5055
end
5156

5257
def add_or_subtract_spent_time
53-
# Exit if time to subtract exceeds the total time spent.
54-
return if time_spent < 0 && (time_spent.abs > total_time_spent)
55-
5658
timelogs.new(time_spent: time_spent, user: @time_spent_user)
5759
end
60+
61+
def check_negative_time_spent
62+
return if time_spent.nil? || time_spent == :reset
63+
64+
# we need to cache the total time spent so multiple calls to #valid?
65+
# doesn't give a false error
66+
@original_total_time_spent ||= total_time_spent
67+
68+
if time_spent < 0 && (time_spent.abs > @original_total_time_spent)
69+
errors.add(:time_spent, 'Time to subtract exceeds the total time spent')
70+
end
71+
end
5872
end

app/services/issuable_base_service.rb

+1-9
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ def create_issuable(issuable, attributes, label_ids:)
164164
def create(issuable)
165165
merge_slash_commands_into_params!(issuable)
166166
filter_params(issuable)
167-
change_time_spent(issuable)
168167

169168
params.delete(:state_event)
170169
params[:author] ||= current_user
@@ -207,14 +206,13 @@ def update(issuable)
207206
change_subscription(issuable)
208207
change_todo(issuable)
209208
filter_params(issuable)
210-
time_spent = change_time_spent(issuable)
211209
old_labels = issuable.labels.to_a
212210
old_mentioned_users = issuable.mentioned_users.to_a
213211

214212
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
215213
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
216214

217-
if (params.present? || time_spent) && update_issuable(issuable, params)
215+
if params.present? && update_issuable(issuable, params)
218216
# We do not touch as it will affect a update on updated_at field
219217
ActiveRecord::Base.no_touching do
220218
handle_common_system_notes(issuable, old_labels: old_labels)
@@ -261,12 +259,6 @@ def change_todo(issuable)
261259
end
262260
end
263261

264-
def change_time_spent(issuable)
265-
time_spent = params.delete(:spend_time)
266-
267-
issuable.spend_time(time_spent, current_user) if time_spent
268-
end
269-
270262
def has_changes?(issuable, old_labels: [])
271263
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
272264

app/services/slash_commands/interpret_service.rb

+2-5
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,10 @@ def extractor
262262
current_user.can?(:"admin_#{issuable.to_ability_name}", issuable)
263263
end
264264
command :spend do |raw_duration|
265-
reduce_time = raw_duration.sub!(/\A-/, '')
266265
time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration)
267266

268267
if time_spent
269-
time_spent *= -1 if reduce_time
270-
271-
@updates[:spend_time] = time_spent
268+
@updates[:spend_time] = { duration: time_spent, user: current_user }
272269
end
273270
end
274271

@@ -287,7 +284,7 @@ def extractor
287284
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
288285
end
289286
command :remove_time_spent do
290-
@updates[:spend_time] = :reset
287+
@updates[:spend_time] = { duration: :reset, user: current_user }
291288
end
292289

293290
# This is a dummy command, so that it appears in the autocomplete commands
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: Add new endpoints for Time Tracking.
3+
merge_request: 8483
4+
author:

doc/api/issues.md

+140
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,146 @@ Example response:
712712
}
713713
```
714714

715+
## Set a time estimate for an issue
716+
717+
Sets an estimated time of work for this issue.
718+
719+
```
720+
POST /projects/:id/issues/:issue_id/time_estimate
721+
```
722+
723+
| Attribute | Type | Required | Description |
724+
| --------- | ---- | -------- | ----------- |
725+
| `id` | integer | yes | The ID of a project |
726+
| `issue_id` | integer | yes | The ID of a project's issue |
727+
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
728+
729+
```bash
730+
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m
731+
```
732+
733+
Example response:
734+
735+
```json
736+
{
737+
"human_time_estimate": "3h 30m",
738+
"human_total_time_spent": null,
739+
"time_estimate": 12600,
740+
"total_time_spent": 0
741+
}
742+
```
743+
744+
## Reset the time estimate for an issue
745+
746+
Resets the estimated time for this issue to 0 seconds.
747+
748+
```
749+
POST /projects/:id/issues/:issue_id/reset_time_estimate
750+
```
751+
752+
| Attribute | Type | Required | Description |
753+
| --------- | ---- | -------- | ----------- |
754+
| `id` | integer | yes | The ID of a project |
755+
| `issue_id` | integer | yes | The ID of a project's issue |
756+
757+
```bash
758+
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate
759+
```
760+
761+
Example response:
762+
763+
```json
764+
{
765+
"human_time_estimate": null,
766+
"human_total_time_spent": null,
767+
"time_estimate": 0,
768+
"total_time_spent": 0
769+
}
770+
```
771+
772+
## Add spent time for an issue
773+
774+
Adds spent time for this issue
775+
776+
```
777+
POST /projects/:id/issues/:issue_id/add_spent_time
778+
```
779+
780+
| Attribute | Type | Required | Description |
781+
| --------- | ---- | -------- | ----------- |
782+
| `id` | integer | yes | The ID of a project |
783+
| `issue_id` | integer | yes | The ID of a project's issue |
784+
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
785+
786+
```bash
787+
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h
788+
```
789+
790+
Example response:
791+
792+
```json
793+
{
794+
"human_time_estimate": null,
795+
"human_total_time_spent": "1h",
796+
"time_estimate": 0,
797+
"total_time_spent": 3600
798+
}
799+
```
800+
801+
## Reset spent time for an issue
802+
803+
Resets the total spent time for this issue to 0 seconds.
804+
805+
```
806+
POST /projects/:id/issues/:issue_id/reset_spent_time
807+
```
808+
809+
| Attribute | Type | Required | Description |
810+
| --------- | ---- | -------- | ----------- |
811+
| `id` | integer | yes | The ID of a project |
812+
| `issue_id` | integer | yes | The ID of a project's issue |
813+
814+
```bash
815+
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time
816+
```
817+
818+
Example response:
819+
820+
```json
821+
{
822+
"human_time_estimate": null,
823+
"human_total_time_spent": null,
824+
"time_estimate": 0,
825+
"total_time_spent": 0
826+
}
827+
```
828+
829+
## Get time tracking stats
830+
831+
```
832+
GET /projects/:id/issues/:issue_id/time_stats
833+
```
834+
835+
| Attribute | Type | Required | Description |
836+
| --------- | ---- | -------- | ----------- |
837+
| `id` | integer | yes | The ID of a project |
838+
| `issue_id` | integer | yes | The ID of a project's issue |
839+
840+
```bash
841+
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats
842+
```
843+
844+
Example response:
845+
846+
```json
847+
{
848+
"human_time_estimate": "2h",
849+
"human_total_time_spent": "1h",
850+
"time_estimate": 7200,
851+
"total_time_spent": 3600
852+
}
853+
```
854+
715855
## Comments on issues
716856

717857
Comments are done via the [notes](notes.md) resource.

0 commit comments

Comments
 (0)