Skip to content

[Notifications Step 6] Per issue/PR watch/unwatch #1410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 1, 2017
1 change: 1 addition & 0 deletions cmd/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down
15 changes: 15 additions & 0 deletions models/fixtures/issue_watch.yml
Original file line number Diff line number Diff line change
@@ -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
96 changes: 96 additions & 0 deletions models/issue_watch.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright header please


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
}
51 changes: 51 additions & 0 deletions models/issue_watch_test.go
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also check an issue with multiple watchers and check the actual content of the returned slice


iws, err = GetIssueWatchers(5)
assert.NoError(t, err)
assert.Equal(t, 0, len(iws))
}
1 change: 1 addition & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func init() {
new(ExternalLoginUser),
new(ProtectedBranch),
new(UserOpenID),
new(IssueWatch),
)

gonicNames := []string{"SSL", "UID"}
Expand Down
40 changes: 32 additions & 8 deletions models/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions routers/repo/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
38 changes: 38 additions & 0 deletions routers/repo/issue_watch.go
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copyright header please


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)
}
21 changes: 21 additions & 0 deletions templates/repo/issue/view_content/sidebar.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>