aboutsummaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
author6543 <6543@obermui.de>2020-01-09 12:56:32 +0100
committerzeripath <art27@cantab.net>2020-01-09 11:56:32 +0000
commit6baa5d7588bcf0e1fee8f4e4d77381b39b973363 (patch)
treec7fe9835cc8ead37a1a8d90555b3caeab11f027b /models
parentee9ce0cfa9c003b359d2d3fba9346fd0929e88f4 (diff)
downloadgitea-6baa5d7588bcf0e1fee8f4e4d77381b39b973363.tar.gz
gitea-6baa5d7588bcf0e1fee8f4e4d77381b39b973363.zip
[API] Add notification endpoint (#9488)
* [API] Add notification endpoints * add func GetNotifications(opts FindNotificationOptions) * add func (n *Notification) APIFormat() * add func (nl NotificationList) APIFormat() * add func (n *Notification) APIURL() * add func (nl NotificationList) APIFormat() * add LoadAttributes functions (loadRepo, loadIssue, loadComment, loadUser) * add func (c *Comment) APIURL() * add func (issue *Issue) GetLastComment() * add endpoint GET /notifications * add endpoint PUT /notifications * add endpoint GET /repos/{owner}/{repo}/notifications * add endpoint PUT /repos/{owner}/{repo}/notifications * add endpoint GET /notifications/threads/{id} * add endpoint PATCH /notifications/threads/{id} * Add TEST * code format * code format
Diffstat (limited to 'models')
-rw-r--r--models/fixtures/notification.yml29
-rw-r--r--models/issue.go14
-rw-r--r--models/issue_comment.go17
-rw-r--r--models/notification.go218
-rw-r--r--models/notification_test.go6
5 files changed, 258 insertions, 26 deletions
diff --git a/models/fixtures/notification.yml b/models/fixtures/notification.yml
index fe5c47287d..bd279d4bb2 100644
--- a/models/fixtures/notification.yml
+++ b/models/fixtures/notification.yml
@@ -7,7 +7,7 @@
updated_by: 2
issue_id: 1
created_unix: 946684800
- updated_unix: 946684800
+ updated_unix: 946684820
-
id: 2
@@ -17,8 +17,8 @@
source: 1 # issue
updated_by: 1
issue_id: 2
- created_unix: 946684800
- updated_unix: 946684800
+ created_unix: 946685800
+ updated_unix: 946685820
-
id: 3
@@ -27,9 +27,9 @@
status: 3 # pinned
source: 1 # issue
updated_by: 1
- issue_id: 2
- created_unix: 946684800
- updated_unix: 946684800
+ issue_id: 3
+ created_unix: 946686800
+ updated_unix: 946686800
-
id: 4
@@ -38,6 +38,17 @@
status: 1 # unread
source: 1 # issue
updated_by: 1
- issue_id: 2
- created_unix: 946684800
- updated_unix: 946684800 \ No newline at end of file
+ issue_id: 5
+ created_unix: 946687800
+ updated_unix: 946687800
+
+-
+ id: 5
+ user_id: 2
+ repo_id: 2
+ status: 1 # unread
+ source: 1 # issue
+ updated_by: 5
+ issue_id: 4
+ created_unix: 946688800
+ updated_unix: 946688820
diff --git a/models/issue.go b/models/issue.go
index 3986aeee15..aeeb70d27b 100644
--- a/models/issue.go
+++ b/models/issue.go
@@ -843,6 +843,20 @@ func (issue *Issue) GetLastEventLabel() string {
return "repo.issues.opened_by"
}
+// GetLastComment return last comment for the current issue.
+func (issue *Issue) GetLastComment() (*Comment, error) {
+ var c Comment
+ exist, err := x.Where("type = ?", CommentTypeComment).
+ And("issue_id = ?", issue.ID).Desc("id").Get(&c)
+ if err != nil {
+ return nil, err
+ }
+ if !exist {
+ return nil, nil
+ }
+ return &c, nil
+}
+
// GetLastEventLabelFake returns the localization label for the current issue without providing a link in the username.
func (issue *Issue) GetLastEventLabelFake() string {
if issue.IsClosed {
diff --git a/models/issue_comment.go b/models/issue_comment.go
index 3ba6790216..9caab1dc45 100644
--- a/models/issue_comment.go
+++ b/models/issue_comment.go
@@ -8,6 +8,7 @@ package models
import (
"fmt"
+ "path"
"strings"
"code.gitea.io/gitea/modules/git"
@@ -235,6 +236,22 @@ func (c *Comment) HTMLURL() string {
return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
}
+// APIURL formats a API-string to the issue-comment
+func (c *Comment) APIURL() string {
+ err := c.LoadIssue()
+ if err != nil { // Silently dropping errors :unamused:
+ log.Error("LoadIssue(%d): %v", c.IssueID, err)
+ return ""
+ }
+ err = c.Issue.loadRepo(x)
+ if err != nil { // Silently dropping errors :unamused:
+ log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
+ return ""
+ }
+
+ return c.Issue.Repo.APIURL() + "/" + path.Join("issues/comments", fmt.Sprint(c.ID))
+}
+
// IssueURL formats a URL-string to the issue
func (c *Comment) IssueURL() string {
err := c.LoadIssue()
diff --git a/models/notification.go b/models/notification.go
index 5c03b49257..8e9bca0dc6 100644
--- a/models/notification.go
+++ b/models/notification.go
@@ -6,8 +6,14 @@ package models
import (
"fmt"
+ "path"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
+
+ "xorm.io/builder"
+ "xorm.io/xorm"
)
type (
@@ -47,17 +53,67 @@ type Notification struct {
IssueID int64 `xorm:"INDEX NOT NULL"`
CommitID string `xorm:"INDEX"`
CommentID int64
- Comment *Comment `xorm:"-"`
UpdatedBy int64 `xorm:"INDEX NOT NULL"`
Issue *Issue `xorm:"-"`
Repository *Repository `xorm:"-"`
+ Comment *Comment `xorm:"-"`
+ User *User `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"created INDEX NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated INDEX NOT NULL"`
}
+// FindNotificationOptions represent the filters for notifications. If an ID is 0 it will be ignored.
+type FindNotificationOptions struct {
+ UserID int64
+ RepoID int64
+ IssueID int64
+ Status NotificationStatus
+ UpdatedAfterUnix int64
+ UpdatedBeforeUnix int64
+}
+
+// ToCond will convert each condition into a xorm-Cond
+func (opts *FindNotificationOptions) ToCond() builder.Cond {
+ cond := builder.NewCond()
+ if opts.UserID != 0 {
+ cond = cond.And(builder.Eq{"notification.user_id": opts.UserID})
+ }
+ if opts.RepoID != 0 {
+ cond = cond.And(builder.Eq{"notification.repo_id": opts.RepoID})
+ }
+ if opts.IssueID != 0 {
+ cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
+ }
+ if opts.Status != 0 {
+ cond = cond.And(builder.Eq{"notification.status": opts.Status})
+ }
+ if opts.UpdatedAfterUnix != 0 {
+ cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})
+ }
+ if opts.UpdatedBeforeUnix != 0 {
+ cond = cond.And(builder.Lte{"notification.updated_unix": opts.UpdatedBeforeUnix})
+ }
+ return cond
+}
+
+// ToSession will convert the given options to a xorm Session by using the conditions from ToCond and joining with issue table if required
+func (opts *FindNotificationOptions) ToSession(e Engine) *xorm.Session {
+ return e.Where(opts.ToCond())
+}
+
+func getNotifications(e Engine, options FindNotificationOptions) (nl NotificationList, err error) {
+ err = options.ToSession(e).OrderBy("notification.updated_unix DESC").Find(&nl)
+ return
+}
+
+// GetNotifications returns all notifications that fit to the given options.
+func GetNotifications(opts FindNotificationOptions) (NotificationList, error) {
+ return getNotifications(x, opts)
+}
+
// CreateOrUpdateIssueNotifications creates an issue notification
// for each watcher, or updates it if already exists
func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error {
@@ -238,22 +294,124 @@ func notificationsForUser(e Engine, user *User, statuses []NotificationStatus, p
return
}
+// APIFormat converts a Notification to api.NotificationThread
+func (n *Notification) APIFormat() *api.NotificationThread {
+ result := &api.NotificationThread{
+ ID: n.ID,
+ Unread: !(n.Status == NotificationStatusRead || n.Status == NotificationStatusPinned),
+ Pinned: n.Status == NotificationStatusPinned,
+ UpdatedAt: n.UpdatedUnix.AsTime(),
+ URL: n.APIURL(),
+ }
+
+ //since user only get notifications when he has access to use minimal access mode
+ if n.Repository != nil {
+ result.Repository = n.Repository.APIFormat(AccessModeRead)
+ }
+
+ //handle Subject
+ switch n.Source {
+ case NotificationSourceIssue:
+ result.Subject = &api.NotificationSubject{Type: "Issue"}
+ if n.Issue != nil {
+ result.Subject.Title = n.Issue.Title
+ result.Subject.URL = n.Issue.APIURL()
+ comment, err := n.Issue.GetLastComment()
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL()
+ }
+ }
+ case NotificationSourcePullRequest:
+ result.Subject = &api.NotificationSubject{Type: "Pull"}
+ if n.Issue != nil {
+ result.Subject.Title = n.Issue.Title
+ result.Subject.URL = n.Issue.APIURL()
+ comment, err := n.Issue.GetLastComment()
+ if err == nil && comment != nil {
+ result.Subject.LatestCommentURL = comment.APIURL()
+ }
+ }
+ case NotificationSourceCommit:
+ result.Subject = &api.NotificationSubject{
+ Type: "Commit",
+ Title: n.CommitID,
+ }
+ //unused until now
+ }
+
+ return result
+}
+
+// LoadAttributes load Repo Issue User and Comment if not loaded
+func (n *Notification) LoadAttributes() (err error) {
+ return n.loadAttributes(x)
+}
+
+func (n *Notification) loadAttributes(e Engine) (err error) {
+ if err = n.loadRepo(e); err != nil {
+ return
+ }
+ if err = n.loadIssue(e); err != nil {
+ return
+ }
+ if err = n.loadUser(e); err != nil {
+ return
+ }
+ if err = n.loadComment(e); err != nil {
+ return
+ }
+ return
+}
+
+func (n *Notification) loadRepo(e Engine) (err error) {
+ if n.Repository == nil {
+ n.Repository, err = getRepositoryByID(e, n.RepoID)
+ if err != nil {
+ return fmt.Errorf("getRepositoryByID [%d]: %v", n.RepoID, err)
+ }
+ }
+ return nil
+}
+
+func (n *Notification) loadIssue(e Engine) (err error) {
+ if n.Issue == nil {
+ n.Issue, err = getIssueByID(e, n.IssueID)
+ if err != nil {
+ return fmt.Errorf("getIssueByID [%d]: %v", n.IssueID, err)
+ }
+ return n.Issue.loadAttributes(e)
+ }
+ return nil
+}
+
+func (n *Notification) loadComment(e Engine) (err error) {
+ if n.Comment == nil && n.CommentID > 0 {
+ n.Comment, err = GetCommentByID(n.CommentID)
+ if err != nil {
+ return fmt.Errorf("GetCommentByID [%d]: %v", n.CommentID, err)
+ }
+ }
+ return nil
+}
+
+func (n *Notification) loadUser(e Engine) (err error) {
+ if n.User == nil {
+ n.User, err = getUserByID(e, n.UserID)
+ if err != nil {
+ return fmt.Errorf("getUserByID [%d]: %v", n.UserID, err)
+ }
+ }
+ return nil
+}
+
// GetRepo returns the repo of the notification
func (n *Notification) GetRepo() (*Repository, error) {
- n.Repository = new(Repository)
- _, err := x.
- Where("id = ?", n.RepoID).
- Get(n.Repository)
- return n.Repository, err
+ return n.Repository, n.loadRepo(x)
}
// GetIssue returns the issue of the notification
func (n *Notification) GetIssue() (*Issue, error) {
- n.Issue = new(Issue)
- _, err := x.
- Where("id = ?", n.IssueID).
- Get(n.Issue)
- return n.Issue, err
+ return n.Issue, n.loadIssue(x)
}
// HTMLURL formats a URL-string to the notification
@@ -264,9 +422,34 @@ func (n *Notification) HTMLURL() string {
return n.Issue.HTMLURL()
}
+// APIURL formats a URL-string to the notification
+func (n *Notification) APIURL() string {
+ return setting.AppURL + path.Join("api/v1/notifications/threads", fmt.Sprintf("%d", n.ID))
+}
+
// NotificationList contains a list of notifications
type NotificationList []*Notification
+// APIFormat converts a NotificationList to api.NotificationThread list
+func (nl NotificationList) APIFormat() []*api.NotificationThread {
+ var result = make([]*api.NotificationThread, 0, len(nl))
+ for _, n := range nl {
+ result = append(result, n.APIFormat())
+ }
+ return result
+}
+
+// LoadAttributes load Repo Issue User and Comment if not loaded
+func (nl NotificationList) LoadAttributes() (err error) {
+ for i := 0; i < len(nl); i++ {
+ err = nl[i].LoadAttributes()
+ if err != nil {
+ return
+ }
+ }
+ return
+}
+
func (nl NotificationList) getPendingRepoIDs() []int64 {
var ids = make(map[int64]struct{}, len(nl))
for _, notification := range nl {
@@ -486,7 +669,7 @@ func setNotificationStatusReadIfUnread(e Engine, userID, issueID int64) error {
// SetNotificationStatus change the notification status
func SetNotificationStatus(notificationID int64, user *User, status NotificationStatus) error {
- notification, err := getNotificationByID(notificationID)
+ notification, err := getNotificationByID(x, notificationID)
if err != nil {
return err
}
@@ -501,9 +684,14 @@ func SetNotificationStatus(notificationID int64, user *User, status Notification
return err
}
-func getNotificationByID(notificationID int64) (*Notification, error) {
+// GetNotificationByID return notification by ID
+func GetNotificationByID(notificationID int64) (*Notification, error) {
+ return getNotificationByID(x, notificationID)
+}
+
+func getNotificationByID(e Engine, notificationID int64) (*Notification, error) {
notification := new(Notification)
- ok, err := x.
+ ok, err := e.
Where("id = ?", notificationID).
Get(notification)
@@ -512,7 +700,7 @@ func getNotificationByID(notificationID int64) (*Notification, error) {
}
if !ok {
- return nil, fmt.Errorf("Notification %d does not exists", notificationID)
+ return nil, ErrNotExist{ID: notificationID}
}
return notification, nil
diff --git a/models/notification_test.go b/models/notification_test.go
index 728be7182c..6485f8dc7a 100644
--- a/models/notification_test.go
+++ b/models/notification_test.go
@@ -31,11 +31,13 @@ func TestNotificationsForUser(t *testing.T) {
statuses := []NotificationStatus{NotificationStatusRead, NotificationStatusUnread}
notfs, err := NotificationsForUser(user, statuses, 1, 10)
assert.NoError(t, err)
- if assert.Len(t, notfs, 2) {
- assert.EqualValues(t, 2, notfs[0].ID)
+ if assert.Len(t, notfs, 3) {
+ assert.EqualValues(t, 5, notfs[0].ID)
assert.EqualValues(t, user.ID, notfs[0].UserID)
assert.EqualValues(t, 4, notfs[1].ID)
assert.EqualValues(t, user.ID, notfs[1].UserID)
+ assert.EqualValues(t, 2, notfs[2].ID)
+ assert.EqualValues(t, user.ID, notfs[2].UserID)
}
}