From a0d0de7233cd8a85d6572ae13d74078482a1ee27 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sun, 19 Mar 2017 16:54:12 -0300 Subject: [PATCH 01/12] Create issue_watch table --- models/issue_watch.go | 20 ++++++++++++++++++++ models/models.go | 1 + 2 files changed, 21 insertions(+) create mode 100644 models/issue_watch.go diff --git a/models/issue_watch.go b/models/issue_watch.go new file mode 100644 index 000000000..96e080136 --- /dev/null +++ b/models/issue_watch.go @@ -0,0 +1,20 @@ +package models + +import ( + "time" +) + +// IssueWatch is connection request for receiving issue notification. +type IssueWatch struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(watch) NOT NULL"` + IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"` + IsWatching bool `xorm:"NOT NULL"` + Created time.Time `xorm:"-"` + CreatedUnix int64 `xorm:"NOT NULL"` +} + +// BeforeInsert is invoked from XORM before inserting an object of this type. +func (iw *IssueWatch) BeforeInsert() { + iw.CreatedUnix = time.Now().Unix() +} diff --git a/models/models.go b/models/models.go index 2ae6e355f..a1332ac23 100644 --- a/models/models.go +++ b/models/models.go @@ -117,6 +117,7 @@ func init() { new(ExternalLoginUser), new(ProtectedBranch), new(UserOpenID), + new(IssueWatch), ) gonicNames := []string{"SSL", "UID"} From b6744607484008826d18f129326664105b9d7bfc Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 29 Mar 2017 20:31:47 -0300 Subject: [PATCH 02/12] Add watch button on issue --- cmd/web.go | 1 + models/issue_watch.go | 40 +++++++++++++++++++ options/locale/locale_en-US.ini | 3 ++ routers/repo/issue.go | 14 +++++++ routers/repo/issue_watch.go | 34 ++++++++++++++++ .../repo/issue/view_content/sidebar.tmpl | 19 +++++++++ 6 files changed, 111 insertions(+) create mode 100644 routers/repo/issue_watch.go diff --git a/cmd/web.go b/cmd/web.go index 1f2561ca6..b2cc3959a 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -491,6 +491,7 @@ func runWeb(ctx *cli.Context) error { m.Group("/:index", func() { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) + m.Post("/watch", repo.IssueWatch) m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment) }) diff --git a/models/issue_watch.go b/models/issue_watch.go index 96e080136..d082211c7 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -16,5 +16,45 @@ type IssueWatch struct { // BeforeInsert is invoked from XORM before inserting an object of this type. func (iw *IssueWatch) BeforeInsert() { + iw.Created = time.Now() iw.CreatedUnix = time.Now().Unix() } + +// CreateOrUpdateIssueWatch set watching for a user and issue +func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error { + iw, exists, err := getIssueWatch(x, userID, issueID) + if err != nil { + return err + } + + if !exists { + iw = &IssueWatch{ + UserID: userID, + IssueID: issueID, + IsWatching: isWatching, + } + + if _, err := x.Insert(iw); err != nil { + return err + } + } else { + if _, err := x.Table(&IssueWatch{}).Id(iw.ID).Update(map[string]interface{}{"is_watching": isWatching}); err != nil { + return err + } + } + return nil +} + +// GetIssueWatch returns an issue watch by user and issue +func GetIssueWatch(userID, issueID int64) (iw *IssueWatch, exists bool, err error) { + iw, exists, err = getIssueWatch(x, userID, issueID) + return +} +func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool, err error) { + iw = new(IssueWatch) + exists, err = e. + Where("user_id = ?", userID). + And("issue_id = ?", issueID). + Get(iw) + return +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 80260d4b7..822d9cdc9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -652,6 +652,9 @@ issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically issues.num_participants = %d Participants issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.download = `Click to download "%s"` +issues.watch = Watch +issues.watch_issue = Watch issue +issues.unwatch_issue = Unwatch issue pulls.new = New Pull Request pulls.compare_changes = Compare Changes diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 0a723d755..61f79a239 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -465,6 +465,20 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) + iw, exists, err := models.GetIssueWatch(ctx.User.ID, issue.ID) + if err != nil { + ctx.Handle(500, "GetIssueWatch", err) + return + } + if !exists { + iw = &models.IssueWatch{ + UserID: ctx.User.ID, + IssueID: issue.ID, + IsWatching: models.IsWatching(ctx.User.ID, ctx.Repo.Repository.ID), + } + } + ctx.Data["IssueWatch"] = iw + // Make sure type and URL matches. if ctx.Params(":type") == "issues" && issue.IsPull { ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index)) diff --git a/routers/repo/issue_watch.go b/routers/repo/issue_watch.go new file mode 100644 index 000000000..64c99c5f7 --- /dev/null +++ b/routers/repo/issue_watch.go @@ -0,0 +1,34 @@ +package repo + +import ( + "fmt" + "net/http" + "strconv" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" +) + +// IssueWatch sets issue watching +func IssueWatch(c *context.Context) { + watch, err := strconv.ParseBool(c.Req.PostForm.Get("watch")) + if err != nil { + c.Handle(http.StatusInternalServerError, "watch is not bool", err) + return + } + + issueIndex := c.ParamsInt64("index") + issue, err := models.GetIssueByIndex(c.Repo.Repository.ID, issueIndex) + if err != nil { + c.Handle(http.StatusInternalServerError, "GetIssueByIndex", err) + return + } + + if err := models.CreateOrUpdateIssueWatch(c.User.ID, issue.ID, watch); err != nil { + c.Handle(http.StatusInternalServerError, "CreateOrUpdateIssueWatch", err) + return + } + + url := fmt.Sprintf("%s/issues/%d", c.Repo.RepoLink, issueIndex) + c.Redirect(url, http.StatusSeeOther) +} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index ea46e5f94..9a4a6cb1a 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -98,5 +98,24 @@ {{end}} + +
+ +
+ {{.i18n.Tr "repo.issues.watch"}} +
+
+ + {{$.CsrfTokenHtml}} + +
+
+
From aa6e949b3de11e675311e54b59e027cd82a230f4 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 29 Mar 2017 20:54:57 -0300 Subject: [PATCH 03/12] Consider issue_watchers while sending notifications --- models/issue_watch.go | 10 ++++++++++ models/notification.go | 42 +++++++++++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/models/issue_watch.go b/models/issue_watch.go index d082211c7..03a677a3a 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -58,3 +58,13 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool Get(iw) return } + +func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) { + return getIssueWatchers(x, issueID) +} +func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) { + err = e. + Where("issue_id = ?", issueID). + Find(&watches) + return +} diff --git a/models/notification.go b/models/notification.go index bba662c06..a59c6f140 100644 --- a/models/notification.go +++ b/models/notification.go @@ -96,6 +96,11 @@ func CreateOrUpdateIssueNotifications(issue *Issue, notificationAuthorID int64) } func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthorID int64) error { + issueWatches, err := getIssueWatchers(e, issue.ID) + if err != nil { + return err + } + watches, err := getWatchers(e, issue.RepoID) if err != nil { return err @@ -106,23 +111,42 @@ func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthor return err } - for _, watch := range watches { + alreadyNotified := make(map[int64]struct{}, len(issueWatches)+len(watches)) + + notifyUser := func(userID int64) error { // do not send notification for the own issuer/commenter - if watch.UserID == notificationAuthorID { + if userID == notificationAuthorID { + return nil + } + + if _, ok := alreadyNotified[userID]; ok { + return nil + } + alreadyNotified[userID] = struct{}{} + + if notificationExists(notifications, issue.ID, userID) { + return updateIssueNotification(e, userID, issue.ID, notificationAuthorID) + } + return createIssueNotification(e, userID, issue, notificationAuthorID) + } + + for _, issueWatch := range issueWatches { + // ignore if user unwatched the issue + if !issueWatch.IsWatching { + alreadyNotified[issueWatch.UserID] = struct{}{} continue } - if notificationExists(notifications, issue.ID, watch.UserID) { - err = updateIssueNotification(e, watch.UserID, issue.ID, notificationAuthorID) - } else { - err = createIssueNotification(e, watch.UserID, issue, notificationAuthorID) - } - - if err != nil { + if err := notifyUser(issueWatch.UserID); err != nil { return err } } + for _, watch := range watches { + if err := notifyUser(watch.UserID); err != nil { + return err + } + } return nil } From cb362513f027ca8e2c53204f5f2ea447ad06bf05 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 29 Mar 2017 20:59:28 -0300 Subject: [PATCH 04/12] Add updated_unix column on issue_watch --- models/issue_watch.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/models/issue_watch.go b/models/issue_watch.go index 03a677a3a..1e2650963 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -12,12 +12,21 @@ type IssueWatch struct { IsWatching bool `xorm:"NOT NULL"` Created time.Time `xorm:"-"` CreatedUnix int64 `xorm:"NOT NULL"` + Updated time.Time `xorm:"-"` + UpdatedUnix int64 `xorm:"NOT NULL"` } // BeforeInsert is invoked from XORM before inserting an object of this type. func (iw *IssueWatch) BeforeInsert() { iw.Created = time.Now() iw.CreatedUnix = time.Now().Unix() + iw.Updated = time.Now() + iw.UpdatedUnix = time.Now().Unix() +} + +func (iw *IssueWatch) BeforeUpdate() { + iw.Updated = time.Now() + iw.UpdatedUnix = time.Now().Unix() } // CreateOrUpdateIssueWatch set watching for a user and issue @@ -38,7 +47,9 @@ func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error { return err } } else { - if _, err := x.Table(&IssueWatch{}).Id(iw.ID).Update(map[string]interface{}{"is_watching": isWatching}); err != nil { + iw.IsWatching = isWatching + + if _, err := x.Id(iw.ID).Cols("is_watching", "updated_unix").Update(iw); err != nil { return err } } From e4a33ed4d0d078c6d8a5b9025865c805f7eee179 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 29 Mar 2017 21:08:46 -0300 Subject: [PATCH 05/12] Add octicons to watch/unwatch buttons --- templates/repo/issue/view_content/sidebar.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 9a4a6cb1a..a9df130df 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -109,8 +109,10 @@ {{$.CsrfTokenHtml}} From caed86fc6ef87d11035889a11b8949c839e19d52 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 29 Mar 2017 21:18:28 -0300 Subject: [PATCH 06/12] Fix lint --- models/issue_watch.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/issue_watch.go b/models/issue_watch.go index 1e2650963..d08e1253e 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -24,6 +24,7 @@ func (iw *IssueWatch) BeforeInsert() { iw.UpdatedUnix = time.Now().Unix() } +// BeforeUpdate is invoked from XORM before updating an object of this type. func (iw *IssueWatch) BeforeUpdate() { iw.Updated = time.Now() iw.UpdatedUnix = time.Now().Unix() @@ -70,6 +71,7 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool return } +// GetIssueWatchers returns watchers/unwatchers of a given issue func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) { return getIssueWatchers(x, issueID) } From 4b284f814c21c34b61f94f7daa39d6254246ab5f Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 30 Mar 2017 19:10:30 -0300 Subject: [PATCH 07/12] UI and translation improvements --- options/locale/locale_en-US.ini | 5 ++--- templates/repo/issue/view_content/sidebar.tmpl | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 822d9cdc9..35a524494 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -652,9 +652,8 @@ issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically issues.num_participants = %d Participants issues.attachment.open_tab = `Click to see "%s" in a new tab` issues.attachment.download = `Click to download "%s"` -issues.watch = Watch -issues.watch_issue = Watch issue -issues.unwatch_issue = Unwatch issue +issues.subscribe = Subscribe +issues.unsubscribe = Unsubscribe pulls.new = New Pull Request pulls.compare_changes = Compare Changes diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index a9df130df..28bd755e4 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -102,7 +102,7 @@
- {{.i18n.Tr "repo.issues.watch"}} + {{.i18n.Tr "notification.notifications"}}
@@ -110,10 +110,10 @@
From 18952c40f80d41d2edc582305265dbfe3b62120d Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 30 Mar 2017 19:11:58 -0300 Subject: [PATCH 08/12] Add copyright headers --- models/issue_watch.go | 4 ++++ routers/repo/issue_watch.go | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/models/issue_watch.go b/models/issue_watch.go index d08e1253e..c48f05b11 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -1,3 +1,7 @@ +// 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 ( diff --git a/routers/repo/issue_watch.go b/routers/repo/issue_watch.go index 64c99c5f7..382798025 100644 --- a/routers/repo/issue_watch.go +++ b/routers/repo/issue_watch.go @@ -1,3 +1,7 @@ +// 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 repo import ( From a90ffffb1a84d79f3e08bd1c7b90ab2f565833a0 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 30 Mar 2017 19:14:16 -0300 Subject: [PATCH 09/12] Use variables for times --- models/issue_watch.go | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/models/issue_watch.go b/models/issue_watch.go index c48f05b11..8262ae2ea 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -22,16 +22,24 @@ type IssueWatch struct { // BeforeInsert is invoked from XORM before inserting an object of this type. func (iw *IssueWatch) BeforeInsert() { - iw.Created = time.Now() - iw.CreatedUnix = time.Now().Unix() - iw.Updated = time.Now() - iw.UpdatedUnix = time.Now().Unix() + var ( + t = time.Now() + u = t.Unix() + ) + iw.Created = t + iw.CreatedUnix = u + iw.Updated = t + iw.UpdatedUnix = u } // BeforeUpdate is invoked from XORM before updating an object of this type. func (iw *IssueWatch) BeforeUpdate() { - iw.Updated = time.Now() - iw.UpdatedUnix = time.Now().Unix() + var ( + t = time.Now() + u = t.Unix() + ) + iw.Updated = t + iw.UpdatedUnix = u } // CreateOrUpdateIssueWatch set watching for a user and issue From e6781d5488849d9415abc2402e914259214a13c4 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 30 Mar 2017 20:20:08 -0300 Subject: [PATCH 10/12] Add unit tests for issue_watch --- models/fixtures/issue_watch.yml | 15 ++++++++++++ models/issue_watch_test.go | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 models/fixtures/issue_watch.yml create mode 100644 models/issue_watch_test.go diff --git a/models/fixtures/issue_watch.yml b/models/fixtures/issue_watch.yml new file mode 100644 index 000000000..596662d20 --- /dev/null +++ b/models/fixtures/issue_watch.yml @@ -0,0 +1,15 @@ +- + id: 1 + user_id: 1 + issue_id: 1 + is_watching: true + created_unix: 946684800 + updated_unix: 946684800 + +- + id: 2 + user_id: 2 + issue_id: 2 + is_watching: false + created_unix: 946684800 + updated_unix: 946684800 diff --git a/models/issue_watch_test.go b/models/issue_watch_test.go new file mode 100644 index 000000000..5b820ded7 --- /dev/null +++ b/models/issue_watch_test.go @@ -0,0 +1,42 @@ +// 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 ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateOrUpdateIssueWatch(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + assert.NoError(t, CreateOrUpdateIssueWatch(3, 1, true)) + iw := AssertExistsAndLoadBean(t, &IssueWatch{UserID: 3, IssueID: 1}).(*IssueWatch) + assert.Equal(t, true, iw.IsWatching) + + assert.NoError(t, CreateOrUpdateIssueWatch(1, 1, false)) + iw = AssertExistsAndLoadBean(t, &IssueWatch{UserID: 1, IssueID: 1}).(*IssueWatch) + assert.Equal(t, false, iw.IsWatching) +} + +func TestGetIssueWatch(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + _, exists, err := GetIssueWatch(1, 1) + assert.Equal(t, true, exists) + assert.NoError(t, err) + _, exists, err = GetIssueWatch(2, 2) + assert.Equal(t, true, exists) + assert.NoError(t, err) +} + +func TestGetIssueWatchers(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + iws, err := GetIssueWatchers(1) + assert.NoError(t, err) + assert.Equal(t, 1, len(iws)) +} From e5c56fe30ddfe2ea5b065a286cb467a914d044e8 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 1 Apr 2017 09:58:20 -0300 Subject: [PATCH 11/12] Code style fixes --- models/issue_watch.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/models/issue_watch.go b/models/issue_watch.go index 8262ae2ea..37511787e 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -71,9 +71,9 @@ func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error { // GetIssueWatch returns an issue watch by user and issue func GetIssueWatch(userID, issueID int64) (iw *IssueWatch, exists bool, err error) { - iw, exists, err = getIssueWatch(x, userID, issueID) - return + return getIssueWatch(x, userID, issueID) } + func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool, err error) { iw = new(IssueWatch) exists, err = e. @@ -87,6 +87,7 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) { return getIssueWatchers(x, issueID) } + func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) { err = e. Where("issue_id = ?", issueID). From f6e5ce65b28fb6c97d9011c1fbf2950acf7c0647 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 1 Apr 2017 10:05:58 -0300 Subject: [PATCH 12/12] Improve tests a little --- models/issue_watch_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/models/issue_watch_test.go b/models/issue_watch_test.go index 5b820ded7..d8b456c3a 100644 --- a/models/issue_watch_test.go +++ b/models/issue_watch_test.go @@ -28,9 +28,14 @@ func TestGetIssueWatch(t *testing.T) { _, exists, err := GetIssueWatch(1, 1) assert.Equal(t, true, exists) assert.NoError(t, err) + _, exists, err = GetIssueWatch(2, 2) assert.Equal(t, true, exists) assert.NoError(t, err) + + _, exists, err = GetIssueWatch(3, 1) + assert.Equal(t, false, exists) + assert.NoError(t, err) } func TestGetIssueWatchers(t *testing.T) { @@ -39,4 +44,8 @@ func TestGetIssueWatchers(t *testing.T) { iws, err := GetIssueWatchers(1) assert.NoError(t, err) assert.Equal(t, 1, len(iws)) + + iws, err = GetIssueWatchers(5) + assert.NoError(t, err) + assert.Equal(t, 0, len(iws)) }