code.gitea.io/gitea@v1.22.3/routers/api/v1/repo/issue_tracked_time.go (about)

     1  // Copyright 2017 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"time"
    10  
    11  	"code.gitea.io/gitea/models/db"
    12  	issues_model "code.gitea.io/gitea/models/issues"
    13  	"code.gitea.io/gitea/models/unit"
    14  	user_model "code.gitea.io/gitea/models/user"
    15  	api "code.gitea.io/gitea/modules/structs"
    16  	"code.gitea.io/gitea/modules/web"
    17  	"code.gitea.io/gitea/routers/api/v1/utils"
    18  	"code.gitea.io/gitea/services/context"
    19  	"code.gitea.io/gitea/services/convert"
    20  )
    21  
    22  // ListTrackedTimes list all the tracked times of an issue
    23  func ListTrackedTimes(ctx *context.APIContext) {
    24  	// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/times issue issueTrackedTimes
    25  	// ---
    26  	// summary: List an issue's tracked times
    27  	// produces:
    28  	// - application/json
    29  	// parameters:
    30  	// - name: owner
    31  	//   in: path
    32  	//   description: owner of the repo
    33  	//   type: string
    34  	//   required: true
    35  	// - name: repo
    36  	//   in: path
    37  	//   description: name of the repo
    38  	//   type: string
    39  	//   required: true
    40  	// - name: index
    41  	//   in: path
    42  	//   description: index of the issue
    43  	//   type: integer
    44  	//   format: int64
    45  	//   required: true
    46  	// - name: user
    47  	//   in: query
    48  	//   description: optional filter by user (available for issue managers)
    49  	//   type: string
    50  	// - name: since
    51  	//   in: query
    52  	//   description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
    53  	//   type: string
    54  	//   format: date-time
    55  	// - name: before
    56  	//   in: query
    57  	//   description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
    58  	//   type: string
    59  	//   format: date-time
    60  	// - name: page
    61  	//   in: query
    62  	//   description: page number of results to return (1-based)
    63  	//   type: integer
    64  	// - name: limit
    65  	//   in: query
    66  	//   description: page size of results
    67  	//   type: integer
    68  	// responses:
    69  	//   "200":
    70  	//     "$ref": "#/responses/TrackedTimeList"
    71  	//   "404":
    72  	//     "$ref": "#/responses/notFound"
    73  
    74  	if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
    75  		ctx.NotFound("Timetracker is disabled")
    76  		return
    77  	}
    78  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
    79  	if err != nil {
    80  		if issues_model.IsErrIssueNotExist(err) {
    81  			ctx.NotFound(err)
    82  		} else {
    83  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
    84  		}
    85  		return
    86  	}
    87  
    88  	opts := &issues_model.FindTrackedTimesOptions{
    89  		ListOptions:  utils.GetListOptions(ctx),
    90  		RepositoryID: ctx.Repo.Repository.ID,
    91  		IssueID:      issue.ID,
    92  	}
    93  
    94  	qUser := ctx.FormTrim("user")
    95  	if qUser != "" {
    96  		user, err := user_model.GetUserByName(ctx, qUser)
    97  		if user_model.IsErrUserNotExist(err) {
    98  			ctx.Error(http.StatusNotFound, "User does not exist", err)
    99  		} else if err != nil {
   100  			ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
   101  			return
   102  		}
   103  		opts.UserID = user.ID
   104  	}
   105  
   106  	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
   107  		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
   108  		return
   109  	}
   110  
   111  	cantSetUser := !ctx.Doer.IsAdmin &&
   112  		opts.UserID != ctx.Doer.ID &&
   113  		!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
   114  
   115  	if cantSetUser {
   116  		if opts.UserID == 0 {
   117  			opts.UserID = ctx.Doer.ID
   118  		} else {
   119  			ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights"))
   120  			return
   121  		}
   122  	}
   123  
   124  	count, err := issues_model.CountTrackedTimes(ctx, opts)
   125  	if err != nil {
   126  		ctx.InternalServerError(err)
   127  		return
   128  	}
   129  
   130  	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
   131  	if err != nil {
   132  		ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
   133  		return
   134  	}
   135  	if err = trackedTimes.LoadAttributes(ctx); err != nil {
   136  		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
   137  		return
   138  	}
   139  
   140  	ctx.SetTotalCountHeader(count)
   141  	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
   142  }
   143  
   144  // AddTime add time manual to the given issue
   145  func AddTime(ctx *context.APIContext) {
   146  	// swagger:operation Post /repos/{owner}/{repo}/issues/{index}/times issue issueAddTime
   147  	// ---
   148  	// summary: Add tracked time to a issue
   149  	// consumes:
   150  	// - application/json
   151  	// produces:
   152  	// - application/json
   153  	// parameters:
   154  	// - name: owner
   155  	//   in: path
   156  	//   description: owner of the repo
   157  	//   type: string
   158  	//   required: true
   159  	// - name: repo
   160  	//   in: path
   161  	//   description: name of the repo
   162  	//   type: string
   163  	//   required: true
   164  	// - name: index
   165  	//   in: path
   166  	//   description: index of the issue
   167  	//   type: integer
   168  	//   format: int64
   169  	//   required: true
   170  	// - name: body
   171  	//   in: body
   172  	//   schema:
   173  	//     "$ref": "#/definitions/AddTimeOption"
   174  	// responses:
   175  	//   "200":
   176  	//     "$ref": "#/responses/TrackedTime"
   177  	//   "400":
   178  	//     "$ref": "#/responses/error"
   179  	//   "403":
   180  	//     "$ref": "#/responses/forbidden"
   181  	//   "404":
   182  	//     "$ref": "#/responses/notFound"
   183  	form := web.GetForm(ctx).(*api.AddTimeOption)
   184  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   185  	if err != nil {
   186  		if issues_model.IsErrIssueNotExist(err) {
   187  			ctx.NotFound(err)
   188  		} else {
   189  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   190  		}
   191  		return
   192  	}
   193  
   194  	if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
   195  		if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
   196  			ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
   197  			return
   198  		}
   199  		ctx.Status(http.StatusForbidden)
   200  		return
   201  	}
   202  
   203  	user := ctx.Doer
   204  	if form.User != "" {
   205  		if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
   206  			// allow only RepoAdmin, Admin and User to add time
   207  			user, err = user_model.GetUserByName(ctx, form.User)
   208  			if err != nil {
   209  				ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
   210  			}
   211  		}
   212  	}
   213  
   214  	created := time.Time{}
   215  	if !form.Created.IsZero() {
   216  		created = form.Created
   217  	}
   218  
   219  	trackedTime, err := issues_model.AddTime(ctx, user, issue, form.Time, created)
   220  	if err != nil {
   221  		ctx.Error(http.StatusInternalServerError, "AddTime", err)
   222  		return
   223  	}
   224  	if err = trackedTime.LoadAttributes(ctx); err != nil {
   225  		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
   226  		return
   227  	}
   228  	ctx.JSON(http.StatusOK, convert.ToTrackedTime(ctx, user, trackedTime))
   229  }
   230  
   231  // ResetIssueTime reset time manual to the given issue
   232  func ResetIssueTime(ctx *context.APIContext) {
   233  	// swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times issue issueResetTime
   234  	// ---
   235  	// summary: Reset a tracked time of an issue
   236  	// consumes:
   237  	// - application/json
   238  	// produces:
   239  	// - application/json
   240  	// parameters:
   241  	// - name: owner
   242  	//   in: path
   243  	//   description: owner of the repo
   244  	//   type: string
   245  	//   required: true
   246  	// - name: repo
   247  	//   in: path
   248  	//   description: name of the repo
   249  	//   type: string
   250  	//   required: true
   251  	// - name: index
   252  	//   in: path
   253  	//   description: index of the issue to add tracked time to
   254  	//   type: integer
   255  	//   format: int64
   256  	//   required: true
   257  	// responses:
   258  	//   "204":
   259  	//     "$ref": "#/responses/empty"
   260  	//   "400":
   261  	//     "$ref": "#/responses/error"
   262  	//   "403":
   263  	//     "$ref": "#/responses/forbidden"
   264  	//   "404":
   265  	//     "$ref": "#/responses/notFound"
   266  
   267  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   268  	if err != nil {
   269  		if issues_model.IsErrIssueNotExist(err) {
   270  			ctx.NotFound(err)
   271  		} else {
   272  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   273  		}
   274  		return
   275  	}
   276  
   277  	if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
   278  		if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
   279  			ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
   280  			return
   281  		}
   282  		ctx.Status(http.StatusForbidden)
   283  		return
   284  	}
   285  
   286  	err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
   287  	if err != nil {
   288  		if db.IsErrNotExist(err) {
   289  			ctx.Error(http.StatusNotFound, "DeleteIssueUserTimes", err)
   290  		} else {
   291  			ctx.Error(http.StatusInternalServerError, "DeleteIssueUserTimes", err)
   292  		}
   293  		return
   294  	}
   295  	ctx.Status(http.StatusNoContent)
   296  }
   297  
   298  // DeleteTime delete a specific time by id
   299  func DeleteTime(ctx *context.APIContext) {
   300  	// swagger:operation Delete /repos/{owner}/{repo}/issues/{index}/times/{id} issue issueDeleteTime
   301  	// ---
   302  	// summary: Delete specific tracked time
   303  	// consumes:
   304  	// - application/json
   305  	// produces:
   306  	// - application/json
   307  	// parameters:
   308  	// - name: owner
   309  	//   in: path
   310  	//   description: owner of the repo
   311  	//   type: string
   312  	//   required: true
   313  	// - name: repo
   314  	//   in: path
   315  	//   description: name of the repo
   316  	//   type: string
   317  	//   required: true
   318  	// - name: index
   319  	//   in: path
   320  	//   description: index of the issue
   321  	//   type: integer
   322  	//   format: int64
   323  	//   required: true
   324  	// - name: id
   325  	//   in: path
   326  	//   description: id of time to delete
   327  	//   type: integer
   328  	//   format: int64
   329  	//   required: true
   330  	// responses:
   331  	//   "204":
   332  	//     "$ref": "#/responses/empty"
   333  	//   "400":
   334  	//     "$ref": "#/responses/error"
   335  	//   "403":
   336  	//     "$ref": "#/responses/forbidden"
   337  	//   "404":
   338  	//     "$ref": "#/responses/notFound"
   339  
   340  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   341  	if err != nil {
   342  		if issues_model.IsErrIssueNotExist(err) {
   343  			ctx.NotFound(err)
   344  		} else {
   345  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   346  		}
   347  		return
   348  	}
   349  
   350  	if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) {
   351  		if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
   352  			ctx.JSON(http.StatusBadRequest, struct{ Message string }{Message: "time tracking disabled"})
   353  			return
   354  		}
   355  		ctx.Status(http.StatusForbidden)
   356  		return
   357  	}
   358  
   359  	time, err := issues_model.GetTrackedTimeByID(ctx, ctx.ParamsInt64(":id"))
   360  	if err != nil {
   361  		if db.IsErrNotExist(err) {
   362  			ctx.NotFound(err)
   363  			return
   364  		}
   365  		ctx.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err)
   366  		return
   367  	}
   368  	if time.Deleted {
   369  		ctx.NotFound(fmt.Errorf("tracked time [%d] already deleted", time.ID))
   370  		return
   371  	}
   372  
   373  	if !ctx.Doer.IsAdmin && time.UserID != ctx.Doer.ID {
   374  		// Only Admin and User itself can delete their time
   375  		ctx.Status(http.StatusForbidden)
   376  		return
   377  	}
   378  
   379  	err = issues_model.DeleteTime(ctx, time)
   380  	if err != nil {
   381  		ctx.Error(http.StatusInternalServerError, "DeleteTime", err)
   382  		return
   383  	}
   384  	ctx.Status(http.StatusNoContent)
   385  }
   386  
   387  // ListTrackedTimesByUser  lists all tracked times of the user
   388  func ListTrackedTimesByUser(ctx *context.APIContext) {
   389  	// swagger:operation GET /repos/{owner}/{repo}/times/{user} repository userTrackedTimes
   390  	// ---
   391  	// summary: List a user's tracked times in a repo
   392  	// deprecated: true
   393  	// produces:
   394  	// - application/json
   395  	// parameters:
   396  	// - name: owner
   397  	//   in: path
   398  	//   description: owner of the repo
   399  	//   type: string
   400  	//   required: true
   401  	// - name: repo
   402  	//   in: path
   403  	//   description: name of the repo
   404  	//   type: string
   405  	//   required: true
   406  	// - name: user
   407  	//   in: path
   408  	//   description: username of user
   409  	//   type: string
   410  	//   required: true
   411  	// responses:
   412  	//   "200":
   413  	//     "$ref": "#/responses/TrackedTimeList"
   414  	//   "400":
   415  	//     "$ref": "#/responses/error"
   416  	//   "403":
   417  	//     "$ref": "#/responses/forbidden"
   418  	//   "404":
   419  	//     "$ref": "#/responses/notFound"
   420  
   421  	if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
   422  		ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
   423  		return
   424  	}
   425  	user, err := user_model.GetUserByName(ctx, ctx.Params(":timetrackingusername"))
   426  	if err != nil {
   427  		if user_model.IsErrUserNotExist(err) {
   428  			ctx.NotFound(err)
   429  		} else {
   430  			ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
   431  		}
   432  		return
   433  	}
   434  	if user == nil {
   435  		ctx.NotFound()
   436  		return
   437  	}
   438  
   439  	if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID {
   440  		ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights"))
   441  		return
   442  	}
   443  
   444  	opts := &issues_model.FindTrackedTimesOptions{
   445  		UserID:       user.ID,
   446  		RepositoryID: ctx.Repo.Repository.ID,
   447  	}
   448  
   449  	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
   450  	if err != nil {
   451  		ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
   452  		return
   453  	}
   454  	if err = trackedTimes.LoadAttributes(ctx); err != nil {
   455  		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
   456  		return
   457  	}
   458  	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
   459  }
   460  
   461  // ListTrackedTimesByRepository lists all tracked times of the repository
   462  func ListTrackedTimesByRepository(ctx *context.APIContext) {
   463  	// swagger:operation GET /repos/{owner}/{repo}/times repository repoTrackedTimes
   464  	// ---
   465  	// summary: List a repo's tracked times
   466  	// produces:
   467  	// - application/json
   468  	// parameters:
   469  	// - name: owner
   470  	//   in: path
   471  	//   description: owner of the repo
   472  	//   type: string
   473  	//   required: true
   474  	// - name: repo
   475  	//   in: path
   476  	//   description: name of the repo
   477  	//   type: string
   478  	//   required: true
   479  	// - name: user
   480  	//   in: query
   481  	//   description: optional filter by user (available for issue managers)
   482  	//   type: string
   483  	// - name: since
   484  	//   in: query
   485  	//   description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
   486  	//   type: string
   487  	//   format: date-time
   488  	// - name: before
   489  	//   in: query
   490  	//   description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
   491  	//   type: string
   492  	//   format: date-time
   493  	// - name: page
   494  	//   in: query
   495  	//   description: page number of results to return (1-based)
   496  	//   type: integer
   497  	// - name: limit
   498  	//   in: query
   499  	//   description: page size of results
   500  	//   type: integer
   501  	// responses:
   502  	//   "200":
   503  	//     "$ref": "#/responses/TrackedTimeList"
   504  	//   "400":
   505  	//     "$ref": "#/responses/error"
   506  	//   "403":
   507  	//     "$ref": "#/responses/forbidden"
   508  	//   "404":
   509  	//     "$ref": "#/responses/notFound"
   510  
   511  	if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
   512  		ctx.Error(http.StatusBadRequest, "", "time tracking disabled")
   513  		return
   514  	}
   515  
   516  	opts := &issues_model.FindTrackedTimesOptions{
   517  		ListOptions:  utils.GetListOptions(ctx),
   518  		RepositoryID: ctx.Repo.Repository.ID,
   519  	}
   520  
   521  	// Filters
   522  	qUser := ctx.FormTrim("user")
   523  	if qUser != "" {
   524  		user, err := user_model.GetUserByName(ctx, qUser)
   525  		if user_model.IsErrUserNotExist(err) {
   526  			ctx.Error(http.StatusNotFound, "User does not exist", err)
   527  		} else if err != nil {
   528  			ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
   529  			return
   530  		}
   531  		opts.UserID = user.ID
   532  	}
   533  
   534  	var err error
   535  	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
   536  		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
   537  		return
   538  	}
   539  
   540  	cantSetUser := !ctx.Doer.IsAdmin &&
   541  		opts.UserID != ctx.Doer.ID &&
   542  		!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
   543  
   544  	if cantSetUser {
   545  		if opts.UserID == 0 {
   546  			opts.UserID = ctx.Doer.ID
   547  		} else {
   548  			ctx.Error(http.StatusForbidden, "", fmt.Errorf("query by user not allowed; not enough rights"))
   549  			return
   550  		}
   551  	}
   552  
   553  	count, err := issues_model.CountTrackedTimes(ctx, opts)
   554  	if err != nil {
   555  		ctx.InternalServerError(err)
   556  		return
   557  	}
   558  
   559  	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
   560  	if err != nil {
   561  		ctx.Error(http.StatusInternalServerError, "GetTrackedTimes", err)
   562  		return
   563  	}
   564  	if err = trackedTimes.LoadAttributes(ctx); err != nil {
   565  		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
   566  		return
   567  	}
   568  
   569  	ctx.SetTotalCountHeader(count)
   570  	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
   571  }
   572  
   573  // ListMyTrackedTimes lists all tracked times of the current user
   574  func ListMyTrackedTimes(ctx *context.APIContext) {
   575  	// swagger:operation GET /user/times user userCurrentTrackedTimes
   576  	// ---
   577  	// summary: List the current user's tracked times
   578  	// produces:
   579  	// - application/json
   580  	// parameters:
   581  	// - name: page
   582  	//   in: query
   583  	//   description: page number of results to return (1-based)
   584  	//   type: integer
   585  	// - name: limit
   586  	//   in: query
   587  	//   description: page size of results
   588  	//   type: integer
   589  	// - name: since
   590  	//   in: query
   591  	//   description: Only show times updated after the given time. This is a timestamp in RFC 3339 format
   592  	//   type: string
   593  	//   format: date-time
   594  	// - name: before
   595  	//   in: query
   596  	//   description: Only show times updated before the given time. This is a timestamp in RFC 3339 format
   597  	//   type: string
   598  	//   format: date-time
   599  	// responses:
   600  	//   "200":
   601  	//     "$ref": "#/responses/TrackedTimeList"
   602  
   603  	opts := &issues_model.FindTrackedTimesOptions{
   604  		ListOptions: utils.GetListOptions(ctx),
   605  		UserID:      ctx.Doer.ID,
   606  	}
   607  
   608  	var err error
   609  	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
   610  		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
   611  		return
   612  	}
   613  
   614  	count, err := issues_model.CountTrackedTimes(ctx, opts)
   615  	if err != nil {
   616  		ctx.InternalServerError(err)
   617  		return
   618  	}
   619  
   620  	trackedTimes, err := issues_model.GetTrackedTimes(ctx, opts)
   621  	if err != nil {
   622  		ctx.Error(http.StatusInternalServerError, "GetTrackedTimesByUser", err)
   623  		return
   624  	}
   625  
   626  	if err = trackedTimes.LoadAttributes(ctx); err != nil {
   627  		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
   628  		return
   629  	}
   630  
   631  	ctx.SetTotalCountHeader(count)
   632  	ctx.JSON(http.StatusOK, convert.ToTrackedTimeList(ctx, ctx.Doer, trackedTimes))
   633  }