mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Never use /api/v1 from Gitea UI Pages (#19318)
Reusing `/api/v1` from Gitea UI Pages have pros and cons. Pros: 1) Less code copy Cons: 1) API/v1 have to support shared session with page requests. 2) You need to consider for each other when you want to change something about api/v1 or page. This PR moves all dependencies to API/v1 from UI Pages. Partially replace #16052
This commit is contained in:
		| @@ -7,6 +7,7 @@ package integrations | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @@ -20,6 +21,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/indexer/issues" | ||||
| 	"code.gitea.io/gitea/modules/references" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
|  | ||||
| 	"github.com/PuerkitoBio/goquery" | ||||
| @@ -347,3 +349,209 @@ func TestIssueRedirect(t *testing.T) { | ||||
| 	resp = session.MakeRequest(t, req, http.StatusSeeOther) | ||||
| 	assert.Equal(t, "/"+path.Join("org26", "repo_external_tracker_alpha", "pulls", "1"), test.RedirectURL(resp)) | ||||
| } | ||||
|  | ||||
| func TestSearchIssues(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
|  | ||||
| 	session := loginUser(t, "user2") | ||||
|  | ||||
| 	link, _ := url.Parse("/issues/search") | ||||
| 	req := NewRequest(t, "GET", link.String()) | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var apiIssues []*api.Issue | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 10) | ||||
|  | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 10) | ||||
|  | ||||
| 	since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 | ||||
| 	before := time.Unix(999307200, 0).Format(time.RFC3339) | ||||
| 	query := url.Values{} | ||||
| 	query.Add("since", since) | ||||
| 	query.Add("before", before) | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 8) | ||||
| 	query.Del("since") | ||||
| 	query.Del("before") | ||||
|  | ||||
| 	query.Add("state", "closed") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 2) | ||||
|  | ||||
| 	query.Set("state", "all") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.EqualValues(t, "15", resp.Header().Get("X-Total-Count")) | ||||
| 	assert.Len(t, apiIssues, 10) // there are more but 10 is page item limit | ||||
|  | ||||
| 	query.Add("limit", "20") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 15) | ||||
|  | ||||
| 	query = url.Values{"assigned": {"true"}, "state": {"all"}} | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 1) | ||||
|  | ||||
| 	query = url.Values{"milestones": {"milestone1"}, "state": {"all"}} | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 1) | ||||
|  | ||||
| 	query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}} | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 2) | ||||
|  | ||||
| 	query = url.Values{"owner": {"user2"}} // user | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 6) | ||||
|  | ||||
| 	query = url.Values{"owner": {"user3"}} // organization | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 3) | ||||
|  | ||||
| 	query = url.Values{"owner": {"user3"}, "team": {"team1"}} // organization + team | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 2) | ||||
| } | ||||
|  | ||||
| func TestSearchIssuesWithLabels(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
|  | ||||
| 	session := loginUser(t, "user1") | ||||
|  | ||||
| 	link, _ := url.Parse("/api/v1/repos/issues/search") | ||||
| 	req := NewRequest(t, "GET", link.String()) | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var apiIssues []*api.Issue | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
|  | ||||
| 	assert.Len(t, apiIssues, 10) | ||||
|  | ||||
| 	query := url.Values{} | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 10) | ||||
|  | ||||
| 	query.Add("labels", "label1") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 2) | ||||
|  | ||||
| 	// multiple labels | ||||
| 	query.Set("labels", "label1,label2") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 2) | ||||
|  | ||||
| 	// an org label | ||||
| 	query.Set("labels", "orglabel4") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 1) | ||||
|  | ||||
| 	// org and repo label | ||||
| 	query.Set("labels", "label2,orglabel4") | ||||
| 	query.Add("state", "all") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 2) | ||||
|  | ||||
| 	// org and repo label which share the same issue | ||||
| 	query.Set("labels", "label1,orglabel4") | ||||
| 	link.RawQuery = query.Encode() | ||||
| 	req = NewRequest(t, "GET", link.String()) | ||||
| 	resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &apiIssues) | ||||
| 	assert.Len(t, apiIssues, 2) | ||||
| } | ||||
|  | ||||
| func TestGetIssueInfo(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
|  | ||||
| 	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) | ||||
| 	assert.NoError(t, issue.LoadAttributes()) | ||||
| 	assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix)) | ||||
| 	assert.Equal(t, api.StateOpen, issue.State()) | ||||
|  | ||||
| 	session := loginUser(t, owner.Name) | ||||
|  | ||||
| 	urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index) | ||||
| 	req := NewRequest(t, "GET", urlStr) | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var apiIssue api.Issue | ||||
| 	DecodeJSON(t, resp, &apiIssue) | ||||
|  | ||||
| 	assert.EqualValues(t, issue.ID, apiIssue.ID) | ||||
| } | ||||
|  | ||||
| func TestUpdateIssueDeadline(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
|  | ||||
| 	issueBefore := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue) | ||||
| 	repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}).(*user_model.User) | ||||
| 	assert.NoError(t, issueBefore.LoadAttributes()) | ||||
| 	assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix)) | ||||
| 	assert.Equal(t, api.StateOpen, issueBefore.State()) | ||||
|  | ||||
| 	session := loginUser(t, owner.Name) | ||||
|  | ||||
| 	issueURL := fmt.Sprintf("%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index) | ||||
| 	req := NewRequest(t, "GET", issueURL) | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	htmlDoc := NewHTMLParser(t, resp.Body) | ||||
|  | ||||
| 	urlStr := issueURL + "/deadline?_csrf=" + htmlDoc.GetCSRF() | ||||
| 	req = NewRequestWithJSON(t, "POST", urlStr, map[string]string{ | ||||
| 		"due_date": "2022-04-06T00:00:00.000Z", | ||||
| 	}) | ||||
|  | ||||
| 	resp = session.MakeRequest(t, req, http.StatusCreated) | ||||
| 	var apiIssue api.IssueDeadline | ||||
| 	DecodeJSON(t, resp, &apiIssue) | ||||
|  | ||||
| 	assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02")) | ||||
| } | ||||
|   | ||||
| @@ -10,6 +10,8 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| @@ -173,3 +175,30 @@ func TestOrgRestrictedUser(t *testing.T) { | ||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName)) | ||||
| 	restrictedSession.MakeRequest(t, req, http.StatusOK) | ||||
| } | ||||
|  | ||||
| func TestTeamSearch(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
|  | ||||
| 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User) | ||||
| 	org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) | ||||
|  | ||||
| 	var results TeamSearchResults | ||||
|  | ||||
| 	session := loginUser(t, user.Name) | ||||
| 	csrf := GetCSRF(t, session, "/"+org.Name) | ||||
| 	req := NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "_team") | ||||
| 	req.Header.Add("X-Csrf-Token", csrf) | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	DecodeJSON(t, resp, &results) | ||||
| 	assert.NotEmpty(t, results.Data) | ||||
| 	assert.Len(t, results.Data, 1) | ||||
| 	assert.Equal(t, "test_team", results.Data[0].Name) | ||||
|  | ||||
| 	// no access if not organization member | ||||
| 	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User) | ||||
| 	session = loginUser(t, user5.Name) | ||||
| 	csrf = GetCSRF(t, session, "/"+org.Name) | ||||
| 	req = NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "team") | ||||
| 	req.Header.Add("X-Csrf-Token", csrf) | ||||
| 	session.MakeRequest(t, req, http.StatusNotFound) | ||||
| } | ||||
|   | ||||
							
								
								
									
										46
									
								
								integrations/repo_topic_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								integrations/repo_topic_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package integrations | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
|  | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestTopicSearch(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
| 	searchURL, _ := url.Parse("/explore/topics/search") | ||||
| 	var topics struct { | ||||
| 		TopicNames []*api.TopicResponse `json:"topics"` | ||||
| 	} | ||||
|  | ||||
| 	query := url.Values{"page": []string{"1"}, "limit": []string{"4"}} | ||||
|  | ||||
| 	searchURL.RawQuery = query.Encode() | ||||
| 	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) | ||||
| 	DecodeJSON(t, res, &topics) | ||||
| 	assert.Len(t, topics.TopicNames, 4) | ||||
| 	assert.EqualValues(t, "6", res.Header().Get("x-total-count")) | ||||
|  | ||||
| 	query.Add("q", "topic") | ||||
| 	searchURL.RawQuery = query.Encode() | ||||
| 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) | ||||
| 	DecodeJSON(t, res, &topics) | ||||
| 	assert.Len(t, topics.TopicNames, 2) | ||||
|  | ||||
| 	query.Set("q", "database") | ||||
| 	searchURL.RawQuery = query.Encode() | ||||
| 	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK) | ||||
| 	DecodeJSON(t, res, &topics) | ||||
| 	if assert.Len(t, topics.TopicNames, 1) { | ||||
| 		assert.EqualValues(t, 2, topics.TopicNames[0].ID) | ||||
| 		assert.EqualValues(t, "database", topics.TopicNames[0].Name) | ||||
| 		assert.EqualValues(t, 1, topics.TopicNames[0].RepoCount) | ||||
| 	} | ||||
| } | ||||
| @@ -8,8 +8,11 @@ import ( | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/models/unittest" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
| 	"code.gitea.io/gitea/modules/translation/i18n" | ||||
|  | ||||
| @@ -222,3 +225,26 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) { | ||||
| 	// t.Log(resp.Body.String()) | ||||
| 	assert.Equal(t, expected, resp.Body.String()) | ||||
| } | ||||
|  | ||||
| func TestListStopWatches(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
|  | ||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository) | ||||
| 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User) | ||||
|  | ||||
| 	session := loginUser(t, owner.Name) | ||||
| 	req := NewRequestf(t, "GET", "/user/stopwatches") | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var apiWatches []*api.StopWatch | ||||
| 	DecodeJSON(t, resp, &apiWatches) | ||||
| 	stopwatch := unittest.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch) | ||||
| 	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue) | ||||
| 	if assert.Len(t, apiWatches, 1) { | ||||
| 		assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix()) | ||||
| 		assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex) | ||||
| 		assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle) | ||||
| 		assert.EqualValues(t, repo.Name, apiWatches[0].RepoName) | ||||
| 		assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName) | ||||
| 		assert.Greater(t, int64(apiWatches[0].Seconds), int64(0)) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -191,22 +191,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SetTotalCountHeader set "X-Total-Count" header | ||||
| func (ctx *APIContext) SetTotalCountHeader(total int64) { | ||||
| 	ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) | ||||
| 	ctx.AppendAccessControlExposeHeaders("X-Total-Count") | ||||
| } | ||||
|  | ||||
| // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header | ||||
| func (ctx *APIContext) AppendAccessControlExposeHeaders(names ...string) { | ||||
| 	val := ctx.RespHeader().Get("Access-Control-Expose-Headers") | ||||
| 	if len(val) != 0 { | ||||
| 		ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) | ||||
| 	} else { | ||||
| 		ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // RequireCSRF requires a validated a CSRF token | ||||
| func (ctx *APIContext) RequireCSRF() { | ||||
| 	headerToken := ctx.Req.Header.Get(ctx.csrf.GetHeaderName()) | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import ( | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"html/template" | ||||
| 	"io" | ||||
| @@ -21,6 +22,7 @@ import ( | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/unit" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| @@ -577,6 +579,22 @@ func (ctx *Context) Value(key interface{}) interface{} { | ||||
| 	return ctx.Req.Context().Value(key) | ||||
| } | ||||
|  | ||||
| // SetTotalCountHeader set "X-Total-Count" header | ||||
| func (ctx *Context) SetTotalCountHeader(total int64) { | ||||
| 	ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total)) | ||||
| 	ctx.AppendAccessControlExposeHeaders("X-Total-Count") | ||||
| } | ||||
|  | ||||
| // AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header | ||||
| func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) { | ||||
| 	val := ctx.RespHeader().Get("Access-Control-Expose-Headers") | ||||
| 	if len(val) != 0 { | ||||
| 		ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", "))) | ||||
| 	} else { | ||||
| 		ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", ")) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Handler represents a custom handler | ||||
| type Handler func(*Context) | ||||
|  | ||||
| @@ -780,3 +798,21 @@ func Contexter() func(next http.Handler) http.Handler { | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SearchOrderByMap represents all possible search order | ||||
| var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ | ||||
| 	"asc": { | ||||
| 		"alpha":   db.SearchOrderByAlphabetically, | ||||
| 		"created": db.SearchOrderByOldest, | ||||
| 		"updated": db.SearchOrderByLeastUpdated, | ||||
| 		"size":    db.SearchOrderBySize, | ||||
| 		"id":      db.SearchOrderByID, | ||||
| 	}, | ||||
| 	"desc": { | ||||
| 		"alpha":   db.SearchOrderByAlphabeticallyReverse, | ||||
| 		"created": db.SearchOrderByNewest, | ||||
| 		"updated": db.SearchOrderByRecentUpdated, | ||||
| 		"size":    db.SearchOrderBySizeReverse, | ||||
| 		"id":      db.SearchOrderByIDReverse, | ||||
| 	}, | ||||
| } | ||||
|   | ||||
| @@ -2,20 +2,16 @@ | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
| 
 | ||||
| package utils | ||||
| package context | ||||
| 
 | ||||
| import ( | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| ) | ||||
| 
 | ||||
| // GetQueryBeforeSince return parsed time (unix format) from URL query's before and since | ||||
| func GetQueryBeforeSince(ctx *context.APIContext) (before, since int64, err error) { | ||||
| func GetQueryBeforeSince(ctx *Context) (before, since int64, err error) { | ||||
| 	qCreatedBefore, err := prepareQueryArg(ctx, "before") | ||||
| 	if err != nil { | ||||
| 		return 0, 0, err | ||||
| @@ -53,16 +49,8 @@ func parseTime(value string) (int64, error) { | ||||
| } | ||||
| 
 | ||||
| // prepareQueryArg unescape and trim a query arg | ||||
| func prepareQueryArg(ctx *context.APIContext, name string) (value string, err error) { | ||||
| func prepareQueryArg(ctx *Context, name string) (value string, err error) { | ||||
| 	value, err = url.PathUnescape(ctx.FormString(name)) | ||||
| 	value = strings.TrimSpace(value) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // GetListOptions returns list options using the page and limit parameters | ||||
| func GetListOptions(ctx *context.APIContext) db.ListOptions { | ||||
| 	return db.ListOptions{ | ||||
| 		Page:     ctx.FormInt("page"), | ||||
| 		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 	} | ||||
| } | ||||
| @@ -26,7 +26,7 @@ func NewAvailable(ctx *context.APIContext) { | ||||
| } | ||||
|  | ||||
| func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions { | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return nil | ||||
|   | ||||
| @@ -111,7 +111,7 @@ func SearchIssues(ctx *context.APIContext) { | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/IssueList" | ||||
|  | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
| @@ -359,7 +359,7 @@ func ListIssues(ctx *context.APIContext) { | ||||
| 	// responses: | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/IssueList" | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
|   | ||||
| @@ -58,7 +58,7 @@ func ListIssueComments(ctx *context.APIContext) { | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/CommentList" | ||||
|  | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
| @@ -150,7 +150,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/TimelineList" | ||||
|  | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
| @@ -253,7 +253,7 @@ func ListRepoIssueComments(ctx *context.APIContext) { | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/CommentList" | ||||
|  | ||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
|   | ||||
| @@ -103,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) { | ||||
| 		opts.UserID = user.ID | ||||
| 	} | ||||
|  | ||||
| 	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { | ||||
| 	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
| 	} | ||||
| @@ -522,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { | ||||
| 	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
| 	} | ||||
| @@ -597,7 +597,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) { | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil { | ||||
| 	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||
| 		return | ||||
| 	} | ||||
|   | ||||
| @@ -31,23 +31,6 @@ import ( | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| ) | ||||
|  | ||||
| var searchOrderByMap = map[string]map[string]db.SearchOrderBy{ | ||||
| 	"asc": { | ||||
| 		"alpha":   db.SearchOrderByAlphabetically, | ||||
| 		"created": db.SearchOrderByOldest, | ||||
| 		"updated": db.SearchOrderByLeastUpdated, | ||||
| 		"size":    db.SearchOrderBySize, | ||||
| 		"id":      db.SearchOrderByID, | ||||
| 	}, | ||||
| 	"desc": { | ||||
| 		"alpha":   db.SearchOrderByAlphabeticallyReverse, | ||||
| 		"created": db.SearchOrderByNewest, | ||||
| 		"updated": db.SearchOrderByRecentUpdated, | ||||
| 		"size":    db.SearchOrderBySizeReverse, | ||||
| 		"id":      db.SearchOrderByIDReverse, | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| // Search repositories via options | ||||
| func Search(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/search repository repoSearch | ||||
| @@ -193,7 +176,7 @@ func Search(ctx *context.APIContext) { | ||||
| 		if len(sortOrder) == 0 { | ||||
| 			sortOrder = "asc" | ||||
| 		} | ||||
| 		if searchModeMap, ok := searchOrderByMap[sortOrder]; ok { | ||||
| 		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok { | ||||
| 			if orderBy, ok := searchModeMap[sortMode]; ok { | ||||
| 				opts.OrderBy = orderBy | ||||
| 			} else { | ||||
|   | ||||
							
								
								
									
										19
									
								
								routers/api/v1/utils/page.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								routers/api/v1/utils/page.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| // Copyright 2017 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| ) | ||||
|  | ||||
| // GetListOptions returns list options using the page and limit parameters | ||||
| func GetListOptions(ctx *context.APIContext) db.ListOptions { | ||||
| 	return db.ListOptions{ | ||||
| 		Page:     ctx.FormInt("page"), | ||||
| 		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										42
									
								
								routers/web/explore/topic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								routers/web/explore/topic.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package explore | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| ) | ||||
|  | ||||
| // TopicSearch search for creating topic | ||||
| func TopicSearch(ctx *context.Context) { | ||||
| 	opts := &repo_model.FindTopicOptions{ | ||||
| 		Keyword: ctx.FormString("q"), | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			Page:     ctx.FormInt("page"), | ||||
| 			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	topics, total, err := repo_model.FindTopics(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	topicResponses := make([]*api.TopicResponse, len(topics)) | ||||
| 	for i, topic := range topics { | ||||
| 		topicResponses[i] = convert.ToTopicResponse(topic) | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetTotalCountHeader(total) | ||||
| 	ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||
| 		"topics": topicResponses, | ||||
| 	}) | ||||
| } | ||||
| @@ -13,6 +13,7 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/organization" | ||||
| 	"code.gitea.io/gitea/models/perm" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| @@ -20,7 +21,9 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/routers/utils" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| @@ -329,6 +332,51 @@ func TeamRepositories(ctx *context.Context) { | ||||
| 	ctx.HTML(http.StatusOK, tplTeamRepositories) | ||||
| } | ||||
|  | ||||
| // SearchTeam api for searching teams | ||||
| func SearchTeam(ctx *context.Context) { | ||||
| 	listOptions := db.ListOptions{ | ||||
| 		Page:     ctx.FormInt("page"), | ||||
| 		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 	} | ||||
|  | ||||
| 	opts := &organization.SearchTeamOptions{ | ||||
| 		UserID:      ctx.Doer.ID, | ||||
| 		Keyword:     ctx.FormTrim("q"), | ||||
| 		OrgID:       ctx.Org.Organization.ID, | ||||
| 		IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"), | ||||
| 		ListOptions: listOptions, | ||||
| 	} | ||||
|  | ||||
| 	teams, maxResults, err := organization.SearchTeam(opts) | ||||
| 	if err != nil { | ||||
| 		log.Error("SearchTeam failed: %v", err) | ||||
| 		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": "SearchTeam internal failure", | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	apiTeams := make([]*api.Team, len(teams)) | ||||
| 	for i := range teams { | ||||
| 		if err := teams[i].GetUnits(); err != nil { | ||||
| 			log.Error("Team GetUnits failed: %v", err) | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"ok":    false, | ||||
| 				"error": "SearchTeam failed to get units", | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 		apiTeams[i] = convert.ToTeam(teams[i]) | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetTotalCountHeader(maxResults) | ||||
| 	ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||
| 		"ok":   true, | ||||
| 		"data": apiTeams, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // EditTeam render team edit page | ||||
| func EditTeam(ctx *context.Context) { | ||||
| 	ctx.Data["Title"] = ctx.Org.Organization.FullName | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import ( | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| @@ -36,6 +37,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/templates/vars" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/upload" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| @@ -1762,6 +1764,20 @@ func getActionIssues(ctx *context.Context) []*models.Issue { | ||||
| 	return issues | ||||
| } | ||||
|  | ||||
| // GetIssueInfo get an issue of a repository | ||||
| func GetIssueInfo(ctx *context.Context) { | ||||
| 	issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrIssueNotExist(err) { | ||||
| 			ctx.Error(http.StatusNotFound) | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue)) | ||||
| } | ||||
|  | ||||
| // UpdateIssueTitle change issue's title | ||||
| func UpdateIssueTitle(ctx *context.Context) { | ||||
| 	issue := GetActionIssue(ctx) | ||||
| @@ -1856,6 +1872,40 @@ func UpdateIssueContent(ctx *context.Context) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // UpdateIssueDeadline updates an issue deadline | ||||
| func UpdateIssueDeadline(ctx *context.Context) { | ||||
| 	form := web.GetForm(ctx).(*api.EditDeadlineOption) | ||||
| 	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrIssueNotExist(err) { | ||||
| 			ctx.NotFound("GetIssueByIndex", err) | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) | ||||
| 		} | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { | ||||
| 		ctx.Error(http.StatusForbidden, "", "Not repo writer") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var deadlineUnix timeutil.TimeStamp | ||||
| 	var deadline time.Time | ||||
| 	if form.Deadline != nil && !form.Deadline.IsZero() { | ||||
| 		deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(), | ||||
| 			23, 59, 59, 0, time.Local) | ||||
| 		deadlineUnix = timeutil.TimeStamp(deadline.Unix()) | ||||
| 	} | ||||
|  | ||||
| 	if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) | ||||
| } | ||||
|  | ||||
| // UpdateIssueMilestone change issue's milestone | ||||
| func UpdateIssueMilestone(ctx *context.Context) { | ||||
| 	issues := getActionIssues(ctx) | ||||
| @@ -2052,6 +2102,338 @@ func UpdatePullReviewRequest(ctx *context.Context) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // SearchIssues searches for issues across the repositories that the user has access to | ||||
| func SearchIssues(ctx *context.Context) { | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var isClosed util.OptionalBool | ||||
| 	switch ctx.FormString("state") { | ||||
| 	case "closed": | ||||
| 		isClosed = util.OptionalBoolTrue | ||||
| 	case "all": | ||||
| 		isClosed = util.OptionalBoolNone | ||||
| 	default: | ||||
| 		isClosed = util.OptionalBoolFalse | ||||
| 	} | ||||
|  | ||||
| 	// find repos user can access (for issue search) | ||||
| 	opts := &models.SearchRepoOptions{ | ||||
| 		Private:     false, | ||||
| 		AllPublic:   true, | ||||
| 		TopicOnly:   false, | ||||
| 		Collaborate: util.OptionalBoolNone, | ||||
| 		// This needs to be a column that is not nil in fixtures or | ||||
| 		// MySQL will return different results when sorting by null in some cases | ||||
| 		OrderBy: db.SearchOrderByAlphabetically, | ||||
| 		Actor:   ctx.Doer, | ||||
| 	} | ||||
| 	if ctx.IsSigned { | ||||
| 		opts.Private = true | ||||
| 		opts.AllLimited = true | ||||
| 	} | ||||
| 	if ctx.FormString("owner") != "" { | ||||
| 		owner, err := user_model.GetUserByName(ctx.FormString("owner")) | ||||
| 		if err != nil { | ||||
| 			if user_model.IsErrUserNotExist(err) { | ||||
| 				ctx.Error(http.StatusBadRequest, "Owner not found", err.Error()) | ||||
| 			} else { | ||||
| 				ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		opts.OwnerID = owner.ID | ||||
| 		opts.AllLimited = false | ||||
| 		opts.AllPublic = false | ||||
| 		opts.Collaborate = util.OptionalBoolFalse | ||||
| 	} | ||||
| 	if ctx.FormString("team") != "" { | ||||
| 		if ctx.FormString("owner") == "" { | ||||
| 			ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team") | ||||
| 			return | ||||
| 		} | ||||
| 		team, err := organization.GetTeam(opts.OwnerID, ctx.FormString("team")) | ||||
| 		if err != nil { | ||||
| 			if organization.IsErrTeamNotExist(err) { | ||||
| 				ctx.Error(http.StatusBadRequest, "Team not found", err.Error()) | ||||
| 			} else { | ||||
| 				ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) | ||||
| 			} | ||||
| 			return | ||||
| 		} | ||||
| 		opts.TeamID = team.ID | ||||
| 	} | ||||
|  | ||||
| 	repoIDs, _, err := models.SearchRepositoryIDs(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var issues []*models.Issue | ||||
| 	var filteredCount int64 | ||||
|  | ||||
| 	keyword := ctx.FormTrim("q") | ||||
| 	if strings.IndexByte(keyword, 0) >= 0 { | ||||
| 		keyword = "" | ||||
| 	} | ||||
| 	var issueIDs []int64 | ||||
| 	if len(keyword) > 0 && len(repoIDs) > 0 { | ||||
| 		if issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, repoIDs, keyword); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var isPull util.OptionalBool | ||||
| 	switch ctx.FormString("type") { | ||||
| 	case "pulls": | ||||
| 		isPull = util.OptionalBoolTrue | ||||
| 	case "issues": | ||||
| 		isPull = util.OptionalBoolFalse | ||||
| 	default: | ||||
| 		isPull = util.OptionalBoolNone | ||||
| 	} | ||||
|  | ||||
| 	labels := ctx.FormTrim("labels") | ||||
| 	var includedLabelNames []string | ||||
| 	if len(labels) > 0 { | ||||
| 		includedLabelNames = strings.Split(labels, ",") | ||||
| 	} | ||||
|  | ||||
| 	milestones := ctx.FormTrim("milestones") | ||||
| 	var includedMilestones []string | ||||
| 	if len(milestones) > 0 { | ||||
| 		includedMilestones = strings.Split(milestones, ",") | ||||
| 	} | ||||
|  | ||||
| 	// this api is also used in UI, | ||||
| 	// so the default limit is set to fit UI needs | ||||
| 	limit := ctx.FormInt("limit") | ||||
| 	if limit == 0 { | ||||
| 		limit = setting.UI.IssuePagingNum | ||||
| 	} else if limit > setting.API.MaxResponseItems { | ||||
| 		limit = setting.API.MaxResponseItems | ||||
| 	} | ||||
|  | ||||
| 	// Only fetch the issues if we either don't have a keyword or the search returned issues | ||||
| 	// This would otherwise return all issues if no issues were found by the search. | ||||
| 	if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 { | ||||
| 		issuesOpt := &models.IssuesOptions{ | ||||
| 			ListOptions: db.ListOptions{ | ||||
| 				Page:     ctx.FormInt("page"), | ||||
| 				PageSize: limit, | ||||
| 			}, | ||||
| 			RepoIDs:            repoIDs, | ||||
| 			IsClosed:           isClosed, | ||||
| 			IssueIDs:           issueIDs, | ||||
| 			IncludedLabelNames: includedLabelNames, | ||||
| 			IncludeMilestones:  includedMilestones, | ||||
| 			SortType:           "priorityrepo", | ||||
| 			PriorityRepoID:     ctx.FormInt64("priority_repo_id"), | ||||
| 			IsPull:             isPull, | ||||
| 			UpdatedBeforeUnix:  before, | ||||
| 			UpdatedAfterUnix:   since, | ||||
| 		} | ||||
|  | ||||
| 		ctxUserID := int64(0) | ||||
| 		if ctx.IsSigned { | ||||
| 			ctxUserID = ctx.Doer.ID | ||||
| 		} | ||||
|  | ||||
| 		// Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested | ||||
| 		if ctx.FormBool("created") { | ||||
| 			issuesOpt.PosterID = ctxUserID | ||||
| 		} | ||||
| 		if ctx.FormBool("assigned") { | ||||
| 			issuesOpt.AssigneeID = ctxUserID | ||||
| 		} | ||||
| 		if ctx.FormBool("mentioned") { | ||||
| 			issuesOpt.MentionedID = ctxUserID | ||||
| 		} | ||||
| 		if ctx.FormBool("review_requested") { | ||||
| 			issuesOpt.ReviewRequestedID = ctxUserID | ||||
| 		} | ||||
|  | ||||
| 		if issues, err = models.Issues(issuesOpt); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "Issues", err.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		issuesOpt.ListOptions = db.ListOptions{ | ||||
| 			Page: -1, | ||||
| 		} | ||||
| 		if filteredCount, err = models.CountIssues(issuesOpt); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetTotalCountHeader(filteredCount) | ||||
| 	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) | ||||
| } | ||||
|  | ||||
| func getUserIDForFilter(ctx *context.Context, queryName string) int64 { | ||||
| 	userName := ctx.FormString(queryName) | ||||
| 	if len(userName) == 0 { | ||||
| 		return 0 | ||||
| 	} | ||||
|  | ||||
| 	user, err := user_model.GetUserByName(userName) | ||||
| 	if user_model.IsErrUserNotExist(err) { | ||||
| 		ctx.NotFound("", err) | ||||
| 		return 0 | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 		return 0 | ||||
| 	} | ||||
|  | ||||
| 	return user.ID | ||||
| } | ||||
|  | ||||
| // ListIssues list the issues of a repository | ||||
| func ListIssues(ctx *context.Context) { | ||||
| 	before, since, err := context.GetQueryBeforeSince(ctx) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var isClosed util.OptionalBool | ||||
| 	switch ctx.FormString("state") { | ||||
| 	case "closed": | ||||
| 		isClosed = util.OptionalBoolTrue | ||||
| 	case "all": | ||||
| 		isClosed = util.OptionalBoolNone | ||||
| 	default: | ||||
| 		isClosed = util.OptionalBoolFalse | ||||
| 	} | ||||
|  | ||||
| 	var issues []*models.Issue | ||||
| 	var filteredCount int64 | ||||
|  | ||||
| 	keyword := ctx.FormTrim("q") | ||||
| 	if strings.IndexByte(keyword, 0) >= 0 { | ||||
| 		keyword = "" | ||||
| 	} | ||||
| 	var issueIDs []int64 | ||||
| 	var labelIDs []int64 | ||||
| 	if len(keyword) > 0 { | ||||
| 		issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{ctx.Repo.Repository.ID}, keyword) | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { | ||||
| 		labelIDs, err = models.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted) | ||||
| 		if err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var mileIDs []int64 | ||||
| 	if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 { | ||||
| 		for i := range part { | ||||
| 			// uses names and fall back to ids | ||||
| 			// non existent milestones are discarded | ||||
| 			mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i]) | ||||
| 			if err == nil { | ||||
| 				mileIDs = append(mileIDs, mile.ID) | ||||
| 				continue | ||||
| 			} | ||||
| 			if !models.IsErrMilestoneNotExist(err) { | ||||
| 				ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 				return | ||||
| 			} | ||||
| 			id, err := strconv.ParseInt(part[i], 10, 64) | ||||
| 			if err != nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id) | ||||
| 			if err == nil { | ||||
| 				mileIDs = append(mileIDs, mile.ID) | ||||
| 				continue | ||||
| 			} | ||||
| 			if models.IsErrMilestoneNotExist(err) { | ||||
| 				continue | ||||
| 			} | ||||
| 			ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	listOptions := db.ListOptions{ | ||||
| 		Page:     ctx.FormInt("page"), | ||||
| 		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 	} | ||||
|  | ||||
| 	var isPull util.OptionalBool | ||||
| 	switch ctx.FormString("type") { | ||||
| 	case "pulls": | ||||
| 		isPull = util.OptionalBoolTrue | ||||
| 	case "issues": | ||||
| 		isPull = util.OptionalBoolFalse | ||||
| 	default: | ||||
| 		isPull = util.OptionalBoolNone | ||||
| 	} | ||||
|  | ||||
| 	// FIXME: we should be more efficient here | ||||
| 	createdByID := getUserIDForFilter(ctx, "created_by") | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 	assignedByID := getUserIDForFilter(ctx, "assigned_by") | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 	mentionedByID := getUserIDForFilter(ctx, "mentioned_by") | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Only fetch the issues if we either don't have a keyword or the search returned issues | ||||
| 	// This would otherwise return all issues if no issues were found by the search. | ||||
| 	if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { | ||||
| 		issuesOpt := &models.IssuesOptions{ | ||||
| 			ListOptions:       listOptions, | ||||
| 			RepoIDs:           []int64{ctx.Repo.Repository.ID}, | ||||
| 			IsClosed:          isClosed, | ||||
| 			IssueIDs:          issueIDs, | ||||
| 			LabelIDs:          labelIDs, | ||||
| 			MilestoneIDs:      mileIDs, | ||||
| 			IsPull:            isPull, | ||||
| 			UpdatedBeforeUnix: before, | ||||
| 			UpdatedAfterUnix:  since, | ||||
| 			PosterID:          createdByID, | ||||
| 			AssigneeID:        assignedByID, | ||||
| 			MentionedID:       mentionedByID, | ||||
| 		} | ||||
|  | ||||
| 		if issues, err = models.Issues(issuesOpt); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		issuesOpt.ListOptions = db.ListOptions{ | ||||
| 			Page: -1, | ||||
| 		} | ||||
| 		if filteredCount, err = models.CountIssues(issuesOpt); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetTotalCountHeader(filteredCount) | ||||
| 	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues)) | ||||
| } | ||||
|  | ||||
| // UpdateIssueStatus change issue's status | ||||
| func UpdateIssueStatus(ctx *context.Context) { | ||||
| 	issues := getActionIssues(ctx) | ||||
|   | ||||
| @@ -20,11 +20,14 @@ import ( | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| 	"code.gitea.io/gitea/modules/graceful" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/storage" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| @@ -503,3 +506,112 @@ func InitiateDownload(ctx *context.Context) { | ||||
| 		"complete": completed, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // SearchRepo repositories via options | ||||
| func SearchRepo(ctx *context.Context) { | ||||
| 	opts := &models.SearchRepoOptions{ | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			Page:     ctx.FormInt("page"), | ||||
| 			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 		}, | ||||
| 		Actor:              ctx.Doer, | ||||
| 		Keyword:            ctx.FormTrim("q"), | ||||
| 		OwnerID:            ctx.FormInt64("uid"), | ||||
| 		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"), | ||||
| 		TeamID:             ctx.FormInt64("team_id"), | ||||
| 		TopicOnly:          ctx.FormBool("topic"), | ||||
| 		Collaborate:        util.OptionalBoolNone, | ||||
| 		Private:            ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")), | ||||
| 		Template:           util.OptionalBoolNone, | ||||
| 		StarredByID:        ctx.FormInt64("starredBy"), | ||||
| 		IncludeDescription: ctx.FormBool("includeDesc"), | ||||
| 	} | ||||
|  | ||||
| 	if ctx.FormString("template") != "" { | ||||
| 		opts.Template = util.OptionalBoolOf(ctx.FormBool("template")) | ||||
| 	} | ||||
|  | ||||
| 	if ctx.FormBool("exclusive") { | ||||
| 		opts.Collaborate = util.OptionalBoolFalse | ||||
| 	} | ||||
|  | ||||
| 	mode := ctx.FormString("mode") | ||||
| 	switch mode { | ||||
| 	case "source": | ||||
| 		opts.Fork = util.OptionalBoolFalse | ||||
| 		opts.Mirror = util.OptionalBoolFalse | ||||
| 	case "fork": | ||||
| 		opts.Fork = util.OptionalBoolTrue | ||||
| 	case "mirror": | ||||
| 		opts.Mirror = util.OptionalBoolTrue | ||||
| 	case "collaborative": | ||||
| 		opts.Mirror = util.OptionalBoolFalse | ||||
| 		opts.Collaborate = util.OptionalBoolTrue | ||||
| 	case "": | ||||
| 	default: | ||||
| 		ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ctx.FormString("archived") != "" { | ||||
| 		opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived")) | ||||
| 	} | ||||
|  | ||||
| 	if ctx.FormString("is_private") != "" { | ||||
| 		opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private")) | ||||
| 	} | ||||
|  | ||||
| 	sortMode := ctx.FormString("sort") | ||||
| 	if len(sortMode) > 0 { | ||||
| 		sortOrder := ctx.FormString("order") | ||||
| 		if len(sortOrder) == 0 { | ||||
| 			sortOrder = "asc" | ||||
| 		} | ||||
| 		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok { | ||||
| 			if orderBy, ok := searchModeMap[sortMode]; ok { | ||||
| 				opts.OrderBy = orderBy | ||||
| 			} else { | ||||
| 				ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode)) | ||||
| 				return | ||||
| 			} | ||||
| 		} else { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder)) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	repos, count, err := models.SearchRepository(opts) | ||||
| 	if err != nil { | ||||
| 		ctx.JSON(http.StatusInternalServerError, api.SearchError{ | ||||
| 			OK:    false, | ||||
| 			Error: err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	results := make([]*api.Repository, len(repos)) | ||||
| 	for i, repo := range repos { | ||||
| 		if err = repo.GetOwner(ctx); err != nil { | ||||
| 			ctx.JSON(http.StatusInternalServerError, api.SearchError{ | ||||
| 				OK:    false, | ||||
| 				Error: err.Error(), | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 		accessMode, err := models.AccessLevel(ctx.Doer, repo) | ||||
| 		if err != nil { | ||||
| 			ctx.JSON(http.StatusInternalServerError, api.SearchError{ | ||||
| 				OK:    false, | ||||
| 				Error: err.Error(), | ||||
| 			}) | ||||
| 		} | ||||
| 		results[i] = convert.ToRepo(repo, accessMode) | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetTotalCountHeader(count) | ||||
| 	ctx.JSON(http.StatusOK, api.SearchResults{ | ||||
| 		OK:   true, | ||||
| 		Data: results, | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -191,3 +192,8 @@ func NotificationPurgePost(c *context.Context) { | ||||
|  | ||||
| 	c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther) | ||||
| } | ||||
|  | ||||
| // NewAvailable returns the notification counts | ||||
| func NewAvailable(ctx *context.APIContext) { | ||||
| 	ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.Doer)}) | ||||
| } | ||||
|   | ||||
							
								
								
									
										44
									
								
								routers/web/user/search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								routers/web/user/search.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package user | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| ) | ||||
|  | ||||
| // Search search users | ||||
| func Search(ctx *context.Context) { | ||||
| 	listOptions := db.ListOptions{ | ||||
| 		Page:     ctx.FormInt("page"), | ||||
| 		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 	} | ||||
|  | ||||
| 	users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{ | ||||
| 		Actor:       ctx.Doer, | ||||
| 		Keyword:     ctx.FormTrim("q"), | ||||
| 		UID:         ctx.FormInt64("uid"), | ||||
| 		Type:        user_model.UserTypeIndividual, | ||||
| 		ListOptions: listOptions, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": err.Error(), | ||||
| 		}) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetTotalCountHeader(maxResults) | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||
| 		"ok":   true, | ||||
| 		"data": convert.ToUsers(ctx.Doer, users), | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										41
									
								
								routers/web/user/stop_watch.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								routers/web/user/stop_watch.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| // Copyright 2022 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package user | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| ) | ||||
|  | ||||
| // GetStopwatches get all stopwatches | ||||
| func GetStopwatches(ctx *context.Context) { | ||||
| 	sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{ | ||||
| 		Page:     ctx.FormInt("page"), | ||||
| 		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	count, err := models.CountUserStopwatches(ctx.Doer.ID) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	apiSWs, err := convert.ToStopWatches(sws) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetTotalCountHeader(count) | ||||
| 	ctx.JSON(http.StatusOK, apiSWs) | ||||
| } | ||||
| @@ -20,6 +20,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/public" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/storage" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/templates" | ||||
| 	"code.gitea.io/gitea/modules/validation" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| @@ -289,8 +290,13 @@ func RegisterRoutes(m *web.Route) { | ||||
| 		m.Get("/users", explore.Users) | ||||
| 		m.Get("/organizations", explore.Organizations) | ||||
| 		m.Get("/code", explore.Code) | ||||
| 		m.Get("/topics/search", explore.TopicSearch) | ||||
| 	}, ignExploreSignIn) | ||||
| 	m.Get("/issues", reqSignIn, user.Issues) | ||||
| 	m.Group("/issues", func() { | ||||
| 		m.Get("", user.Issues) | ||||
| 		m.Get("/search", repo.SearchIssues) | ||||
| 	}, reqSignIn) | ||||
|  | ||||
| 	m.Get("/pulls", reqSignIn, user.Pulls) | ||||
| 	m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones) | ||||
|  | ||||
| @@ -421,6 +427,8 @@ func RegisterRoutes(m *web.Route) { | ||||
| 		m.Post("/forgot_password", auth.ForgotPasswdPost) | ||||
| 		m.Post("/logout", auth.SignOut) | ||||
| 		m.Get("/task/{task}", user.TaskStatus) | ||||
| 		m.Get("/stopwatches", user.GetStopwatches, reqSignIn) | ||||
| 		m.Get("/search", user.Search, ignExploreSignIn) | ||||
| 	}) | ||||
| 	// ***** END: User ***** | ||||
|  | ||||
| @@ -605,6 +613,7 @@ func RegisterRoutes(m *web.Route) { | ||||
| 		m.Group("/{org}", func() { | ||||
| 			m.Get("/teams/new", org.NewTeam) | ||||
| 			m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost) | ||||
| 			m.Get("/teams/-/search", org.SearchTeam) | ||||
| 			m.Get("/teams/{team}/edit", org.EditTeam) | ||||
| 			m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost) | ||||
| 			m.Post("/teams/{team}/delete", org.DeleteTeam) | ||||
| @@ -669,6 +678,7 @@ func RegisterRoutes(m *web.Route) { | ||||
| 			m.Combo("/{repoid}").Get(repo.Fork). | ||||
| 				Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost) | ||||
| 		}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader) | ||||
| 		m.Get("/search", repo.SearchRepo) | ||||
| 	}, reqSignIn) | ||||
|  | ||||
| 	m.Group("/{username}/-", func() { | ||||
| @@ -811,13 +821,16 @@ func RegisterRoutes(m *web.Route) { | ||||
| 					Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost) | ||||
| 				m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) | ||||
| 			}) | ||||
| 			m.Get("/search", repo.ListIssues) | ||||
| 		}, context.RepoMustNotBeArchived(), reqRepoIssueReader) | ||||
| 		// FIXME: should use different URLs but mostly same logic for comments of issue and pull request. | ||||
| 		// So they can apply their own enable/disable logic on routers. | ||||
| 		m.Group("/{type:issues|pulls}", func() { | ||||
| 			m.Group("/{index}", func() { | ||||
| 				m.Get("/info", repo.GetIssueInfo) | ||||
| 				m.Post("/title", repo.UpdateIssueTitle) | ||||
| 				m.Post("/content", repo.UpdateIssueContent) | ||||
| 				m.Post("/deadline", bindIgnErr(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline) | ||||
| 				m.Post("/watch", repo.IssueWatch) | ||||
| 				m.Post("/ref", repo.UpdateIssueRef) | ||||
| 				m.Group("/dependency", func() { | ||||
| @@ -1195,6 +1208,7 @@ func RegisterRoutes(m *web.Route) { | ||||
| 		m.Get("", user.Notifications) | ||||
| 		m.Post("/status", user.NotificationStatusPost) | ||||
| 		m.Post("/purge", user.NotificationPurgePost) | ||||
| 		m.Get("/new", user.NewAvailable) | ||||
| 	}, reqSignIn) | ||||
|  | ||||
| 	if setting.API.EnableSwagger { | ||||
|   | ||||
| @@ -429,7 +429,7 @@ | ||||
|  | ||||
| 			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} | ||||
| 				<div {{if ne .Issue.DeadlineUnix 0}} style="display: none;"{{end}} id="deadlineForm"> | ||||
| 					<form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/api/v1/repos/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}" method="post" id="update-issue-deadline-form"> | ||||
| 					<form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" method="post" id="update-issue-deadline-form"> | ||||
| 						{{$.CsrfTokenHtml}} | ||||
| 						<input required placeholder="{{.i18n.Tr "repo.issues.due_date_form"}}" {{if gt .Issue.DeadlineUnix 0}}value="{{.Issue.DeadlineUnix.Format "2006-01-02"}}"{{end}} type="date" name="deadlineDate" id="deadlineDate"> | ||||
| 						<button class="ui green icon button"> | ||||
|   | ||||
| @@ -120,7 +120,7 @@ export default { | ||||
|     load(data, callback) { | ||||
|       this.loading = true; | ||||
|       this.i18nErrorMessage = null; | ||||
|       $.get(`${appSubUrl}/api/v1/repos/${data.owner}/${data.repo}/issues/${data.index}`).done((issue) => { | ||||
|       $.get(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`).done((issue) => { | ||||
|         this.issue = issue; | ||||
|       }).fail((jqXHR) => { | ||||
|         if (jqXHR.responseJSON && jqXHR.responseJSON.message) { | ||||
|   | ||||
| @@ -124,7 +124,7 @@ function initVueComponents() { | ||||
|         return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||
|       }, | ||||
|       searchURL() { | ||||
|         return `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery | ||||
|         return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery | ||||
|         }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode | ||||
|         }${this.reposFilter !== 'all' ? '&exclusive=1' : '' | ||||
|         }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' | ||||
| @@ -302,7 +302,7 @@ function initVueComponents() { | ||||
|         this.isLoading = true; | ||||
|  | ||||
|         if (!this.reposTotalCount) { | ||||
|           const totalCountSearchURL = `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; | ||||
|           const totalCountSearchURL = `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; | ||||
|           $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => { | ||||
|             this.reposTotalCount = request.getResponseHeader('X-Total-Count'); | ||||
|           }); | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export function initCompSearchUserBox() { | ||||
|   $searchUserBox.search({ | ||||
|     minCharacters: 2, | ||||
|     apiSettings: { | ||||
|       url: `${appSubUrl}/api/v1/users/search?q={query}`, | ||||
|       url: `${appSubUrl}/user/search?q={query}`, | ||||
|       onResponse(response) { | ||||
|         const items = []; | ||||
|         const searchQueryUppercase = $searchUserBox.find('input').val().toUpperCase(); | ||||
|   | ||||
| @@ -158,7 +158,7 @@ async function updateNotificationTable() { | ||||
| async function updateNotificationCount() { | ||||
|   const data = await $.ajax({ | ||||
|     type: 'GET', | ||||
|     url: `${appSubUrl}/api/v1/notifications/new`, | ||||
|     url: `${appSubUrl}/notifications/new`, | ||||
|     headers: { | ||||
|       'X-Csrf-Token': csrfToken, | ||||
|     }, | ||||
|   | ||||
| @@ -20,7 +20,7 @@ export function initOrgTeamSearchRepoBox() { | ||||
|   $searchRepoBox.search({ | ||||
|     minCharacters: 2, | ||||
|     apiSettings: { | ||||
|       url: `${appSubUrl}/api/v1/repos/search?q={query}&uid=${$searchRepoBox.data('uid')}`, | ||||
|       url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`, | ||||
|       onResponse(response) { | ||||
|         const items = []; | ||||
|         $.each(response.data, (_i, item) => { | ||||
|   | ||||
| @@ -91,7 +91,7 @@ export function initRepoTopicBar() { | ||||
|       label: 'ui small label' | ||||
|     }, | ||||
|     apiSettings: { | ||||
|       url: `${appSubUrl}/api/v1/topics/search?q={query}`, | ||||
|       url: `${appSubUrl}/explore/topics/search?q={query}`, | ||||
|       throttle: 500, | ||||
|       cache: false, | ||||
|       onResponse(res) { | ||||
|   | ||||
| @@ -54,7 +54,7 @@ function updateDeadline(deadlineString) { | ||||
|     realDeadline = new Date(newDate); | ||||
|   } | ||||
|  | ||||
|   $.ajax(`${$('#update-issue-deadline-form').attr('action')}/deadline`, { | ||||
|   $.ajax(`${$('#update-issue-deadline-form').attr('action')}`, { | ||||
|     data: JSON.stringify({ | ||||
|       due_date: realDeadline, | ||||
|     }), | ||||
| @@ -91,9 +91,9 @@ export function initRepoIssueList() { | ||||
|   const repoId = $('#repoId').val(); | ||||
|   const crossRepoSearch = $('#crossRepoSearch').val(); | ||||
|   const tp = $('#type').val(); | ||||
|   let issueSearchUrl = `${appSubUrl}/api/v1/repos/${repolink}/issues?q={query}&type=${tp}`; | ||||
|   let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`; | ||||
|   if (crossRepoSearch === 'true') { | ||||
|     issueSearchUrl = `${appSubUrl}/api/v1/repos/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`; | ||||
|     issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`; | ||||
|   } | ||||
|   $('#new-dependency-drop-list') | ||||
|     .dropdown({ | ||||
| @@ -292,7 +292,7 @@ export function initRepoIssueReferenceRepositorySearch() { | ||||
|   $('.issue_reference_repository_search') | ||||
|     .dropdown({ | ||||
|       apiSettings: { | ||||
|         url: `${appSubUrl}/api/v1/repos/search?q={query}&limit=20`, | ||||
|         url: `${appSubUrl}/repo/search?q={query}&limit=20`, | ||||
|         onResponse(response) { | ||||
|           const filteredResponse = {success: true, results: []}; | ||||
|           $.each(response.data, (_r, repo) => { | ||||
|   | ||||
| @@ -21,7 +21,7 @@ export function initRepoSettingSearchTeamBox() { | ||||
|   $searchTeamBox.search({ | ||||
|     minCharacters: 2, | ||||
|     apiSettings: { | ||||
|       url: `${appSubUrl}/api/v1/orgs/${$searchTeamBox.data('org')}/teams/search?q={query}`, | ||||
|       url: `${appSubUrl}/org/${$searchTeamBox.data('org')}/teams/-/search?q={query}`, | ||||
|       headers: {'X-Csrf-Token': csrfToken}, | ||||
|       onResponse(response) { | ||||
|         const items = []; | ||||
|   | ||||
| @@ -23,7 +23,7 @@ export function initRepoTemplateSearch() { | ||||
|     $('#repo_template_search') | ||||
|       .dropdown({ | ||||
|         apiSettings: { | ||||
|           url: `${appSubUrl}/api/v1/repos/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`, | ||||
|           url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`, | ||||
|           onResponse(response) { | ||||
|             const filteredResponse = {success: true, results: []}; | ||||
|             filteredResponse.results.push({ | ||||
|   | ||||
| @@ -111,7 +111,7 @@ async function updateStopwatchWithCallback(callback, timeout) { | ||||
| async function updateStopwatch() { | ||||
|   const data = await $.ajax({ | ||||
|     type: 'GET', | ||||
|     url: `${appSubUrl}/api/v1/user/stopwatches`, | ||||
|     url: `${appSubUrl}/user/stopwatches`, | ||||
|     headers: {'X-Csrf-Token': csrfToken}, | ||||
|   }); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user