2014-04-13 01:57:42 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2019-02-08 09:45:43 -07:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2014-04-13 01:57:42 -04:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
2014-11-23 02:33:47 -05:00
"bytes"
2014-04-13 01:57:42 -04:00
"fmt"
2021-04-05 17:30:52 +02:00
"net/http"
2019-12-02 04:50:36 +01:00
"regexp"
2017-12-03 20:39:01 -08:00
"sort"
2019-12-02 04:50:36 +01:00
"strconv"
2019-01-23 06:10:38 +02:00
"strings"
2014-04-13 01:57:42 -04:00
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/models"
2021-09-24 19:32:56 +08:00
"code.gitea.io/gitea/models/db"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
2020-02-29 00:52:05 -06:00
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
2021-07-25 00:03:58 +08:00
"code.gitea.io/gitea/modules/json"
2019-10-09 01:55:16 +08:00
"code.gitea.io/gitea/modules/log"
2021-04-20 06:25:08 +08:00
"code.gitea.io/gitea/modules/markup"
2019-12-15 08:20:08 -06:00
"code.gitea.io/gitea/modules/markup/markdown"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/setting"
2017-01-24 21:43:02 -05:00
"code.gitea.io/gitea/modules/util"
2020-05-15 00:55:43 +02:00
issue_service "code.gitea.io/gitea/services/issue"
2020-04-10 13:26:37 +02:00
pull_service "code.gitea.io/gitea/services/pull"
2017-12-25 18:25:16 -05:00
2019-04-14 18:43:56 +02:00
"github.com/keybase/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/armor"
2020-03-31 15:47:00 +08:00
"xorm.io/builder"
2014-04-13 01:57:42 -04:00
)
2014-06-22 23:11:12 -04:00
const (
2019-12-15 08:20:08 -06:00
tplDashboard base . TplName = "user/dashboard/dashboard"
tplIssues base . TplName = "user/dashboard/issues"
tplMilestones base . TplName = "user/dashboard/milestones"
tplProfile base . TplName = "user/profile"
2014-06-22 23:11:12 -04:00
)
2021-01-13 05:19:17 +01:00
// getDashboardContextUser finds out which context user dashboard is being viewed as .
2016-03-11 11:56:52 -05:00
func getDashboardContextUser ( ctx * context . Context ) * models . User {
2015-08-25 22:58:34 +08:00
ctxUser := ctx . User
2014-07-26 23:53:16 -04:00
orgName := ctx . Params ( ":org" )
if len ( orgName ) > 0 {
2020-12-27 20:58:03 +01:00
ctxUser = ctx . Org . Organization
ctx . Data [ "Teams" ] = ctx . Org . Organization . Teams
2015-08-25 22:58:34 +08:00
}
ctx . Data [ "ContextUser" ] = ctxUser
2021-09-01 06:31:42 +01:00
orgs , err := models . GetUserOrgsList ( ctx . User )
2021-06-14 20:18:09 +08:00
if err != nil {
ctx . ServerError ( "GetUserOrgsList" , err )
2015-08-25 22:58:34 +08:00
return nil
}
2021-06-14 20:18:09 +08:00
ctx . Data [ "Orgs" ] = orgs
2015-08-25 22:58:34 +08:00
return ctxUser
}
2017-06-01 20:42:25 -04:00
// retrieveFeeds loads feeds for the specified user
2017-08-23 03:30:54 +02:00
func retrieveFeeds ( ctx * context . Context , options models . GetFeedsOptions ) {
actions , err := models . GetFeeds ( options )
2015-11-22 01:32:09 -05:00
if err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "GetFeeds" , err )
2015-11-22 01:32:09 -05:00
return
}
2017-08-23 03:30:54 +02:00
userCache := map [ int64 ] * models . User { options . RequestedUser . ID : options . RequestedUser }
2017-06-07 22:11:41 -04:00
if ctx . User != nil {
userCache [ ctx . User . ID ] = ctx . User
}
2015-11-22 01:32:09 -05:00
for _ , act := range actions {
2018-02-21 18:55:34 +08:00
if act . ActUser != nil {
userCache [ act . ActUserID ] = act . ActUser
2017-05-25 21:38:18 -04:00
}
2019-12-16 04:44:23 +08:00
}
2017-05-25 21:38:18 -04:00
2019-12-16 04:44:23 +08:00
for _ , act := range actions {
2018-02-21 18:55:34 +08:00
repoOwner , ok := userCache [ act . Repo . OwnerID ]
2017-05-25 21:38:18 -04:00
if ! ok {
2018-02-21 18:55:34 +08:00
repoOwner , err = models . GetUserByID ( act . Repo . OwnerID )
2017-05-25 21:38:18 -04:00
if err != nil {
if models . IsErrUserNotExist ( err ) {
continue
}
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "GetUserByID" , err )
2015-11-22 01:32:09 -05:00
return
}
2018-02-21 18:55:34 +08:00
userCache [ repoOwner . ID ] = repoOwner
2015-11-22 01:32:09 -05:00
}
2018-02-21 18:55:34 +08:00
act . Repo . Owner = repoOwner
2015-11-22 01:32:09 -05:00
}
2017-06-01 20:42:25 -04:00
ctx . Data [ "Feeds" ] = actions
2015-11-22 01:32:09 -05:00
}
2020-08-17 04:07:38 +01:00
// Dashboard render the dashboard page
2016-03-11 11:56:52 -05:00
func Dashboard ( ctx * context . Context ) {
2016-03-09 22:56:03 -06:00
ctxUser := getDashboardContextUser ( ctx )
2015-08-25 22:58:34 +08:00
if ctx . Written ( ) {
return
}
2016-07-24 01:08:22 +08:00
ctx . Data [ "Title" ] = ctxUser . DisplayName ( ) + " - " + ctx . Tr ( "dashboard" )
ctx . Data [ "PageIsDashboard" ] = true
ctx . Data [ "PageIsNews" ] = true
2017-08-17 03:31:34 +02:00
ctx . Data [ "SearchLimit" ] = setting . UI . User . RepoPagingNum
2020-12-27 20:58:03 +01:00
2021-03-04 22:59:13 +00:00
if setting . Service . EnableUserHeatmap {
2020-12-27 20:58:03 +01:00
data , err := models . GetUserHeatmapDataByUserTeam ( ctxUser , ctx . Org . Team , ctx . User )
2020-11-18 23:00:16 +01:00
if err != nil {
2020-12-27 20:58:03 +01:00
ctx . ServerError ( "GetUserHeatmapDataByUserTeam" , err )
2020-11-18 23:00:16 +01:00
return
}
ctx . Data [ "HeatmapData" ] = data
}
2016-07-24 01:08:22 +08:00
2016-07-24 14:32:46 +08:00
var err error
2017-08-23 03:30:54 +02:00
var mirrors [ ] * models . Repository
2016-02-02 19:29:35 +00:00
if ctxUser . IsOrganization ( ) {
2020-12-27 20:58:03 +01:00
var env models . AccessibleReposEnvironment
if ctx . Org . Team != nil {
env = ctxUser . AccessibleTeamReposEnv ( ctx . Org . Team )
} else {
env , err = ctxUser . AccessibleReposEnv ( ctx . User . ID )
if err != nil {
ctx . ServerError ( "AccessibleReposEnv" , err )
return
}
2017-01-25 10:41:38 -05:00
}
mirrors , err = env . MirrorRepos ( )
2016-02-02 19:29:35 +00:00
if err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "env.MirrorRepos" , err )
2016-07-24 14:32:46 +08:00
return
}
} else {
mirrors , err = ctxUser . GetMirrorRepositories ( )
if err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "GetMirrorRepositories" , err )
2016-07-24 14:32:46 +08:00
return
}
2014-04-13 01:57:42 -04:00
}
2016-07-24 14:32:46 +08:00
ctx . Data [ "MaxShowRepoNum" ] = setting . UI . User . RepoPagingNum
2014-04-13 01:57:42 -04:00
2016-07-24 14:32:46 +08:00
if err := models . MirrorRepositoryList ( mirrors ) . LoadAttributes ( ) ; err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "MirrorRepositoryList.LoadAttributes" , err )
2016-07-24 14:32:46 +08:00
return
2014-07-26 02:28:04 -04:00
}
2014-07-26 23:53:16 -04:00
ctx . Data [ "MirrorCount" ] = len ( mirrors )
ctx . Data [ "Mirrors" ] = mirrors
2014-05-12 15:14:22 -04:00
2018-02-21 18:55:34 +08:00
retrieveFeeds ( ctx , models . GetFeedsOptions {
RequestedUser : ctxUser ,
2020-12-27 20:58:03 +01:00
RequestedTeam : ctx . Org . Team ,
2020-01-13 19:33:46 +02:00
Actor : ctx . User ,
2017-08-23 03:30:54 +02:00
IncludePrivate : true ,
OnlyPerformedBy : false ,
IncludeDeleted : false ,
2021-08-11 02:31:13 +02:00
Date : ctx . FormString ( "date" ) ,
2017-08-23 03:30:54 +02:00
} )
2018-12-13 23:55:43 +08:00
2015-11-22 01:32:09 -05:00
if ctx . Written ( ) {
2014-04-13 01:57:42 -04:00
return
}
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tplDashboard )
2014-04-13 01:57:42 -04:00
}
2019-12-15 08:20:08 -06:00
// Milestones render the user milestones page
func Milestones ( ctx * context . Context ) {
2020-01-17 08:34:37 +01:00
if models . UnitTypeIssues . UnitGlobalDisabled ( ) && models . UnitTypePullRequests . UnitGlobalDisabled ( ) {
log . Debug ( "Milestones overview page not available as both issues and pull requests are globally disabled" )
ctx . Status ( 404 )
return
}
2019-12-15 08:20:08 -06:00
ctx . Data [ "Title" ] = ctx . Tr ( "milestones" )
ctx . Data [ "PageIsMilestonesDashboard" ] = true
ctxUser := getDashboardContextUser ( ctx )
if ctx . Written ( ) {
return
}
2020-12-27 20:58:03 +01:00
repoOpts := models . SearchRepoOptions {
Actor : ctxUser ,
OwnerID : ctxUser . ID ,
Private : true ,
AllPublic : false , // Include also all public repositories of users and public organisations
AllLimited : false , // Include also all public repositories of limited organisations
HasMilestones : util . OptionalBoolTrue , // Just needs display repos has milestones
}
if ctxUser . IsOrganization ( ) && ctx . Org . Team != nil {
repoOpts . TeamID = ctx . Org . Team . ID
}
2019-12-15 08:20:08 -06:00
2020-12-27 20:58:03 +01:00
var (
2020-03-31 15:47:00 +08:00
userRepoCond = models . SearchRepositoryCondition ( & repoOpts ) // all repo condition user could visit
repoCond = userRepoCond
repoIDs [ ] int64
2019-12-15 08:20:08 -06:00
2021-08-11 02:31:13 +02:00
reposQuery = ctx . FormString ( "repos" )
isShowClosed = ctx . FormString ( "state" ) == "closed"
sortType = ctx . FormString ( "sort" )
2021-07-29 09:42:15 +08:00
page = ctx . FormInt ( "page" )
2021-08-11 17:08:52 +02:00
keyword = ctx . FormTrim ( "q" )
2020-03-31 15:47:00 +08:00
)
if page <= 1 {
page = 1
2019-12-15 08:20:08 -06:00
}
2020-01-05 02:23:29 +01:00
if len ( reposQuery ) != 0 {
if issueReposQueryPattern . MatchString ( reposQuery ) {
// remove "[" and "]" from string
reposQuery = reposQuery [ 1 : len ( reposQuery ) - 1 ]
//for each ID (delimiter ",") add to int to repoIDs
2020-03-31 15:47:00 +08:00
2020-01-05 02:23:29 +01:00
for _ , rID := range strings . Split ( reposQuery , "," ) {
// Ensure nonempty string entries
if rID != "" && rID != "0" {
rIDint64 , err := strconv . ParseInt ( rID , 10 , 64 )
// If the repo id specified by query is not parseable or not accessible by user, just ignore it.
2020-03-31 15:47:00 +08:00
if err == nil {
2020-01-05 02:23:29 +01:00
repoIDs = append ( repoIDs , rIDint64 )
}
2019-12-15 08:20:08 -06:00
}
}
2020-03-31 15:47:00 +08:00
if len ( repoIDs ) > 0 {
// Don't just let repoCond = builder.In("id", repoIDs) because user may has no permission on repoIDs
// But the original repoCond has a limitation
repoCond = repoCond . And ( builder . In ( "id" , repoIDs ) )
2020-01-05 02:23:29 +01:00
}
} else {
log . Warn ( "issueReposQueryPattern not match with query" )
2019-12-15 08:20:08 -06:00
}
}
2021-04-08 19:53:59 +08:00
counts , err := models . CountMilestonesByRepoCondAndKw ( userRepoCond , keyword , isShowClosed )
2019-12-15 08:20:08 -06:00
if err != nil {
ctx . ServerError ( "CountMilestonesByRepoIDs" , err )
return
}
2021-04-08 19:53:59 +08:00
milestones , err := models . SearchMilestones ( repoCond , page , isShowClosed , sortType , keyword )
2019-12-15 08:20:08 -06:00
if err != nil {
2021-04-08 19:53:59 +08:00
ctx . ServerError ( "SearchMilestones" , err )
2019-12-15 08:20:08 -06:00
return
}
2020-03-31 15:47:00 +08:00
showRepos , _ , err := models . SearchRepositoryByCondition ( & repoOpts , userRepoCond , false )
if err != nil {
ctx . ServerError ( "SearchRepositoryByCondition" , err )
2019-12-15 08:20:08 -06:00
return
}
2020-03-31 15:47:00 +08:00
sort . Sort ( showRepos )
2019-12-15 08:20:08 -06:00
2020-03-31 15:47:00 +08:00
for i := 0 ; i < len ( milestones ) ; {
for _ , repo := range showRepos {
if milestones [ i ] . RepoID == repo . ID {
milestones [ i ] . Repo = repo
break
}
}
if milestones [ i ] . Repo == nil {
log . Warn ( "Cannot find milestone %d 's repository %d" , milestones [ i ] . ID , milestones [ i ] . RepoID )
milestones = append ( milestones [ : i ] , milestones [ i + 1 : ] ... )
continue
}
2021-04-20 06:25:08 +08:00
milestones [ i ] . RenderedContent , err = markdown . RenderString ( & markup . RenderContext {
URLPrefix : milestones [ i ] . Repo . Link ( ) ,
Metas : milestones [ i ] . Repo . ComposeMetas ( ) ,
2021-08-28 21:15:56 +01:00
Ctx : ctx ,
2021-04-20 06:25:08 +08:00
} , milestones [ i ] . Content )
if err != nil {
ctx . ServerError ( "RenderString" , err )
return
}
2020-03-31 15:47:00 +08:00
if milestones [ i ] . Repo . IsTimetrackerEnabled ( ) {
err := milestones [ i ] . LoadTotalTrackedTime ( )
2019-12-15 08:20:08 -06:00
if err != nil {
ctx . ServerError ( "LoadTotalTrackedTime" , err )
return
}
}
2020-03-31 15:47:00 +08:00
i ++
2019-12-15 08:20:08 -06:00
}
2021-04-08 19:53:59 +08:00
milestoneStats , err := models . GetMilestonesStatsByRepoCondAndKw ( repoCond , keyword )
2019-12-15 08:20:08 -06:00
if err != nil {
ctx . ServerError ( "GetMilestoneStats" , err )
return
}
2020-03-31 15:47:00 +08:00
var totalMilestoneStats * models . MilestonesStats
if len ( repoIDs ) == 0 {
totalMilestoneStats = milestoneStats
} else {
2021-04-08 19:53:59 +08:00
totalMilestoneStats , err = models . GetMilestonesStatsByRepoCondAndKw ( userRepoCond , keyword )
2020-03-31 15:47:00 +08:00
if err != nil {
ctx . ServerError ( "GetMilestoneStats" , err )
return
}
2019-12-15 08:20:08 -06:00
}
var pagerCount int
if isShowClosed {
ctx . Data [ "State" ] = "closed"
ctx . Data [ "Total" ] = totalMilestoneStats . ClosedCount
pagerCount = int ( milestoneStats . ClosedCount )
} else {
ctx . Data [ "State" ] = "open"
ctx . Data [ "Total" ] = totalMilestoneStats . OpenCount
pagerCount = int ( milestoneStats . OpenCount )
}
ctx . Data [ "Milestones" ] = milestones
ctx . Data [ "Repos" ] = showRepos
ctx . Data [ "Counts" ] = counts
ctx . Data [ "MilestoneStats" ] = milestoneStats
ctx . Data [ "SortType" ] = sortType
2021-04-08 19:53:59 +08:00
ctx . Data [ "Keyword" ] = keyword
2020-03-31 15:47:00 +08:00
if milestoneStats . Total ( ) != totalMilestoneStats . Total ( ) {
2019-12-15 08:20:08 -06:00
ctx . Data [ "RepoIDs" ] = repoIDs
}
ctx . Data [ "IsShowClosed" ] = isShowClosed
pager := context . NewPagination ( pagerCount , setting . UI . IssuePagingNum , page , 5 )
2021-04-08 19:53:59 +08:00
pager . AddParam ( ctx , "q" , "Keyword" )
2019-12-15 08:20:08 -06:00
pager . AddParam ( ctx , "repos" , "RepoIDs" )
pager . AddParam ( ctx , "sort" , "SortType" )
pager . AddParam ( ctx , "state" , "State" )
ctx . Data [ "Page" ] = pager
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tplMilestones )
2019-12-15 08:20:08 -06:00
}
2021-01-13 05:19:17 +01:00
// Pulls renders the user's pull request overview page
func Pulls ( ctx * context . Context ) {
if models . UnitTypePullRequests . UnitGlobalDisabled ( ) {
log . Debug ( "Pull request overview page not available as it is globally disabled." )
ctx . Status ( 404 )
return
}
ctx . Data [ "Title" ] = ctx . Tr ( "pull_requests" )
ctx . Data [ "PageIsPulls" ] = true
buildIssueOverview ( ctx , models . UnitTypePullRequests )
}
2019-12-02 04:50:36 +01:00
2021-01-13 05:19:17 +01:00
// Issues renders the user's issues overview page
2016-03-11 11:56:52 -05:00
func Issues ( ctx * context . Context ) {
2021-01-13 05:19:17 +01:00
if models . UnitTypeIssues . UnitGlobalDisabled ( ) {
log . Debug ( "Issues overview page not available as it is globally disabled." )
ctx . Status ( 404 )
return
}
2020-01-17 08:34:37 +01:00
2021-01-13 05:19:17 +01:00
ctx . Data [ "Title" ] = ctx . Tr ( "issues" )
ctx . Data [ "PageIsIssues" ] = true
buildIssueOverview ( ctx , models . UnitTypeIssues )
}
2020-01-17 08:34:37 +01:00
2021-01-13 05:19:17 +01:00
// Regexp for repos query
var issueReposQueryPattern = regexp . MustCompile ( ` ^\[\d+(,\d+)*,?\]$ ` )
func buildIssueOverview ( ctx * context . Context , unitType models . UnitType ) {
// ----------------------------------------------------
// Determine user; can be either user or organization.
// Return with NotFound or ServerError if unsuccessful.
// ----------------------------------------------------
2015-08-25 22:58:34 +08:00
ctxUser := getDashboardContextUser ( ctx )
if ctx . Written ( ) {
return
}
var (
viewType string
2021-08-11 02:31:13 +02:00
sortType = ctx . FormString ( "sort" )
2016-11-07 17:24:59 +01:00
filterMode = models . FilterModeAll
2015-08-25 22:58:34 +08:00
)
2017-02-14 22:15:18 +08:00
2021-01-13 05:19:17 +01:00
// --------------------------------------------------------------------------------
// Distinguish User from Organization.
// Org:
// - Remember pre-determined viewType string for later. Will be posted to ctx.Data.
// Organization does not have view type and filter mode.
// User:
2021-08-11 02:31:13 +02:00
// - Use ctx.FormString("type") to determine filterMode.
2021-01-13 05:19:17 +01:00
// The type is set when clicking for example "assigned to me" on the overview page.
// - Remember either this or a fallback. Will be posted to ctx.Data.
// --------------------------------------------------------------------------------
// TODO: distinguish during routing
2021-08-11 02:31:13 +02:00
viewType = ctx . FormString ( "type" )
2021-01-03 18:29:12 +01:00
switch viewType {
case "assigned" :
filterMode = models . FilterModeAssign
case "created_by" :
filterMode = models . FilterModeCreate
case "mentioned" :
filterMode = models . FilterModeMention
2021-01-17 17:34:19 +01:00
case "review_requested" :
filterMode = models . FilterModeReviewRequested
2021-01-03 18:29:12 +01:00
case "your_repositories" : // filterMode already set to All
default :
2019-12-03 00:01:29 -06:00
viewType = "your_repositories"
2015-08-25 22:58:34 +08:00
}
2021-01-13 05:19:17 +01:00
// --------------------------------------------------------------------------
// Build opts (IssuesOptions), which contains filter information.
// Will eventually be used to retrieve issues relevant for the overview page.
// Note: Non-final states of opts are used in-between, namely for:
// - Keyword search
// - Count Issues by repo
// --------------------------------------------------------------------------
2017-02-14 22:15:18 +08:00
2021-01-13 05:19:17 +01:00
isPullList := unitType == models . UnitTypePullRequests
opts := & models . IssuesOptions {
IsPull : util . OptionalBoolOf ( isPullList ) ,
SortType : sortType ,
IsArchived : util . OptionalBoolFalse ,
2019-12-02 04:50:36 +01:00
}
2021-01-13 05:19:17 +01:00
// Get repository IDs where User/Org/Team has access.
var team * models . Team
if ctx . Org != nil {
team = ctx . Org . Team
2017-02-17 08:58:19 +08:00
}
2021-01-13 05:19:17 +01:00
userRepoIDs , err := getActiveUserRepoIDs ( ctxUser , team , unitType )
if err != nil {
ctx . ServerError ( "userRepoIDs" , err )
return
2019-10-09 01:55:16 +08:00
}
2017-02-14 22:15:18 +08:00
switch filterMode {
2019-10-09 01:55:16 +08:00
case models . FilterModeAll :
2019-12-02 04:50:36 +01:00
opts . RepoIDs = userRepoIDs
2017-02-14 22:15:18 +08:00
case models . FilterModeAssign :
2021-01-03 18:29:12 +01:00
opts . AssigneeID = ctx . User . ID
2017-02-14 22:15:18 +08:00
case models . FilterModeCreate :
2021-01-03 18:29:12 +01:00
opts . PosterID = ctx . User . ID
2017-02-14 22:15:18 +08:00
case models . FilterModeMention :
2021-03-12 03:06:33 +00:00
opts . MentionedID = ctx . User . ID
2021-01-17 17:34:19 +01:00
case models . FilterModeReviewRequested :
2021-03-12 03:06:33 +00:00
opts . ReviewRequestedID = ctx . User . ID
2021-01-03 18:29:12 +01:00
}
if ctxUser . IsOrganization ( ) {
opts . RepoIDs = userRepoIDs
2017-02-14 22:15:18 +08:00
}
2021-01-13 05:19:17 +01:00
// keyword holds the search term entered into the search field.
2021-08-11 02:31:13 +02:00
keyword := strings . Trim ( ctx . FormString ( "q" ) , " " )
2021-01-13 05:19:17 +01:00
ctx . Data [ "Keyword" ] = keyword
2020-02-29 00:52:05 -06:00
2021-01-13 05:19:17 +01:00
// Execute keyword search for issues.
// USING NON-FINAL STATE OF opts FOR A QUERY.
issueIDsFromSearch , err := issueIDsFromSearch ( ctxUser , keyword , opts )
if err != nil {
ctx . ServerError ( "issueIDsFromSearch" , err )
return
2020-02-29 00:52:05 -06:00
}
2021-01-13 05:19:17 +01:00
// Ensure no issues are returned if a keyword was provided that didn't match any issues.
var forceEmpty bool
if len ( issueIDsFromSearch ) > 0 {
opts . IssueIDs = issueIDsFromSearch
} else if len ( keyword ) > 0 {
forceEmpty = true
}
2020-02-29 00:52:05 -06:00
2021-01-13 05:19:17 +01:00
// Educated guess: Do or don't show closed issues.
2021-08-11 02:31:13 +02:00
isShowClosed := ctx . FormString ( "state" ) == "closed"
2020-02-29 00:52:05 -06:00
opts . IsClosed = util . OptionalBoolOf ( isShowClosed )
2021-01-13 05:19:17 +01:00
// Filter repos and count issues in them. Count will be used later.
// USING NON-FINAL STATE OF opts FOR A QUERY.
var issueCountByRepo map [ int64 ] int64
2020-02-29 00:52:05 -06:00
if ! forceEmpty {
2021-01-13 05:19:17 +01:00
issueCountByRepo , err = models . CountIssuesByRepo ( opts )
2020-02-29 00:52:05 -06:00
if err != nil {
ctx . ServerError ( "CountIssuesByRepo" , err )
return
}
2019-10-09 01:55:16 +08:00
}
2021-01-13 05:19:17 +01:00
// Make sure page number is at least 1. Will be posted to ctx.Data.
2021-07-29 09:42:15 +08:00
page := ctx . FormInt ( "page" )
2021-01-13 05:19:17 +01:00
if page <= 1 {
page = 1
}
2019-10-09 01:55:16 +08:00
opts . Page = page
opts . PageSize = setting . UI . IssuePagingNum
2021-01-13 05:19:17 +01:00
// Get IDs for labels (a filter option for issues/pulls).
// Required for IssuesOptions.
2019-01-23 06:10:38 +02:00
var labelIDs [ ] int64
2021-08-11 02:31:13 +02:00
selectedLabels := ctx . FormString ( "labels" )
2021-01-13 05:19:17 +01:00
if len ( selectedLabels ) > 0 && selectedLabels != "0" {
labelIDs , err = base . StringsToInt64s ( strings . Split ( selectedLabels , "," ) )
2019-01-23 06:10:38 +02:00
if err != nil {
ctx . ServerError ( "StringsToInt64s" , err )
return
}
}
opts . LabelIDs = labelIDs
2018-10-28 03:55:01 -03:00
2021-08-11 02:31:13 +02:00
// Parse ctx.FormString("repos") and remember matched repo IDs for later.
2021-01-13 05:19:17 +01:00
// Gets set when clicking filters on the issues overview page.
2021-08-11 02:31:13 +02:00
repoIDs := getRepoIDs ( ctx . FormString ( "repos" ) )
2019-12-03 08:26:02 +01:00
if len ( repoIDs ) > 0 {
opts . RepoIDs = repoIDs
}
2019-12-02 04:50:36 +01:00
2021-01-13 05:19:17 +01:00
// ------------------------------
// Get issues as defined by opts.
// ------------------------------
// Slice of Issues that will be displayed on the overview page
// USING FINAL STATE OF opts FOR A QUERY.
2020-02-29 00:52:05 -06:00
var issues [ ] * models . Issue
if ! forceEmpty {
issues , err = models . Issues ( opts )
if err != nil {
ctx . ServerError ( "Issues" , err )
return
}
} else {
issues = [ ] * models . Issue { }
2017-02-17 08:58:19 +08:00
}
2015-09-02 16:18:09 -04:00
2021-01-13 05:19:17 +01:00
// ----------------------------------
// Add repository pointers to Issues.
// ----------------------------------
2019-10-09 01:55:16 +08:00
2021-01-13 05:19:17 +01:00
// showReposMap maps repository IDs to their Repository pointers.
showReposMap , err := repoIDMap ( ctxUser , issueCountByRepo , unitType )
if err != nil {
if models . IsErrRepoNotExist ( err ) {
ctx . NotFound ( "GetRepositoryByID" , err )
return
2019-10-09 01:55:16 +08:00
}
2021-01-13 05:19:17 +01:00
ctx . ServerError ( "repoIDMap" , err )
return
2019-10-09 01:55:16 +08:00
}
2021-01-13 05:19:17 +01:00
// a RepositoryList
2017-08-02 22:09:16 -07:00
showRepos := models . RepositoryListOfMap ( showReposMap )
2017-12-03 20:39:01 -08:00
sort . Sort ( showRepos )
2017-08-02 22:09:16 -07:00
if err = showRepos . LoadAttributes ( ) ; err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "LoadAttributes" , err )
2017-02-17 08:58:19 +08:00
return
2015-08-25 23:22:05 +08:00
}
2021-01-13 05:19:17 +01:00
// maps pull request IDs to their CommitStatus. Will be posted to ctx.Data.
2017-08-02 22:09:16 -07:00
for _ , issue := range issues {
issue . Repo = showReposMap [ issue . RepoID ]
2021-04-16 01:34:43 +08:00
}
2019-04-02 21:54:29 +02:00
2021-04-16 01:34:43 +08:00
commitStatus , err := pull_service . GetIssuesLastCommitStatus ( issues )
if err != nil {
ctx . ServerError ( "GetIssuesLastCommitStatus" , err )
return
2017-08-02 22:09:16 -07:00
}
2021-01-13 05:19:17 +01:00
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
2020-02-29 00:52:05 -06:00
userIssueStatsOpts := models . UserIssueStatsOptions {
2021-01-03 18:29:12 +01:00
UserID : ctx . User . ID ,
2019-10-09 01:55:16 +08:00
UserRepoIDs : userRepoIDs ,
FilterMode : filterMode ,
IsPull : isPullList ,
IsClosed : isShowClosed ,
2021-01-13 05:19:17 +01:00
IsArchived : util . OptionalBoolFalse ,
2021-01-02 01:49:42 +08:00
LabelIDs : opts . LabelIDs ,
2020-02-03 03:21:50 +01:00
}
if len ( repoIDs ) > 0 {
2020-02-29 00:52:05 -06:00
userIssueStatsOpts . UserRepoIDs = repoIDs
2020-02-03 03:21:50 +01:00
}
2021-01-03 18:29:12 +01:00
if ctxUser . IsOrganization ( ) {
userIssueStatsOpts . RepoIDs = userRepoIDs
}
2020-02-29 00:52:05 -06:00
userIssueStats , err := models . GetUserIssueStats ( userIssueStatsOpts )
2017-12-25 18:25:16 -05:00
if err != nil {
2020-02-29 00:52:05 -06:00
ctx . ServerError ( "GetUserIssueStats User" , err )
2017-12-25 18:25:16 -05:00
return
}
2017-02-14 22:15:18 +08:00
2020-02-29 00:52:05 -06:00
var shownIssueStats * models . IssueStats
if ! forceEmpty {
statsOpts := models . UserIssueStatsOptions {
2021-01-03 18:29:12 +01:00
UserID : ctx . User . ID ,
2020-02-29 00:52:05 -06:00
UserRepoIDs : userRepoIDs ,
FilterMode : filterMode ,
IsPull : isPullList ,
IsClosed : isShowClosed ,
IssueIDs : issueIDsFromSearch ,
2021-01-13 05:19:17 +01:00
IsArchived : util . OptionalBoolFalse ,
2021-01-02 01:49:42 +08:00
LabelIDs : opts . LabelIDs ,
2020-02-29 00:52:05 -06:00
}
if len ( repoIDs ) > 0 {
statsOpts . RepoIDs = repoIDs
2021-01-03 18:29:12 +01:00
} else if ctxUser . IsOrganization ( ) {
statsOpts . RepoIDs = userRepoIDs
2020-02-29 00:52:05 -06:00
}
shownIssueStats , err = models . GetUserIssueStats ( statsOpts )
if err != nil {
ctx . ServerError ( "GetUserIssueStats Shown" , err )
return
}
} else {
shownIssueStats = & models . IssueStats { }
}
var allIssueStats * models . IssueStats
if ! forceEmpty {
2021-01-03 18:29:12 +01:00
allIssueStatsOpts := models . UserIssueStatsOptions {
UserID : ctx . User . ID ,
2020-02-29 00:52:05 -06:00
UserRepoIDs : userRepoIDs ,
FilterMode : filterMode ,
IsPull : isPullList ,
IsClosed : isShowClosed ,
IssueIDs : issueIDsFromSearch ,
2021-01-13 05:19:17 +01:00
IsArchived : util . OptionalBoolFalse ,
2021-01-02 01:49:42 +08:00
LabelIDs : opts . LabelIDs ,
2021-01-03 18:29:12 +01:00
}
if ctxUser . IsOrganization ( ) {
allIssueStatsOpts . RepoIDs = userRepoIDs
}
allIssueStats , err = models . GetUserIssueStats ( allIssueStatsOpts )
2020-02-29 00:52:05 -06:00
if err != nil {
ctx . ServerError ( "GetUserIssueStats All" , err )
return
}
} else {
allIssueStats = & models . IssueStats { }
2019-12-02 04:50:36 +01:00
}
2021-01-13 05:19:17 +01:00
// Will be posted to ctx.Data.
2019-12-02 04:50:36 +01:00
var shownIssues int
2015-08-25 23:22:05 +08:00
if ! isShowClosed {
2020-02-29 00:52:05 -06:00
shownIssues = int ( shownIssueStats . OpenCount )
2021-01-13 05:19:17 +01:00
ctx . Data [ "TotalIssueCount" ] = int ( allIssueStats . OpenCount )
2015-08-25 23:22:05 +08:00
} else {
2020-02-29 00:52:05 -06:00
shownIssues = int ( shownIssueStats . ClosedCount )
2021-01-13 05:19:17 +01:00
ctx . Data [ "TotalIssueCount" ] = int ( allIssueStats . ClosedCount )
2015-08-25 23:22:05 +08:00
}
2021-01-13 05:19:17 +01:00
ctx . Data [ "IsShowClosed" ] = isShowClosed
2020-05-15 00:55:43 +02:00
ctx . Data [ "IssueRefEndNames" ] , ctx . Data [ "IssueRefURLs" ] =
2021-08-11 02:31:13 +02:00
issue_service . GetRefEndNamesAndURLs ( issues , ctx . FormString ( "RepoLink" ) )
2020-05-15 00:55:43 +02:00
2015-08-25 22:58:34 +08:00
ctx . Data [ "Issues" ] = issues
2021-01-13 05:19:17 +01:00
approvalCounts , err := models . IssueList ( issues ) . GetApprovalCounts ( )
if err != nil {
ctx . ServerError ( "ApprovalCounts" , err )
return
}
2020-03-06 03:44:06 +00:00
ctx . Data [ "ApprovalCounts" ] = func ( issueID int64 , typ string ) int64 {
counts , ok := approvalCounts [ issueID ]
if ! ok || len ( counts ) == 0 {
return 0
}
reviewTyp := models . ReviewTypeApprove
if typ == "reject" {
reviewTyp = models . ReviewTypeReject
2020-04-07 00:33:34 +08:00
} else if typ == "waiting" {
reviewTyp = models . ReviewTypeRequest
2020-03-06 03:44:06 +00:00
}
for _ , count := range counts {
if count . Type == reviewTyp {
return count . Count
}
}
return 0
}
2019-04-02 21:54:29 +02:00
ctx . Data [ "CommitStatus" ] = commitStatus
2017-02-14 22:15:18 +08:00
ctx . Data [ "Repos" ] = showRepos
2021-01-13 05:19:17 +01:00
ctx . Data [ "Counts" ] = issueCountByRepo
2020-02-29 00:52:05 -06:00
ctx . Data [ "IssueStats" ] = userIssueStats
ctx . Data [ "ShownIssueStats" ] = shownIssueStats
2015-08-25 22:58:34 +08:00
ctx . Data [ "ViewType" ] = viewType
2015-11-04 12:50:02 -05:00
ctx . Data [ "SortType" ] = sortType
2019-12-02 04:50:36 +01:00
ctx . Data [ "RepoIDs" ] = repoIDs
2015-08-25 22:58:34 +08:00
ctx . Data [ "IsShowClosed" ] = isShowClosed
2021-01-13 05:19:17 +01:00
ctx . Data [ "SelectLabels" ] = selectedLabels
2017-02-14 22:15:18 +08:00
2015-08-25 22:58:34 +08:00
if isShowClosed {
ctx . Data [ "State" ] = "closed"
} else {
ctx . Data [ "State" ] = "open"
}
2019-12-02 04:50:36 +01:00
// Convert []int64 to string
reposParam , _ := json . Marshal ( repoIDs )
ctx . Data [ "ReposParam" ] = string ( reposParam )
pager := context . NewPagination ( shownIssues , setting . UI . IssuePagingNum , page , 5 )
2020-02-29 00:52:05 -06:00
pager . AddParam ( ctx , "q" , "Keyword" )
2019-04-20 06:15:19 +02:00
pager . AddParam ( ctx , "type" , "ViewType" )
2019-12-02 04:50:36 +01:00
pager . AddParam ( ctx , "repos" , "ReposParam" )
2019-04-20 06:15:19 +02:00
pager . AddParam ( ctx , "sort" , "SortType" )
pager . AddParam ( ctx , "state" , "State" )
pager . AddParam ( ctx , "labels" , "SelectLabels" )
pager . AddParam ( ctx , "milestone" , "MilestoneID" )
pager . AddParam ( ctx , "assignee" , "AssigneeID" )
ctx . Data [ "Page" ] = pager
2021-04-05 17:30:52 +02:00
ctx . HTML ( http . StatusOK , tplIssues )
2015-08-25 22:58:34 +08:00
}
2021-01-13 05:19:17 +01:00
func getRepoIDs ( reposQuery string ) [ ] int64 {
2021-03-12 03:06:33 +00:00
if len ( reposQuery ) == 0 || reposQuery == "[]" {
2021-01-13 05:19:17 +01:00
return [ ] int64 { }
}
if ! issueReposQueryPattern . MatchString ( reposQuery ) {
log . Warn ( "issueReposQueryPattern does not match query" )
return [ ] int64 { }
}
var repoIDs [ ] int64
// remove "[" and "]" from string
reposQuery = reposQuery [ 1 : len ( reposQuery ) - 1 ]
//for each ID (delimiter ",") add to int to repoIDs
for _ , rID := range strings . Split ( reposQuery , "," ) {
// Ensure nonempty string entries
if rID != "" && rID != "0" {
rIDint64 , err := strconv . ParseInt ( rID , 10 , 64 )
if err == nil {
repoIDs = append ( repoIDs , rIDint64 )
}
}
}
return repoIDs
}
func getActiveUserRepoIDs ( ctxUser * models . User , team * models . Team , unitType models . UnitType ) ( [ ] int64 , error ) {
var userRepoIDs [ ] int64
var err error
if ctxUser . IsOrganization ( ) {
userRepoIDs , err = getActiveTeamOrOrgRepoIds ( ctxUser , team , unitType )
if err != nil {
return nil , fmt . Errorf ( "orgRepoIds: %v" , err )
}
} else {
userRepoIDs , err = ctxUser . GetActiveAccessRepoIDs ( unitType )
if err != nil {
return nil , fmt . Errorf ( "ctxUser.GetAccessRepoIDs: %v" , err )
}
}
if len ( userRepoIDs ) == 0 {
userRepoIDs = [ ] int64 { - 1 }
}
return userRepoIDs , nil
}
// getActiveTeamOrOrgRepoIds gets RepoIDs for ctxUser as Organization.
// Should be called if and only if ctxUser.IsOrganization == true.
func getActiveTeamOrOrgRepoIds ( ctxUser * models . User , team * models . Team , unitType models . UnitType ) ( [ ] int64 , error ) {
var orgRepoIDs [ ] int64
var err error
var env models . AccessibleReposEnvironment
if team != nil {
env = ctxUser . AccessibleTeamReposEnv ( team )
} else {
env , err = ctxUser . AccessibleReposEnv ( ctxUser . ID )
if err != nil {
return nil , fmt . Errorf ( "AccessibleReposEnv: %v" , err )
}
}
orgRepoIDs , err = env . RepoIDs ( 1 , ctxUser . NumRepos )
if err != nil {
return nil , fmt . Errorf ( "env.RepoIDs: %v" , err )
}
orgRepoIDs , err = models . FilterOutRepoIdsWithoutUnitAccess ( ctxUser , orgRepoIDs , unitType )
if err != nil {
return nil , fmt . Errorf ( "FilterOutRepoIdsWithoutUnitAccess: %v" , err )
}
return orgRepoIDs , nil
}
func issueIDsFromSearch ( ctxUser * models . User , keyword string , opts * models . IssuesOptions ) ( [ ] int64 , error ) {
if len ( keyword ) == 0 {
return [ ] int64 { } , nil
}
searchRepoIDs , err := models . GetRepoIDsForIssuesOptions ( opts , ctxUser )
if err != nil {
return nil , fmt . Errorf ( "GetRepoIDsForIssuesOptions: %v" , err )
}
issueIDsFromSearch , err := issue_indexer . SearchIssuesByKeyword ( searchRepoIDs , keyword )
if err != nil {
return nil , fmt . Errorf ( "SearchIssuesByKeyword: %v" , err )
}
return issueIDsFromSearch , nil
}
func repoIDMap ( ctxUser * models . User , issueCountByRepo map [ int64 ] int64 , unitType models . UnitType ) ( map [ int64 ] * models . Repository , error ) {
repoByID := make ( map [ int64 ] * models . Repository , len ( issueCountByRepo ) )
for id := range issueCountByRepo {
if id <= 0 {
continue
}
if _ , ok := repoByID [ id ] ; ! ok {
repo , err := models . GetRepositoryByID ( id )
if models . IsErrRepoNotExist ( err ) {
return nil , err
} else if err != nil {
return nil , fmt . Errorf ( "GetRepositoryByID: [%d]%v" , id , err )
}
repoByID [ id ] = repo
}
repo := repoByID [ id ]
// Check if user has access to given repository.
perm , err := models . GetUserRepoPermission ( repo , ctxUser )
if err != nil {
return nil , fmt . Errorf ( "GetUserRepoPermission: [%d]%v" , id , err )
}
if ! perm . CanRead ( unitType ) {
log . Debug ( "User created Issues in Repository which they no longer have access to: [%d]" , id )
}
}
return repoByID , nil
}
2016-11-27 12:59:12 +01:00
// ShowSSHKeys output all the ssh keys of user by uid
2016-03-11 11:56:52 -05:00
func ShowSSHKeys ( ctx * context . Context , uid int64 ) {
2021-09-24 19:32:56 +08:00
keys , err := models . ListPublicKeys ( uid , db . ListOptions { } )
2014-11-23 02:33:47 -05:00
if err != nil {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "ListPublicKeys" , err )
2014-11-23 02:33:47 -05:00
return
}
var buf bytes . Buffer
for i := range keys {
buf . WriteString ( keys [ i ] . OmitEmail ( ) )
2015-06-08 00:40:38 -07:00
buf . WriteString ( "\n" )
2014-11-23 02:33:47 -05:00
}
2015-10-15 21:28:12 -04:00
ctx . PlainText ( 200 , buf . Bytes ( ) )
2014-11-23 02:33:47 -05:00
}
2019-04-14 18:43:56 +02:00
// ShowGPGKeys output all the public GPG keys of user by uid
func ShowGPGKeys ( ctx * context . Context , uid int64 ) {
2021-09-24 19:32:56 +08:00
keys , err := models . ListGPGKeys ( uid , db . ListOptions { } )
2019-04-14 18:43:56 +02:00
if err != nil {
ctx . ServerError ( "ListGPGKeys" , err )
return
}
entities := make ( [ ] * openpgp . Entity , 0 )
failedEntitiesID := make ( [ ] string , 0 )
for _ , k := range keys {
e , err := models . GPGKeyToEntity ( k )
if err != nil {
if models . IsErrGPGKeyImportNotExist ( err ) {
failedEntitiesID = append ( failedEntitiesID , k . KeyID )
continue //Skip previous import without backup of imported armored key
}
ctx . ServerError ( "ShowGPGKeys" , err )
return
}
entities = append ( entities , e )
}
var buf bytes . Buffer
headers := make ( map [ string ] string )
if len ( failedEntitiesID ) > 0 { //If some key need re-import to be exported
headers [ "Note" ] = fmt . Sprintf ( "The keys with the following IDs couldn't be exported and need to be reuploaded %s" , strings . Join ( failedEntitiesID , ", " ) )
}
writer , _ := armor . Encode ( & buf , "PGP PUBLIC KEY BLOCK" , headers )
for _ , e := range entities {
err = e . Serialize ( writer ) //TODO find why key are exported with a different cipherTypeByte as original (should not be blocking but strange)
if err != nil {
ctx . ServerError ( "ShowGPGKeys" , err )
return
}
}
writer . Close ( )
ctx . PlainText ( 200 , buf . Bytes ( ) )
}
2016-11-18 11:03:03 +08:00
// Email2User show user page via email
2016-03-11 11:56:52 -05:00
func Email2User ( ctx * context . Context ) {
2021-08-11 02:31:13 +02:00
u , err := models . GetUserByEmail ( ctx . FormString ( "email" ) )
2014-04-13 01:57:42 -04:00
if err != nil {
2015-08-05 11:14:17 +08:00
if models . IsErrUserNotExist ( err ) {
2018-01-10 22:34:17 +01:00
ctx . NotFound ( "GetUserByEmail" , err )
2014-04-13 01:57:42 -04:00
} else {
2018-01-10 22:34:17 +01:00
ctx . ServerError ( "GetUserByEmail" , err )
2014-04-13 01:57:42 -04:00
}
return
}
2016-11-27 18:14:25 +08:00
ctx . Redirect ( setting . AppSubURL + "/user/" + u . Name )
2014-04-13 01:57:42 -04:00
}