From 6baa5d7588bcf0e1fee8f4e4d77381b39b973363 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 9 Jan 2020 12:56:32 +0100 Subject: [PATCH 01/15] [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 --- integrations/api_notification_test.go | 106 +++++++++ models/fixtures/notification.yml | 29 ++- models/issue.go | 14 ++ models/issue_comment.go | 17 ++ models/notification.go | 218 ++++++++++++++++-- models/notification_test.go | 6 +- modules/structs/notifications.go | 28 +++ routers/api/v1/admin/user.go | 4 +- routers/api/v1/api.go | 14 ++ routers/api/v1/notify/repo.go | 151 +++++++++++++ routers/api/v1/notify/threads.go | 101 +++++++++ routers/api/v1/notify/user.go | 129 +++++++++++ routers/api/v1/repo/pull.go | 2 + routers/api/v1/swagger/notify.go | 23 ++ templates/swagger/v1_json.tmpl | 310 ++++++++++++++++++++++++++ 15 files changed, 1124 insertions(+), 28 deletions(-) create mode 100644 integrations/api_notification_test.go create mode 100644 modules/structs/notifications.go create mode 100644 routers/api/v1/notify/repo.go create mode 100644 routers/api/v1/notify/threads.go create mode 100644 routers/api/v1/notify/user.go create mode 100644 routers/api/v1/swagger/notify.go diff --git a/integrations/api_notification_test.go b/integrations/api_notification_test.go new file mode 100644 index 0000000000..2c5477dfb0 --- /dev/null +++ b/integrations/api_notification_test.go @@ -0,0 +1,106 @@ +// Copyright 2020 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 integrations + +import ( + "fmt" + "net/http" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestAPINotification(t *testing.T) { + defer prepareTestEnv(t)() + + user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) + repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + thread5 := models.AssertExistsAndLoadBean(t, &models.Notification{ID: 5}).(*models.Notification) + assert.NoError(t, thread5.LoadAttributes()) + session := loginUser(t, user2.Name) + token := getTokenForLoggedInUser(t, session) + + // -- GET /notifications -- + // test filter + since := "2000-01-01T00%3A50%3A01%2B00%3A00" //946687801 + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?since=%s&token=%s", since, token)) + resp := session.MakeRequest(t, req, http.StatusOK) + var apiNL []api.NotificationThread + DecodeJSON(t, resp, &apiNL) + + assert.Len(t, apiNL, 1) + assert.EqualValues(t, 5, apiNL[0].ID) + + // test filter + before := "2000-01-01T01%3A06%3A59%2B00%3A00" //946688819 + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?all=%s&before=%s&token=%s", "true", before, token)) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiNL) + + assert.Len(t, apiNL, 3) + assert.EqualValues(t, 4, apiNL[0].ID) + assert.EqualValues(t, true, apiNL[0].Unread) + assert.EqualValues(t, false, apiNL[0].Pinned) + assert.EqualValues(t, 3, apiNL[1].ID) + assert.EqualValues(t, false, apiNL[1].Unread) + assert.EqualValues(t, true, apiNL[1].Pinned) + assert.EqualValues(t, 2, apiNL[2].ID) + assert.EqualValues(t, false, apiNL[2].Unread) + assert.EqualValues(t, false, apiNL[2].Pinned) + + // -- GET /repos/{owner}/{repo}/notifications -- + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?token=%s", user2.Name, repo1.Name, token)) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiNL) + + assert.Len(t, apiNL, 1) + assert.EqualValues(t, 4, apiNL[0].ID) + + // -- GET /notifications/threads/{id} -- + // get forbidden + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", 1, token)) + resp = session.MakeRequest(t, req, http.StatusForbidden) + + // get own + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token)) + resp = session.MakeRequest(t, req, http.StatusOK) + var apiN api.NotificationThread + DecodeJSON(t, resp, &apiN) + + assert.EqualValues(t, 5, apiN.ID) + assert.EqualValues(t, false, apiN.Pinned) + assert.EqualValues(t, true, apiN.Unread) + assert.EqualValues(t, "issue4", apiN.Subject.Title) + assert.EqualValues(t, "Issue", apiN.Subject.Type) + assert.EqualValues(t, thread5.Issue.APIURL(), apiN.Subject.URL) + assert.EqualValues(t, thread5.Repository.HTMLURL(), apiN.Repository.HTMLURL) + + // -- mark notifications as read -- + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiNL) + assert.Len(t, apiNL, 2) + + lastReadAt := "2000-01-01T00%3A50%3A01%2B00%3A00" //946687801 <- only Notification 4 is in this filter ... + req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token)) + resp = session.MakeRequest(t, req, http.StatusResetContent) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiNL) + assert.Len(t, apiNL, 1) + + // -- PATCH /notifications/threads/{id} -- + req = NewRequest(t, "PATCH", fmt.Sprintf("/api/v1/notifications/threads/%d?token=%s", thread5.ID, token)) + resp = session.MakeRequest(t, req, http.StatusResetContent) + + assert.Equal(t, models.NotificationStatusUnread, thread5.Status) + thread5 = models.AssertExistsAndLoadBean(t, &models.Notification{ID: 5}).(*models.Notification) + assert.Equal(t, models.NotificationStatusRead, thread5.Status) +} 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) } } diff --git a/modules/structs/notifications.go b/modules/structs/notifications.go new file mode 100644 index 0000000000..b1e8b7781c --- /dev/null +++ b/modules/structs/notifications.go @@ -0,0 +1,28 @@ +// Copyright 2019 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 structs + +import ( + "time" +) + +// NotificationThread expose Notification on API +type NotificationThread struct { + ID int64 `json:"id"` + Repository *Repository `json:"repository"` + Subject *NotificationSubject `json:"subject"` + Unread bool `json:"unread"` + Pinned bool `json:"pinned"` + UpdatedAt time.Time `json:"updated_at"` + URL string `json:"url"` +} + +// NotificationSubject contains the notification subject (Issue/Pull/Commit) +type NotificationSubject struct { + Title string `json:"title"` + URL string `json:"url"` + LatestCommentURL string `json:"latest_comment_url"` + Type string `json:"type" binding:"In(Issue,Pull,Commit)"` +} diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 7387037d33..ebc651516a 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -56,10 +56,10 @@ func CreateUser(ctx *context.APIContext, form api.CreateUserOption) { // responses: // "201": // "$ref": "#/responses/User" - // "403": - // "$ref": "#/responses/forbidden" // "400": // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" // "422": // "$ref": "#/responses/validationError" diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9f18951893..ccce00e2b2 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -70,6 +70,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/admin" "code.gitea.io/gitea/routers/api/v1/misc" + "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" "code.gitea.io/gitea/routers/api/v1/repo" _ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation @@ -512,6 +513,16 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", misc.MarkdownRaw) + // Notifications + m.Group("/notifications", func() { + m.Combo(""). + Get(notify.ListNotifications). + Put(notify.ReadNotifications) + m.Combo("/threads/:id"). + Get(notify.GetThread). + Patch(notify.ReadThread) + }, reqToken()) + // Users m.Group("/users", func() { m.Get("/search", user.Search) @@ -610,6 +621,9 @@ func RegisterRoutes(m *macaron.Macaron) { m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqOwner(), repo.Delete). Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), context.RepoRef(), repo.Edit) + m.Combo("/notifications"). + Get(reqToken(), notify.ListRepoNotifications). + Put(reqToken(), notify.ReadRepoNotifications) m.Group("/hooks", func() { m.Combo("").Get(repo.ListHooks). Post(bind(api.CreateHookOption{}), repo.CreateHook) diff --git a/routers/api/v1/notify/repo.go b/routers/api/v1/notify/repo.go new file mode 100644 index 0000000000..b939d90f06 --- /dev/null +++ b/routers/api/v1/notify/repo.go @@ -0,0 +1,151 @@ +// Copyright 2020 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 notify + +import ( + "net/http" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/routers/api/v1/utils" +) + +// ListRepoNotifications list users's notification threads on a specific repo +func ListRepoNotifications(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/notifications notification notifyGetRepoList + // --- + // summary: List users's notification threads on a specific repo + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: all + // in: query + // description: If true, show notifications marked as read. Default value is false + // type: string + // required: false + // - name: since + // in: query + // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format + // type: string + // format: date-time + // required: false + // - name: before + // in: query + // description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format + // type: string + // format: date-time + // required: false + // responses: + // "200": + // "$ref": "#/responses/NotificationThreadList" + + before, since, err := utils.GetQueryBeforeSince(ctx) + if err != nil { + ctx.InternalServerError(err) + return + } + opts := models.FindNotificationOptions{ + UserID: ctx.User.ID, + RepoID: ctx.Repo.Repository.ID, + UpdatedBeforeUnix: before, + UpdatedAfterUnix: since, + } + qAll := strings.Trim(ctx.Query("all"), " ") + if qAll != "true" { + opts.Status = models.NotificationStatusUnread + } + nl, err := models.GetNotifications(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + err = nl.LoadAttributes() + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, nl.APIFormat()) +} + +// ReadRepoNotifications mark notification threads as read on a specific repo +func ReadRepoNotifications(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/notifications notification notifyReadRepoList + // --- + // summary: Mark notification threads as read on a specific repo + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: last_read_at + // in: query + // description: Describes the last point that notifications were checked. Anything updated since this time will not be updated. + // type: string + // format: date-time + // required: false + // responses: + // "205": + // "$ref": "#/responses/empty" + + lastRead := int64(0) + qLastRead := strings.Trim(ctx.Query("last_read_at"), " ") + if len(qLastRead) > 0 { + tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) + if err != nil { + ctx.InternalServerError(err) + return + } + if !tmpLastRead.IsZero() { + lastRead = tmpLastRead.Unix() + } + } + opts := models.FindNotificationOptions{ + UserID: ctx.User.ID, + RepoID: ctx.Repo.Repository.ID, + UpdatedBeforeUnix: lastRead, + Status: models.NotificationStatusUnread, + } + nl, err := models.GetNotifications(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + for _, n := range nl { + err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) + if err != nil { + ctx.InternalServerError(err) + return + } + ctx.Status(http.StatusResetContent) + } + + ctx.Status(http.StatusResetContent) +} diff --git a/routers/api/v1/notify/threads.go b/routers/api/v1/notify/threads.go new file mode 100644 index 0000000000..d0119e9938 --- /dev/null +++ b/routers/api/v1/notify/threads.go @@ -0,0 +1,101 @@ +// Copyright 2020 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 notify + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" +) + +// GetThread get notification by ID +func GetThread(ctx *context.APIContext) { + // swagger:operation GET /notifications/threads/{id} notification notifyGetThread + // --- + // summary: Get notification thread by ID + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of notification thread + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/NotificationThread" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + n := getThread(ctx) + if n == nil { + return + } + if err := n.LoadAttributes(); err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, n.APIFormat()) +} + +// ReadThread mark notification as read by ID +func ReadThread(ctx *context.APIContext) { + // swagger:operation PATCH /notifications/threads/{id} notification notifyReadThread + // --- + // summary: Mark notification thread as read by ID + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of notification thread + // type: string + // required: true + // responses: + // "205": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + n := getThread(ctx) + if n == nil { + return + } + + err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) + if err != nil { + ctx.InternalServerError(err) + return + } + ctx.Status(http.StatusResetContent) +} + +func getThread(ctx *context.APIContext) *models.Notification { + n, err := models.GetNotificationByID(ctx.ParamsInt64(":id")) + if err != nil { + if models.IsErrNotExist(err) { + ctx.Error(http.StatusNotFound, "GetNotificationByID", err) + } else { + ctx.InternalServerError(err) + } + return nil + } + if n.UserID != ctx.User.ID && !ctx.User.IsAdmin { + ctx.Error(http.StatusForbidden, "GetNotificationByID", fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID)) + return nil + } + return n +} diff --git a/routers/api/v1/notify/user.go b/routers/api/v1/notify/user.go new file mode 100644 index 0000000000..d16e4da0e0 --- /dev/null +++ b/routers/api/v1/notify/user.go @@ -0,0 +1,129 @@ +// Copyright 2020 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 notify + +import ( + "net/http" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/routers/api/v1/utils" +) + +// ListNotifications list users's notification threads +func ListNotifications(ctx *context.APIContext) { + // swagger:operation GET /notifications notification notifyGetList + // --- + // summary: List users's notification threads + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: all + // in: query + // description: If true, show notifications marked as read. Default value is false + // type: string + // required: false + // - name: since + // in: query + // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format + // type: string + // format: date-time + // required: false + // - name: before + // in: query + // description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format + // type: string + // format: date-time + // required: false + // responses: + // "200": + // "$ref": "#/responses/NotificationThreadList" + + before, since, err := utils.GetQueryBeforeSince(ctx) + if err != nil { + ctx.InternalServerError(err) + return + } + opts := models.FindNotificationOptions{ + UserID: ctx.User.ID, + UpdatedBeforeUnix: before, + UpdatedAfterUnix: since, + } + qAll := strings.Trim(ctx.Query("all"), " ") + if qAll != "true" { + opts.Status = models.NotificationStatusUnread + } + nl, err := models.GetNotifications(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + err = nl.LoadAttributes() + if err != nil { + ctx.InternalServerError(err) + return + } + + ctx.JSON(http.StatusOK, nl.APIFormat()) +} + +// ReadNotifications mark notification threads as read +func ReadNotifications(ctx *context.APIContext) { + // swagger:operation PUT /notifications notification notifyReadList + // --- + // summary: Mark notification threads as read + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: last_read_at + // in: query + // description: Describes the last point that notifications were checked. Anything updated since this time will not be updated. + // type: string + // format: date-time + // required: false + // responses: + // "205": + // "$ref": "#/responses/empty" + + lastRead := int64(0) + qLastRead := strings.Trim(ctx.Query("last_read_at"), " ") + if len(qLastRead) > 0 { + tmpLastRead, err := time.Parse(time.RFC3339, qLastRead) + if err != nil { + ctx.InternalServerError(err) + return + } + if !tmpLastRead.IsZero() { + lastRead = tmpLastRead.Unix() + } + } + opts := models.FindNotificationOptions{ + UserID: ctx.User.ID, + UpdatedBeforeUnix: lastRead, + Status: models.NotificationStatusUnread, + } + nl, err := models.GetNotifications(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + for _, n := range nl { + err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) + if err != nil { + ctx.InternalServerError(err) + return + } + ctx.Status(http.StatusResetContent) + } + + ctx.Status(http.StatusResetContent) +} diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index d0551320fd..85ef419780 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -136,6 +136,8 @@ func GetPullRequest(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/PullRequest" + // "404": + // "$ref": "#/responses/notFound" pr, err := models.GetPullRequestByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) if err != nil { diff --git a/routers/api/v1/swagger/notify.go b/routers/api/v1/swagger/notify.go new file mode 100644 index 0000000000..7d45da0e12 --- /dev/null +++ b/routers/api/v1/swagger/notify.go @@ -0,0 +1,23 @@ +// Copyright 2019 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 swagger + +import ( + api "code.gitea.io/gitea/modules/structs" +) + +// NotificationThread +// swagger:response NotificationThread +type swaggerNotificationThread struct { + // in:body + Body api.NotificationThread `json:"body"` +} + +// NotificationThreadList +// swagger:response NotificationThreadList +type swaggerNotificationThreadList struct { + // in:body + Body []api.NotificationThread `json:"body"` +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4e37f65d19..79f760b7ab 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -425,6 +425,143 @@ } } }, + "/notifications": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification" + ], + "summary": "List users's notification threads", + "operationId": "notifyGetList", + "parameters": [ + { + "type": "string", + "description": "If true, show notifications marked as read. Default value is false", + "name": "all", + "in": "query" + }, + { + "type": "string", + "format": "date-time", + "description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format", + "name": "since", + "in": "query" + }, + { + "type": "string", + "format": "date-time", + "description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format", + "name": "before", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/NotificationThreadList" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification" + ], + "summary": "Mark notification threads as read", + "operationId": "notifyReadList", + "parameters": [ + { + "type": "string", + "format": "date-time", + "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", + "name": "last_read_at", + "in": "query" + } + ], + "responses": { + "205": { + "$ref": "#/responses/empty" + } + } + } + }, + "/notifications/threads/{id}": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification" + ], + "summary": "Get notification thread by ID", + "operationId": "notifyGetThread", + "parameters": [ + { + "type": "string", + "description": "id of notification thread", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/NotificationThread" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification" + ], + "summary": "Mark notification thread as read by ID", + "operationId": "notifyReadThread", + "parameters": [ + { + "type": "string", + "description": "id of notification thread", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "205": { + "$ref": "#/responses/empty" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/org/{org}/repos": { "post": { "consumes": [ @@ -5231,6 +5368,103 @@ } } }, + "/repos/{owner}/{repo}/notifications": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification" + ], + "summary": "List users's notification threads on a specific repo", + "operationId": "notifyGetRepoList", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "If true, show notifications marked as read. Default value is false", + "name": "all", + "in": "query" + }, + { + "type": "string", + "format": "date-time", + "description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format", + "name": "since", + "in": "query" + }, + { + "type": "string", + "format": "date-time", + "description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format", + "name": "before", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/NotificationThreadList" + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification" + ], + "summary": "Mark notification threads as read on a specific repo", + "operationId": "notifyReadRepoList", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "date-time", + "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", + "name": "last_read_at", + "in": "query" + } + ], + "responses": { + "205": { + "$ref": "#/responses/empty" + } + } + } + }, "/repos/{owner}/{repo}/pulls": { "get": { "produces": [ @@ -5397,6 +5631,9 @@ "responses": { "200": { "$ref": "#/responses/PullRequest" + }, + "404": { + "$ref": "#/responses/notFound" } } }, @@ -10584,6 +10821,64 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "NotificationSubject": { + "description": "NotificationSubject contains the notification subject (Issue/Pull/Commit)", + "type": "object", + "properties": { + "latest_comment_url": { + "type": "string", + "x-go-name": "LatestCommentURL" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "type": { + "type": "string", + "x-go-name": "Type" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "NotificationThread": { + "description": "NotificationThread expose Notification on API", + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "pinned": { + "type": "boolean", + "x-go-name": "Pinned" + }, + "repository": { + "$ref": "#/definitions/Repository" + }, + "subject": { + "$ref": "#/definitions/NotificationSubject" + }, + "unread": { + "type": "boolean", + "x-go-name": "Unread" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "UpdatedAt" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Organization": { "description": "Organization represents an organization", "type": "object", @@ -12012,6 +12307,21 @@ } } }, + "NotificationThread": { + "description": "NotificationThread", + "schema": { + "$ref": "#/definitions/NotificationThread" + } + }, + "NotificationThreadList": { + "description": "NotificationThreadList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/NotificationThread" + } + } + }, "Organization": { "description": "Organization", "schema": { From 71fe01897743915b8fc8bb8d07f44ee6214d1e50 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Thu, 9 Jan 2020 11:58:47 +0000 Subject: [PATCH 02/15] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-BR.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 3367b45879..78ed5a9598 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1412,6 +1412,8 @@ settings.protect_approvals_whitelist_enabled=Restringir aprovações a usuários settings.protect_approvals_whitelist_enabled_desc=Somente as avaliações de usuários ou equipes da lista permitida serão contadas com as aprovações necessárias. Sem aprovação da lista permitida, as revisões de qualquer pessoa com acesso de escrita contam para as aprovações necessárias. settings.protect_approvals_whitelist_users=Usuários com permissão de revisão: settings.protect_approvals_whitelist_teams=Equipes com permissão de revisão: +settings.dismiss_stale_approvals=Descartar aprovações obsoletas +settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo do pull request são enviados para o branch, as antigas aprovações serão descartadas. settings.add_protected_branch=Habilitar proteção settings.delete_protected_branch=Desabilitar proteção settings.update_protect_branch_success=Proteção do branch '%s' foi atualizada. From 1080c768d33a2c4846467d2e2913df87237b8b23 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 9 Jan 2020 14:15:14 +0100 Subject: [PATCH 03/15] [API] orgEditTeam make Fields optional (#9556) * API: orgEditTeam make Fields optional * add TestCase * Update integrations/api_team_test.go * suggestions from lafriks use len() to check if string is empty Co-Authored-By: Lauris BH * change ... * use Where not ID to get mssql * add return and code format * fix test * fix test ... null pointer exept * update specific colums * only specific colums too Co-authored-by: Lauris BH Co-authored-by: Lunny Xiao --- integrations/api_team_test.go | 24 +++++++++++++++---- models/org_team.go | 6 ++--- modules/structs/org_team.go | 8 +++---- routers/api/v1/org/team.go | 43 +++++++++++++++++++++++------------ 4 files changed, 55 insertions(+), 26 deletions(-) diff --git a/integrations/api_team_test.go b/integrations/api_team_test.go index be56e37edf..d893854470 100644 --- a/integrations/api_team_test.go +++ b/integrations/api_team_test.go @@ -71,19 +71,33 @@ func TestAPITeam(t *testing.T) { teamID := apiTeam.ID // Edit team. + editDescription := "team 1" + editFalse := false teamToEdit := &api.EditTeamOption{ Name: "teamone", - Description: "team 1", - IncludesAllRepositories: false, + Description: &editDescription, Permission: "admin", + IncludesAllRepositories: &editFalse, Units: []string{"repo.code", "repo.pulls", "repo.releases"}, } + req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEdit) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiTeam) - checkTeamResponse(t, &apiTeam, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories, + checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories, teamToEdit.Permission, teamToEdit.Units) - checkTeamBean(t, apiTeam.ID, teamToEdit.Name, teamToEdit.Description, teamToEdit.IncludesAllRepositories, + checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories, + teamToEdit.Permission, teamToEdit.Units) + + // Edit team Description only + editDescription = "first team" + teamToEditDesc := api.EditTeamOption{Description: &editDescription} + req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d?token=%s", teamID, token), teamToEditDesc) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiTeam) + checkTeamResponse(t, &apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories, + teamToEdit.Permission, teamToEdit.Units) + checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories, teamToEdit.Permission, teamToEdit.Units) // Read team. @@ -91,7 +105,7 @@ func TestAPITeam(t *testing.T) { req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamID) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiTeam) - checkTeamResponse(t, &apiTeam, teamRead.Name, teamRead.Description, teamRead.IncludesAllRepositories, + checkTeamResponse(t, &apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories, teamRead.Authorize.String(), teamRead.GetUnitNames()) // Delete team. diff --git a/models/org_team.go b/models/org_team.go index 63c6e11636..0c0a1e7b79 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -590,7 +590,8 @@ func UpdateTeam(t *Team, authChanged bool, includeAllChanged bool) (err error) { return ErrTeamAlreadyExist{t.OrgID, t.LowerName} } - if _, err = sess.ID(t.ID).AllCols().Update(t); err != nil { + if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description", + "can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil { return fmt.Errorf("update: %v", err) } @@ -605,8 +606,7 @@ func UpdateTeam(t *Team, authChanged bool, includeAllChanged bool) (err error) { Delete(new(TeamUnit)); err != nil { return err } - - if _, err = sess.Insert(&t.Units); err != nil { + if _, err = sess.Cols("org_id", "team_id", "type").Insert(&t.Units); err != nil { errRollback := sess.Rollback() if errRollback != nil { log.Error("UpdateTeam sess.Rollback: %v", errRollback) diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index 16f83823d6..a742b7b224 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -35,12 +35,12 @@ type CreateTeamOption struct { // EditTeamOption options for editing a team type EditTeamOption struct { // required: true - Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"` - Description string `json:"description" binding:"MaxSize(255)"` - IncludesAllRepositories bool `json:"includes_all_repositories"` + Name string `json:"name" binding:"AlphaDashDot;MaxSize(30)"` + Description *string `json:"description" binding:"MaxSize(255)"` + IncludesAllRepositories *bool `json:"includes_all_repositories"` // enum: read,write,admin Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.ext_wiki"] Units []string `json:"units"` - CanCreateOrgRepo bool `json:"can_create_org_repo"` + CanCreateOrgRepo *bool `json:"can_create_org_repo"` } diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 73714e6a66..446287a343 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -192,37 +192,52 @@ func EditTeam(ctx *context.APIContext, form api.EditTeamOption) { // "$ref": "#/responses/Team" team := ctx.Org.Team - team.Description = form.Description - unitTypes := models.FindUnitTypes(form.Units...) - team.CanCreateOrgRepo = form.CanCreateOrgRepo + if err := team.GetUnits(); err != nil { + ctx.InternalServerError(err) + return + } + + if form.CanCreateOrgRepo != nil { + team.CanCreateOrgRepo = *form.CanCreateOrgRepo + } + + if len(form.Name) > 0 { + team.Name = form.Name + } + + if form.Description != nil { + team.Description = *form.Description + } isAuthChanged := false isIncludeAllChanged := false - if !team.IsOwnerTeam() { + if !team.IsOwnerTeam() && len(form.Permission) != 0 { // Validate permission level. auth := models.ParseAccessMode(form.Permission) - team.Name = form.Name if team.Authorize != auth { isAuthChanged = true team.Authorize = auth } - if team.IncludesAllRepositories != form.IncludesAllRepositories { + if form.IncludesAllRepositories != nil { isIncludeAllChanged = true - team.IncludesAllRepositories = form.IncludesAllRepositories + team.IncludesAllRepositories = *form.IncludesAllRepositories } } if team.Authorize < models.AccessModeOwner { - var units = make([]*models.TeamUnit, 0, len(form.Units)) - for _, tp := range unitTypes { - units = append(units, &models.TeamUnit{ - OrgID: ctx.Org.Team.OrgID, - Type: tp, - }) + if len(form.Units) > 0 { + var units = make([]*models.TeamUnit, 0, len(form.Units)) + unitTypes := models.FindUnitTypes(form.Units...) + for _, tp := range unitTypes { + units = append(units, &models.TeamUnit{ + OrgID: ctx.Org.Team.OrgID, + Type: tp, + }) + } + team.Units = units } - team.Units = units } if err := models.UpdateTeam(team, isAuthChanged, isIncludeAllChanged); err != nil { From 07520431aec1cbe115e5d85b038f6a9f1e0e7296 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Thu, 9 Jan 2020 22:31:09 +0800 Subject: [PATCH 04/15] chore(PR): Add Reviewed-on in commit message (#9623) --- templates/repo/issue/view_content/pull.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index b1e6feeba0..9dedc7dad7 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -153,7 +153,7 @@
- +