* Add 'mark all read' option to notifications Signed-off-by: Sasha Varlamov <sasha@sashavarlamov.com> * Fix exported comment Signed-off-by: Sasha Varlamov <sasha@sashavarlamov.com> * Format method comments Signed-off-by: Sasha Varlamov <sasha@sashavarlamov.com> * Fix exported comment Signed-off-by: Sasha Varlamov <sasha@sashavarlamov.com> Format method comments Signed-off-by: Sasha Varlamov <sasha@sashavarlamov.com> Tests for reactions (#3083) * Unit tests for reactions * Fix import order Signed-off-by: Lauris Bukšis-Haberkorns <lauris@nix.lv> Fix reaction possition when there is attachments (#3099) Refactor notifications swap function * Accept change to drop beforeupdate call * Update purge notifications error message for consistency * Drop unnecessary check for mark all as read button * Remove debugging commenttags/v1.4.0-rc1
issue_id: 2 | issue_id: 2 | ||||
created_unix: 946684800 | created_unix: 946684800 | ||||
updated_unix: 946684800 | updated_unix: 946684800 | ||||
- | |||||
id: 3 | |||||
user_id: 2 | |||||
repo_id: 1 | |||||
status: 3 # pinned | |||||
source: 1 # issue | |||||
updated_by: 1 | |||||
issue_id: 2 | |||||
created_unix: 946684800 | |||||
updated_unix: 946684800 | |||||
- | |||||
id: 4 | |||||
user_id: 2 | |||||
repo_id: 1 | |||||
status: 1 # unread | |||||
source: 1 # issue | |||||
updated_by: 1 | |||||
issue_id: 2 | |||||
created_unix: 946684800 | |||||
updated_unix: 946684800 |
return notification, nil | return notification, nil | ||||
} | } | ||||
// UpdateNotificationStatuses updates the statuses of all of a user's notifications that are of the currentStatus type to the desiredStatus | |||||
func UpdateNotificationStatuses(user *User, currentStatus NotificationStatus, desiredStatus NotificationStatus) error { | |||||
n := &Notification{Status: desiredStatus, UpdatedBy: user.ID} | |||||
_, err := x. | |||||
Where("user_id = ? AND status = ?", user.ID, currentStatus). | |||||
Cols("status", "updated_by", "updated_unix"). | |||||
Update(n) | |||||
return err | |||||
} |
statuses := []NotificationStatus{NotificationStatusRead, NotificationStatusUnread} | statuses := []NotificationStatus{NotificationStatusRead, NotificationStatusUnread} | ||||
notfs, err := NotificationsForUser(user, statuses, 1, 10) | notfs, err := NotificationsForUser(user, statuses, 1, 10) | ||||
assert.NoError(t, err) | assert.NoError(t, err) | ||||
if assert.Len(t, notfs, 1) { | |||||
if assert.Len(t, notfs, 2) { | |||||
assert.EqualValues(t, 2, notfs[0].ID) | assert.EqualValues(t, 2, notfs[0].ID) | ||||
assert.EqualValues(t, user.ID, notfs[0].UserID) | assert.EqualValues(t, user.ID, notfs[0].UserID) | ||||
assert.EqualValues(t, 4, notfs[1].ID) | |||||
assert.EqualValues(t, user.ID, notfs[1].UserID) | |||||
} | } | ||||
} | } | ||||
func TestGetNotificationCount(t *testing.T) { | func TestGetNotificationCount(t *testing.T) { | ||||
assert.NoError(t, PrepareTestDatabase()) | assert.NoError(t, PrepareTestDatabase()) | ||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||||
cnt, err := GetNotificationCount(user, NotificationStatusUnread) | |||||
user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) | |||||
cnt, err := GetNotificationCount(user, NotificationStatusRead) | |||||
assert.NoError(t, err) | assert.NoError(t, err) | ||||
assert.EqualValues(t, 0, cnt) | assert.EqualValues(t, 0, cnt) | ||||
cnt, err = GetNotificationCount(user, NotificationStatusRead) | |||||
cnt, err = GetNotificationCount(user, NotificationStatusUnread) | |||||
assert.NoError(t, err) | assert.NoError(t, err) | ||||
assert.EqualValues(t, 1, cnt) | assert.EqualValues(t, 1, cnt) | ||||
} | } | ||||
assert.Error(t, SetNotificationStatus(1, user, NotificationStatusRead)) | assert.Error(t, SetNotificationStatus(1, user, NotificationStatusRead)) | ||||
assert.Error(t, SetNotificationStatus(NonexistentID, user, NotificationStatusRead)) | assert.Error(t, SetNotificationStatus(NonexistentID, user, NotificationStatusRead)) | ||||
} | } | ||||
func TestUpdateNotificationStatuses(t *testing.T) { | |||||
assert.NoError(t, PrepareTestDatabase()) | |||||
user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | |||||
notfUnread := AssertExistsAndLoadBean(t, | |||||
&Notification{UserID: user.ID, Status: NotificationStatusUnread}).(*Notification) | |||||
notfRead := AssertExistsAndLoadBean(t, | |||||
&Notification{UserID: user.ID, Status: NotificationStatusRead}).(*Notification) | |||||
notfPinned := AssertExistsAndLoadBean(t, | |||||
&Notification{UserID: user.ID, Status: NotificationStatusPinned}).(*Notification) | |||||
assert.NoError(t, UpdateNotificationStatuses(user, NotificationStatusUnread, NotificationStatusRead)) | |||||
AssertExistsAndLoadBean(t, | |||||
&Notification{ID: notfUnread.ID, Status: NotificationStatusRead}) | |||||
AssertExistsAndLoadBean(t, | |||||
&Notification{ID: notfRead.ID, Status: NotificationStatusRead}) | |||||
AssertExistsAndLoadBean(t, | |||||
&Notification{ID: notfPinned.ID, Status: NotificationStatusPinned}) | |||||
} |
pin = Pin notification | pin = Pin notification | ||||
mark_as_read = Mark as read | mark_as_read = Mark as read | ||||
mark_as_unread = Mark as unread | mark_as_unread = Mark as unread | ||||
mark_all_as_read = Mark all as read | |||||
[gpg] | [gpg] | ||||
error.extract_sign = Failed to extract signature | error.extract_sign = Failed to extract signature |
m.Group("/notifications", func() { | m.Group("/notifications", func() { | ||||
m.Get("", user.Notifications) | m.Get("", user.Notifications) | ||||
m.Post("/status", user.NotificationStatusPost) | m.Post("/status", user.NotificationStatusPost) | ||||
m.Post("/purge", user.NotificationPurgePost) | |||||
}, reqSignIn) | }, reqSignIn) | ||||
m.Group("/api", func() { | m.Group("/api", func() { |
url := fmt.Sprintf("%s/notifications", setting.AppSubURL) | url := fmt.Sprintf("%s/notifications", setting.AppSubURL) | ||||
c.Redirect(url, 303) | c.Redirect(url, 303) | ||||
} | } | ||||
// NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read | |||||
func NotificationPurgePost(c *context.Context) { | |||||
err := models.UpdateNotificationStatuses(c.User, models.NotificationStatusUnread, models.NotificationStatusRead) | |||||
if err != nil { | |||||
c.Handle(500, "ErrUpdateNotificationStatuses", err) | |||||
return | |||||
} | |||||
url := fmt.Sprintf("%s/notifications", setting.AppSubURL) | |||||
c.Redirect(url, 303) | |||||
} |
<a href="{{AppSubUrl}}/notifications?q=read" class="{{if eq .Status 2}}active{{end}} item"> | <a href="{{AppSubUrl}}/notifications?q=read" class="{{if eq .Status 2}}active{{end}} item"> | ||||
{{.i18n.Tr "notification.read"}} | {{.i18n.Tr "notification.read"}} | ||||
</a> | </a> | ||||
{{if and (eq .Status 1) (.NotificationUnreadCount)}} | |||||
<form action="{{AppSubUrl}}/notifications/purge" method="POST" style="margin-left: auto;"> | |||||
{{$.CsrfTokenHtml}} | |||||
<button class="ui mini button primary" title='{{$.i18n.Tr "notification.mark_all_as_read"}}'> | |||||
<i class="octicon octicon-checklist"></i> | |||||
</button> | |||||
</form> | |||||
{{end}} | |||||
</div> | </div> | ||||
<div class="ui bottom attached active tab segment"> | <div class="ui bottom attached active tab segment"> | ||||
{{if eq (len .Notifications) 0}} | {{if eq (len .Notifications) 0}} |