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