times Add filters (#9373)

(extend #9200)
 * add query param for GET functions (created Bevore & after)
 * add test
 * generalize func GetQueryBeforeSince

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
release/v1.15
6543 2020-01-08 22:14:00 +01:00 committed by techknowlogick
parent f8dcc5f9f8
commit 14a9687444
6 changed files with 234 additions and 32 deletions

View File

@ -44,6 +44,18 @@ func TestAPIGetTrackedTimes(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, user.Name, apiTimes[i].UserName) assert.Equal(t, user.Name, apiTimes[i].UserName)
} }
// test filter
since := "2000-01-01T00%3A00%3A02%2B00%3A00" //946684802
before := "2000-01-01T00%3A00%3A12%2B00%3A00" //946684812
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/times?since=%s&before=%s&token=%s", user2.Name, issue2.Repo.Name, issue2.Index, since, before, token)
resp = session.MakeRequest(t, req, http.StatusOK)
var filterAPITimes api.TrackedTimeList
DecodeJSON(t, resp, &filterAPITimes)
assert.Len(t, filterAPITimes, 2)
assert.Equal(t, int64(3), filterAPITimes[0].ID)
assert.Equal(t, int64(6), filterAPITimes[1].ID)
} }
func TestAPIDeleteTrackedTime(t *testing.T) { func TestAPIDeleteTrackedTime(t *testing.T) {

View File

@ -100,10 +100,12 @@ func (tl TrackedTimeList) APIFormat() api.TrackedTimeList {
// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored. // FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
type FindTrackedTimesOptions struct { type FindTrackedTimesOptions struct {
IssueID int64 IssueID int64
UserID int64 UserID int64
RepositoryID int64 RepositoryID int64
MilestoneID int64 MilestoneID int64
CreatedAfterUnix int64
CreatedBeforeUnix int64
} }
// ToCond will convert each condition into a xorm-Cond // ToCond will convert each condition into a xorm-Cond
@ -121,6 +123,12 @@ func (opts *FindTrackedTimesOptions) ToCond() builder.Cond {
if opts.MilestoneID != 0 { if opts.MilestoneID != 0 {
cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID}) cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID})
} }
if opts.CreatedAfterUnix != 0 {
cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix})
}
if opts.CreatedBeforeUnix != 0 {
cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix})
}
return cond return cond
} }

View File

@ -654,7 +654,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/times", func() { m.Group("/times", func() {
m.Combo("").Get(repo.ListTrackedTimesByRepository) m.Combo("").Get(repo.ListTrackedTimesByRepository)
m.Combo("/:timetrackingusername").Get(repo.ListTrackedTimesByUser) m.Combo("/:timetrackingusername").Get(repo.ListTrackedTimesByUser)
}, mustEnableIssues) }, mustEnableIssues, reqToken())
m.Group("/issues", func() { m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues). m.Combo("").Get(repo.ListIssues).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue) Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
@ -688,12 +688,12 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Delete("/:id", reqToken(), repo.DeleteIssueLabel) m.Delete("/:id", reqToken(), repo.DeleteIssueLabel)
}) })
m.Group("/times", func() { m.Group("/times", func() {
m.Combo("", reqToken()). m.Combo("").
Get(repo.ListTrackedTimes). Get(repo.ListTrackedTimes).
Post(bind(api.AddTimeOption{}), repo.AddTime). Post(bind(api.AddTimeOption{}), repo.AddTime).
Delete(repo.ResetIssueTime) Delete(repo.ResetIssueTime)
m.Delete("/:id", reqToken(), repo.DeleteTime) m.Delete("/:id", repo.DeleteTime)
}) }, reqToken())
m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
m.Group("/stopwatch", func() { m.Group("/stopwatch", func() {
m.Post("/start", reqToken(), repo.StartIssueStopwatch) m.Post("/start", reqToken(), repo.StartIssueStopwatch)

View File

@ -5,12 +5,15 @@
package repo package repo
import ( import (
"fmt"
"net/http" "net/http"
"strings"
"time" "time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
) )
// ListTrackedTimes list all the tracked times of an issue // ListTrackedTimes list all the tracked times of an issue
@ -37,6 +40,16 @@ func ListTrackedTimes(ctx *context.APIContext) {
// type: integer // type: integer
// format: int64 // format: int64
// required: true // required: true
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
@ -62,6 +75,11 @@ func ListTrackedTimes(ctx *context.APIContext) {
IssueID: issue.ID, IssueID: issue.ID,
} }
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
ctx.InternalServerError(err)
return
}
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin { if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin {
opts.UserID = ctx.User.ID opts.UserID = ctx.User.ID
} }
@ -141,7 +159,7 @@ func AddTime(ctx *context.APIContext, form api.AddTimeOption) {
//allow only RepoAdmin, Admin and User to add time //allow only RepoAdmin, Admin and User to add time
user, err = models.GetUserByName(form.User) user, err = models.GetUserByName(form.User)
if err != nil { if err != nil {
ctx.Error(500, "GetUserByName", err) ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
} }
} }
} }
@ -195,33 +213,33 @@ func ResetIssueTime(ctx *context.APIContext) {
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403": // "403":
// "$ref": "#/responses/error" // "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil { if err != nil {
if models.IsErrIssueNotExist(err) { if models.IsErrIssueNotExist(err) {
ctx.NotFound(err) ctx.NotFound(err)
} else { } else {
ctx.Error(500, "GetIssueByIndex", err) ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
} }
return return
} }
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) { if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.JSON(400, struct{ Message string }{Message: "time tracking disabled"}) ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return return
} }
ctx.Status(403) ctx.Status(http.StatusForbidden)
return return
} }
err = models.DeleteIssueUserTimes(issue, ctx.User) err = models.DeleteIssueUserTimes(issue, ctx.User)
if err != nil { if err != nil {
if models.IsErrNotExist(err) { if models.IsErrNotExist(err) {
ctx.Error(404, "DeleteIssueUserTimes", err) ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err)
} else { } else {
ctx.Error(500, "DeleteIssueUserTimes", err) ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err)
} }
return return
} }
@ -266,52 +284,53 @@ func DeleteTime(ctx *context.APIContext) {
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403": // "403":
// "$ref": "#/responses/error" // "$ref": "#/responses/forbidden"
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
if err != nil { if err != nil {
if models.IsErrIssueNotExist(err) { if models.IsErrIssueNotExist(err) {
ctx.NotFound(err) ctx.NotFound(err)
} else { } else {
ctx.Error(500, "GetIssueByIndex", err) ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
} }
return return
} }
if !ctx.Repo.CanUseTimetracker(issue, ctx.User) { if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.JSON(400, struct{ Message string }{Message: "time tracking disabled"}) ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
return return
} }
ctx.Status(403) ctx.Status(http.StatusForbidden)
return return
} }
time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id")) time, err := models.GetTrackedTimeByID(ctx.ParamsInt64(":id"))
if err != nil { if err != nil {
ctx.Error(500, "GetTrackedTimeByID", err) ctx.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err)
return return
} }
if !ctx.User.IsAdmin && time.UserID != ctx.User.ID { if !ctx.User.IsAdmin && time.UserID != ctx.User.ID {
//Only Admin and User itself can delete their time //Only Admin and User itself can delete their time
ctx.Status(403) ctx.Status(http.StatusForbidden)
return return
} }
err = models.DeleteTime(time) err = models.DeleteTime(time)
if err != nil { if err != nil {
ctx.Error(500, "DeleteTime", err) ctx.Error(http.StatusInternalServerError, "DeleteTime", err)
return return
} }
ctx.Status(204) ctx.Status(http.StatusNoContent)
} }
// ListTrackedTimesByUser lists all tracked times of the user // ListTrackedTimesByUser lists all tracked times of the user
func ListTrackedTimesByUser(ctx *context.APIContext) { func ListTrackedTimesByUser(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/times/{user} user userTrackedTimes // swagger:operation GET /repos/{owner}/{repo}/times/{user} repository userTrackedTimes
// --- // ---
// summary: List a user's tracked times in a repo // summary: List a user's tracked times in a repo
// deprecated: true
// produces: // produces:
// - application/json // - application/json
// parameters: // parameters:
@ -335,6 +354,8 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.Error(http.StatusBadRequest, "", "time tracking disabled") ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
@ -353,9 +374,23 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
ctx.NotFound() ctx.NotFound()
return return
} }
trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin && ctx.User.ID != user.ID {
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query user not allowed not enouth rights"))
return
}
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin && ctx.User.ID != user.ID {
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query user not allowed not enouth rights"))
return
}
opts := models.FindTrackedTimesOptions{
UserID: user.ID, UserID: user.ID,
RepositoryID: ctx.Repo.Repository.ID}) RepositoryID: ctx.Repo.Repository.ID,
}
trackedTimes, err := models.GetTrackedTimes(opts)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err) ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
return return
@ -385,11 +420,27 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
// description: name of the repo // description: name of the repo
// type: string // type: string
// required: true // required: true
// - name: user
// in: query
// description: optional filter by user
// type: string
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
// "400": // "400":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
if !ctx.Repo.Repository.IsTimetrackerEnabled() { if !ctx.Repo.Repository.IsTimetrackerEnabled() {
ctx.Error(http.StatusBadRequest, "", "time tracking disabled") ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
@ -400,8 +451,30 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
RepositoryID: ctx.Repo.Repository.ID, RepositoryID: ctx.Repo.Repository.ID,
} }
// Filters
qUser := strings.Trim(ctx.Query("user"), " ")
if qUser != "" {
user, err := models.GetUserByName(qUser)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
return
}
opts.UserID = user.ID
}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
ctx.InternalServerError(err)
return
}
if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin { if !ctx.IsUserRepoAdmin() && !ctx.User.IsAdmin {
opts.UserID = ctx.User.ID if opts.UserID == 0 {
opts.UserID = ctx.User.ID
} else {
ctx.Error(http.StatusForbidden, "", fmt.Errorf("query user not allowed not enouth rights"))
return
}
} }
trackedTimes, err := models.GetTrackedTimes(opts) trackedTimes, err := models.GetTrackedTimes(opts)
@ -423,18 +496,39 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
// summary: List the current user's tracked times // summary: List the current user's tracked times
// produces: // produces:
// - application/json // - application/json
// parameters:
// - name: since
// in: query
// description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// - name: before
// in: query
// description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/TrackedTimeList" // "$ref": "#/responses/TrackedTimeList"
trackedTimes, err := models.GetTrackedTimes(models.FindTrackedTimesOptions{UserID: ctx.User.ID}) opts := models.FindTrackedTimesOptions{UserID: ctx.User.ID}
var err error
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
ctx.InternalServerError(err)
return
}
trackedTimes, err := models.GetTrackedTimes(opts)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err) ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err)
return return
} }
if err = trackedTimes.LoadAttributes(); err != nil { if err = trackedTimes.LoadAttributes(); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
return return
} }
ctx.JSON(http.StatusOK, trackedTimes.APIFormat()) ctx.JSON(http.StatusOK, trackedTimes.APIFormat())
} }

View File

@ -4,7 +4,12 @@
package utils package utils
import "code.gitea.io/gitea/modules/context" import (
"strings"
"time"
"code.gitea.io/gitea/modules/context"
)
// UserID user ID of authenticated user, or 0 if not authenticated // UserID user ID of authenticated user, or 0 if not authenticated
func UserID(ctx *context.APIContext) int64 { func UserID(ctx *context.APIContext) int64 {
@ -13,3 +18,29 @@ func UserID(ctx *context.APIContext) int64 {
} }
return ctx.User.ID return ctx.User.ID
} }
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
func GetQueryBeforeSince(ctx *context.APIContext) (before, since int64, err error) {
qCreatedBefore := strings.Trim(ctx.Query("before"), " ")
if qCreatedBefore != "" {
createdBefore, err := time.Parse(time.RFC3339, qCreatedBefore)
if err != nil {
return 0, 0, err
}
if !createdBefore.IsZero() {
before = createdBefore.Unix()
}
}
qCreatedAfter := strings.Trim(ctx.Query("since"), " ")
if qCreatedAfter != "" {
createdAfter, err := time.Parse(time.RFC3339, qCreatedAfter)
if err != nil {
return 0, 0, err
}
if !createdAfter.IsZero() {
since = createdAfter.Unix()
}
}
return before, since, nil
}

View File

@ -4433,6 +4433,20 @@
"name": "index", "name": "index",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated after the given time. This is a timestamp in RFC 3339 format",
"name": "since",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated before the given time. This is a timestamp in RFC 3339 format",
"name": "before",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -4543,7 +4557,7 @@
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"403": { "403": {
"$ref": "#/responses/error" "$ref": "#/responses/forbidden"
} }
} }
} }
@ -4601,7 +4615,7 @@
"$ref": "#/responses/error" "$ref": "#/responses/error"
}, },
"403": { "403": {
"$ref": "#/responses/error" "$ref": "#/responses/forbidden"
} }
} }
} }
@ -6419,6 +6433,26 @@
"name": "repo", "name": "repo",
"in": "path", "in": "path",
"required": true "required": true
},
{
"type": "string",
"description": "optional filter by user",
"name": "user",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated after the given time. This is a timestamp in RFC 3339 format",
"name": "since",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated before the given time. This is a timestamp in RFC 3339 format",
"name": "before",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -6427,6 +6461,9 @@
}, },
"400": { "400": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
} }
} }
} }
@ -6437,10 +6474,11 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"user" "repository"
], ],
"summary": "List a user's tracked times in a repo", "summary": "List a user's tracked times in a repo",
"operationId": "userTrackedTimes", "operationId": "userTrackedTimes",
"deprecated": true,
"parameters": [ "parameters": [
{ {
"type": "string", "type": "string",
@ -6470,6 +6508,9 @@
}, },
"400": { "400": {
"$ref": "#/responses/error" "$ref": "#/responses/error"
},
"403": {
"$ref": "#/responses/forbidden"
} }
} }
} }
@ -7685,6 +7726,22 @@
], ],
"summary": "List the current user's tracked times", "summary": "List the current user's tracked times",
"operationId": "userCurrentTrackedTimes", "operationId": "userCurrentTrackedTimes",
"parameters": [
{
"type": "string",
"format": "date-time",
"description": "Only show times updated after the given time. This is a timestamp in RFC 3339 format",
"name": "since",
"in": "query"
},
{
"type": "string",
"format": "date-time",
"description": "Only show times updated before the given time. This is a timestamp in RFC 3339 format",
"name": "before",
"in": "query"
}
],
"responses": { "responses": {
"200": { "200": {
"$ref": "#/responses/TrackedTimeList" "$ref": "#/responses/TrackedTimeList"