Skip to content

Commit adbc995

Browse files
authored
Show total TrackedTime on issue/pull/milestone lists (#26672)
TODOs: - [x] write test for `GetIssueTotalTrackedTime` - [x] frontport kitharas template changes and make them mobile-friendly --- ![image](https://github.com/go-gitea/gitea/assets/24977596/6713da97-201f-4217-8588-4c4cec157171) ![image](https://github.com/go-gitea/gitea/assets/24977596/3a45aba8-26b5-4e6a-b97d-68bfc2bf9024) --- *Sponsored by Kithara Software GmbH*
1 parent e83f2cb commit adbc995

File tree

8 files changed

+129
-36
lines changed

8 files changed

+129
-36
lines changed

models/issues/issue_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ func TestIssues(t *testing.T) {
191191
},
192192
[]int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
193193
},
194+
{
195+
issues_model.IssuesOptions{
196+
MilestoneIDs: []int64{1},
197+
},
198+
[]int64{2},
199+
},
194200
} {
195201
issues, err := issues_model.Issues(db.DefaultContext, &test.Opts)
196202
assert.NoError(t, err)

models/issues/tracked_time.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"code.gitea.io/gitea/modules/util"
1616

1717
"xorm.io/builder"
18+
"xorm.io/xorm"
1819
)
1920

2021
// TrackedTime represents a time that was spent for a specific issue.
@@ -325,3 +326,46 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
325326
}
326327
return time, nil
327328
}
329+
330+
// GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
331+
func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed bool) (int64, error) {
332+
if len(opts.IssueIDs) <= MaxQueryParameters {
333+
return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
334+
}
335+
336+
// If too long a list of IDs is provided,
337+
// we get the statistics in smaller chunks and get accumulates
338+
var accum int64
339+
for i := 0; i < len(opts.IssueIDs); {
340+
chunk := i + MaxQueryParameters
341+
if chunk > len(opts.IssueIDs) {
342+
chunk = len(opts.IssueIDs)
343+
}
344+
time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk])
345+
if err != nil {
346+
return 0, err
347+
}
348+
accum += time
349+
i = chunk
350+
}
351+
return accum, nil
352+
}
353+
354+
func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed bool, issueIDs []int64) (int64, error) {
355+
sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
356+
sess := db.GetEngine(ctx).
357+
Table("tracked_time").
358+
Where("tracked_time.deleted = ?", false).
359+
Join("INNER", "issue", "tracked_time.issue_id = issue.id")
360+
361+
return applyIssuesOptions(sess, opts, issueIDs)
362+
}
363+
364+
type trackedTime struct {
365+
Time int64
366+
}
367+
368+
return sumSession(opts, issueIDs).
369+
And("issue.is_closed = ?", isClosed).
370+
SumInt(new(trackedTime), "tracked_time.time")
371+
}

models/issues/tracked_time_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
115115
assert.NoError(t, err)
116116
assert.Len(t, total, 2)
117117
}
118+
119+
func TestGetIssueTotalTrackedTime(t *testing.T) {
120+
assert.NoError(t, unittest.PrepareTestDatabase())
121+
122+
ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, false)
123+
assert.NoError(t, err)
124+
assert.EqualValues(t, 3682, ttt)
125+
126+
ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, true)
127+
assert.NoError(t, err)
128+
assert.EqualValues(t, 0, ttt)
129+
}

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ template = Template
1717
language = Language
1818
notifications = Notifications
1919
active_stopwatch = Active Time Tracker
20+
tracked_time_summary = Summary of tracked time based on filters of issue list
2021
create_new = Create…
2122
user_profile_and_more = Profile and Settings…
2223
signed_in_as = Signed in as

routers/web/repo/issue.go

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -198,46 +198,43 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
198198
}
199199

200200
var issueStats *issues_model.IssueStats
201-
{
202-
statsOpts := &issues_model.IssuesOptions{
203-
RepoIDs: []int64{repo.ID},
204-
LabelIDs: labelIDs,
205-
MilestoneIDs: mileIDs,
206-
ProjectID: projectID,
207-
AssigneeID: assigneeID,
208-
MentionedID: mentionedID,
209-
PosterID: posterID,
210-
ReviewRequestedID: reviewRequestedID,
211-
ReviewedID: reviewedID,
212-
IsPull: isPullOption,
213-
IssueIDs: nil,
214-
}
215-
if keyword != "" {
216-
allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts)
217-
if err != nil {
218-
if issue_indexer.IsAvailable(ctx) {
219-
ctx.ServerError("issueIDsFromSearch", err)
220-
return
221-
}
222-
ctx.Data["IssueIndexerUnavailable"] = true
201+
statsOpts := &issues_model.IssuesOptions{
202+
RepoIDs: []int64{repo.ID},
203+
LabelIDs: labelIDs,
204+
MilestoneIDs: mileIDs,
205+
ProjectID: projectID,
206+
AssigneeID: assigneeID,
207+
MentionedID: mentionedID,
208+
PosterID: posterID,
209+
ReviewRequestedID: reviewRequestedID,
210+
ReviewedID: reviewedID,
211+
IsPull: isPullOption,
212+
IssueIDs: nil,
213+
}
214+
if keyword != "" {
215+
allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts)
216+
if err != nil {
217+
if issue_indexer.IsAvailable(ctx) {
218+
ctx.ServerError("issueIDsFromSearch", err)
223219
return
224220
}
225-
statsOpts.IssueIDs = allIssueIDs
221+
ctx.Data["IssueIndexerUnavailable"] = true
222+
return
226223
}
227-
if keyword != "" && len(statsOpts.IssueIDs) == 0 {
228-
// So it did search with the keyword, but no issue found.
229-
// Just set issueStats to empty.
230-
issueStats = &issues_model.IssueStats{}
231-
} else {
232-
// So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
233-
// Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
234-
issueStats, err = issues_model.GetIssueStats(ctx, statsOpts)
235-
if err != nil {
236-
ctx.ServerError("GetIssueStats", err)
237-
return
238-
}
224+
statsOpts.IssueIDs = allIssueIDs
225+
}
226+
if keyword != "" && len(statsOpts.IssueIDs) == 0 {
227+
// So it did search with the keyword, but no issue found.
228+
// Just set issueStats to empty.
229+
issueStats = &issues_model.IssueStats{}
230+
} else {
231+
// So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
232+
// Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
233+
issueStats, err = issues_model.GetIssueStats(ctx, statsOpts)
234+
if err != nil {
235+
ctx.ServerError("GetIssueStats", err)
236+
return
239237
}
240-
241238
}
242239

243240
isShowClosed := ctx.FormString("state") == "closed"
@@ -246,6 +243,15 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
246243
isShowClosed = true
247244
}
248245

246+
if repo.IsTimetrackerEnabled(ctx) {
247+
totalTrackedTime, err := issues_model.GetIssueTotalTrackedTime(ctx, statsOpts, isShowClosed)
248+
if err != nil {
249+
ctx.ServerError("GetIssueTotalTrackedTime", err)
250+
return
251+
}
252+
ctx.Data["TotalTrackedTime"] = totalTrackedTime
253+
}
254+
249255
archived := ctx.FormBool("archived")
250256

251257
page := ctx.FormInt("page")

templates/repo/issue/filters.tmpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
<input type="checkbox" autocomplete="off" class="issue-checkbox-all gt-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
55
{{end}}
66
{{template "repo/issue/openclose" .}}
7+
<!-- Total Tracked Time -->
8+
{{if .TotalTrackedTime}}
9+
<div class="ui compact tiny secondary menu">
10+
<span class="item" data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
11+
{{svg "octicon-clock"}}
12+
{{.TotalTrackedTime | Sec2Time}}
13+
</span>
14+
</div>
15+
{{end}}
716
</div>
817
<div class="issue-list-toolbar-right">
918
<div class="ui secondary filter menu labels">

templates/repo/issue/list.tmpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@
3434
<div id="issue-actions" class="issue-list-toolbar gt-hidden">
3535
<div class="issue-list-toolbar-left">
3636
{{template "repo/issue/openclose" .}}
37+
<!-- Total Tracked Time -->
38+
{{if .TotalTrackedTime}}
39+
<div class="ui compact tiny secondary menu">
40+
<span class="item" data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
41+
{{svg "octicon-clock"}}
42+
{{.TotalTrackedTime | Sec2Time}}
43+
</span>
44+
</div>
45+
{{end}}
3746
</div>
3847
<div class="issue-list-toolbar-right">
3948
{{template "repo/issue/filter_actions" .}}

templates/repo/issue/milestone_issues.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
{{end}}
4747
</div>
4848
<div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}</div>
49+
{{if .TotalTrackedTime}}
50+
<div data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
51+
{{svg "octicon-clock"}}
52+
{{.TotalTrackedTime | Sec2Time}}
53+
</div>
54+
{{end}}
4955
</div>
5056
</div>
5157
<div class="divider"></div>

0 commit comments

Comments
 (0)