mirror of
https://github.com/go-gitea/gitea.git
synced 2024-05-09 09:46:08 +02:00
Compare commits
91 Commits
bdebd52c44
...
931498d082
Author | SHA1 | Date | |
---|---|---|---|
Illya Marchenko | 931498d082 | ||
Kemal Zebari | dd301cae1c | ||
stuzer05 | 0a8fd353ef | ||
stuzer05 | 12553d7dc2 | ||
stuzer05 | b97139b232 | ||
stuzer05 | b452e13f35 | ||
stuzer05 | e92bedc81d | ||
stuzer05 | 1e22cc242f | ||
stuzer05 | 93d09fa8fc | ||
stuzer05 | 0820db04ed | ||
stuzer05 | 09723c5e0b | ||
stuzer05 | c272512d9a | ||
stuzer05 | 0c4b2dfa8f | ||
stuzer05 | 805af19ef1 | ||
stuzer05 | bb5ca4c40b | ||
stuzer05 | a737a8c10b | ||
stuzer05 | 92dc2cd22c | ||
stuzer05 | 39b8b1929d | ||
stuzer05 | a999055954 | ||
stuzer05 | a6fa4c3d04 | ||
stuzer05 | 2a9009fb02 | ||
stuzer05 | 3037d6cdde | ||
stuzer05 | 349b959022 | ||
stuzer05 | 64de74d540 | ||
stuzer05 | fa662ec087 | ||
stuzer05 | 62094d8cf3 | ||
スツゼル | e933a89bd9 | ||
stuzer05 | db49783fa8 | ||
stuzer05 | a45c1e9c86 | ||
stuzer05 | 721069d766 | ||
stuzer05 | 3310440ed1 | ||
スツゼル | 598e2d5bed | ||
silverwind | b211b9e66d | ||
stuzer05 | ffaa4babcb | ||
stuzer05 | cef496af2a | ||
stuzer05 | 015ad01513 | ||
stuzer05 | 748bd67814 | ||
スツゼル | 3924cb0062 | ||
stuzer05 | e9afd60d6b | ||
stuzer05 | 40e1373dce | ||
stuzer05 | bf4fa112d0 | ||
stuzer05 | fc93006f50 | ||
スツゼル | 879d96f077 | ||
スツゼル | fd5adc55f3 | ||
stuzer05 | 57a3664eb2 | ||
stuzer05 | bf323cf26c | ||
スツゼル | fb8126035a | ||
stuzer05 | deddce59cf | ||
stuzer05 | 79e18c6711 | ||
stuzer05 | 49a176d37c | ||
stuzer05 | 8ef3a478aa | ||
stuzer05 | 37e8e8ddaf | ||
stuzer05 | e20e23b60a | ||
スツゼル | 8c0bf885b2 | ||
stuzer05 | b9cdc7c670 | ||
stuzer05 | 7be748f63b | ||
stuzer05 | 29dd61722b | ||
stuzer05 | 875087061f | ||
スツゼル | 775c663805 | ||
stuzer05 | db3c697178 | ||
stuzer05 | 5eea230714 | ||
stuzer05 | 0ab85af1ce | ||
stuzer05 | 5c4dc8739c | ||
stuzer05 | 79f507b81f | ||
stuzer05 | 7a570440f1 | ||
stuzer05 | f463765fec | ||
stuzer05 | e15549501d | ||
stuzer05 | 1cff1a9e05 | ||
stuzer05 | f7427d8b50 | ||
stuzer05 | 1b7ba4189b | ||
stuzer05 | 11b9719b5f | ||
stuzer05 | 0f5b609f9d | ||
stuzer05 | 09c05e8b76 | ||
stuzer05 | d247b0ffd1 | ||
stuzer05 | 870bb922cc | ||
stuzer05 | b062fc9849 | ||
stuzer05 | 2fc2f63c6c | ||
スツゼル | 4e1aed8a61 | ||
stuzer05 | f33b0a0773 | ||
stuzer05 | e187364d7a | ||
スツゼル | f7a4c9e0aa | ||
スツゼル | 5f3edad64f | ||
stuzer05 | 4be8c50a6f | ||
stuzer05 | d75b7acac6 | ||
stuzer05 | c248042c8e | ||
stuzer05 | 7ed86ff00a | ||
stuzer05 | d9446629c3 | ||
stuzer05 | a8778f4db7 | ||
stuzer05 | c783692f6c | ||
stuzer05 | d11ba9f46c | ||
stuzer05 | 1363205f29 |
|
@ -112,6 +112,8 @@ const (
|
|||
|
||||
CommentTypePin // 36 pin Issue
|
||||
CommentTypeUnpin // 37 unpin Issue
|
||||
|
||||
CommentTypeChangeTimeEstimate // 38 Change time estimate
|
||||
)
|
||||
|
||||
var commentStrings = []string{
|
||||
|
@ -151,6 +153,7 @@ var commentStrings = []string{
|
|||
"change_issue_ref",
|
||||
"pull_scheduled_merge",
|
||||
"pull_cancel_scheduled_merge",
|
||||
"change_time_estimate",
|
||||
"pin",
|
||||
"unpin",
|
||||
}
|
||||
|
|
|
@ -140,6 +140,9 @@ type Issue struct {
|
|||
|
||||
// For view issue page.
|
||||
ShowRole RoleDescriptor `xorm:"-"`
|
||||
|
||||
// Time estimate
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -909,3 +912,33 @@ func insertIssue(ctx context.Context, issue *Issue) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeIssueTimeEstimate changes the plan time of this issue, as the given user.
|
||||
func ChangeIssueTimeEstimate(issue *Issue, doer *user_model.User, timeEstimate int64) (err error) {
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil {
|
||||
return fmt.Errorf("updateIssueCols: %w", err)
|
||||
}
|
||||
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return fmt.Errorf("loadRepo: %w", err)
|
||||
}
|
||||
|
||||
opts := &CreateCommentOptions{
|
||||
Type: CommentTypeChangeTimeEstimate,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Content: fmt.Sprintf("%d", timeEstimate),
|
||||
}
|
||||
if _, err = CreateComment(ctx, opts); err != nil {
|
||||
return fmt.Errorf("createComment: %w", err)
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
|
|
@ -586,6 +586,8 @@ var migrations = []Migration{
|
|||
NewMigration("Add everyone_access_mode for repo_unit", v1_23.AddRepoUnitEveryoneAccessMode),
|
||||
// v298 -> v299
|
||||
NewMigration("Drop wrongly created table o_auth2_application", v1_23.DropWronglyCreatedTable),
|
||||
// v300 -> v301
|
||||
NewMigration("Add TimeEstimate to issue table", v1_23.AddTimeEstimateColumnToIssueTable),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
|
||||
type Issue struct {
|
||||
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Issue))
|
||||
}
|
|
@ -65,12 +65,14 @@ func NewFuncMap() template.FuncMap {
|
|||
|
||||
// -----------------------------------------------------------------
|
||||
// time / number / format
|
||||
"FileSize": base.FileSize,
|
||||
"CountFmt": base.FormatNumberSI,
|
||||
"TimeSince": timeutil.TimeSince,
|
||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||
"DateTime": timeutil.DateTime,
|
||||
"Sec2Time": util.SecToTime,
|
||||
"FileSize": base.FileSize,
|
||||
"CountFmt": base.FormatNumberSI,
|
||||
"TimeSince": timeutil.TimeSince,
|
||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||
"DateTime": timeutil.DateTime,
|
||||
"Sec2Time": util.SecToTime,
|
||||
"SecToTimeExact": util.SecToTimeExact,
|
||||
"TimeEstimateToStr": util.TimeEstimateToStr,
|
||||
"LoadTimes": func(startTime time.Time) string {
|
||||
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"
|
||||
},
|
||||
|
|
|
@ -66,6 +66,43 @@ func SecToTime(durationVal any) string {
|
|||
return strings.TrimRight(formattedTime, " ")
|
||||
}
|
||||
|
||||
func SecToTimeExact(duration int64, withSeconds bool) string {
|
||||
formattedTime := ""
|
||||
|
||||
// The following four variables are calculated by taking
|
||||
// into account the previously calculated variables, this avoids
|
||||
// pitfalls when using remainders. As that could lead to incorrect
|
||||
// results when the calculated number equals the quotient number.
|
||||
remainingDays := duration / (60 * 60 * 24)
|
||||
years := remainingDays / 365
|
||||
remainingDays -= years * 365
|
||||
months := remainingDays * 12 / 365
|
||||
remainingDays -= months * 365 / 12
|
||||
weeks := remainingDays / 7
|
||||
remainingDays -= weeks * 7
|
||||
days := remainingDays
|
||||
|
||||
// The following three variables are calculated without depending
|
||||
// on the previous calculated variables.
|
||||
hours := (duration / 3600) % 24
|
||||
minutes := (duration / 60) % 60
|
||||
seconds := duration % 60
|
||||
|
||||
// Show exact time information
|
||||
formattedTime = formatTime(years, "year", formattedTime)
|
||||
formattedTime = formatTime(months, "month", formattedTime)
|
||||
formattedTime = formatTime(weeks, "week", formattedTime)
|
||||
formattedTime = formatTime(days, "day", formattedTime)
|
||||
formattedTime = formatTime(hours, "hour", formattedTime)
|
||||
formattedTime = formatTime(minutes, "minute", formattedTime)
|
||||
if withSeconds {
|
||||
formattedTime = formatTime(seconds, "second", formattedTime)
|
||||
}
|
||||
|
||||
// The formatTime() function always appends a space at the end. This will be trimmed
|
||||
return strings.TrimRight(formattedTime, " ")
|
||||
}
|
||||
|
||||
// formatTime appends the given value to the existing forammattedTime. E.g:
|
||||
// formattedTime = "1 year"
|
||||
// input: value = 3, name = "month"
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright 2022 Gitea. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// Time estimate match regex
|
||||
rTimeEstimateOnlyHours = regexp.MustCompile(`^([\d]+)$`)
|
||||
rTimeEstimateWeeks = regexp.MustCompile(`([\d]+)w`)
|
||||
rTimeEstimateDays = regexp.MustCompile(`([\d]+)d`)
|
||||
rTimeEstimateHours = regexp.MustCompile(`([\d]+)h`)
|
||||
rTimeEstimateMinutes = regexp.MustCompile(`([\d]+)m`)
|
||||
)
|
||||
|
||||
// TimeEstimateFromStr returns time estimate in seconds from formatted string
|
||||
func TimeEstimateFromStr(timeStr string) int64 {
|
||||
timeTotal := 0
|
||||
|
||||
// If single number entered, assume hours
|
||||
timeStrMatches := rTimeEstimateOnlyHours.FindStringSubmatch(timeStr)
|
||||
if len(timeStrMatches) > 0 {
|
||||
raw, _ := strconv.Atoi(timeStrMatches[1])
|
||||
timeTotal += raw * (60 * 60)
|
||||
} else {
|
||||
// Find time weeks
|
||||
timeStrMatches = rTimeEstimateWeeks.FindStringSubmatch(timeStr)
|
||||
if len(timeStrMatches) > 0 {
|
||||
raw, _ := strconv.Atoi(timeStrMatches[1])
|
||||
timeTotal += raw * (60 * 60 * 24 * 7)
|
||||
}
|
||||
|
||||
// Find time days
|
||||
timeStrMatches = rTimeEstimateDays.FindStringSubmatch(timeStr)
|
||||
if len(timeStrMatches) > 0 {
|
||||
raw, _ := strconv.Atoi(timeStrMatches[1])
|
||||
timeTotal += raw * (60 * 60 * 24)
|
||||
}
|
||||
|
||||
// Find time hours
|
||||
timeStrMatches = rTimeEstimateHours.FindStringSubmatch(timeStr)
|
||||
if len(timeStrMatches) > 0 {
|
||||
raw, _ := strconv.Atoi(timeStrMatches[1])
|
||||
timeTotal += raw * (60 * 60)
|
||||
}
|
||||
|
||||
// Find time minutes
|
||||
timeStrMatches = rTimeEstimateMinutes.FindStringSubmatch(timeStr)
|
||||
if len(timeStrMatches) > 0 {
|
||||
raw, _ := strconv.Atoi(timeStrMatches[1])
|
||||
timeTotal += raw * (60)
|
||||
}
|
||||
}
|
||||
|
||||
return int64(timeTotal)
|
||||
}
|
||||
|
||||
// TimeEstimateStr returns formatted time estimate string from seconds (e.g. "2w 4d 12h 5m")
|
||||
func TimeEstimateToStr(amount int64) string {
|
||||
var timeParts []string
|
||||
|
||||
timeSeconds := float64(amount)
|
||||
|
||||
// Format weeks
|
||||
weeks := math.Floor(timeSeconds / (60 * 60 * 24 * 7))
|
||||
if weeks > 0 {
|
||||
timeParts = append(timeParts, fmt.Sprintf("%dw", int64(weeks)))
|
||||
}
|
||||
timeSeconds -= weeks * (60 * 60 * 24 * 7)
|
||||
|
||||
// Format days
|
||||
days := math.Floor(timeSeconds / (60 * 60 * 24))
|
||||
if days > 0 {
|
||||
timeParts = append(timeParts, fmt.Sprintf("%dd", int64(days)))
|
||||
}
|
||||
timeSeconds -= days * (60 * 60 * 24)
|
||||
|
||||
// Format hours
|
||||
hours := math.Floor(timeSeconds / (60 * 60))
|
||||
if hours > 0 {
|
||||
timeParts = append(timeParts, fmt.Sprintf("%dh", int64(hours)))
|
||||
}
|
||||
timeSeconds -= hours * (60 * 60)
|
||||
|
||||
// Format minutes
|
||||
minutes := math.Floor(timeSeconds / (60))
|
||||
if minutes > 0 {
|
||||
timeParts = append(timeParts, fmt.Sprintf("%dm", int64(minutes)))
|
||||
}
|
||||
|
||||
return strings.Join(timeParts, " ")
|
||||
}
|
|
@ -1480,6 +1480,11 @@ issues.add_assignee_at = `was assigned by <b>%s</b> %s`
|
|||
issues.remove_assignee_at = `was unassigned by <b>%s</b> %s`
|
||||
issues.remove_self_assignment = `removed their assignment %s`
|
||||
issues.change_title_at = `changed title from <b><strike>%s</strike></b> to <b>%s</b> %s`
|
||||
issues.time_estimate = `Time Estimate`
|
||||
issues.add_time_estimate = `3w 4d 12h`
|
||||
issues.change_time_estimate_at = `changed time estimate to <b>%s</b> %s`
|
||||
issues.remove_time_estimate = `removed time estimate %s`
|
||||
issues.time_estimate_invalid = `Time estimate format is invalid`
|
||||
issues.change_ref_at = `changed reference from <b><strike>%s</strike></b> to <b>%s</b> %s`
|
||||
issues.remove_ref_at = `removed reference <b>%s</b> %s`
|
||||
issues.add_ref_at = `added reference <b>%s</b> %s`
|
||||
|
@ -1650,20 +1655,20 @@ issues.start_tracking_history = `started working %s`
|
|||
issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed
|
||||
issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!`
|
||||
issues.stop_tracking = Stop Timer
|
||||
issues.stop_tracking_history = `stopped working %s`
|
||||
issues.stop_tracking_history = `worked for <b>%s</b> %s`
|
||||
issues.cancel_tracking = Discard
|
||||
issues.cancel_tracking_history = `canceled time tracking %s`
|
||||
issues.add_time = Manually Add Time
|
||||
issues.del_time = Delete this time log
|
||||
issues.add_time_short = Add Time
|
||||
issues.add_time_cancel = Cancel
|
||||
issues.add_time_history = `added spent time %s`
|
||||
issues.del_time_history= `deleted spent time %s`
|
||||
issues.add_time_history = `added spent time <b>%s</b> %s`
|
||||
issues.del_time_history= `deleted spent time <b>%s</b> %s`
|
||||
issues.add_time_hours = Hours
|
||||
issues.add_time_minutes = Minutes
|
||||
issues.add_time_sum_to_small = No time was entered.
|
||||
issues.time_spent_total = Total Time Spent
|
||||
issues.time_spent_from_all_authors = `Total Time Spent: %s`
|
||||
issues.time_spent_from_all_authors = `Total Time Spent:`
|
||||
issues.due_date = Due Date
|
||||
issues.invalid_due_date_format = "Due date format must be 'yyyy-mm-dd'."
|
||||
issues.error_modifying_due_date = "Failed to modify the due date."
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
|
@ -4,6 +4,7 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -372,7 +373,11 @@ func CreatePullReview(ctx *context.APIContext) {
|
|||
// create review and associate all pending review comments
|
||||
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -460,7 +465,11 @@ func SubmitPullReview(ctx *context.APIContext) {
|
|||
// create review and associate all pending review comments
|
||||
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
@ -1771,6 +1772,9 @@ func ViewIssue(ctx *context.Context) {
|
|||
comment.Content = comment.Content[1:]
|
||||
}
|
||||
}
|
||||
} else if comment.Type == issues_model.CommentTypeChangeTimeEstimate {
|
||||
timeSec, _ := util.ToInt64(comment.Content)
|
||||
comment.Content = util.SecToTimeExact(timeSec, timeSec < 60)
|
||||
}
|
||||
|
||||
if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull {
|
||||
|
@ -2208,6 +2212,57 @@ func UpdateIssueTitle(ctx *context.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// UpdateIssueTimeEstimate change issue's planned time
|
||||
var (
|
||||
rTimeEstimateStr = regexp.MustCompile(`^([\d]+w)?\s?([\d]+d)?\s?([\d]+h)?\s?([\d]+m)?$`)
|
||||
rTimeEstimateStrHoursOnly = regexp.MustCompile(`^([\d]+)$`)
|
||||
)
|
||||
|
||||
func UpdateIssueTimeEstimate(ctx *context.Context) {
|
||||
issue := GetActionIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
url := issue.Link()
|
||||
|
||||
timeStr := ctx.FormString("time_estimate")
|
||||
|
||||
// Validate input
|
||||
if !rTimeEstimateStr.MatchString(timeStr) && !rTimeEstimateStrHoursOnly.MatchString(timeStr) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.time_estimate_invalid"))
|
||||
ctx.Redirect(url, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
total := util.TimeEstimateFromStr(timeStr)
|
||||
|
||||
// User entered something wrong
|
||||
if total == 0 && len(timeStr) != 0 {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.time_estimate_invalid"))
|
||||
ctx.Redirect(url, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// No time changed
|
||||
if issue.TimeEstimate == total {
|
||||
ctx.Redirect(url, http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.ChangeTimeEstimate(issue, ctx.Doer, total); err != nil {
|
||||
ctx.ServerError("ChangeTimeEstimate", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(url, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// UpdateIssueRef change issue's ref (branch)
|
||||
func UpdateIssueRef(ctx *context.Context) {
|
||||
issue := GetActionIssue(ctx)
|
||||
|
|
|
@ -34,7 +34,7 @@ func AddTimeManually(c *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
total := time.Duration(form.Hours)*time.Hour + time.Duration(form.Minutes)*time.Minute
|
||||
total := util.TimeEstimateFromStr(form.TimeString)
|
||||
|
||||
if total <= 0 {
|
||||
c.Flash.Error(c.Tr("repo.issues.add_time_sum_to_small"))
|
||||
|
@ -42,7 +42,7 @@ func AddTimeManually(c *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if _, err := issues_model.AddTime(c, c.Doer, issue, int64(total.Seconds()), time.Now()); err != nil {
|
||||
if _, err := issues_model.AddTime(c, c.Doer, issue, total, time.Now()); err != nil {
|
||||
c.ServerError("AddTime", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -264,6 +264,8 @@ func SubmitReview(ctx *context.Context) {
|
|||
if issues_model.IsContentEmptyErr(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
|
||||
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
|
||||
} else if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
|
||||
ctx.Status(http.StatusUnprocessableEntity)
|
||||
} else {
|
||||
ctx.ServerError("SubmitReview", err)
|
||||
}
|
||||
|
|
|
@ -1202,6 +1202,7 @@ func registerRoutes(m *web.Route) {
|
|||
m.Post("/cancel", repo.CancelStopwatch)
|
||||
})
|
||||
})
|
||||
m.Post("/time_estimate", repo.UpdateIssueTimeEstimate)
|
||||
m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeIssueReaction)
|
||||
m.Post("/lock", reqRepoIssuesOrPullsWriter, web.Bind(forms.IssueLockForm{}), repo.LockIssue)
|
||||
m.Post("/unlock", reqRepoIssuesOrPullsWriter, repo.UnlockIssue)
|
||||
|
|
|
@ -76,6 +76,11 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
|
|||
// so we check for the "|" delimiter and convert new to legacy format on demand
|
||||
c.Content = util.SecToTime(c.Content[1:])
|
||||
}
|
||||
|
||||
if c.Type == issues_model.CommentTypeChangeTimeEstimate {
|
||||
timeSec, _ := util.ToInt64(c.Content)
|
||||
c.Content = util.SecToTimeExact(timeSec, timeSec < 60)
|
||||
}
|
||||
}
|
||||
|
||||
comment := &api.TimelineComment{
|
||||
|
|
|
@ -877,8 +877,7 @@ func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) bi
|
|||
|
||||
// AddTimeManuallyForm form that adds spent time manually.
|
||||
type AddTimeManuallyForm struct {
|
||||
Hours int `binding:"Range(0,1000)"`
|
||||
Minutes int `binding:"Range(0,1000)"`
|
||||
TimeString string
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
|
|
@ -43,6 +43,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{
|
|||
/*14*/ issues_model.CommentTypeAddTimeManual,
|
||||
/*15*/ issues_model.CommentTypeCancelTracking,
|
||||
/*26*/ issues_model.CommentTypeDeleteTimeManual,
|
||||
/*38*/ issues_model.CommentTypeChangeTimeEstimate,
|
||||
},
|
||||
"deadline": {
|
||||
/*16*/ issues_model.CommentTypeAddedDeadline,
|
||||
|
|
|
@ -9,8 +9,6 @@ import (
|
|||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -23,64 +21,11 @@ import (
|
|||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
secondsByMinute = float64(time.Minute / time.Second) // seconds in a minute
|
||||
secondsByHour = 60 * secondsByMinute // seconds in an hour
|
||||
secondsByDay = 8 * secondsByHour // seconds in a day
|
||||
secondsByWeek = 5 * secondsByDay // seconds in a week
|
||||
secondsByMonth = 4 * secondsByWeek // seconds in a month
|
||||
)
|
||||
|
||||
var reDuration = regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`)
|
||||
|
||||
// timeLogToAmount parses time log string and returns amount in seconds
|
||||
func timeLogToAmount(str string) int64 {
|
||||
matches := reDuration.FindAllStringSubmatch(str, -1)
|
||||
if len(matches) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
match := matches[0]
|
||||
|
||||
var a int64
|
||||
|
||||
// months
|
||||
if len(match[1]) > 0 {
|
||||
mo, _ := strconv.ParseFloat(strings.Replace(match[1], ",", ".", 1), 64)
|
||||
a += int64(mo * secondsByMonth)
|
||||
}
|
||||
|
||||
// weeks
|
||||
if len(match[3]) > 0 {
|
||||
w, _ := strconv.ParseFloat(strings.Replace(match[3], ",", ".", 1), 64)
|
||||
a += int64(w * secondsByWeek)
|
||||
}
|
||||
|
||||
// days
|
||||
if len(match[5]) > 0 {
|
||||
d, _ := strconv.ParseFloat(strings.Replace(match[5], ",", ".", 1), 64)
|
||||
a += int64(d * secondsByDay)
|
||||
}
|
||||
|
||||
// hours
|
||||
if len(match[7]) > 0 {
|
||||
h, _ := strconv.ParseFloat(strings.Replace(match[7], ",", ".", 1), 64)
|
||||
a += int64(h * secondsByHour)
|
||||
}
|
||||
|
||||
// minutes
|
||||
if len(match[9]) > 0 {
|
||||
d, _ := strconv.ParseFloat(strings.Replace(match[9], ",", ".", 1), 64)
|
||||
a += int64(d * secondsByMinute)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func issueAddTime(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, time time.Time, timeLog string) error {
|
||||
amount := timeLogToAmount(timeLog)
|
||||
amount := util.TimeEstimateFromStr(timeLog)
|
||||
if amount == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -105,6 +105,13 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
|
|||
return nil
|
||||
}
|
||||
|
||||
// ChangeTimeEstimate changes the time estimate of this issue, as the given user.
|
||||
func ChangeTimeEstimate(issue *issues_model.Issue, doer *user_model.User, timeEstimate int64) (err error) {
|
||||
issue.TimeEstimate = timeEstimate
|
||||
|
||||
return issues_model.ChangeIssueTimeEstimate(issue, doer, timeEstimate)
|
||||
}
|
||||
|
||||
// ChangeIssueRef changes the branch of this issue, as the given user.
|
||||
func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, ref string) error {
|
||||
oldRef := issue.Ref
|
||||
|
|
|
@ -6,6 +6,7 @@ package pull
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
|
@ -43,6 +44,9 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
|
|||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR.
|
||||
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
|
||||
|
||||
// checkInvalidation checks if the line of code comment got changed by another commit.
|
||||
// If the line got changed the comment is going to be invalidated.
|
||||
func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
|
||||
|
@ -293,6 +297,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
|
|||
if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
|
||||
stale = false
|
||||
} else {
|
||||
if issue.IsClosed {
|
||||
return nil, nil, ErrSubmitReviewOnClosedPR
|
||||
}
|
||||
|
||||
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
{{end -}}
|
||||
{{- range .ReviewComments}}
|
||||
<hr>
|
||||
{{$.locale.Tr "mail.issue.in_tree_path" .TreePath}}
|
||||
{{ctx.Locale.Tr "mail.issue.in_tree_path" .TreePath}}
|
||||
<div class="review">
|
||||
<pre>{{.Patch}}</pre>
|
||||
<div>{{.RenderedContent}}</div>
|
||||
|
|
|
@ -30,20 +30,24 @@
|
|||
{{end}}
|
||||
<div class="divider"></div>
|
||||
{{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
|
||||
<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
{{if not $.Issue.IsClosed}}
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
|
||||
<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
|
||||
<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
{{if not $.Issue.IsClosed}}
|
||||
{{if $showSelfTooltip}}
|
||||
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
|
||||
<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
</span>
|
||||
{{else}}
|
||||
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
26 = DELETE_TIME_MANUAL, 27 = REVIEW_REQUEST, 28 = MERGE_PULL_REQUEST,
|
||||
29 = PULL_PUSH_EVENT, 30 = PROJECT_CHANGED, 31 = PROJECT_BOARD_CHANGED
|
||||
32 = DISMISSED_REVIEW, 33 = COMMENT_TYPE_CHANGE_ISSUE_REF, 34 = PR_SCHEDULE_TO_AUTO_MERGE,
|
||||
35 = CANCEL_SCHEDULED_AUTO_MERGE_PR, 36 = PIN_ISSUE, 37 = UNPIN_ISSUE -->
|
||||
35 = CANCEL_SCHEDULED_AUTO_MERGE_PR, 36 = PIN_ISSUE, 37 = UNPIN_ISSUE,
|
||||
38 = COMMENT_TYPE_CHANGE_TIME_ESTIMATE -->
|
||||
{{if eq .Type 0}}
|
||||
<div class="timeline-item comment" id="{{.HashTag}}">
|
||||
{{if .OriginalAuthor}}
|
||||
|
@ -249,18 +250,18 @@
|
|||
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||
<span class="text grey muted-links">
|
||||
{{template "shared/user/authorlink" .Poster}}
|
||||
{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $createdStr}}
|
||||
</span>
|
||||
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
|
||||
<div class="detail flex-text-block">
|
||||
{{svg "octicon-clock"}}
|
||||
|
||||
{{$timeStr := ""}}
|
||||
{{if .RenderedContent}}
|
||||
{{/* compatibility with time comments made before v1.21 */}}
|
||||
<span class="text grey muted-links">{{.RenderedContent}}</span>
|
||||
{{$timeStr = .RenderedContent}}
|
||||
{{else}}
|
||||
<span class="text grey muted-links">{{.Content|Sec2Time}}</span>
|
||||
{{$timeStr = .Content|Sec2Time}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $timeStr $createdStr | SafeHTML}}
|
||||
</span>
|
||||
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
|
||||
</div>
|
||||
{{else if eq .Type 14}}
|
||||
<div class="timeline-item event" id="{{.HashTag}}">
|
||||
|
@ -268,18 +269,18 @@
|
|||
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||
<span class="text grey muted-links">
|
||||
{{template "shared/user/authorlink" .Poster}}
|
||||
{{ctx.Locale.Tr "repo.issues.add_time_history" $createdStr}}
|
||||
</span>
|
||||
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
|
||||
<div class="detail flex-text-block">
|
||||
{{svg "octicon-clock"}}
|
||||
|
||||
{{$timeStr := ""}}
|
||||
{{if .RenderedContent}}
|
||||
{{/* compatibility with time comments made before v1.21 */}}
|
||||
<span class="text grey muted-links">{{.RenderedContent}}</span>
|
||||
{{$timeStr = .RenderedContent}}
|
||||
{{else}}
|
||||
<span class="text grey muted-links">{{.Content|Sec2Time}}</span>
|
||||
{{$timeStr = .Content|Sec2Time}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{ctx.Locale.Tr "repo.issues.add_time_history" $timeStr $createdStr | SafeHTML}}
|
||||
</span>
|
||||
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
|
||||
</div>
|
||||
{{else if eq .Type 15}}
|
||||
<div class="timeline-item event" id="{{.HashTag}}">
|
||||
|
@ -680,6 +681,15 @@
|
|||
{{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{else if eq .Type 38}}
|
||||
<div class="timeline-item event" id="{{.HashTag}}">
|
||||
<span class="badge">{{svg "octicon-clock"}}</span>
|
||||
{{template "shared/user/avatarlink" dict "Context" $.Context "user" .Poster}}
|
||||
<span class="text grey muted-links">
|
||||
{{template "shared/user/authorlink" .Poster}}
|
||||
{{ctx.Locale.Tr "repo.issues.change_time_estimate_at" .Content $createdStr | SafeHTML}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
@ -278,8 +278,22 @@
|
|||
{{end}}
|
||||
{{if .Repository.IsTimetrackerEnabled $.Context}}
|
||||
{{if and .CanUseTimetracker (not .Repository.IsArchived)}}
|
||||
<div class="divider"></div>
|
||||
<div class="ui timetrack">
|
||||
<div class="ui divider"></div>
|
||||
<div>
|
||||
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_estimate"}}</strong></span>
|
||||
|
||||
<form method="post" id="set_time_estimate_form" class="gt-mt-3" action="{{.Issue.Link}}/time_estimate">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<div class="ui input fluid">
|
||||
<input name="time_estimate" placeholder='{{ctx.Locale.Tr "repo.issues.add_time_estimate"}}' value="{{TimeEstimateToStr .Issue.TimeEstimate}}" data-value="{{$.Issue.TimeEstimate}}" type="text" >
|
||||
</div>
|
||||
<button class="ui fluid button green tooltip tw-mt-1">
|
||||
{{ctx.Locale.Tr "repo.issues.save"}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="ui timetrack">
|
||||
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.tracker"}}</strong></span>
|
||||
<div class="tw-mt-2">
|
||||
<form method="post" action="{{.Issue.Link}}/times/stopwatch/toggle" id="toggle_stopwatch_form">
|
||||
|
@ -311,9 +325,8 @@
|
|||
<div class="header">{{ctx.Locale.Tr "repo.issues.add_time"}}</div>
|
||||
<div class="content">
|
||||
<form method="post" id="add_time_manual_form" action="{{.Issue.Link}}/times/add" class="ui input fluid tw-gap-2">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_hours"}}' type="number" name="hours">
|
||||
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_minutes"}}' type="number" name="minutes" class="ui compact">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input placeholder='{{ctx.Locale.Tr "repo.issues.add_time_estimate"}}' type="text" name="time_string">
|
||||
</form>
|
||||
</div>
|
||||
<div class="actions">
|
||||
|
@ -332,8 +345,9 @@
|
|||
{{if .WorkingUsers}}
|
||||
<div class="divider"></div>
|
||||
<div class="ui comments">
|
||||
<span class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}</strong></span>
|
||||
<div>
|
||||
<div class="text"><strong>{{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" | SafeHTML}}</strong></div>
|
||||
<div>{{SecToTimeExact .Issue.TotalTrackedTime false}}</div>
|
||||
<div class="gt-mt-3">
|
||||
{{range $user, $trackedtime := .WorkingUsers}}
|
||||
<div class="comment tw-mt-2">
|
||||
<a class="avatar">
|
||||
|
|
|
@ -5,12 +5,15 @@ package integration
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
@ -176,3 +179,82 @@ func TestPullView_CodeOwner(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
user1Session := loginUser(t, "user1")
|
||||
user2Session := loginUser(t, "user2")
|
||||
|
||||
// Have user1 create a fork of repo1.
|
||||
testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1")
|
||||
|
||||
t.Run("Submit approve/reject review on merged PR", func(t *testing.T) {
|
||||
// Create a merged PR (made by user1) in the upstream repo1.
|
||||
testEditFile(t, user1Session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
|
||||
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "master", "This is a pull title")
|
||||
elem := strings.Split(test.RedirectURL(resp), "/")
|
||||
assert.EqualValues(t, "pulls", elem[3])
|
||||
testPullMerge(t, user1Session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
|
||||
|
||||
// Grab the CSRF token.
|
||||
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Submit an approve review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
|
||||
|
||||
// Submit a reject review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
|
||||
})
|
||||
|
||||
t.Run("Submit approve/reject review on closed PR", func(t *testing.T) {
|
||||
// Created a closed PR (made by user1) in the upstream repo1.
|
||||
testEditFileToNewBranch(t, user1Session, "user1", "repo1", "master", "a-test-branch", "README.md", "Hello, World (Editied...again)\n")
|
||||
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title")
|
||||
elem := strings.Split(test.RedirectURL(resp), "/")
|
||||
assert.EqualValues(t, "pulls", elem[3])
|
||||
testIssueClose(t, user1Session, elem[1], elem[2], elem[4])
|
||||
|
||||
// Grab the CSRF token.
|
||||
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
// Submit an approve review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
|
||||
|
||||
// Submit a reject review on the PR.
|
||||
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pullNumber, reviewType string, expectedSubmitStatus int) *httptest.ResponseRecorder {
|
||||
options := map[string]string{
|
||||
"_csrf": csrf,
|
||||
"commit_id": "",
|
||||
"content": "test",
|
||||
"type": reviewType,
|
||||
}
|
||||
|
||||
submitURL := path.Join(owner, repo, "pulls", pullNumber, "files", "reviews", "submit")
|
||||
req := NewRequestWithValues(t, "POST", submitURL, options)
|
||||
return session.MakeRequest(t, req, expectedSubmitStatus)
|
||||
}
|
||||
|
||||
func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", path.Join(owner, repo, "pulls", issueNumber))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
closeURL := path.Join(owner, repo, "issues", issueNumber, "comments")
|
||||
|
||||
options := map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"status": "close",
|
||||
}
|
||||
|
||||
req = NewRequestWithValues(t, "POST", closeURL, options)
|
||||
return session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -73,8 +73,7 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo
|
|||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
|
||||
events = htmlDoc.doc.Find(".event > span.text")
|
||||
assert.Contains(t, events.Last().Text(), "stopped working")
|
||||
htmlDoc.AssertElement(t, ".event .detail .octicon-clock", true)
|
||||
assert.Contains(t, events.Last().Text(), "worked for ")
|
||||
} else {
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue