mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-24 18:10:52 +00:00
See https://codeberg.org/forgejo/forgejo/pulls/4801#issuecomment-5094525 and #8152 for more context. The current implementation is limited to self-hosted actions and buggy as soon as multiple repos are involved, like for the homepage (because each permission must be fetched individually). Ideally this feature should work for all kind of status (with some setting indicating which collaborator can access with status). Probably inside the `git_model.ParseCommitsWithStatus` function. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8177 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: oliverpool <git@olivier.pfad.fr> Co-committed-by: oliverpool <git@olivier.pfad.fr>
485 lines
13 KiB
Go
485 lines
13 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package user
|
|
|
|
import (
|
|
goctx "context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
activities_model "forgejo.org/models/activities"
|
|
"forgejo.org/models/db"
|
|
issues_model "forgejo.org/models/issues"
|
|
repo_model "forgejo.org/models/repo"
|
|
"forgejo.org/modules/base"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/optional"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/structs"
|
|
"forgejo.org/modules/util"
|
|
"forgejo.org/services/context"
|
|
issue_service "forgejo.org/services/issue"
|
|
pull_service "forgejo.org/services/pull"
|
|
)
|
|
|
|
const (
|
|
tplNotification base.TplName = "user/notification/notification"
|
|
tplNotificationDiv base.TplName = "user/notification/notification_div"
|
|
tplNotificationSubscriptions base.TplName = "user/notification/notification_subscriptions"
|
|
)
|
|
|
|
// GetNotificationCount is the middleware that sets the notification count in the context
|
|
func GetNotificationCount(ctx *context.Context) {
|
|
if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
|
|
return
|
|
}
|
|
|
|
if !ctx.IsSigned {
|
|
return
|
|
}
|
|
|
|
ctx.Data["NotificationUnreadCount"] = func() int64 {
|
|
count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
|
|
UserID: ctx.Doer.ID,
|
|
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
|
|
})
|
|
if err != nil {
|
|
if err != goctx.Canceled {
|
|
log.Error("Unable to GetNotificationCount for user:%-v: %v", ctx.Doer, err)
|
|
}
|
|
return -1
|
|
}
|
|
|
|
return count
|
|
}
|
|
}
|
|
|
|
// Notifications is the notifications page
|
|
func Notifications(ctx *context.Context) {
|
|
getNotifications(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
if ctx.FormBool("div-only") {
|
|
ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
|
|
ctx.HTML(http.StatusOK, tplNotificationDiv)
|
|
return
|
|
}
|
|
ctx.HTML(http.StatusOK, tplNotification)
|
|
}
|
|
|
|
func getNotifications(ctx *context.Context) {
|
|
var (
|
|
keyword = ctx.FormTrim("q")
|
|
status activities_model.NotificationStatus
|
|
page = ctx.FormInt("page")
|
|
perPage = ctx.FormInt("perPage")
|
|
)
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if perPage < 1 {
|
|
perPage = 20
|
|
}
|
|
|
|
switch keyword {
|
|
case "read":
|
|
status = activities_model.NotificationStatusRead
|
|
default:
|
|
status = activities_model.NotificationStatusUnread
|
|
}
|
|
|
|
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
|
|
UserID: ctx.Doer.ID,
|
|
Status: []activities_model.NotificationStatus{status},
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("ErrGetNotificationCount", err)
|
|
return
|
|
}
|
|
|
|
// redirect to last page if request page is more than total pages
|
|
pager := context.NewPagination(int(total), perPage, page, 5)
|
|
if pager.Paginater.Current() < page {
|
|
ctx.Redirect(fmt.Sprintf("%s/notifications?q=%s&page=%d", setting.AppSubURL, url.QueryEscape(ctx.FormString("q")), pager.Paginater.Current()))
|
|
return
|
|
}
|
|
|
|
sess := db.GetEngine(ctx).Table("notification")
|
|
if setting.Database.Type.IsMySQL() {
|
|
sess = sess.IndexHint("USE", "", "IDX_notification_user_id")
|
|
}
|
|
sess.Where("user_id = ?", ctx.Doer.ID).
|
|
And("status = ? OR status = ?", status, activities_model.NotificationStatusPinned).
|
|
OrderBy("notification.updated_unix DESC")
|
|
|
|
if perPage > 0 {
|
|
if page == 0 {
|
|
page = 1
|
|
}
|
|
sess.Limit(perPage, (page-1)*perPage)
|
|
}
|
|
|
|
nls := make([]*activities_model.Notification, 0, perPage)
|
|
if err := sess.Find(&nls); err != nil {
|
|
ctx.ServerError("FindNotifications", err)
|
|
return
|
|
}
|
|
notifications := activities_model.NotificationList(nls)
|
|
|
|
failCount := 0
|
|
|
|
repos, failures, err := notifications.LoadRepos(ctx)
|
|
if err != nil {
|
|
ctx.ServerError("LoadRepos", err)
|
|
return
|
|
}
|
|
notifications = notifications.Without(failures)
|
|
if err := repos.LoadAttributes(ctx); err != nil {
|
|
ctx.ServerError("LoadAttributes", err)
|
|
return
|
|
}
|
|
failCount += len(failures)
|
|
|
|
failures, err = notifications.LoadIssues(ctx)
|
|
if err != nil {
|
|
ctx.ServerError("LoadIssues", err)
|
|
return
|
|
}
|
|
|
|
if err = notifications.LoadIssuePullRequests(ctx); err != nil {
|
|
ctx.ServerError("LoadIssuePullRequests", err)
|
|
return
|
|
}
|
|
|
|
notifications = notifications.Without(failures)
|
|
failCount += len(failures)
|
|
|
|
failures, err = notifications.LoadComments(ctx)
|
|
if err != nil {
|
|
ctx.ServerError("LoadComments", err)
|
|
return
|
|
}
|
|
notifications = notifications.Without(failures)
|
|
failCount += len(failures)
|
|
|
|
if failCount > 0 {
|
|
ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
|
|
}
|
|
|
|
ctx.Data["Title"] = ctx.Tr("notifications")
|
|
ctx.Data["Keyword"] = keyword
|
|
ctx.Data["Status"] = status
|
|
ctx.Data["Notifications"] = notifications
|
|
|
|
pager.SetDefaultParams(ctx)
|
|
ctx.Data["Page"] = pager
|
|
}
|
|
|
|
// NotificationStatusPost is a route for changing the status of a notification
|
|
func NotificationStatusPost(ctx *context.Context) {
|
|
var (
|
|
notificationID = ctx.FormInt64("notification_id")
|
|
statusStr = ctx.FormString("status")
|
|
status activities_model.NotificationStatus
|
|
)
|
|
|
|
switch statusStr {
|
|
case "read":
|
|
status = activities_model.NotificationStatusRead
|
|
case "unread":
|
|
status = activities_model.NotificationStatusUnread
|
|
case "pinned":
|
|
status = activities_model.NotificationStatusPinned
|
|
default:
|
|
ctx.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
|
|
return
|
|
}
|
|
|
|
if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, status); err != nil {
|
|
ctx.ServerError("SetNotificationStatus", err)
|
|
return
|
|
}
|
|
|
|
if !ctx.FormBool("noredirect") {
|
|
url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(ctx.FormString("page")))
|
|
ctx.Redirect(url, http.StatusSeeOther)
|
|
}
|
|
|
|
getNotifications(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
ctx.Data["Link"] = setting.AppSubURL + "/notifications"
|
|
ctx.Data["SequenceNumber"] = ctx.Req.PostFormValue("sequence-number")
|
|
|
|
ctx.HTML(http.StatusOK, tplNotificationDiv)
|
|
}
|
|
|
|
// NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read
|
|
func NotificationPurgePost(ctx *context.Context) {
|
|
err := activities_model.UpdateNotificationStatuses(ctx, ctx.Doer, activities_model.NotificationStatusUnread, activities_model.NotificationStatusRead)
|
|
if err != nil {
|
|
ctx.ServerError("UpdateNotificationStatuses", err)
|
|
return
|
|
}
|
|
|
|
ctx.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
|
|
}
|
|
|
|
// NotificationSubscriptions returns the list of subscribed issues
|
|
func NotificationSubscriptions(ctx *context.Context) {
|
|
page := ctx.FormInt("page")
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
|
|
sortType := ctx.FormString("sort")
|
|
ctx.Data["SortType"] = sortType
|
|
|
|
state := ctx.FormString("state")
|
|
if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) {
|
|
state = "all"
|
|
}
|
|
|
|
ctx.Data["State"] = state
|
|
// default state filter is "all"
|
|
showClosed := optional.None[bool]()
|
|
switch state {
|
|
case "closed":
|
|
showClosed = optional.Some(true)
|
|
case "open":
|
|
showClosed = optional.Some(false)
|
|
}
|
|
|
|
issueType := ctx.FormString("issueType")
|
|
// default issue type is no filter
|
|
issueTypeBool := optional.None[bool]()
|
|
switch issueType {
|
|
case "issues":
|
|
issueTypeBool = optional.Some(false)
|
|
case "pulls":
|
|
issueTypeBool = optional.Some(true)
|
|
}
|
|
ctx.Data["IssueType"] = issueType
|
|
|
|
var labelIDs []int64
|
|
selectedLabels := ctx.FormString("labels")
|
|
ctx.Data["Labels"] = selectedLabels
|
|
if len(selectedLabels) > 0 && selectedLabels != "0" {
|
|
var err error
|
|
labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
|
|
if err != nil {
|
|
ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
|
|
}
|
|
}
|
|
|
|
count, err := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{
|
|
SubscriberID: ctx.Doer.ID,
|
|
IsClosed: showClosed,
|
|
IsPull: issueTypeBool,
|
|
LabelIDs: labelIDs,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("CountIssues", err)
|
|
return
|
|
}
|
|
issues, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
|
|
Paginator: &db.ListOptions{
|
|
PageSize: setting.UI.IssuePagingNum,
|
|
Page: page,
|
|
},
|
|
SubscriberID: ctx.Doer.ID,
|
|
SortType: sortType,
|
|
IsClosed: showClosed,
|
|
IsPull: issueTypeBool,
|
|
LabelIDs: labelIDs,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("Issues", err)
|
|
return
|
|
}
|
|
|
|
commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
|
|
if err != nil {
|
|
ctx.ServerError("GetIssuesAllCommitStatus", err)
|
|
return
|
|
}
|
|
ctx.Data["CommitLastStatus"] = lastStatus
|
|
ctx.Data["CommitStatuses"] = commitStatuses
|
|
ctx.Data["Issues"] = issues
|
|
|
|
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
|
|
|
|
commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
|
|
if err != nil {
|
|
ctx.ServerError("GetIssuesLastCommitStatus", err)
|
|
return
|
|
}
|
|
ctx.Data["CommitStatus"] = commitStatus
|
|
|
|
approvalCounts, err := issues.GetApprovalCounts(ctx)
|
|
if err != nil {
|
|
ctx.ServerError("ApprovalCounts", err)
|
|
return
|
|
}
|
|
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
|
|
counts, ok := approvalCounts[issueID]
|
|
if !ok || len(counts) == 0 {
|
|
return 0
|
|
}
|
|
reviewTyp := issues_model.ReviewTypeApprove
|
|
switch typ {
|
|
case "reject":
|
|
reviewTyp = issues_model.ReviewTypeReject
|
|
case "waiting":
|
|
reviewTyp = issues_model.ReviewTypeRequest
|
|
}
|
|
for _, count := range counts {
|
|
if count.Type == reviewTyp {
|
|
return count.Count
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
ctx.Data["Status"] = 1
|
|
ctx.Data["Title"] = ctx.Tr("notification.subscriptions")
|
|
|
|
// redirect to last page if request page is more than total pages
|
|
pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5)
|
|
if pager.Paginater.Current() < page {
|
|
ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
|
|
return
|
|
}
|
|
pager.AddParam(ctx, "sort", "SortType")
|
|
pager.AddParam(ctx, "state", "State")
|
|
ctx.Data["Page"] = pager
|
|
|
|
ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
|
|
}
|
|
|
|
// NotificationWatching returns the list of watching repos
|
|
func NotificationWatching(ctx *context.Context) {
|
|
page := ctx.FormInt("page")
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
|
|
keyword := ctx.FormTrim("q")
|
|
ctx.Data["Keyword"] = keyword
|
|
|
|
var orderBy db.SearchOrderBy
|
|
ctx.Data["SortType"] = ctx.FormString("sort")
|
|
switch ctx.FormString("sort") {
|
|
case "newest":
|
|
orderBy = db.SearchOrderByNewest
|
|
case "oldest":
|
|
orderBy = db.SearchOrderByOldest
|
|
case "recentupdate":
|
|
orderBy = db.SearchOrderByRecentUpdated
|
|
case "leastupdate":
|
|
orderBy = db.SearchOrderByLeastUpdated
|
|
case "reversealphabetically":
|
|
orderBy = db.SearchOrderByAlphabeticallyReverse
|
|
case "alphabetically":
|
|
orderBy = db.SearchOrderByAlphabetically
|
|
case "moststars":
|
|
orderBy = db.SearchOrderByStarsReverse
|
|
case "feweststars":
|
|
orderBy = db.SearchOrderByStars
|
|
case "mostforks":
|
|
orderBy = db.SearchOrderByForksReverse
|
|
case "fewestforks":
|
|
orderBy = db.SearchOrderByForks
|
|
default:
|
|
ctx.Data["SortType"] = "recentupdate"
|
|
orderBy = db.SearchOrderByRecentUpdated
|
|
}
|
|
|
|
archived := ctx.FormOptionalBool("archived")
|
|
ctx.Data["IsArchived"] = archived
|
|
|
|
fork := ctx.FormOptionalBool("fork")
|
|
ctx.Data["IsFork"] = fork
|
|
|
|
mirror := ctx.FormOptionalBool("mirror")
|
|
ctx.Data["IsMirror"] = mirror
|
|
|
|
template := ctx.FormOptionalBool("template")
|
|
ctx.Data["IsTemplate"] = template
|
|
|
|
private := ctx.FormOptionalBool("private")
|
|
ctx.Data["IsPrivate"] = private
|
|
|
|
repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
PageSize: setting.UI.User.RepoPagingNum,
|
|
Page: page,
|
|
},
|
|
Actor: ctx.Doer,
|
|
Keyword: keyword,
|
|
OrderBy: orderBy,
|
|
Private: ctx.IsSigned,
|
|
WatchedByID: ctx.Doer.ID,
|
|
Collaborate: optional.Some(false),
|
|
TopicOnly: ctx.FormBool("topic"),
|
|
IncludeDescription: setting.UI.SearchRepoDescription,
|
|
Archived: archived,
|
|
Fork: fork,
|
|
Mirror: mirror,
|
|
Template: template,
|
|
IsPrivate: private,
|
|
})
|
|
if err != nil {
|
|
ctx.ServerError("SearchRepository", err)
|
|
return
|
|
}
|
|
total := int(count)
|
|
ctx.Data["Total"] = total
|
|
ctx.Data["Repos"] = repos
|
|
|
|
// redirect to last page if request page is more than total pages
|
|
pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
|
|
pager.SetDefaultParams(ctx)
|
|
if archived.Has() {
|
|
pager.AddParamString("archived", fmt.Sprint(archived.Value()))
|
|
}
|
|
if fork.Has() {
|
|
pager.AddParamString("fork", fmt.Sprint(fork.Value()))
|
|
}
|
|
if mirror.Has() {
|
|
pager.AddParamString("mirror", fmt.Sprint(mirror.Value()))
|
|
}
|
|
if template.Has() {
|
|
pager.AddParamString("template", fmt.Sprint(template.Value()))
|
|
}
|
|
if private.Has() {
|
|
pager.AddParamString("private", fmt.Sprint(private.Value()))
|
|
}
|
|
ctx.Data["Page"] = pager
|
|
|
|
ctx.Data["Status"] = 2
|
|
ctx.Data["Title"] = ctx.Tr("notification.watching")
|
|
|
|
ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
|
|
}
|
|
|
|
// NewAvailable returns the notification counts
|
|
func NewAvailable(ctx *context.Context) {
|
|
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
|
|
UserID: ctx.Doer.ID,
|
|
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
|
|
})
|
|
if err != nil {
|
|
log.Error("db.Count[activities_model.Notification]", err)
|
|
ctx.JSON(http.StatusOK, structs.NotificationCount{New: 0})
|
|
return
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, structs.NotificationCount{New: total})
|
|
}
|