summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrey Nering <andrey.nering@gmail.com>2017-04-01 15:12:24 -0300
committerGitHub <noreply@github.com>2017-04-01 15:12:24 -0300
commit37a34c1a28c1a49e00adfef751b44de2f1b84d4b (patch)
treed47958e8e7de22bb32c1cff28d19b819653233bd
parent88112a53249077adf26e5200f723faa1570641e6 (diff)
parentf6e5ce65b28fb6c97d9011c1fbf2950acf7c0647 (diff)
downloadgitea-37a34c1a28c1a49e00adfef751b44de2f1b84d4b.tar.gz
gitea-37a34c1a28c1a49e00adfef751b44de2f1b84d4b.zip
Merge pull request #1410 from andreynering/notification/issue-watch
[Notifications Step 6] Per issue/PR watch/unwatch
-rw-r--r--cmd/web.go1
-rw-r--r--models/fixtures/issue_watch.yml15
-rw-r--r--models/issue_watch.go96
-rw-r--r--models/issue_watch_test.go51
-rw-r--r--models/models.go1
-rw-r--r--models/notification.go40
-rw-r--r--options/locale/locale_en-US.ini2
-rw-r--r--routers/repo/issue.go14
-rw-r--r--routers/repo/issue_watch.go38
-rw-r--r--templates/repo/issue/view_content/sidebar.tmpl21
10 files changed, 271 insertions, 8 deletions
diff --git a/cmd/web.go b/cmd/web.go
index 1f2561ca68..b2cc3959a2 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/fixtures/issue_watch.yml b/models/fixtures/issue_watch.yml
new file mode 100644
index 0000000000..596662d20c
--- /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.go b/models/issue_watch.go
new file mode 100644
index 0000000000..37511787e5
--- /dev/null
+++ b/models/issue_watch.go
@@ -0,0 +1,96 @@
+// 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"
+)
+
+// 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"`
+ 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() {
+ 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() {
+ var (
+ t = time.Now()
+ u = t.Unix()
+ )
+ iw.Updated = t
+ iw.UpdatedUnix = u
+}
+
+// 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 {
+ iw.IsWatching = isWatching
+
+ if _, err := x.Id(iw.ID).Cols("is_watching", "updated_unix").Update(iw); 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) {
+ return getIssueWatch(x, userID, issueID)
+}
+
+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
+}
+
+// GetIssueWatchers returns watchers/unwatchers of a given issue
+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/issue_watch_test.go b/models/issue_watch_test.go
new file mode 100644
index 0000000000..d8b456c3ae
--- /dev/null
+++ b/models/issue_watch_test.go
@@ -0,0 +1,51 @@
+// 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)
+
+ _, exists, err = GetIssueWatch(3, 1)
+ assert.Equal(t, false, 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))
+
+ iws, err = GetIssueWatchers(5)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(iws))
+}
diff --git a/models/models.go b/models/models.go
index 2ae6e355fc..a1332ac23e 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"}
diff --git a/models/notification.go b/models/notification.go
index bba662c06c..a59c6f1401 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 {
- continue
+ if userID == notificationAuthorID {
+ return nil
}
- if notificationExists(notifications, issue.ID, watch.UserID) {
- err = updateIssueNotification(e, watch.UserID, issue.ID, notificationAuthorID)
- } else {
- err = createIssueNotification(e, watch.UserID, issue, notificationAuthorID)
+ if _, ok := alreadyNotified[userID]; ok {
+ return nil
}
+ alreadyNotified[userID] = struct{}{}
- if err != nil {
+ 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 err := notifyUser(issueWatch.UserID); err != nil {
return err
}
}
+ for _, watch := range watches {
+ if err := notifyUser(watch.UserID); err != nil {
+ return err
+ }
+ }
return nil
}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 80260d4b7d..35a5244940 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -652,6 +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.subscribe = Subscribe
+issues.unsubscribe = Unsubscribe
pulls.new = New Pull Request
pulls.compare_changes = Compare Changes
diff --git a/routers/repo/issue.go b/routers/repo/issue.go
index 0a723d755b..61f79a239c 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 0000000000..382798025e
--- /dev/null
+++ b/routers/repo/issue_watch.go
@@ -0,0 +1,38 @@
+// 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 (
+ "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 ea46e5f94d..28bd755e41 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -98,5 +98,26 @@
{{end}}
</div>
</div>
+
+ <div class="ui divider"></div>
+
+ <div class="ui watching">
+ <span class="text"><strong>{{.i18n.Tr "notification.notifications"}}</strong></span>
+ <div>
+ <form method="POST" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/watch">
+ <input type="hidden" name="watch" value="{{if $.IssueWatch.IsWatching}}0{{else}}1{{end}}" />
+ {{$.CsrfTokenHtml}}
+ <button class="fluid ui button">
+ {{if $.IssueWatch.IsWatching}}
+ <i class="octicon octicon-mute"></i>
+ {{.i18n.Tr "repo.issues.unsubscribe"}}
+ {{else}}
+ <i class="octicon octicon-unmute"></i>
+ {{.i18n.Tr "repo.issues.subscribe"}}
+ {{end}}
+ </button>
+ </form>
+ </div>
+ </div>
</div>
</div>