* Extend notifications API and return pinned notifications in notifications list Signed-off-by: Andrew Thornton <art27@cantab.net> * fix swagger Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix swagger again Signed-off-by: Andrew Thornton <art27@cantab.net> * fix test Signed-off-by: Andrew Thornton <art27@cantab.net> * remove spurious debugs * as per @6543 Signed-off-by: Andrew Thornton <art27@cantab.net> * Update models/notification.go * as per @6543 Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>tags/v1.13.0-rc1
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) | ||||
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) | ||||
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) |
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) | ||||
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) |
UserID int64 | UserID int64 | ||||
RepoID int64 | RepoID int64 | ||||
IssueID int64 | IssueID int64 | ||||
Status NotificationStatus | |||||
Status []NotificationStatus | |||||
UpdatedAfterUnix int64 | UpdatedAfterUnix int64 | ||||
UpdatedBeforeUnix int64 | UpdatedBeforeUnix int64 | ||||
} | } | ||||
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 { | |||||
cond = cond.And(builder.Eq{"notification.status": opts.Status}) | |||||
if len(opts.Status) > 0 { | |||||
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}) |
"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 | ||||
// 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 | ||||
UpdatedBeforeUnix: before, | UpdatedBeforeUnix: before, | ||||
UpdatedAfterUnix: since, | UpdatedAfterUnix: since, | ||||
} | } | ||||
qAll := strings.Trim(ctx.Query("all"), " ") | |||||
if qAll != "true" { | |||||
opts.Status = models.NotificationStatusUnread | |||||
if !ctx.QueryBool("all") { | |||||
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 { | ||||
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: | ||||
// 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. | ||||
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 { | ||||
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 |
// 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" | ||||
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 |
// 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 | ||||
UpdatedBeforeUnix: before, | UpdatedBeforeUnix: before, | ||||
UpdatedAfterUnix: since, | UpdatedAfterUnix: since, | ||||
} | } | ||||
qAll := strings.Trim(ctx.Query("all"), " ") | |||||
if qAll != "true" { | |||||
opts.Status = models.NotificationStatusUnread | |||||
if !ctx.QueryBool("all") { | |||||
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 { | ||||
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: | ||||
// 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" | ||||
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 { | ||||
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 |
"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", | ||||
"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": [ | ||||
{ | { | ||||
"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": { | ||||
"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": { | ||||
"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", | ||||
"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": [ | ||||
{ | { | ||||
"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", |