summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
authorJonas Franz <info@jonasfranz.software>2017-09-12 08:48:13 +0200
committerLauris BH <lauris@nix.lv>2017-09-12 09:48:13 +0300
commit5ccecb44adddf17e1a3ec8ae6e1ad75cb0ff94e6 (patch)
treee7ab5e7965be4d1ad92655c74d0ce8fbc0954df2 /models
parent69dfe43ffc865bfa9c7a81375752d064a0013df3 (diff)
downloadgitea-5ccecb44adddf17e1a3ec8ae6e1ad75cb0ff94e6.tar.gz
gitea-5ccecb44adddf17e1a3ec8ae6e1ad75cb0ff94e6.zip
Feature: Timetracking (#2211)
* Added comment's hashtag to url for mail notifications. * Added explanation to return statement + documentation. * Replacing in-line link generation with HTMLURL. (+gofmt) * Replaced action-based model with nil-based model. (+gofmt) * Replaced mailIssueActionToParticipants with mailIssueCommentToParticipants. * Updating comment for mailIssueCommentToParticipants * Added link to comment in "Dashboard" * Deleting feed entry if a comment is going to be deleted * Added migration * Added improved migration to add a CommentID column to action. * Added improved links to comments in feed entries. * Fixes #1956 by filtering for deleted comments that are referenced in actions. * Introducing "IsDeleted" column to action. * Adding design draft (not functional) * Adding database models for stopwatches and trackedtimes * See go-gitea/gitea#967 * Adding design draft (not functional) * Adding translations and improving design * Implementing stopwatch (for timetracking) * Make UI functional * Add hints in timeline for time tracking events * Implementing timetracking feature * Adding "Add time manual" option * Improved stopwatch * Created report of total spent time by user * Only showing total time spent if theire is something to show. * Adding license headers. * Improved error handling for "Add Time Manual" * Adding @sapks 's changes, refactoring * Adding API for feature tracking * Adding unit test * Adding DISABLE/ENABLE option to Repository settings page * Improving translations * Applying @sapk 's changes * Removing repo_unit and using IssuesSetting for disabling/enabling timetracker * Adding DEFAULT_ENABLE_TIMETRACKER to config, installation and admin menu * Improving documentation * Fixing vendor/ folder * Changing timtracking routes by adding subgroups /times and /times/stopwatch (Proposed by @lafriks ) * Restricting write access to timetracking based on the repo settings (Proposed by @lafriks ) * Fixed minor permissions bug. * Adding CanUseTimetracker and IsTimetrackerEnabled in ctx.Repo * Allow assignees and authors to track there time too. * Fixed some build-time-errors + logical errors. * Removing unused Get...ByID functions * Moving IsTimetrackerEnabled from context.Repository to models.Repository * Adding a seperate file for issue related repo functions * Adding license headers * Fixed GetUserByParams return 404 * Moving /users/:username/times to /repos/:username/:reponame/times/:username for security reasons * Adding /repos/:username/times to get all tracked times of the repo * Updating sdk-dependency * Updating swagger.v1.json * Adding warning if user has already a running stopwatch (auto-timetracker) * Replacing GetTrackedTimesBy... with GetTrackedTimes(options FindTrackedTimesOptions) * Changing code.gitea.io/sdk back to code.gitea.io/sdk * Correcting spelling mistake * Updating vendor.json * Changing GET stopwatch/toggle to POST stopwatch/toggle * Changing GET stopwatch/cancel to POST stopwatch/cancel * Added migration for stopwatches/timetracking * Fixed some access bugs for read-only users * Added default allow only contributors to track time value to config * Fixed migration by chaging x.Iterate to x.Find * Resorted imports * Moved Add Time Manually form to repo_form.go * Removed "Seconds" field from Add Time Manually * Resorted imports * Improved permission checking * Fixed some bugs * Added integration test * gofmt * Adding integration test by @lafriks * Added created_unix to comment fixtures * Using last event instead of a fixed event * Adding another integration test by @lafriks * Fixing bug Timetracker enabled causing error 500 at sidebar.tpl * Fixed a refactoring bug that resulted in hiding "HasUserStopwatch" warning. * Returning TrackedTime instead of AddTimeOption at AddTime. * Updating SDK from go-gitea/go-sdk#69 * Resetting Go-SDK back to default repository * Fixing test-vendor by changing ini back to original repository * Adding "tags" to swagger spec * govendor sync * Removed duplicate * Formatting templates * Adding IsTimetrackingEnabled checks to API * Improving translations / english texts * Improving documentation * Updating swagger spec * Fixing integration test caused be translation-changes * Removed encoding issues in local_en-US.ini. * "Added" copyright line * Moved unit.IssuesConfig().EnableTimetracker into a != nil check * Removed some other encoding issues in local_en-US.ini * Improved javascript by checking if data-context exists * Replaced manual comment creation with CreateComment * Removed unnecessary code * Improved error checking * Small cosmetic changes * Replaced int>string>duration parsing with int>duration parsing * Fixed encoding issues * Removed unused imports Signed-off-by: Jonas Franz <info@jonasfranz.software>
Diffstat (limited to 'models')
-rw-r--r--models/error.go44
-rw-r--r--models/fixtures/comment.yml3
-rw-r--r--models/fixtures/issue.yml13
-rw-r--r--models/fixtures/repo_unit.yml4
-rw-r--r--models/fixtures/repository.yml2
-rw-r--r--models/fixtures/stopwatch.yml11
-rw-r--r--models/fixtures/tracked_time.yml34
-rw-r--r--models/issue_comment.go9
-rw-r--r--models/issue_stopwatch.go170
-rw-r--r--models/issue_stopwatch_test.go70
-rw-r--r--models/issue_tracked_time.go117
-rw-r--r--models/issue_tracked_time_test.go103
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v16.go8
-rw-r--r--models/migrations/v39.go65
-rw-r--r--models/models.go2
-rw-r--r--models/repo.go24
-rw-r--r--models/repo_issue.go34
-rw-r--r--models/repo_unit.go30
19 files changed, 724 insertions, 21 deletions
diff --git a/models/error.go b/models/error.go
index 2adb4b45e1..37505e8aa5 100644
--- a/models/error.go
+++ b/models/error.go
@@ -768,6 +768,50 @@ func (err ErrCommentNotExist) Error() string {
return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)
}
+// _________ __ __ .__
+// / _____// |_ ____ ________ _ _______ _/ |_ ____ | |__
+// \_____ \\ __\/ _ \\____ \ \/ \/ /\__ \\ __\/ ___\| | \
+// / \| | ( <_> ) |_> > / / __ \| | \ \___| Y \
+// /_______ /|__| \____/| __/ \/\_/ (____ /__| \___ >___| /
+// \/ |__| \/ \/ \/
+
+// ErrStopwatchNotExist represents a "Stopwatch Not Exist" kind of error.
+type ErrStopwatchNotExist struct {
+ ID int64
+}
+
+// IsErrStopwatchNotExist checks if an error is a ErrStopwatchNotExist.
+func IsErrStopwatchNotExist(err error) bool {
+ _, ok := err.(ErrStopwatchNotExist)
+ return ok
+}
+
+func (err ErrStopwatchNotExist) Error() string {
+ return fmt.Sprintf("stopwatch does not exist [id: %d]", err.ID)
+}
+
+// ___________ __ .______________.__
+// \__ ___/___________ ____ | | __ ____ __| _/\__ ___/|__| _____ ____
+// | | \_ __ \__ \ _/ ___\| |/ // __ \ / __ | | | | |/ \_/ __ \
+// | | | | \// __ \\ \___| <\ ___// /_/ | | | | | Y Y \ ___/
+// |____| |__| (____ /\___ >__|_ \\___ >____ | |____| |__|__|_| /\___ >
+// \/ \/ \/ \/ \/ \/ \/
+
+// ErrTrackedTimeNotExist represents a "TrackedTime Not Exist" kind of error.
+type ErrTrackedTimeNotExist struct {
+ ID int64
+}
+
+// IsErrTrackedTimeNotExist checks if an error is a ErrTrackedTimeNotExist.
+func IsErrTrackedTimeNotExist(err error) bool {
+ _, ok := err.(ErrTrackedTimeNotExist)
+ return ok
+}
+
+func (err ErrTrackedTimeNotExist) Error() string {
+ return fmt.Sprintf("tracked time does not exist [id: %d]", err.ID)
+}
+
// .____ ___. .__
// | | _____ \_ |__ ____ | |
// | | \__ \ | __ \_/ __ \| |
diff --git a/models/fixtures/comment.yml b/models/fixtures/comment.yml
index 3292bb4848..34df02d28c 100644
--- a/models/fixtures/comment.yml
+++ b/models/fixtures/comment.yml
@@ -5,15 +5,18 @@
issue_id: 1 # in repo_id 1
label_id: 1
content: "1"
+ created_unix: 946684810
-
id: 2
type: 0 # comment
poster_id: 3 # user not watching (see watch.yml)
issue_id: 1 # in repo_id 1
content: "good work!"
+ created_unix: 946684811
-
id: 3
type: 0 # comment
poster_id: 5 # user not watching (see watch.yml)
issue_id: 1 # in repo_id 1
content: "meh..."
+ created_unix: 946684812
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index 7bbbab26fe..b80ada1ba4 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -57,3 +57,16 @@
content: content5
is_closed: true
is_pull: false
+-
+ id: 6
+ repo_id: 3
+ index: 1
+ poster_id: 1
+ assignee_id: 1
+ name: issue6
+ content: content6
+ is_closed: false
+ is_pull: false
+ num_comments: 0
+ created_unix: 946684800
+ updated_unix: 978307200
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index 02daa48277..57cf35e198 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -11,7 +11,7 @@
repo_id: 1
type: 2
index: 1
- config: "{}"
+ config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
created_unix: 946684810
-
@@ -51,7 +51,7 @@
repo_id: 3
type: 2
index: 1
- config: "{}"
+ config: "{\"EnableTimetracker\":false,\"AllowOnlyContributorsToTrackTime\":false}"
created_unix: 946684810
-
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index b8f607b2a8..3409ba8113 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -29,7 +29,7 @@
lower_name: repo3
name: repo3
is_private: true
- num_issues: 0
+ num_issues: 1
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
diff --git a/models/fixtures/stopwatch.yml b/models/fixtures/stopwatch.yml
new file mode 100644
index 0000000000..397a8214d4
--- /dev/null
+++ b/models/fixtures/stopwatch.yml
@@ -0,0 +1,11 @@
+-
+ id: 1
+ user_id: 1
+ issue_id: 1
+ created_unix: 1500988502
+
+-
+ id: 2
+ user_id: 2
+ issue_id: 2
+ created_unix: 1500988502
diff --git a/models/fixtures/tracked_time.yml b/models/fixtures/tracked_time.yml
new file mode 100644
index 0000000000..06a71c5ad9
--- /dev/null
+++ b/models/fixtures/tracked_time.yml
@@ -0,0 +1,34 @@
+-
+ id: 1
+ user_id: 1
+ issue_id: 1
+ time: 400
+ created_unix: 946684800
+
+-
+ id: 2
+ user_id: 2
+ issue_id: 2
+ time: 3661
+ created_unix: 946684801
+
+-
+ id: 3
+ user_id: 2
+ issue_id: 2
+ time: 1
+ created_unix: 946684802
+
+-
+ id: 4
+ user_id: -1
+ issue_id: 4
+ time: 1
+ created_unix: 946684802
+
+-
+ id: 5
+ user_id: 2
+ issue_id: 5
+ time: 1
+ created_unix: 946684802
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 79fa23960d..fe2223e9d2 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -52,6 +52,14 @@ const (
CommentTypeChangeTitle
// Delete Branch
CommentTypeDeleteBranch
+ // Start a stopwatch for time tracking
+ CommentTypeStartTracking
+ // Stop a stopwatch for time tracking
+ CommentTypeStopTracking
+ // Add time manual for time tracking
+ CommentTypeAddTimeManual
+ // Cancel a stopwatch for time tracking
+ CommentTypeCancelTracking
)
// CommentTag defines comment tag type
@@ -672,7 +680,6 @@ func DeleteComment(comment *Comment) error {
return err
}
}
-
if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
return err
}
diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go
new file mode 100644
index 0000000000..3b5b4d57f3
--- /dev/null
+++ b/models/issue_stopwatch.go
@@ -0,0 +1,170 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-xorm/xorm"
+)
+
+// Stopwatch represents a stopwatch for time tracking.
+type Stopwatch struct {
+ ID int64 `xorm:"pk autoincr"`
+ IssueID int64 `xorm:"INDEX"`
+ UserID int64 `xorm:"INDEX"`
+ Created time.Time `xorm:"-"`
+ CreatedUnix int64
+}
+
+// BeforeInsert will be invoked by XORM before inserting a record
+// representing this object.
+func (s *Stopwatch) BeforeInsert() {
+ s.CreatedUnix = time.Now().Unix()
+}
+
+// AfterSet is invoked from XORM after setting the value of a field of this object.
+func (s *Stopwatch) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+
+ case "created_unix":
+ s.Created = time.Unix(s.CreatedUnix, 0).Local()
+ }
+}
+
+func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
+ sw = new(Stopwatch)
+ exists, err = e.
+ Where("user_id = ?", userID).
+ And("issue_id = ?", issueID).
+ Get(sw)
+ return
+}
+
+// StopwatchExists returns true if the stopwatch exists
+func StopwatchExists(userID int64, issueID int64) bool {
+ _, exists, _ := getStopwatch(x, userID, issueID)
+ return exists
+}
+
+// HasUserStopwatch returns true if the user has a stopwatch
+func HasUserStopwatch(userID int64) (exists bool, sw *Stopwatch, err error) {
+ sw = new(Stopwatch)
+ exists, err = x.
+ Where("user_id = ?", userID).
+ Get(sw)
+ return
+}
+
+// CreateOrStopIssueStopwatch will create or remove a stopwatch and will log it into issue's timeline.
+func CreateOrStopIssueStopwatch(user *User, issue *Issue) error {
+ sw, exists, err := getStopwatch(x, user.ID, issue.ID)
+ if err != nil {
+ return err
+ }
+ if exists {
+ // Create tracked time out of the time difference between start date and actual date
+ timediff := time.Now().Unix() - sw.CreatedUnix
+
+ // Create TrackedTime
+ tt := &TrackedTime{
+ Created: time.Now(),
+ IssueID: issue.ID,
+ UserID: user.ID,
+ Time: timediff,
+ }
+
+ if _, err := x.Insert(tt); err != nil {
+ return err
+ }
+
+ if _, err := CreateComment(&CreateCommentOptions{
+ Doer: user,
+ Issue: issue,
+ Repo: issue.Repo,
+ Content: secToTime(timediff),
+ Type: CommentTypeStopTracking,
+ }); err != nil {
+ return err
+ }
+ if _, err := x.Delete(sw); err != nil {
+ return err
+ }
+ } else {
+ // Create stopwatch
+ sw = &Stopwatch{
+ UserID: user.ID,
+ IssueID: issue.ID,
+ Created: time.Now(),
+ }
+
+ if _, err := x.Insert(sw); err != nil {
+ return err
+ }
+
+ if _, err := CreateComment(&CreateCommentOptions{
+ Doer: user,
+ Issue: issue,
+ Repo: issue.Repo,
+ Type: CommentTypeStartTracking,
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// CancelStopwatch removes the given stopwatch and logs it into issue's timeline.
+func CancelStopwatch(user *User, issue *Issue) error {
+ sw, exists, err := getStopwatch(x, user.ID, issue.ID)
+ if err != nil {
+ return err
+ }
+
+ if exists {
+ if _, err := x.Delete(sw); err != nil {
+ return err
+ }
+
+ if _, err := CreateComment(&CreateCommentOptions{
+ Doer: user,
+ Issue: issue,
+ Repo: issue.Repo,
+ Type: CommentTypeCancelTracking,
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func secToTime(duration int64) string {
+ seconds := duration % 60
+ minutes := (duration / (60)) % 60
+ hours := duration / (60 * 60)
+
+ var hrs string
+
+ if hours > 0 {
+ hrs = fmt.Sprintf("%dh", hours)
+ }
+ if minutes > 0 {
+ if hours == 0 {
+ hrs = fmt.Sprintf("%dmin", minutes)
+ } else {
+ hrs = fmt.Sprintf("%s %dmin", hrs, minutes)
+ }
+ }
+ if seconds > 0 {
+ if hours == 0 && minutes == 0 {
+ hrs = fmt.Sprintf("%ds", seconds)
+ } else {
+ hrs = fmt.Sprintf("%s %ds", hrs, seconds)
+ }
+ }
+
+ return hrs
+}
diff --git a/models/issue_stopwatch_test.go b/models/issue_stopwatch_test.go
new file mode 100644
index 0000000000..9875066e53
--- /dev/null
+++ b/models/issue_stopwatch_test.go
@@ -0,0 +1,70 @@
+package models
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCancelStopwatch(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ user1, err := GetUserByID(1)
+ assert.NoError(t, err)
+
+ issue1, err := GetIssueByID(1)
+ assert.NoError(t, err)
+ issue2, err := GetIssueByID(2)
+ assert.NoError(t, err)
+
+ err = CancelStopwatch(user1, issue1)
+ assert.NoError(t, err)
+ AssertNotExistsBean(t, &Stopwatch{UserID: user1.ID, IssueID: issue1.ID})
+
+ _ = AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
+
+ assert.Nil(t, CancelStopwatch(user1, issue2))
+}
+
+func TestStopwatchExists(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ assert.True(t, StopwatchExists(1, 1))
+ assert.False(t, StopwatchExists(1, 2))
+}
+
+func TestHasUserStopwatch(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ exists, sw, err := HasUserStopwatch(1)
+ assert.NoError(t, err)
+ assert.True(t, exists)
+ assert.Equal(t, int64(1), sw.ID)
+
+ exists, _, err = HasUserStopwatch(3)
+ assert.NoError(t, err)
+ assert.False(t, exists)
+}
+
+func TestCreateOrStopIssueStopwatch(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ user2, err := GetUserByID(2)
+ assert.NoError(t, err)
+ user3, err := GetUserByID(3)
+ assert.NoError(t, err)
+
+ issue1, err := GetIssueByID(1)
+ assert.NoError(t, err)
+ issue2, err := GetIssueByID(2)
+ assert.NoError(t, err)
+
+ assert.NoError(t, CreateOrStopIssueStopwatch(user3, issue1))
+ sw := AssertExistsAndLoadBean(t, &Stopwatch{UserID: 3, IssueID: 1}).(*Stopwatch)
+ assert.Equal(t, true, sw.Created.Before(time.Now()))
+
+ assert.NoError(t, CreateOrStopIssueStopwatch(user2, issue2))
+ AssertNotExistsBean(t, &Stopwatch{UserID: 2, IssueID: 2})
+ AssertExistsAndLoadBean(t, &TrackedTime{UserID: 2, IssueID: 2})
+}
diff --git a/models/issue_tracked_time.go b/models/issue_tracked_time.go
new file mode 100644
index 0000000000..33914dbb1f
--- /dev/null
+++ b/models/issue_tracked_time.go
@@ -0,0 +1,117 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+ "time"
+
+ "github.com/go-xorm/builder"
+ "github.com/go-xorm/xorm"
+)
+
+// TrackedTime represents a time that was spent for a specific issue.
+type TrackedTime struct {
+ ID int64 `xorm:"pk autoincr" json:"id"`
+ IssueID int64 `xorm:"INDEX" json:"issue_id"`
+ UserID int64 `xorm:"INDEX" json:"user_id"`
+ Created time.Time `xorm:"-" json:"created"`
+ CreatedUnix int64 `json:"-"`
+ Time int64 `json:"time"`
+}
+
+// BeforeInsert will be invoked by XORM before inserting a record
+// representing this object.
+func (t *TrackedTime) BeforeInsert() {
+ t.CreatedUnix = time.Now().Unix()
+}
+
+// AfterSet is invoked from XORM after setting the value of a field of this object.
+func (t *TrackedTime) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ t.Created = time.Unix(t.CreatedUnix, 0).Local()
+ }
+}
+
+// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
+type FindTrackedTimesOptions struct {
+ IssueID int64
+ UserID int64
+ RepositoryID int64
+}
+
+// ToCond will convert each condition into a xorm-Cond
+func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
+ cond := builder.NewCond()
+ if opts.IssueID != 0 {
+ cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
+ }
+ if opts.UserID != 0 {
+ cond = cond.And(builder.Eq{"user_id": opts.UserID})
+ }
+ if opts.RepositoryID != 0 {
+ cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID})
+ }
+ return cond
+}
+
+// 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)
+ return
+}
+
+// AddTime will add the given time (in seconds) to the issue
+func AddTime(user *User, issue *Issue, time int64) (*TrackedTime, error) {
+ tt := &TrackedTime{
+ IssueID: issue.ID,
+ UserID: user.ID,
+ Time: time,
+ }
+ if _, err := x.Insert(tt); err != nil {
+ return nil, err
+ }
+ if _, err := CreateComment(&CreateCommentOptions{
+ Issue: issue,
+ Repo: issue.Repo,
+ Doer: user,
+ Content: secToTime(time),
+ Type: CommentTypeAddTimeManual,
+ }); err != nil {
+ return nil, err
+ }
+ return tt, nil
+}
+
+// TotalTimes returns the spent time for each user by an issue
+func TotalTimes(options FindTrackedTimesOptions) (map[*User]string, error) {
+ trackedTimes, err := GetTrackedTimes(options)
+ if err != nil {
+ return nil, err
+ }
+ //Adding total time per user ID
+ totalTimesByUser := make(map[int64]int64)
+ for _, t := range trackedTimes {
+ totalTimesByUser[t.UserID] += t.Time
+ }
+
+ totalTimes := make(map[*User]string)
+ //Fetching User and making time human readable
+ for userID, total := range totalTimesByUser {
+ user, err := GetUserByID(userID)
+ if err != nil {
+ if IsErrUserNotExist(err) {
+ continue
+ }
+ return nil, err
+ }
+ totalTimes[user] = secToTime(total)
+ }
+ return totalTimes, nil
+}
diff --git a/models/issue_tracked_time_test.go b/models/issue_tracked_time_test.go
new file mode 100644
index 0000000000..130e8f33e2
--- /dev/null
+++ b/models/issue_tracked_time_test.go
@@ -0,0 +1,103 @@
+package models
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAddTime(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ user3, err := GetUserByID(3)
+ assert.NoError(t, err)
+
+ issue1, err := GetIssueByID(1)
+ assert.NoError(t, err)
+
+ //3661 = 1h 1min 1s
+ trackedTime, err := AddTime(user3, issue1, 3661)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(3), trackedTime.UserID)
+ assert.Equal(t, int64(1), trackedTime.IssueID)
+ assert.Equal(t, int64(3661), trackedTime.Time)
+
+ tt := AssertExistsAndLoadBean(t, &TrackedTime{UserID: 3, IssueID: 1}).(*TrackedTime)
+ assert.Equal(t, tt.Time, int64(3661))
+
+ comment := AssertExistsAndLoadBean(t, &Comment{Type: CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*Comment)
+ assert.Equal(t, comment.Content, "1h 1min 1s")
+}
+
+func TestGetTrackedTimes(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ // by Issue
+ times, err := GetTrackedTimes(FindTrackedTimesOptions{IssueID: 1})
+ assert.NoError(t, err)
+ assert.Len(t, times, 1)
+ assert.Equal(t, times[0].Time, int64(400))
+
+ times, err = GetTrackedTimes(FindTrackedTimesOptions{IssueID: -1})
+ assert.NoError(t, err)
+ assert.Len(t, times, 0)
+
+ // by User
+ times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 1})
+ assert.NoError(t, err)
+ assert.Len(t, times, 1)
+ assert.Equal(t, times[0].Time, int64(400))
+
+ times, err = GetTrackedTimes(FindTrackedTimesOptions{UserID: 3})
+ assert.NoError(t, err)
+ assert.Len(t, times, 0)
+
+ // by Repo
+ times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 2})
+ assert.NoError(t, err)
+ assert.Len(t, times, 1)
+ assert.Equal(t, times[0].Time, int64(1))
+ issue, err := GetIssueByID(times[0].IssueID)
+ assert.NoError(t, err)
+ assert.Equal(t, issue.RepoID, int64(2))
+
+ times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 1})
+ assert.NoError(t, err)
+ assert.Len(t, times, 4)
+
+ times, err = GetTrackedTimes(FindTrackedTimesOptions{RepositoryID: 10})
+ assert.NoError(t, err)
+ assert.Len(t, times, 0)
+}
+
+func TestTotalTimes(t *testing.T) {
+ assert.NoError(t, PrepareTestDatabase())
+
+ total, err := TotalTimes(FindTrackedTimesOptions{IssueID: 1})
+ assert.NoError(t, err)
+ assert.Len(t, total, 1)
+ for user, time := range total {
+ assert.Equal(t, int64(1), user.ID)
+ assert.Equal(t, "6min 40s", time)
+ }
+
+ total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 2})
+ assert.NoError(t, err)
+ assert.Len(t, total, 1)
+ for user, time := range total {
+ assert.Equal(t, int64(2), user.ID)
+ assert.Equal(t, "1h 1min 2s", time)
+ }
+
+ total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 5})
+ assert.NoError(t, err)
+ assert.Len(t, total, 1)
+ for user, time := range total {
+ assert.Equal(t, int64(2), user.ID)
+ assert.Equal(t, "1s", time)
+ }
+
+ total, err = TotalTimes(FindTrackedTimesOptions{IssueID: 4})
+ assert.NoError(t, err)
+ assert.Len(t, total, 0)
+}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 75ef8f6c58..a796c6d6af 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -126,6 +126,8 @@ var migrations = []Migration{
NewMigration("unescape user full names", unescapeUserFullNames),
// v38 -> v39
NewMigration("remove commits and settings unit types", removeCommitsUnitType),
+ // v39 -> v40
+ NewMigration("adds time tracking and stopwatches", addTimetracking),
}
// Migrate database to current version
diff --git a/models/migrations/v16.go b/models/migrations/v16.go
index 9cd4ef4da2..2a6d71de41 100644
--- a/models/migrations/v16.go
+++ b/models/migrations/v16.go
@@ -19,9 +19,9 @@ type RepoUnit struct {
RepoID int64 `xorm:"INDEX(s)"`
Type int `xorm:"INDEX(s)"`
Index int
- Config map[string]string `xorm:"JSON"`
- CreatedUnix int64 `xorm:"INDEX CREATED"`
- Created time.Time `xorm:"-"`
+ Config map[string]interface{} `xorm:"JSON"`
+ CreatedUnix int64 `xorm:"INDEX CREATED"`
+ Created time.Time `xorm:"-"`
}
// Enumerate all the unit types
@@ -95,7 +95,7 @@ func addUnitsToTables(x *xorm.Engine) error {
continue
}
- var config = make(map[string]string)
+ var config = make(map[string]interface{})
switch i {
case V16UnitTypeExternalTracker:
config["ExternalTrackerURL"] = repo.ExternalTrackerURL
diff --git a/models/migrations/v39.go b/models/migrations/v39.go
new file mode 100644
index 0000000000..a536cd9d62
--- /dev/null
+++ b/models/migrations/v39.go
@@ -0,0 +1,65 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+ "fmt"
+ "time"
+
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/go-xorm/xorm"
+)
+
+// Stopwatch see models/issue_stopwatch.go
+type Stopwatch struct {
+ ID int64 `xorm:"pk autoincr"`
+ IssueID int64 `xorm:"INDEX"`
+ UserID int64 `xorm:"INDEX"`
+ Created time.Time `xorm:"-"`
+ CreatedUnix int64
+}
+
+// TrackedTime see models/issue_tracked_time.go
+type TrackedTime struct {
+ ID int64 `xorm:"pk autoincr" json:"id"`
+ IssueID int64 `xorm:"INDEX" json:"issue_id"`
+ UserID int64 `xorm:"INDEX" json:"user_id"`
+ Created time.Time `xorm:"-" json:"created"`
+ CreatedUnix int64 `json:"-"`
+ Time int64 `json:"time"`
+}
+
+func addTimetracking(x *xorm.Engine) error {
+ if err := x.Sync2(new(Stopwatch)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+ if err := x.Sync2(new(TrackedTime)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+ //Updating existing issue units
+ var units []*RepoUnit
+ x.Where("type = ?", V16UnitTypeIssues).Find(&units)
+ for _, unit := range units {
+ if unit.Config == nil {
+ unit.Config = make(map[string]interface{})
+ }
+ changes := false
+ if _, ok := unit.Config["EnableTimetracker"]; !ok {
+ unit.Config["EnableTimetracker"] = setting.Service.DefaultEnableTimetracking
+ changes = true
+ }
+ if _, ok := unit.Config["AllowOnlyContributorsToTrackTime"]; !ok {
+ unit.Config["AllowOnlyContributorsToTrackTime"] = setting.Service.DefaultAllowOnlyContributorsToTrackTime
+ changes = true
+ }
+ if changes {
+ if _, err := x.Id(unit.ID).Cols("config").Update(unit); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/models/models.go b/models/models.go
index e5d3597ce7..0bcb3674da 100644
--- a/models/models.go
+++ b/models/models.go
@@ -112,6 +112,8 @@ func init() {
new(UserOpenID),
new(IssueWatch),
new(CommitStatus),
+ new(Stopwatch),
+ new(TrackedTime),
)
gonicNames := []string{"SSL", "UID"}
diff --git a/models/repo.go b/models/repo.go
index a629b7311f..1cce3854ee 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -32,8 +32,8 @@ import (
"github.com/Unknwon/cae/zip"
"github.com/Unknwon/com"
"github.com/go-xorm/xorm"
- version "github.com/mcuadros/go-version"
- ini "gopkg.in/ini.v1"
+ "github.com/mcuadros/go-version"
+ "gopkg.in/ini.v1"
)
const (
@@ -1224,11 +1224,21 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err
// insert units for repo
var units = make([]RepoUnit, 0, len(defaultRepoUnits))
for i, tp := range defaultRepoUnits {
- units = append(units, RepoUnit{
- RepoID: repo.ID,
- Type: tp,
- Index: i,
- })
+ if tp == UnitTypeIssues {
+ units = append(units, RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ Index: i,
+ Config: &IssuesConfig{EnableTimetracker: setting.Service.DefaultEnableTimetracking, AllowOnlyContributorsToTrackTime: setting.Service.DefaultAllowOnlyContributorsToTrackTime},
+ })
+ } else {
+ units = append(units, RepoUnit{
+ RepoID: repo.ID,
+ Type: tp,
+ Index: i,
+ })
+ }
+
}
if _, err = e.Insert(&units); err != nil {
diff --git a/models/repo_issue.go b/models/repo_issue.go
new file mode 100644
index 0000000000..10356d2c98
--- /dev/null
+++ b/models/repo_issue.go
@@ -0,0 +1,34 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import "code.gitea.io/gitea/modules/setting"
+
+// ___________.__ ___________ __
+// \__ ___/|__| _____ ___\__ ___/___________ ____ | | __ ___________
+// | | | |/ \_/ __ \| | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \
+// | | | | Y Y \ ___/| | | | \// __ \\ \___| <\ ___/| | \/
+// |____| |__|__|_| /\___ >____| |__| (____ /\___ >__|_ \\___ >__|
+// \/ \/ \/ \/ \/ \/
+
+// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs.
+func (repo *Repository) IsTimetrackerEnabled() bool {
+ var u *RepoUnit
+ var err error
+ if u, err = repo.GetUnit(UnitTypeIssues); err != nil {
+ return setting.Service.DefaultEnableTimetracking
+ }
+ return u.IssuesConfig().EnableTimetracker
+}
+
+// AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value
+func (repo *Repository) AllowOnlyContributorsToTrackTime() bool {
+ var u *RepoUnit
+ var err error
+ if u, err = repo.GetUnit(UnitTypeIssues); err != nil {
+ return setting.Service.DefaultAllowOnlyContributorsToTrackTime
+ }
+ return u.IssuesConfig().AllowOnlyContributorsToTrackTime
+}
diff --git a/models/repo_unit.go b/models/repo_unit.go
index 2256ff7ef6..1553fa771d 100644
--- a/models/repo_unit.go
+++ b/models/repo_unit.go
@@ -70,18 +70,36 @@ func (cfg *ExternalTrackerConfig) ToDB() ([]byte, error) {
return json.Marshal(cfg)
}
+// IssuesConfig describes issues config
+type IssuesConfig struct {
+ EnableTimetracker bool
+ AllowOnlyContributorsToTrackTime bool
+}
+
+// FromDB fills up a IssuesConfig from serialized format.
+func (cfg *IssuesConfig) FromDB(bs []byte) error {
+ return json.Unmarshal(bs, &cfg)
+}
+
+// ToDB exports a IssuesConfig to a serialized format.
+func (cfg *IssuesConfig) ToDB() ([]byte, error) {
+ return json.Marshal(cfg)
+}
+
// BeforeSet is invoked from XORM before setting the value of a field of this object.
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
switch colName {
case "type":
switch UnitType(Cell2Int64(val)) {
- case UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, UnitTypeReleases,
+ case UnitTypeCode, UnitTypePullRequests, UnitTypeReleases,
UnitTypeWiki:
r.Config = new(UnitConfig)
case UnitTypeExternalWiki:
r.Config = new(ExternalWikiConfig)
case UnitTypeExternalTracker:
r.Config = new(ExternalTrackerConfig)
+ case UnitTypeIssues:
+ r.Config = new(IssuesConfig)
default:
panic("unrecognized repo unit type: " + com.ToStr(*val))
}
@@ -106,11 +124,6 @@ func (r *RepoUnit) CodeConfig() *UnitConfig {
return r.Config.(*UnitConfig)
}
-// IssuesConfig returns config for UnitTypeIssues
-func (r *RepoUnit) IssuesConfig() *UnitConfig {
- return r.Config.(*UnitConfig)
-}
-
// PullRequestsConfig returns config for UnitTypePullRequests
func (r *RepoUnit) PullRequestsConfig() *UnitConfig {
return r.Config.(*UnitConfig)
@@ -126,6 +139,11 @@ func (r *RepoUnit) ExternalWikiConfig() *ExternalWikiConfig {
return r.Config.(*ExternalWikiConfig)
}
+// IssuesConfig returns config for UnitTypeIssues
+func (r *RepoUnit) IssuesConfig() *IssuesConfig {
+ return r.Config.(*IssuesConfig)
+}
+
// ExternalTrackerConfig returns config for UnitTypeExternalTracker
func (r *RepoUnit) ExternalTrackerConfig() *ExternalTrackerConfig {
return r.Config.(*ExternalTrackerConfig)