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*
This commit is contained in:
parent
e83f2cbbac
commit
adbc995c34
8 changed files with 129 additions and 36 deletions
|
@ -191,6 +191,12 @@ func TestIssues(t *testing.T) {
|
||||||
},
|
},
|
||||||
[]int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
|
[]int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
issues_model.IssuesOptions{
|
||||||
|
MilestoneIDs: []int64{1},
|
||||||
|
},
|
||||||
|
[]int64{2},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
issues, err := issues_model.Issues(db.DefaultContext, &test.Opts)
|
issues, err := issues_model.Issues(db.DefaultContext, &test.Opts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TrackedTime represents a time that was spent for a specific issue.
|
// TrackedTime represents a time that was spent for a specific issue.
|
||||||
|
@ -325,3 +326,46 @@ func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
|
||||||
}
|
}
|
||||||
return time, nil
|
return time, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
|
||||||
|
func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed bool) (int64, error) {
|
||||||
|
if len(opts.IssueIDs) <= MaxQueryParameters {
|
||||||
|
return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If too long a list of IDs is provided,
|
||||||
|
// we get the statistics in smaller chunks and get accumulates
|
||||||
|
var accum int64
|
||||||
|
for i := 0; i < len(opts.IssueIDs); {
|
||||||
|
chunk := i + MaxQueryParameters
|
||||||
|
if chunk > len(opts.IssueIDs) {
|
||||||
|
chunk = len(opts.IssueIDs)
|
||||||
|
}
|
||||||
|
time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
accum += time
|
||||||
|
i = chunk
|
||||||
|
}
|
||||||
|
return accum, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed bool, issueIDs []int64) (int64, error) {
|
||||||
|
sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
|
||||||
|
sess := db.GetEngine(ctx).
|
||||||
|
Table("tracked_time").
|
||||||
|
Where("tracked_time.deleted = ?", false).
|
||||||
|
Join("INNER", "issue", "tracked_time.issue_id = issue.id")
|
||||||
|
|
||||||
|
return applyIssuesOptions(sess, opts, issueIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
type trackedTime struct {
|
||||||
|
Time int64
|
||||||
|
}
|
||||||
|
|
||||||
|
return sumSession(opts, issueIDs).
|
||||||
|
And("issue.is_closed = ?", isClosed).
|
||||||
|
SumInt(new(trackedTime), "tracked_time.time")
|
||||||
|
}
|
||||||
|
|
|
@ -115,3 +115,15 @@ func TestTotalTimesForEachUser(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, total, 2)
|
assert.Len(t, total, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetIssueTotalTrackedTime(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
ttt, err := issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 3682, ttt)
|
||||||
|
|
||||||
|
ttt, err = issues_model.GetIssueTotalTrackedTime(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 0, ttt)
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ template = Template
|
||||||
language = Language
|
language = Language
|
||||||
notifications = Notifications
|
notifications = Notifications
|
||||||
active_stopwatch = Active Time Tracker
|
active_stopwatch = Active Time Tracker
|
||||||
|
tracked_time_summary = Summary of tracked time based on filters of issue list
|
||||||
create_new = Create…
|
create_new = Create…
|
||||||
user_profile_and_more = Profile and Settings…
|
user_profile_and_more = Profile and Settings…
|
||||||
signed_in_as = Signed in as
|
signed_in_as = Signed in as
|
||||||
|
|
|
@ -198,7 +198,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
||||||
}
|
}
|
||||||
|
|
||||||
var issueStats *issues_model.IssueStats
|
var issueStats *issues_model.IssueStats
|
||||||
{
|
|
||||||
statsOpts := &issues_model.IssuesOptions{
|
statsOpts := &issues_model.IssuesOptions{
|
||||||
RepoIDs: []int64{repo.ID},
|
RepoIDs: []int64{repo.ID},
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
|
@ -238,14 +237,21 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
isShowClosed := ctx.FormString("state") == "closed"
|
isShowClosed := ctx.FormString("state") == "closed"
|
||||||
// if open issues are zero and close don't, use closed as default
|
// if open issues are zero and close don't, use closed as default
|
||||||
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
|
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
|
||||||
isShowClosed = true
|
isShowClosed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if repo.IsTimetrackerEnabled(ctx) {
|
||||||
|
totalTrackedTime, err := issues_model.GetIssueTotalTrackedTime(ctx, statsOpts, isShowClosed)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetIssueTotalTrackedTime", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["TotalTrackedTime"] = totalTrackedTime
|
||||||
|
}
|
||||||
|
|
||||||
archived := ctx.FormBool("archived")
|
archived := ctx.FormBool("archived")
|
||||||
|
|
||||||
page := ctx.FormInt("page")
|
page := ctx.FormInt("page")
|
||||||
|
|
|
@ -4,6 +4,15 @@
|
||||||
<input type="checkbox" autocomplete="off" class="issue-checkbox-all gt-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
|
<input type="checkbox" autocomplete="off" class="issue-checkbox-all gt-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/issue/openclose" .}}
|
{{template "repo/issue/openclose" .}}
|
||||||
|
<!-- Total Tracked Time -->
|
||||||
|
{{if .TotalTrackedTime}}
|
||||||
|
<div class="ui compact tiny secondary menu">
|
||||||
|
<span class="item" data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
|
||||||
|
{{svg "octicon-clock"}}
|
||||||
|
{{.TotalTrackedTime | Sec2Time}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-list-toolbar-right">
|
<div class="issue-list-toolbar-right">
|
||||||
<div class="ui secondary filter menu labels">
|
<div class="ui secondary filter menu labels">
|
||||||
|
|
|
@ -34,6 +34,15 @@
|
||||||
<div id="issue-actions" class="issue-list-toolbar gt-hidden">
|
<div id="issue-actions" class="issue-list-toolbar gt-hidden">
|
||||||
<div class="issue-list-toolbar-left">
|
<div class="issue-list-toolbar-left">
|
||||||
{{template "repo/issue/openclose" .}}
|
{{template "repo/issue/openclose" .}}
|
||||||
|
<!-- Total Tracked Time -->
|
||||||
|
{{if .TotalTrackedTime}}
|
||||||
|
<div class="ui compact tiny secondary menu">
|
||||||
|
<span class="item" data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
|
||||||
|
{{svg "octicon-clock"}}
|
||||||
|
{{.TotalTrackedTime | Sec2Time}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-list-toolbar-right">
|
<div class="issue-list-toolbar-right">
|
||||||
{{template "repo/issue/filter_actions" .}}
|
{{template "repo/issue/filter_actions" .}}
|
||||||
|
|
|
@ -46,6 +46,12 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}</div>
|
<div class="gt-mr-3">{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness | Safe}}</div>
|
||||||
|
{{if .TotalTrackedTime}}
|
||||||
|
<div data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
|
||||||
|
{{svg "octicon-clock"}}
|
||||||
|
{{.TotalTrackedTime | Sec2Time}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
Loading…
Reference in a new issue