Merge pull request #1410 from andreynering/notification/issue-watch

[Notifications Step 6] Per issue/PR watch/unwatch
This commit is contained in:
Andrey Nering 2017-04-01 15:12:24 -03:00 committed by GitHub
commit 37a34c1a28
10 changed files with 272 additions and 9 deletions

View file

@ -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
models/issue_watch.go Normal file
View file

@ -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
}

View file

@ -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))
}

View file

@ -117,6 +117,7 @@ func init() {
new(ExternalLoginUser),
new(ProtectedBranch),
new(UserOpenID),
new(IssueWatch),
)
gonicNames := []string{"SSL", "UID"}

View file

@ -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
}