diff options
author | 6543 <6543@obermui.de> | 2020-01-09 12:56:32 +0100 |
---|---|---|
committer | zeripath <art27@cantab.net> | 2020-01-09 11:56:32 +0000 |
commit | 6baa5d7588bcf0e1fee8f4e4d77381b39b973363 (patch) | |
tree | c7fe9835cc8ead37a1a8d90555b3caeab11f027b /models | |
parent | ee9ce0cfa9c003b359d2d3fba9346fd0929e88f4 (diff) | |
download | gitea-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.yml | 29 | ||||
-rw-r--r-- | models/issue.go | 14 | ||||
-rw-r--r-- | models/issue_comment.go | 17 | ||||
-rw-r--r-- | models/notification.go | 218 | ||||
-rw-r--r-- | models/notification_test.go | 6 |
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) } } |