summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/issue.go30
-rw-r--r--models/issue_list.go48
-rw-r--r--models/issue_list_test.go9
-rw-r--r--models/issue_milestone.go62
-rw-r--r--models/issue_milestone_test.go11
-rw-r--r--models/issue_stopwatch.go5
-rw-r--r--models/issue_test.go8
-rw-r--r--models/issue_tracked_time.go23
-rw-r--r--modules/templates/helper.go5
-rw-r--r--options/locale/locale_en-US.ini3
-rw-r--r--routers/repo/issue.go6
-rw-r--r--templates/repo/issue/list.tmpl4
-rw-r--r--templates/repo/issue/milestones.tmpl1
-rw-r--r--templates/repo/issue/view_content/sidebar.tmpl2
-rw-r--r--templates/user/dashboard/issues.tmpl3
15 files changed, 200 insertions, 20 deletions
diff --git a/models/issue.go b/models/issue.go
index 9106db281a..190b387530 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -51,9 +51,10 @@ type Issue struct {
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
ClosedUnix util.TimeStamp `xorm:"INDEX"`
- Attachments []*Attachment `xorm:"-"`
- Comments []*Comment `xorm:"-"`
- Reactions ReactionList `xorm:"-"`
+ Attachments []*Attachment `xorm:"-"`
+ Comments []*Comment `xorm:"-"`
+ Reactions ReactionList `xorm:"-"`
+ TotalTrackedTime int64 `xorm:"-"`
}
var (
@@ -69,6 +70,15 @@ func init() {
issueTasksDonePat = regexp.MustCompile(issueTasksDoneRegexpStr)
}
+func (issue *Issue) loadTotalTimes(e Engine) (err error) {
+ opts := FindTrackedTimesOptions{IssueID: issue.ID}
+ issue.TotalTrackedTime, err = opts.ToSession(e).SumInt(&TrackedTime{}, "time")
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func (issue *Issue) loadRepo(e Engine) (err error) {
if issue.Repo == nil {
issue.Repo, err = getRepositoryByID(e, issue.RepoID)
@@ -79,6 +89,15 @@ func (issue *Issue) loadRepo(e Engine) (err error) {
return nil
}
+// IsTimetrackerEnabled returns true if the repo enables timetracking
+func (issue *Issue) IsTimetrackerEnabled() bool {
+ if err := issue.loadRepo(x); err != nil {
+ log.Error(4, fmt.Sprintf("loadRepo: %v", err))
+ return false
+ }
+ return issue.Repo.IsTimetrackerEnabled()
+}
+
// GetPullRequest returns the issue pull request
func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
if !issue.IsPull {
@@ -225,6 +244,11 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
if err = issue.loadComments(e); err != nil {
return err
}
+ if issue.IsTimetrackerEnabled() {
+ if err = issue.loadTotalTimes(e); err != nil {
+ return err
+ }
+ }
return issue.loadReactions(e)
}
diff --git a/models/issue_list.go b/models/issue_list.go
index 4910915cd0..01a1a15f44 100644
--- a/models/issue_list.go
+++ b/models/issue_list.go
@@ -290,6 +290,50 @@ func (issues IssueList) loadComments(e Engine) (err error) {
return nil
}
+func (issues IssueList) loadTotalTrackedTimes(e Engine) (err error) {
+ type totalTimesByIssue struct {
+ IssueID int64
+ Time int64
+ }
+ if len(issues) == 0 {
+ return nil
+ }
+ var trackedTimes = make(map[int64]int64, len(issues))
+
+ var ids = make([]int64, 0, len(issues))
+ for _, issue := range issues {
+ if issue.Repo.IsTimetrackerEnabled() {
+ ids = append(ids, issue.ID)
+ }
+ }
+
+ // select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
+ rows, err := e.Table("tracked_time").
+ Select("issue_id, sum(time) as time").
+ In("issue_id", ids).
+ GroupBy("issue_id").
+ Rows(new(totalTimesByIssue))
+ if err != nil {
+ return err
+ }
+
+ defer rows.Close()
+
+ for rows.Next() {
+ var totalTime totalTimesByIssue
+ err = rows.Scan(&totalTime)
+ if err != nil {
+ return err
+ }
+ trackedTimes[totalTime.IssueID] = totalTime.Time
+ }
+
+ for _, issue := range issues {
+ issue.TotalTrackedTime = trackedTimes[issue.ID]
+ }
+ return nil
+}
+
// loadAttributes loads all attributes, expect for attachments and comments
func (issues IssueList) loadAttributes(e Engine) (err error) {
if _, err = issues.loadRepositories(e); err != nil {
@@ -316,6 +360,10 @@ func (issues IssueList) loadAttributes(e Engine) (err error) {
return
}
+ if err = issues.loadTotalTrackedTimes(e); err != nil {
+ return
+ }
+
return nil
}
diff --git a/models/issue_list_test.go b/models/issue_list_test.go
index 958e074662..9197e0615a 100644
--- a/models/issue_list_test.go
+++ b/models/issue_list_test.go
@@ -7,6 +7,8 @@ package models
import (
"testing"
+ "code.gitea.io/gitea/modules/setting"
+
"github.com/stretchr/testify/assert"
)
@@ -29,7 +31,7 @@ func TestIssueList_LoadRepositories(t *testing.T) {
func TestIssueList_LoadAttributes(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
-
+ setting.Service.EnableTimetracking = true
issueList := IssueList{
AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue),
AssertExistsAndLoadBean(t, &Issue{ID: 2}).(*Issue),
@@ -61,5 +63,10 @@ func TestIssueList_LoadAttributes(t *testing.T) {
for _, comment := range issue.Comments {
assert.EqualValues(t, issue.ID, comment.IssueID)
}
+ if issue.ID == int64(1) {
+ assert.Equal(t, int64(400), issue.TotalTrackedTime)
+ } else if issue.ID == int64(2) {
+ assert.Equal(t, int64(3662), issue.TotalTrackedTime)
+ }
}
}
diff --git a/models/issue_milestone.go b/models/issue_milestone.go
index a5e0bd60df..949932fb2b 100644
--- a/models/issue_milestone.go
+++ b/models/issue_milestone.go
@@ -29,6 +29,8 @@ type Milestone struct {
DeadlineString string `xorm:"-"`
DeadlineUnix util.TimeStamp
ClosedDateUnix util.TimeStamp
+
+ TotalTrackedTime int64 `xorm:"-"`
}
// BeforeUpdate is invoked from XORM before updating this object.
@@ -118,14 +120,69 @@ func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
return getMilestoneByRepoID(x, repoID, id)
}
+// MilestoneList is a list of milestones offering additional functionality
+type MilestoneList []*Milestone
+
+func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
+ type totalTimesByMilestone struct {
+ MilestoneID int64
+ Time int64
+ }
+ if len(milestones) == 0 {
+ return nil
+ }
+ var trackedTimes = make(map[int64]int64, len(milestones))
+
+ // Get total tracked time by milestone_id
+ rows, err := e.Table("issue").
+ Join("INNER", "milestone", "issue.milestone_id = milestone.id").
+ Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
+ Select("milestone_id, sum(time) as time").
+ In("milestone_id", milestones.getMilestoneIDs()).
+ GroupBy("milestone_id").
+ Rows(new(totalTimesByMilestone))
+ if err != nil {
+ return err
+ }
+
+ defer rows.Close()
+
+ for rows.Next() {
+ var totalTime totalTimesByMilestone
+ err = rows.Scan(&totalTime)
+ if err != nil {
+ return err
+ }
+ trackedTimes[totalTime.MilestoneID] = totalTime.Time
+ }
+
+ for _, milestone := range milestones {
+ milestone.TotalTrackedTime = trackedTimes[milestone.ID]
+ }
+ return nil
+}
+
+// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
+func (milestones MilestoneList) LoadTotalTrackedTimes() error {
+ return milestones.loadTotalTrackedTimes(x)
+}
+
+func (milestones MilestoneList) getMilestoneIDs() []int64 {
+ var ids = make([]int64, 0, len(milestones))
+ for _, ms := range milestones {
+ ids = append(ids, ms.ID)
+ }
+ return ids
+}
+
// GetMilestonesByRepoID returns all milestones of a repository.
-func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
+func GetMilestonesByRepoID(repoID int64) (MilestoneList, error) {
miles := make([]*Milestone, 0, 10)
return miles, x.Where("repo_id = ?", repoID).Find(&miles)
}
// GetMilestones returns a list of milestones of given repository and status.
-func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*Milestone, error) {
+func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
if page > 0 {
@@ -146,7 +203,6 @@ func GetMilestones(repoID int64, page int, isClosed bool, sortType string) ([]*M
default:
sess.Asc("deadline_unix")
}
-
return miles, sess.Find(&miles)
}
diff --git a/models/issue_milestone_test.go b/models/issue_milestone_test.go
index c57f92439e..c9b53f4f4a 100644
--- a/models/issue_milestone_test.go
+++ b/models/issue_milestone_test.go
@@ -253,3 +253,14 @@ func TestDeleteMilestoneByRepoID(t *testing.T) {
assert.NoError(t, DeleteMilestoneByRepoID(NonexistentID, NonexistentID))
}
+
+func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ miles := MilestoneList{
+ AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone),
+ }
+
+ assert.NoError(t, miles.LoadTotalTrackedTimes())
+
+ assert.Equal(t, miles[0].TotalTrackedTime, int64(3662))
+}
diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go
index 92b1bb9a5f..178b76c5dd 100644
--- a/models/issue_stopwatch.go
+++ b/models/issue_stopwatch.go
@@ -69,7 +69,7 @@ func CreateOrStopIssueStopwatch(user *User, issue *Issue) error {
Doer: user,
Issue: issue,
Repo: issue.Repo,
- Content: secToTime(timediff),
+ Content: SecToTime(timediff),
Type: CommentTypeStopTracking,
}); err != nil {
return err
@@ -124,7 +124,8 @@ func CancelStopwatch(user *User, issue *Issue) error {
return nil
}
-func secToTime(duration int64) string {
+// SecToTime converts an amount of seconds to a human-readable string (example: 66s -> 1min 6s)
+func SecToTime(duration int64) string {
seconds := duration % 60
minutes := (duration / (60)) % 60
hours := duration / (60 * 60)
diff --git a/models/issue_test.go b/models/issue_test.go
index 851fe684fb..d98debb178 100644
--- a/models/issue_test.go
+++ b/models/issue_test.go
@@ -279,3 +279,11 @@ func TestGetUserIssueStats(t *testing.T) {
assert.Equal(t, test.ExpectedIssueStats, *stats)
}
}
+
+func TestIssue_loadTotalTimes(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+ ms, err := GetIssueByID(2)
+ assert.NoError(t, err)
+ assert.NoError(t, ms.loadTotalTimes(x))
+ assert.Equal(t, int64(3662), ms.TotalTrackedTime)
+}
diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go
index c314f8f44f..6592f06d73 100644
--- a/models/issue_tracked_time.go
+++ b/models/issue_tracked_time.go
@@ -11,6 +11,7 @@ import (
api "code.gitea.io/sdk/gitea"
"github.com/go-xorm/builder"
+ "github.com/go-xorm/xorm"
)
// TrackedTime represents a time that was spent for a specific issue.
@@ -44,6 +45,7 @@ type FindTrackedTimesOptions struct {
IssueID int64
UserID int64
RepositoryID int64
+ MilestoneID int64
}
// ToCond will convert each condition into a xorm-Cond
@@ -58,16 +60,23 @@ func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
if opts.RepositoryID != 0 {
cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID})
}
+ if opts.MilestoneID != 0 {
+ cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID})
+ }
return cond
}
+// ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
+func (opts *FindTrackedTimesOptions) ToSession(e Engine) *xorm.Session {
+ if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
+ return e.Join("INNER", "issue", "issue.id = tracked_time.issue_id").Where(opts.ToCond())
+ }
+ return x.Where(opts.ToCond())
+}
+
// GetTrackedTimes returns all tracked times that fit to the given options.
func GetTrackedTimes(options FindTrackedTimesOptions) (trackedTimes []*TrackedTime, err error) {
- if options.RepositoryID > 0 {
- err = x.Join("INNER", "issue", "issue.id = tracked_time.issue_id").Where(options.ToCond()).Find(&trackedTimes)
- return
- }
- err = x.Where(options.ToCond()).Find(&trackedTimes)
+ err = options.ToSession(x).Find(&trackedTimes)
return
}
@@ -85,7 +94,7 @@ func AddTime(user *User, issue *Issue, time int64) (*TrackedTime, error) {
Issue: issue,
Repo: issue.Repo,
Doer: user,
- Content: secToTime(time),
+ Content: SecToTime(time),
Type: CommentTypeAddTimeManual,
}); err != nil {
return nil, err
@@ -115,7 +124,7 @@ func TotalTimes(options FindTrackedTimesOptions) (map[*User]string, error) {
}
return nil, err
}
- totalTimes[user] = secToTime(total)
+ totalTimes[user] = SecToTime(total)
}
return totalTimes, nil
}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 98900c7538..8dfa6dec8a 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -179,8 +179,9 @@ func NewFuncMap() []template.FuncMap {
}
return dict, nil
},
- "Printf": fmt.Sprintf,
- "Escape": Escape,
+ "Printf": fmt.Sprintf,
+ "Escape": Escape,
+ "Sec2Time": models.SecToTime,
}}
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 6f208262e2..7e9a8da3e4 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -736,7 +736,8 @@ issues.add_time_minutes = Minutes
issues.add_time_sum_to_small = No time was entered.
issues.cancel_tracking = Cancel
issues.cancel_tracking_history = `cancelled time tracking %s`
-issues.time_spent_total = Total Time Spent
+issues.time_spent_from_all_authors = `Total Time Spent: %s`
+
pulls.desc = Enable merge requests and code reviews.
pulls.new = New Pull Request
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 234937b1af..51516b828c 100644
--- a/routers/repo/issue.go
+++ b/routers/repo/issue.go
@@ -1139,6 +1139,12 @@ func Milestones(ctx *context.Context) {
ctx.ServerError("GetMilestones", err)
return
}
+ if ctx.Repo.Repository.IsTimetrackerEnabled() {
+ if miles.LoadTotalTrackedTimes(); err != nil {
+ ctx.ServerError("LoadTotalTrackedTimes", err)
+ return
+ }
+ }
for _, m := range miles {
m.RenderedContent = string(markdown.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()))
}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 04a11ca5cc..180a5dea6c 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -198,6 +198,10 @@
<span class="comment ui right"><i class="octicon octicon-comment"></i> {{.NumComments}}</span>
{{end}}
+ {{if .TotalTrackedTime}}
+ <span class="comment ui right"><i class="octicon octicon-clock"></i> {{.TotalTrackedTime | Sec2Time}}</span>
+ {{end}}
+
<p class="desc">
{{$.i18n.Tr "repo.issues.opened_by" $timeStr .Poster.HomeLink .Poster.Name | Safe}}
{{$tasks := .GetTasks}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index de52bd42f0..369da2e630 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -64,6 +64,7 @@
<span class="issue-stats">
<i class="octicon octicon-issue-opened"></i> {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}}
<i class="octicon octicon-issue-closed"></i> {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}}
+ {{if .TotalTrackedTime}}<i class="octicon octicon-clock"></i> {{.TotalTrackedTime|Sec2Time}}{{end}}
</span>
</div>
{{if $.IsRepositoryWriter}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index dff064f777..dc16ba7499 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -172,7 +172,7 @@
{{if gt (len .WorkingUsers) 0}}
<div class="ui divider"></div>
<div class="ui participants comments">
- <span class="text"><strong>{{.i18n.Tr "repo.issues.time_spent_total"}}</strong></span>
+ <span class="text"><strong>{{.i18n.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time) | Safe}}</strong></span>
<div>
{{range $user, $trackedtime := .WorkingUsers}}
<div class="comment">
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index b41301b108..d0b6511b87 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -79,6 +79,9 @@
{{if .NumComments}}
<span class="comment ui right"><i class="octicon octicon-comment"></i> {{.NumComments}}</span>
{{end}}
+ {{if .TotalTrackedTime}}
+ <span class="comment ui right"><i class="octicon octicon-clock"></i> {{.TotalTrackedTime | Sec2Time}}</span>
+ {{end}}
<p class="desc">
{{$.i18n.Tr "repo.issues.opened_by" $timeStr .Poster.HomeLink .Poster.Name | Safe}}