Extend Notifications API and return pinned notifications by default (#12164) (#12232)

Backport #12164

This PR extends the notifications API to allow specific notification statuses to be searched for and to allow setting of notifications to statuses other than read.

By default unread and pinned statuses will be returned when querying for notifications - however pinned statuses will not be marked as read.

Close #12152

Signed-off-by: Andrew Thornton art27@cantab.net
This commit is contained in:
zeripath 2020-07-13 21:52:05 +01:00 committed by GitHub
parent 5e62137fe3
commit 489e9162fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 206 additions and 24 deletions

View File

@ -55,7 +55,7 @@ func TestAPINotification(t *testing.T) {
assert.EqualValues(t, false, apiNL[2].Pinned) assert.EqualValues(t, false, apiNL[2].Pinned)
// -- GET /repos/{owner}/{repo}/notifications -- // -- GET /repos/{owner}/{repo}/notifications --
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?token=%s", user2.Name, repo1.Name, token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?status-types=unread&token=%s", user2.Name, repo1.Name, token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
@ -92,7 +92,7 @@ func TestAPINotification(t *testing.T) {
assert.True(t, new.New > 0) assert.True(t, new.New > 0)
// -- mark notifications as read -- // -- mark notifications as read --
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
assert.Len(t, apiNL, 2) assert.Len(t, apiNL, 2)
@ -101,7 +101,7 @@ func TestAPINotification(t *testing.T) {
req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token)) 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) resp = session.MakeRequest(t, req, http.StatusResetContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
assert.Len(t, apiNL, 1) assert.Len(t, apiNL, 1)

View File

@ -59,7 +59,7 @@ func TestEventSourceManagerRun(t *testing.T) {
var apiNL []api.NotificationThread var apiNL []api.NotificationThread
// -- mark notifications as read -- // -- mark notifications as read --
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?status-types=unread&token=%s", token))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
@ -69,7 +69,7 @@ func TestEventSourceManagerRun(t *testing.T) {
req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/notifications?last_read_at=%s&token=%s", user2.Name, repo1.Name, lastReadAt, token)) 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) resp = session.MakeRequest(t, req, http.StatusResetContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s", token)) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?token=%s&status-types=unread", token))
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiNL) DecodeJSON(t, resp, &apiNL)
assert.Len(t, apiNL, 1) assert.Len(t, apiNL, 1)

View File

@ -72,7 +72,7 @@ type FindNotificationOptions struct {
UserID int64 UserID int64
RepoID int64 RepoID int64
IssueID int64 IssueID int64
Status NotificationStatus Status []NotificationStatus
UpdatedAfterUnix int64 UpdatedAfterUnix int64
UpdatedBeforeUnix int64 UpdatedBeforeUnix int64
} }
@ -89,8 +89,8 @@ func (opts *FindNotificationOptions) ToCond() builder.Cond {
if opts.IssueID != 0 { if opts.IssueID != 0 {
cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID}) cond = cond.And(builder.Eq{"notification.issue_id": opts.IssueID})
} }
if opts.Status != 0 { if len(opts.Status) > 0 {
cond = cond.And(builder.Eq{"notification.status": opts.Status}) cond = cond.And(builder.In("notification.status", opts.Status))
} }
if opts.UpdatedAfterUnix != 0 { if opts.UpdatedAfterUnix != 0 {
cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix}) cond = cond.And(builder.Gte{"notification.updated_unix": opts.UpdatedAfterUnix})

View File

@ -11,9 +11,37 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
) )
func statusStringToNotificationStatus(status string) models.NotificationStatus {
switch strings.ToLower(strings.TrimSpace(status)) {
case "unread":
return models.NotificationStatusUnread
case "read":
return models.NotificationStatusRead
case "pinned":
return models.NotificationStatusPinned
default:
return 0
}
}
func statusStringsToNotificationStatuses(statuses []string, defaultStatuses []string) []models.NotificationStatus {
if len(statuses) == 0 {
statuses = defaultStatuses
}
results := make([]models.NotificationStatus, 0, len(statuses))
for _, status := range statuses {
notificationStatus := statusStringToNotificationStatus(status)
if notificationStatus > 0 {
results = append(results, notificationStatus)
}
}
return results
}
// ListRepoNotifications list users's notification threads on a specific repo // ListRepoNotifications list users's notification threads on a specific repo
func ListRepoNotifications(ctx *context.APIContext) { func ListRepoNotifications(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/notifications notification notifyGetRepoList // swagger:operation GET /repos/{owner}/{repo}/notifications notification notifyGetRepoList
@ -39,6 +67,14 @@ func ListRepoNotifications(ctx *context.APIContext) {
// description: If true, show notifications marked as read. Default value is false // description: If true, show notifications marked as read. Default value is false
// type: string // type: string
// required: false // required: false
// - name: status-types
// in: query
// description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned"
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: since // - name: since
// in: query // in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
@ -75,9 +111,10 @@ func ListRepoNotifications(ctx *context.APIContext) {
UpdatedBeforeUnix: before, UpdatedBeforeUnix: before,
UpdatedAfterUnix: since, UpdatedAfterUnix: since,
} }
qAll := strings.Trim(ctx.Query("all"), " ")
if qAll != "true" { if !ctx.QueryBool("all") {
opts.Status = models.NotificationStatusUnread statuses := ctx.QueryStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@ -97,7 +134,7 @@ func ListRepoNotifications(ctx *context.APIContext) {
func ReadRepoNotifications(ctx *context.APIContext) { func ReadRepoNotifications(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/notifications notification notifyReadRepoList // swagger:operation PUT /repos/{owner}/{repo}/notifications notification notifyReadRepoList
// --- // ---
// summary: Mark notification threads as read on a specific repo // summary: Mark notification threads as read, pinned or unread on a specific repo
// consumes: // consumes:
// - application/json // - application/json
// produces: // produces:
@ -113,6 +150,24 @@ func ReadRepoNotifications(ctx *context.APIContext) {
// description: name of the repo // description: name of the repo
// type: string // type: string
// required: true // required: true
// - name: all
// in: query
// description: If true, mark all notifications on this repo. Default value is false
// type: string
// required: false
// - name: status-types
// in: query
// description: "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: to-status
// in: query
// description: Status to mark notifications as. Defaults to read.
// type: string
// required: false
// - name: last_read_at // - name: last_read_at
// in: query // in: query
// description: Describes the last point that notifications were checked. Anything updated since this time will not be updated. // description: Describes the last point that notifications were checked. Anything updated since this time will not be updated.
@ -135,11 +190,17 @@ func ReadRepoNotifications(ctx *context.APIContext) {
lastRead = tmpLastRead.Unix() lastRead = tmpLastRead.Unix()
} }
} }
opts := models.FindNotificationOptions{ opts := models.FindNotificationOptions{
UserID: ctx.User.ID, UserID: ctx.User.ID,
RepoID: ctx.Repo.Repository.ID, RepoID: ctx.Repo.Repository.ID,
UpdatedBeforeUnix: lastRead, UpdatedBeforeUnix: lastRead,
Status: models.NotificationStatusUnread, }
if !ctx.QueryBool("all") {
statuses := ctx.QueryStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
log.Error("%v", opts.Status)
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@ -147,8 +208,13 @@ func ReadRepoNotifications(ctx *context.APIContext) {
return return
} }
targetStatus := statusStringToNotificationStatus(ctx.Query("to-status"))
if targetStatus == 0 {
targetStatus = models.NotificationStatusRead
}
for _, n := range nl { for _, n := range nl {
err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View File

@ -62,6 +62,12 @@ func ReadThread(ctx *context.APIContext) {
// description: id of notification thread // description: id of notification thread
// type: string // type: string
// required: true // required: true
// - name: to-status
// in: query
// description: Status to mark notifications as
// type: string
// default: read
// required: false
// responses: // responses:
// "205": // "205":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
@ -75,7 +81,12 @@ func ReadThread(ctx *context.APIContext) {
return return
} }
err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) targetStatus := statusStringToNotificationStatus(ctx.Query("to-status"))
if targetStatus == 0 {
targetStatus = models.NotificationStatusRead
}
err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View File

@ -29,6 +29,14 @@ func ListNotifications(ctx *context.APIContext) {
// description: If true, show notifications marked as read. Default value is false // description: If true, show notifications marked as read. Default value is false
// type: string // type: string
// required: false // required: false
// - name: status-types
// in: query
// description: "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread & pinned."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: since // - name: since
// in: query // in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
@ -64,9 +72,9 @@ func ListNotifications(ctx *context.APIContext) {
UpdatedBeforeUnix: before, UpdatedBeforeUnix: before,
UpdatedAfterUnix: since, UpdatedAfterUnix: since,
} }
qAll := strings.Trim(ctx.Query("all"), " ") if !ctx.QueryBool("all") {
if qAll != "true" { statuses := ctx.QueryStrings("status-types")
opts.Status = models.NotificationStatusUnread opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread", "pinned"})
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@ -82,11 +90,11 @@ func ListNotifications(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, nl.APIFormat()) ctx.JSON(http.StatusOK, nl.APIFormat())
} }
// ReadNotifications mark notification threads as read // ReadNotifications mark notification threads as read, unread, or pinned
func ReadNotifications(ctx *context.APIContext) { func ReadNotifications(ctx *context.APIContext) {
// swagger:operation PUT /notifications notification notifyReadList // swagger:operation PUT /notifications notification notifyReadList
// --- // ---
// summary: Mark notification threads as read // summary: Mark notification threads as read, pinned or unread
// consumes: // consumes:
// - application/json // - application/json
// produces: // produces:
@ -98,6 +106,24 @@ func ReadNotifications(ctx *context.APIContext) {
// type: string // type: string
// format: date-time // format: date-time
// required: false // required: false
// - name: all
// in: query
// description: If true, mark all notifications on this repo. Default value is false
// type: string
// required: false
// - name: status-types
// in: query
// description: "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread."
// type: array
// collectionFormat: multi
// items:
// type: string
// required: false
// - name: to-status
// in: query
// description: Status to mark notifications as, Defaults to read.
// type: string
// required: false
// responses: // responses:
// "205": // "205":
// "$ref": "#/responses/empty" // "$ref": "#/responses/empty"
@ -117,7 +143,10 @@ func ReadNotifications(ctx *context.APIContext) {
opts := models.FindNotificationOptions{ opts := models.FindNotificationOptions{
UserID: ctx.User.ID, UserID: ctx.User.ID,
UpdatedBeforeUnix: lastRead, UpdatedBeforeUnix: lastRead,
Status: models.NotificationStatusUnread, }
if !ctx.QueryBool("all") {
statuses := ctx.QueryStrings("status-types")
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
} }
nl, err := models.GetNotifications(opts) nl, err := models.GetNotifications(opts)
if err != nil { if err != nil {
@ -125,8 +154,13 @@ func ReadNotifications(ctx *context.APIContext) {
return return
} }
targetStatus := statusStringToNotificationStatus(ctx.Query("to-status"))
if targetStatus == 0 {
targetStatus = models.NotificationStatusRead
}
for _, n := range nl { for _, n := range nl {
err := models.SetNotificationStatus(n.ID, ctx.User, models.NotificationStatusRead) err := models.SetNotificationStatus(n.ID, ctx.User, targetStatus)
if err != nil { if err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)
return return

View File

@ -459,6 +459,16 @@
"name": "all", "name": "all",
"in": "query" "in": "query"
}, },
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread \u0026 pinned.",
"name": "status-types",
"in": "query"
},
{ {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@ -502,7 +512,7 @@
"tags": [ "tags": [
"notification" "notification"
], ],
"summary": "Mark notification threads as read", "summary": "Mark notification threads as read, pinned or unread",
"operationId": "notifyReadList", "operationId": "notifyReadList",
"parameters": [ "parameters": [
{ {
@ -511,6 +521,28 @@
"description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.", "description": "Describes the last point that notifications were checked. Anything updated since this time will not be updated.",
"name": "last_read_at", "name": "last_read_at",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "If true, mark all notifications on this repo. Default value is false",
"name": "all",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread.",
"name": "status-types",
"in": "query"
},
{
"type": "string",
"description": "Status to mark notifications as, Defaults to read.",
"name": "to-status",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -587,6 +619,13 @@
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"default": "read",
"description": "Status to mark notifications as",
"name": "to-status",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -6290,6 +6329,16 @@
"name": "all", "name": "all",
"in": "query" "in": "query"
}, },
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Show notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread \u0026 pinned",
"name": "status-types",
"in": "query"
},
{ {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",
@ -6333,7 +6382,7 @@
"tags": [ "tags": [
"notification" "notification"
], ],
"summary": "Mark notification threads as read on a specific repo", "summary": "Mark notification threads as read, pinned or unread on a specific repo",
"operationId": "notifyReadRepoList", "operationId": "notifyReadRepoList",
"parameters": [ "parameters": [
{ {
@ -6350,6 +6399,28 @@
"in": "path", "in": "path",
"required": true "required": true
}, },
{
"type": "string",
"description": "If true, mark all notifications on this repo. Default value is false",
"name": "all",
"in": "query"
},
{
"type": "array",
"items": {
"type": "string"
},
"collectionFormat": "multi",
"description": "Mark notifications with the provided status types. Options are: unread, read and/or pinned. Defaults to unread.",
"name": "status-types",
"in": "query"
},
{
"type": "string",
"description": "Status to mark notifications as. Defaults to read.",
"name": "to-status",
"in": "query"
},
{ {
"type": "string", "type": "string",
"format": "date-time", "format": "date-time",