Skip to content

Commit 5250d8c

Browse files
committed
rework
1 parent 7bf773b commit 5250d8c

File tree

12 files changed

+295
-258
lines changed

12 files changed

+295
-258
lines changed

modules/templates/helper.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ func NewFuncMap() template.FuncMap {
7171
"CountFmt": base.FormatNumberSI,
7272
"Sec2Time": util.SecToTime,
7373

74-
"SecToTimeExact": util.SecToTimeExact,
75-
"TimeEstimateToStr": util.TimeEstimateToStr,
74+
"TimeEstimateString": util.TimeEstimateString,
7675

7776
"LoadTimes": func(startTime time.Time) string {
7877
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"

modules/util/time_str.go

Lines changed: 57 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,80 @@
1-
// Copyright 2022 Gitea. All rights reserved.
1+
// Copyright 2024 Gitea. All rights reserved.
22
// SPDX-License-Identifier: MIT
33

44
package util
55

66
import (
77
"fmt"
8-
"math"
98
"regexp"
109
"strconv"
1110
"strings"
11+
"sync"
1212
)
1313

14-
var (
15-
// Time estimate match regex
16-
rTimeEstimateOnlyHours = regexp.MustCompile(`^([\d]+)$`)
17-
rTimeEstimateWeeks = regexp.MustCompile(`([\d]+)w`)
18-
rTimeEstimateDays = regexp.MustCompile(`([\d]+)d`)
19-
rTimeEstimateHours = regexp.MustCompile(`([\d]+)h`)
20-
rTimeEstimateMinutes = regexp.MustCompile(`([\d]+)m`)
21-
)
22-
23-
// TimeEstimateFromStr returns time estimate in seconds from formatted string
24-
func TimeEstimateFromStr(timeStr string) int64 {
25-
timeTotal := 0
14+
type timeStrGlobalVarsType struct {
15+
units []struct {
16+
name string
17+
num int64
18+
}
19+
re *regexp.Regexp
20+
}
2621

27-
// If single number entered, assume hours
28-
timeStrMatches := rTimeEstimateOnlyHours.FindStringSubmatch(timeStr)
29-
if len(timeStrMatches) > 0 {
30-
raw, _ := strconv.Atoi(timeStrMatches[1])
31-
timeTotal += raw * (60 * 60)
32-
} else {
33-
// Find time weeks
34-
timeStrMatches = rTimeEstimateWeeks.FindStringSubmatch(timeStr)
35-
if len(timeStrMatches) > 0 {
36-
raw, _ := strconv.Atoi(timeStrMatches[1])
37-
timeTotal += raw * (60 * 60 * 24 * 7)
38-
}
22+
var timeStrGlobalVars = sync.OnceValue[*timeStrGlobalVarsType](func() *timeStrGlobalVarsType {
23+
v := &timeStrGlobalVarsType{}
24+
v.re = regexp.MustCompile(`(?i)(\d+)\s*([dhms])`)
25+
v.units = []struct {
26+
name string
27+
num int64
28+
}{
29+
{"d", 60 * 60 * 24},
30+
{"h", 60 * 60},
31+
{"m", 60},
32+
{"s", 1},
33+
}
34+
return v
35+
})
3936

40-
// Find time days
41-
timeStrMatches = rTimeEstimateDays.FindStringSubmatch(timeStr)
42-
if len(timeStrMatches) > 0 {
43-
raw, _ := strconv.Atoi(timeStrMatches[1])
44-
timeTotal += raw * (60 * 60 * 24)
37+
func TimeEstimateParse(timeStr string) (int64, error) {
38+
if timeStr == "" {
39+
return 0, nil
40+
}
41+
var total int64
42+
matches := timeStrGlobalVars().re.FindAllStringSubmatchIndex(timeStr, -1)
43+
if len(matches) == 0 {
44+
return 0, fmt.Errorf("invalid time string: %s", timeStr)
45+
}
46+
if matches[0][0] != 0 || matches[len(matches)-1][1] != len(timeStr) {
47+
return 0, fmt.Errorf("invalid time string: %s", timeStr)
48+
}
49+
for _, match := range matches {
50+
amount, err := strconv.ParseInt(timeStr[match[2]:match[3]], 10, 64)
51+
if err != nil {
52+
return 0, fmt.Errorf("invalid time string: %v", err)
4553
}
46-
47-
// Find time hours
48-
timeStrMatches = rTimeEstimateHours.FindStringSubmatch(timeStr)
49-
if len(timeStrMatches) > 0 {
50-
raw, _ := strconv.Atoi(timeStrMatches[1])
51-
timeTotal += raw * (60 * 60)
54+
unit := timeStr[match[4]:match[5]]
55+
found := false
56+
for _, u := range timeStrGlobalVars().units {
57+
if strings.ToLower(unit) == u.name {
58+
total += amount * u.num
59+
found = true
60+
break
61+
}
5262
}
53-
54-
// Find time minutes
55-
timeStrMatches = rTimeEstimateMinutes.FindStringSubmatch(timeStr)
56-
if len(timeStrMatches) > 0 {
57-
raw, _ := strconv.Atoi(timeStrMatches[1])
58-
timeTotal += raw * (60)
63+
if !found {
64+
return 0, fmt.Errorf("invalid time unit: %s", unit)
5965
}
6066
}
61-
62-
return int64(timeTotal)
67+
return total, nil
6368
}
6469

65-
// TimeEstimateStr returns formatted time estimate string from seconds (e.g. "2w 4d 12h 5m")
66-
func TimeEstimateToStr(amount int64) string {
70+
func TimeEstimateString(amount int64) string {
6771
var timeParts []string
68-
69-
timeSeconds := float64(amount)
70-
71-
// Format weeks
72-
weeks := math.Floor(timeSeconds / (60 * 60 * 24 * 7))
73-
if weeks > 0 {
74-
timeParts = append(timeParts, fmt.Sprintf("%dw", int64(weeks)))
75-
}
76-
timeSeconds -= weeks * (60 * 60 * 24 * 7)
77-
78-
// Format days
79-
days := math.Floor(timeSeconds / (60 * 60 * 24))
80-
if days > 0 {
81-
timeParts = append(timeParts, fmt.Sprintf("%dd", int64(days)))
82-
}
83-
timeSeconds -= days * (60 * 60 * 24)
84-
85-
// Format hours
86-
hours := math.Floor(timeSeconds / (60 * 60))
87-
if hours > 0 {
88-
timeParts = append(timeParts, fmt.Sprintf("%dh", int64(hours)))
89-
}
90-
timeSeconds -= hours * (60 * 60)
91-
92-
// Format minutes
93-
minutes := math.Floor(timeSeconds / (60))
94-
if minutes > 0 {
95-
timeParts = append(timeParts, fmt.Sprintf("%dm", int64(minutes)))
72+
for _, u := range timeStrGlobalVars().units {
73+
if amount >= u.num {
74+
num := amount / u.num
75+
amount %= u.num
76+
timeParts = append(timeParts, fmt.Sprintf("%d%s", num, u.name))
77+
}
9678
}
97-
9879
return strings.Join(timeParts, " ")
9980
}

modules/util/time_str_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2024 Gitea. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package util
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestTimeStr(t *testing.T) {
13+
t.Run("Parse", func(t *testing.T) {
14+
// Test TimeEstimateParse
15+
tests := []struct {
16+
input string
17+
output int64
18+
err bool
19+
}{
20+
{"1d", 86400, false},
21+
{"1h", 3600, false},
22+
{"1m", 60, false},
23+
{"1s", 1, false},
24+
{"1d 1h 1m 1s", 86400 + 3600 + 60 + 1, false},
25+
{"1d1x", 0, true},
26+
}
27+
for _, test := range tests {
28+
t.Run(test.input, func(t *testing.T) {
29+
output, err := TimeEstimateParse(test.input)
30+
if test.err {
31+
assert.NotNil(t, err)
32+
} else {
33+
assert.Nil(t, err)
34+
}
35+
assert.Equal(t, test.output, output)
36+
})
37+
}
38+
})
39+
t.Run("String", func(t *testing.T) {
40+
tests := []struct {
41+
input int64
42+
output string
43+
}{
44+
{86400, "1d"},
45+
{3600, "1h"},
46+
{60, "1m"},
47+
{1, "1s"},
48+
{86400 + 60 + 1, "1d 1m 1s"},
49+
}
50+
for _, test := range tests {
51+
t.Run(test.output, func(t *testing.T) {
52+
output := TimeEstimateString(test.input)
53+
assert.Equal(t, test.output, output)
54+
})
55+
}
56+
})
57+
}

options/locale/locale_en-US.ini

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,11 +1507,6 @@ issues.add_assignee_at = `was assigned by <b>%s</b> %s`
15071507
issues.remove_assignee_at = `was unassigned by <b>%s</b> %s`
15081508
issues.remove_self_assignment = `removed their assignment %s`
15091509
issues.change_title_at = `changed title from <b><strike>%s</strike></b> to <b>%s</b> %s`
1510-
issues.time_estimate = `Time Estimate`
1511-
issues.add_time_estimate = `3w 4d 12h`
1512-
issues.change_time_estimate_at = `changed time estimate to <b>%s</b> %s`
1513-
issues.remove_time_estimate = `removed time estimate %s`
1514-
issues.time_estimate_invalid = `Time estimate format is invalid`
15151510
issues.change_ref_at = `changed reference from <b><strike>%s</strike></b> to <b>%s</b> %s`
15161511
issues.remove_ref_at = `removed reference <b>%s</b> %s`
15171512
issues.add_ref_at = `added reference <b>%s</b> %s`
@@ -1675,27 +1670,34 @@ issues.comment_on_locked = You cannot comment on a locked issue.
16751670
issues.delete = Delete
16761671
issues.delete.title = Delete this issue?
16771672
issues.delete.text = Do you really want to delete this issue? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
1673+
16781674
issues.tracker = Time Tracker
1679-
issues.start_tracking_short = Start Timer
1680-
issues.start_tracking = Start Time Tracking
1681-
issues.start_tracking_history = `started working %s`
1675+
issues.timetracker_timer_start = Start timer
1676+
issues.timetracker_timer_stop = Stop timer
1677+
issues.timetracker_timer_discard = Discard timer
1678+
issues.timetracker_timer_manually_add = Add Time
1679+
1680+
issues.time_estimate_placeholder = 1d 2h 3m
1681+
issues.time_estimate_set = Set estimated time
1682+
issues.time_estimate_display = Estimate: %s
1683+
issues.change_time_estimate_at = changed time estimate to <b>%s</b> %s
1684+
issues.remove_time_estimate = removed time estimate %s
1685+
issues.time_estimate_invalid = Time estimate format is invalid
1686+
issues.start_tracking_history = started working %s
16821687
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
16831688
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
1684-
issues.stop_tracking = Stop Timer
16851689
issues.stop_tracking_history = `worked for <b>%s</b> %s`
1686-
issues.cancel_tracking = Discard
16871690
issues.cancel_tracking_history = `canceled time tracking %s`
1688-
issues.add_time = Manually Add Time
16891691
issues.del_time = Delete this time log
1690-
issues.add_time_short = Add Time
1691-
issues.add_time_cancel = Cancel
16921692
issues.add_time_history = `added spent time <b>%s</b> %s`
16931693
issues.del_time_history= `deleted spent time %s`
1694+
issues.add_time_manually = Manually Add Time
16941695
issues.add_time_hours = Hours
16951696
issues.add_time_minutes = Minutes
16961697
issues.add_time_sum_to_small = No time was entered.
16971698
issues.time_spent_total = Total Time Spent
16981699
issues.time_spent_from_all_authors = `Total Time Spent: %s`
1700+
16991701
issues.due_date = Due Date
17001702
issues.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'."
17011703
issues.error_modifying_due_date = "Failed to modify the due date."

routers/web/repo/issue_new.go

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99
"html/template"
1010
"net/http"
11-
"regexp"
1211
"slices"
1312
"sort"
1413
"strconv"
@@ -402,54 +401,3 @@ func NewIssuePost(ctx *context.Context) {
402401
ctx.JSONRedirect(issue.Link())
403402
}
404403
}
405-
406-
// UpdateIssueTimeEstimate change issue's planned time
407-
var (
408-
rTimeEstimateStr = regexp.MustCompile(`^([\d]+w)?\s?([\d]+d)?\s?([\d]+h)?\s?([\d]+m)?$`)
409-
rTimeEstimateStrHoursOnly = regexp.MustCompile(`^([\d]+)$`)
410-
)
411-
412-
func UpdateIssueTimeEstimate(ctx *context.Context) {
413-
issue := GetActionIssue(ctx)
414-
if ctx.Written() {
415-
return
416-
}
417-
418-
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
419-
ctx.Error(http.StatusForbidden)
420-
return
421-
}
422-
423-
url := issue.Link()
424-
425-
timeStr := ctx.FormString("time_estimate")
426-
427-
// Validate input
428-
if !rTimeEstimateStr.MatchString(timeStr) && !rTimeEstimateStrHoursOnly.MatchString(timeStr) {
429-
ctx.Flash.Error(ctx.Tr("repo.issues.time_estimate_invalid"))
430-
ctx.Redirect(url, http.StatusSeeOther)
431-
return
432-
}
433-
434-
total := util.TimeEstimateFromStr(timeStr)
435-
436-
// User entered something wrong
437-
if total == 0 && len(timeStr) != 0 {
438-
ctx.Flash.Error(ctx.Tr("repo.issues.time_estimate_invalid"))
439-
ctx.Redirect(url, http.StatusSeeOther)
440-
return
441-
}
442-
443-
// No time changed
444-
if issue.TimeEstimate == total {
445-
ctx.Redirect(url, http.StatusSeeOther)
446-
return
447-
}
448-
449-
if err := issue_service.ChangeTimeEstimate(ctx, issue, ctx.Doer, total); err != nil {
450-
ctx.ServerError("ChangeTimeEstimate", err)
451-
return
452-
}
453-
454-
ctx.Redirect(url, http.StatusSeeOther)
455-
}

routers/web/repo/issue_stopwatch.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
package repo
55

66
import (
7-
"net/http"
87
"strings"
98

109
"code.gitea.io/gitea/models/db"
@@ -40,8 +39,7 @@ func IssueStopwatch(c *context.Context) {
4039
c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
4140
}
4241

43-
url := issue.Link()
44-
c.Redirect(url, http.StatusSeeOther)
42+
c.JSONRedirect("")
4543
}
4644

4745
// CancelStopwatch cancel the stopwatch
@@ -72,8 +70,7 @@ func CancelStopwatch(c *context.Context) {
7270
})
7371
}
7472

75-
url := issue.Link()
76-
c.Redirect(url, http.StatusSeeOther)
73+
c.JSONRedirect("")
7774
}
7875

7976
// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context

0 commit comments

Comments
 (0)