Merge pull request 'Add an 'updated_at' field to the EditIssueOption struct' (#764) from fluzz/forgejo:add_update_at into forgejo-development
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/764
This commit is contained in:
commit
4af241b9fb
26 changed files with 795 additions and 32 deletions
|
@ -823,6 +823,11 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
|
|||
IsForcePush: opts.IsForcePush,
|
||||
Invalidated: opts.Invalidated,
|
||||
}
|
||||
if opts.Issue.NoAutoTime {
|
||||
comment.CreatedUnix = opts.Issue.UpdatedUnix
|
||||
comment.UpdatedUnix = opts.Issue.UpdatedUnix
|
||||
e.NoAutoTime()
|
||||
}
|
||||
if _, err = e.Insert(comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -1101,9 +1106,17 @@ func UpdateComment(c *Comment, doer *user_model.User) error {
|
|||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
|
||||
sess := db.GetEngine(ctx).ID(c.ID).AllCols()
|
||||
if c.Issue.NoAutoTime {
|
||||
// update the DataBase
|
||||
sess = sess.NoAutoTime().SetExpr("updated_unix", c.Issue.UpdatedUnix)
|
||||
// the UpdatedUnix value of the Comment also has to be set,
|
||||
// to return the adequate valuè
|
||||
// see https://codeberg.org/forgejo/forgejo/pulls/764#issuecomment-1023801
|
||||
c.UpdatedUnix = c.Issue.UpdatedUnix
|
||||
}
|
||||
if _, err := sess.Update(c); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.LoadIssue(ctx); err != nil {
|
||||
|
|
|
@ -125,6 +125,7 @@ type Issue struct {
|
|||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ClosedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
NoAutoTime bool `xorm:"-"`
|
||||
|
||||
Attachments []*repo_model.Attachment `xorm:"-"`
|
||||
Comments CommentList `xorm:"-"`
|
||||
|
|
|
@ -27,7 +27,12 @@ import (
|
|||
|
||||
// UpdateIssueCols updates cols of issue
|
||||
func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
|
||||
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil {
|
||||
sess := db.GetEngine(ctx).ID(issue.ID)
|
||||
if issue.NoAutoTime {
|
||||
cols = append(cols, []string{"updated_unix"}...)
|
||||
sess.NoAutoTime()
|
||||
}
|
||||
if _, err := sess.Cols(cols...).Update(issue); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -71,7 +76,11 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
|
|||
}
|
||||
|
||||
if issue.IsClosed {
|
||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||
if issue.NoAutoTime {
|
||||
issue.ClosedUnix = issue.UpdatedUnix
|
||||
} else {
|
||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
} else {
|
||||
issue.ClosedUnix = 0
|
||||
}
|
||||
|
@ -92,8 +101,14 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
|
|||
|
||||
// Update issue count of milestone
|
||||
if issue.MilestoneID > 0 {
|
||||
if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
|
||||
return nil, err
|
||||
if issue.NoAutoTime {
|
||||
if err := UpdateMilestoneCountersWithDate(ctx, issue.MilestoneID, issue.UpdatedUnix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -259,8 +274,12 @@ func ChangeIssueContent(issue *Issue, doer *user_model.User, content string) (er
|
|||
return fmt.Errorf("UpdateIssueCols: %w", err)
|
||||
}
|
||||
|
||||
historyDate := timeutil.TimeStampNow()
|
||||
if issue.NoAutoTime {
|
||||
historyDate = issue.UpdatedUnix
|
||||
}
|
||||
if err = SaveIssueContentHistory(ctx, doer.ID, issue.ID, 0,
|
||||
timeutil.TimeStampNow(), issue.Content, false); err != nil {
|
||||
historyDate, issue.Content, false); err != nil {
|
||||
return fmt.Errorf("SaveIssueContentHistory: %w", err)
|
||||
}
|
||||
|
||||
|
@ -449,10 +468,13 @@ func UpdateIssueByAPI(issue *Issue, doer *user_model.User) (statusChangeComment
|
|||
return nil, false, err
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
|
||||
"name", "content", "milestone_id", "priority",
|
||||
"deadline_unix", "updated_unix", "is_locked").
|
||||
Update(issue); err != nil {
|
||||
sess := db.GetEngine(ctx).ID(issue.ID)
|
||||
cols := []string{"name", "content", "milestone_id", "priority", "deadline_unix", "is_locked"}
|
||||
if issue.NoAutoTime {
|
||||
cols = append(cols, "updated_unix")
|
||||
sess.NoAutoTime()
|
||||
}
|
||||
if _, err := sess.Cols(cols...).Update(issue); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
|
@ -498,7 +520,7 @@ func UpdateIssueDeadline(issue *Issue, deadlineUnix timeutil.TimeStamp, doer *us
|
|||
defer committer.Close()
|
||||
|
||||
// Update the deadline
|
||||
if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix}, "deadline_unix"); err != nil {
|
||||
if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, DeadlineUnix: deadlineUnix, NoAutoTime: issue.NoAutoTime, UpdatedUnix: issue.UpdatedUnix}, "deadline_unix"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -110,6 +110,10 @@ func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossRefe
|
|||
if ctx.OrigComment != nil {
|
||||
refCommentID = ctx.OrigComment.ID
|
||||
}
|
||||
if ctx.OrigIssue.NoAutoTime {
|
||||
xref.Issue.NoAutoTime = true
|
||||
xref.Issue.UpdatedUnix = ctx.OrigIssue.UpdatedUnix
|
||||
}
|
||||
opts := &CreateCommentOptions{
|
||||
Type: ctx.Type,
|
||||
Doer: ctx.Doer,
|
||||
|
|
|
@ -188,10 +188,9 @@ func updateMilestone(ctx context.Context, m *Milestone) error {
|
|||
return UpdateMilestoneCounters(ctx, m.ID)
|
||||
}
|
||||
|
||||
// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
|
||||
func UpdateMilestoneCounters(ctx context.Context, id int64) error {
|
||||
func updateMilestoneCounters(ctx context.Context, id int64, noAutoTime bool, updatedUnix timeutil.TimeStamp) error {
|
||||
e := db.GetEngine(ctx)
|
||||
_, err := e.ID(id).
|
||||
sess := e.ID(id).
|
||||
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
|
||||
builder.Eq{"milestone_id": id},
|
||||
)).
|
||||
|
@ -200,8 +199,11 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error {
|
|||
"milestone_id": id,
|
||||
"is_closed": true,
|
||||
},
|
||||
)).
|
||||
Update(&Milestone{})
|
||||
))
|
||||
if noAutoTime {
|
||||
sess.SetExpr("updated_unix", updatedUnix).NoAutoTime()
|
||||
}
|
||||
_, err := sess.Update(&Milestone{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -211,6 +213,16 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// UpdateMilestoneCounters calculates NumIssues, NumClosesIssues and Completeness
|
||||
func UpdateMilestoneCounters(ctx context.Context, id int64) error {
|
||||
return updateMilestoneCounters(ctx, id, false, 0)
|
||||
}
|
||||
|
||||
// UpdateMilestoneCountersWithDate calculates NumIssues, NumClosesIssues and Completeness and set the UpdatedUnix date
|
||||
func UpdateMilestoneCountersWithDate(ctx context.Context, id int64, updatedUnix timeutil.TimeStamp) error {
|
||||
return updateMilestoneCounters(ctx, id, true, updatedUnix)
|
||||
}
|
||||
|
||||
// ChangeMilestoneStatusByRepoIDAndID changes a milestone open/closed status if the milestone ID is in the repo.
|
||||
func ChangeMilestoneStatusByRepoIDAndID(repoID, milestoneID int64, isClosed bool) error {
|
||||
ctx, committer, err := db.TxContext(db.DefaultContext)
|
||||
|
|
|
@ -28,6 +28,7 @@ type Attachment struct {
|
|||
Name string
|
||||
DownloadCount int64 `xorm:"DEFAULT 0"`
|
||||
Size int64 `xorm:"DEFAULT 0"`
|
||||
NoAutoTime bool `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
CustomDownloadURL string `xorm:"-"`
|
||||
}
|
||||
|
|
|
@ -110,6 +110,8 @@ type EditIssueOption struct {
|
|||
// swagger:strfmt date-time
|
||||
Deadline *time.Time `json:"due_date"`
|
||||
RemoveDeadline *bool `json:"unset_due_date"`
|
||||
// swagger:strfmt date-time
|
||||
Updated *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// EditDeadlineOption options for creating a deadline
|
||||
|
|
|
@ -28,12 +28,16 @@ type Comment struct {
|
|||
type CreateIssueCommentOption struct {
|
||||
// required:true
|
||||
Body string `json:"body" binding:"Required"`
|
||||
// swagger:strfmt date-time
|
||||
Updated *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// EditIssueCommentOption options for editing a comment
|
||||
type EditIssueCommentOption struct {
|
||||
// required: true
|
||||
Body string `json:"body" binding:"Required"`
|
||||
// swagger:strfmt date-time
|
||||
Updated *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TimelineComment represents a timeline comment (comment of any type) on a commit or issue
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Label a label to an issue or a pr
|
||||
// swagger:model
|
||||
type Label struct {
|
||||
|
@ -45,10 +49,18 @@ type EditLabelOption struct {
|
|||
IsArchived *bool `json:"is_archived"`
|
||||
}
|
||||
|
||||
// DeleteLabelOption options for deleting a label
|
||||
type DeleteLabelsOption struct {
|
||||
// swagger:strfmt date-time
|
||||
Updated *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// IssueLabelsOption a collection of labels
|
||||
type IssueLabelsOption struct {
|
||||
// list of label IDs
|
||||
Labels []int64 `json:"labels"`
|
||||
// swagger:strfmt date-time
|
||||
Updated *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// LabelTemplate info of a Label template
|
||||
|
|
|
@ -1201,8 +1201,8 @@ func Routes() *web.Route {
|
|||
m.Combo("").Get(repo.ListIssueLabels).
|
||||
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
|
||||
Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
|
||||
Delete(reqToken(), repo.ClearIssueLabels)
|
||||
m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
|
||||
Delete(reqToken(), bind(api.DeleteLabelsOption{}), repo.ClearIssueLabels)
|
||||
m.Delete("/{id}", reqToken(), bind(api.DeleteLabelsOption{}), repo.DeleteIssueLabel)
|
||||
})
|
||||
m.Group("/times", func() {
|
||||
m.Combo("").
|
||||
|
|
|
@ -774,6 +774,12 @@ func EditIssue(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return
|
||||
}
|
||||
|
||||
oldTitle := issue.Title
|
||||
if len(form.Title) > 0 {
|
||||
issue.Title = form.Title
|
||||
|
|
|
@ -5,6 +5,7 @@ package repo
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
@ -141,6 +142,11 @@ func CreateIssueAttachment(ctx *context.APIContext) {
|
|||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: updated_at
|
||||
// in: query
|
||||
// description: time of the attachment's creation. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
|
@ -163,6 +169,20 @@ func CreateIssueAttachment(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
updatedAt := ctx.Req.FormValue("updated_at")
|
||||
if len(updatedAt) != 0 {
|
||||
updated, err := time.Parse(time.RFC3339, updatedAt)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "time.Parse", err)
|
||||
return
|
||||
}
|
||||
err = issue_service.SetIssueUpdateDate(ctx, issue, &updated, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get uploaded file from request
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
|
@ -177,10 +197,12 @@ func CreateIssueAttachment(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IssueID: issue.ID,
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IssueID: issue.ID,
|
||||
NoAutoTime: issue.NoAutoTime,
|
||||
CreatedUnix: issue.UpdatedUnix,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
|
||||
|
|
|
@ -362,6 +362,12 @@ func CreateIssueComment(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
|
@ -554,6 +560,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
|||
return
|
||||
}
|
||||
|
||||
err = comment.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
err = issue_service.SetIssueUpdateDate(ctx, comment.Issue, form.Updated, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return
|
||||
}
|
||||
|
||||
oldContent := comment.Content
|
||||
comment.Content = form.Body
|
||||
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
||||
|
|
|
@ -5,6 +5,7 @@ package repo
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
@ -144,6 +145,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
|||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// - name: updated_at
|
||||
// in: query
|
||||
// description: time of the attachment's creation. This is a timestamp in RFC 3339 format
|
||||
// type: string
|
||||
// format: date-time
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
|
@ -167,6 +173,25 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
updatedAt := ctx.Req.FormValue("updated_at")
|
||||
if len(updatedAt) != 0 {
|
||||
updated, err := time.Parse(time.RFC3339, updatedAt)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "time.Parse", err)
|
||||
return
|
||||
}
|
||||
err = comment.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
err = issue_service.SetIssueUpdateDate(ctx, comment.Issue, &updated, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get uploaded file from request
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
|
@ -181,11 +206,13 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
attachment, err := attachment.UploadAttachment(file, setting.Attachment.AllowedTypes, header.Size, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IssueID: comment.IssueID,
|
||||
CommentID: comment.ID,
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IssueID: comment.IssueID,
|
||||
CommentID: comment.ID,
|
||||
NoAutoTime: comment.Issue.NoAutoTime,
|
||||
CreatedUnix: comment.Issue.UpdatedUnix,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UploadAttachment", err)
|
||||
|
|
|
@ -149,6 +149,10 @@ func DeleteIssueLabel(ctx *context.APIContext) {
|
|||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/DeleteLabelsOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
@ -156,6 +160,7 @@ func DeleteIssueLabel(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
form := web.GetForm(ctx).(*api.DeleteLabelsOption)
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
|
@ -172,6 +177,11 @@ func DeleteIssueLabel(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return
|
||||
}
|
||||
|
||||
label, err := issues_model.GetLabelByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrLabelNotExist(err) {
|
||||
|
@ -269,11 +279,16 @@ func ClearIssueLabels(ctx *context.APIContext) {
|
|||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/DeleteLabelsOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
form := web.GetForm(ctx).(*api.DeleteLabelsOption)
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
|
@ -290,6 +305,11 @@ func ClearIssueLabels(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issue_service.ClearLabels(issue, ctx.Doer); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ClearLabels", err)
|
||||
return
|
||||
|
@ -320,5 +340,11 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
|||
return nil, nil, nil
|
||||
}
|
||||
|
||||
err = issue_service.SetIssueUpdateDate(ctx, issue, form.Updated, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "SetIssueUpdateDate", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return issue, labels, err
|
||||
}
|
||||
|
|
|
@ -47,6 +47,9 @@ type swaggerParameterBodies struct {
|
|||
// in:body
|
||||
IssueLabelsOption api.IssueLabelsOption
|
||||
|
||||
// in:body
|
||||
DeleteLabelsOption api.DeleteLabelsOption
|
||||
|
||||
// in:body
|
||||
CreateKeyOption api.CreateKeyOption
|
||||
|
||||
|
|
|
@ -32,7 +32,12 @@ func NewAttachment(attach *repo_model.Attachment, file io.Reader, size int64) (*
|
|||
}
|
||||
attach.Size = size
|
||||
|
||||
return db.Insert(ctx, attach)
|
||||
eng := db.GetEngine(ctx)
|
||||
if attach.NoAutoTime {
|
||||
eng.NoAutoTime()
|
||||
}
|
||||
_, err = eng.Insert(attach)
|
||||
return err
|
||||
})
|
||||
|
||||
return attach, err
|
||||
|
|
|
@ -89,7 +89,11 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, doer *user_mode
|
|||
}
|
||||
|
||||
if needsContentHistory {
|
||||
err := issues_model.SaveIssueContentHistory(ctx, doer.ID, c.IssueID, c.ID, timeutil.TimeStampNow(), c.Content, false)
|
||||
historyDate := timeutil.TimeStampNow()
|
||||
if c.Issue.NoAutoTime {
|
||||
historyDate = c.Issue.UpdatedUnix
|
||||
}
|
||||
err := issues_model.SaveIssueContentHistory(ctx, doer.ID, c.IssueID, c.ID, historyDate, c.Content, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ package issue
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/notification"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// NewIssue creates new issue with labels for repository.
|
||||
|
@ -304,3 +306,40 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error {
|
|||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// Set the UpdatedUnix date and the NoAutoTime field of an Issue if a non
|
||||
// nil 'updated' time is provided
|
||||
//
|
||||
// In order to set a specific update time, the DB will be updated with
|
||||
// NoAutoTime(). A 'NoAutoTime' boolean field in the Issue struct is used to
|
||||
// propagate down to the DB update calls the will to apply autoupdate or not.
|
||||
func SetIssueUpdateDate(ctx context.Context, issue *issues_model.Issue, updated *time.Time, doer *user_model.User) error {
|
||||
issue.NoAutoTime = false
|
||||
if updated == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the poster is allowed to set an update date
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !perm.IsAdmin() && !perm.IsOwner() {
|
||||
return fmt.Errorf("user needs to have admin or owner right")
|
||||
}
|
||||
|
||||
// A simple guard against potential inconsistent calls
|
||||
updatedUnix := timeutil.TimeStamp(updated.Unix())
|
||||
if updatedUnix < issue.CreatedUnix || updatedUnix > timeutil.TimeStampNow() {
|
||||
return fmt.Errorf("unallowed update date")
|
||||
}
|
||||
|
||||
issue.UpdatedUnix = updatedUnix
|
||||
issue.NoAutoTime = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -13,6 +13,32 @@ import (
|
|||
"code.gitea.io/gitea/modules/notification"
|
||||
)
|
||||
|
||||
func updateMilestoneCounters(ctx context.Context, issue *issues_model.Issue, id int64) error {
|
||||
if issue.NoAutoTime {
|
||||
// We set the milestone's update date to the max of the
|
||||
// milestone and issue update dates.
|
||||
// Note: we can not call UpdateMilestoneCounters() if the
|
||||
// milestone's update date is to be kept, because that function
|
||||
// auto-updates the dates.
|
||||
milestone, err := issues_model.GetMilestoneByRepoID(ctx, issue.RepoID, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetMilestoneByRepoID: %w", err)
|
||||
}
|
||||
updatedUnix := milestone.UpdatedUnix
|
||||
if issue.UpdatedUnix > updatedUnix {
|
||||
updatedUnix = issue.UpdatedUnix
|
||||
}
|
||||
if err := issues_model.UpdateMilestoneCountersWithDate(ctx, id, updatedUnix); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := issues_model.UpdateMilestoneCounters(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) error {
|
||||
// Only check if milestone exists if we don't remove it.
|
||||
if issue.MilestoneID > 0 {
|
||||
|
@ -30,13 +56,13 @@ func changeMilestoneAssign(ctx context.Context, doer *user_model.User, issue *is
|
|||
}
|
||||
|
||||
if oldMilestoneID > 0 {
|
||||
if err := issues_model.UpdateMilestoneCounters(ctx, oldMilestoneID); err != nil {
|
||||
if err := updateMilestoneCounters(ctx, issue, oldMilestoneID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if issue.MilestoneID > 0 {
|
||||
if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil {
|
||||
if err := updateMilestoneCounters(ctx, issue, issue.MilestoneID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
60
templates/swagger/v1_json.tmpl
generated
60
templates/swagger/v1_json.tmpl
generated
|
@ -6120,6 +6120,13 @@
|
|||
"name": "name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "time of the attachment's creation. This is a timestamp in RFC 3339 format",
|
||||
"name": "updated_at",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"description": "attachment to upload",
|
||||
|
@ -6718,6 +6725,13 @@
|
|||
"name": "name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "time of the attachment's creation. This is a timestamp in RFC 3339 format",
|
||||
"name": "updated_at",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"description": "attachment to upload",
|
||||
|
@ -7651,6 +7665,13 @@
|
|||
"name": "index",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeleteLabelsOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -7703,6 +7724,13 @@
|
|||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeleteLabelsOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -17026,6 +17054,11 @@
|
|||
"body": {
|
||||
"type": "string",
|
||||
"x-go-name": "Body"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
@ -17784,6 +17817,18 @@
|
|||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"DeleteLabelsOption": {
|
||||
"description": "DeleteLabelOption options for deleting a label",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"DeployKey": {
|
||||
"description": "DeployKey a deploy key",
|
||||
"type": "object",
|
||||
|
@ -18037,6 +18082,11 @@
|
|||
"body": {
|
||||
"type": "string",
|
||||
"x-go-name": "Body"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
@ -18086,6 +18136,11 @@
|
|||
"unset_due_date": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "RemoveDeadline"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
@ -19500,6 +19555,11 @@
|
|||
"format": "int64"
|
||||
},
|
||||
"x-go-name": "Labels"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"x-go-name": "Updated"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -111,6 +112,82 @@ func TestAPICreateCommentAttachment(t *testing.T) {
|
|||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
|
||||
}
|
||||
|
||||
func TestAPICreateCommentAttachmentAutoDate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
|
||||
filename := "image.png"
|
||||
buff := generateImg()
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
t.Run("WithAutoDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
_, err = io.Copy(part, &buff)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", urlStr, body)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
apiAttachment := new(api.Attachment)
|
||||
DecodeJSON(t, resp, &apiAttachment)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID})
|
||||
// the execution of the API call supposedly lasted less than one minute
|
||||
updatedSince := time.Since(apiAttachment.Created)
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
|
||||
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
|
||||
updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
})
|
||||
|
||||
t.Run("WithUpdateDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
urlStr += fmt.Sprintf("&updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
|
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
_, err = io.Copy(part, &buff)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", urlStr, body)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
apiAttachment := new(api.Attachment)
|
||||
DecodeJSON(t, resp, &apiAttachment)
|
||||
|
||||
// dates will be converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), apiAttachment.Created.In(utcTZ))
|
||||
|
||||
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIEditCommentAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -110,6 +111,58 @@ func TestAPICreateComment(t *testing.T) {
|
|||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
|
||||
}
|
||||
|
||||
func TestAPICreateCommentAutoDate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
|
||||
repoOwner.Name, repo.Name, issue.Index, token)
|
||||
const commentBody = "Comment body"
|
||||
|
||||
t.Run("WithAutoDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
"body": commentBody,
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var updatedComment api.Comment
|
||||
DecodeJSON(t, resp, &updatedComment)
|
||||
|
||||
// the execution of the API call supposedly lasted less than one minute
|
||||
updatedSince := time.Since(updatedComment.Updated)
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
|
||||
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
|
||||
updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
})
|
||||
|
||||
t.Run("WithUpdateDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueCommentOption{
|
||||
Body: commentBody,
|
||||
Updated: &updatedAt,
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var updatedComment api.Comment
|
||||
DecodeJSON(t, resp, &updatedComment)
|
||||
|
||||
// dates will be converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
assert.Equal(t, updatedAt.In(utcTZ), updatedComment.Updated.In(utcTZ))
|
||||
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIGetComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
@ -161,6 +214,60 @@ func TestAPIEditComment(t *testing.T) {
|
|||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
|
||||
}
|
||||
|
||||
func TestAPIEditCommentWithDate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
|
||||
repoOwner.Name, repo.Name, comment.ID, token)
|
||||
const newCommentBody = "This is the new comment body"
|
||||
|
||||
t.Run("WithAutoDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"body": newCommentBody,
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var updatedComment api.Comment
|
||||
DecodeJSON(t, resp, &updatedComment)
|
||||
|
||||
// the execution of the API call supposedly lasted less than one minute
|
||||
updatedSince := time.Since(updatedComment.Updated)
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
|
||||
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
|
||||
updatedSince = time.Since(commentAfter.UpdatedUnix.AsTime())
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
})
|
||||
|
||||
t.Run("WithUpdateDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditIssueCommentOption{
|
||||
Body: newCommentBody,
|
||||
Updated: &updatedAt,
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var updatedComment api.Comment
|
||||
DecodeJSON(t, resp, &updatedComment)
|
||||
|
||||
// dates will be converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
assert.Equal(t, updatedAt.In(utcTZ), updatedComment.Updated.In(utcTZ))
|
||||
commentAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), commentAfter.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIDeleteComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
|
@ -100,6 +101,82 @@ func TestAPICreateIssueAttachment(t *testing.T) {
|
|||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
|
||||
}
|
||||
|
||||
func TestAPICreateIssueAttachmentAutoDate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
|
||||
repoOwner.Name, repo.Name, issue.Index, token)
|
||||
|
||||
filename := "image.png"
|
||||
buff := generateImg()
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
t.Run("WithAutoDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
_, err = io.Copy(part, &buff)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", urlStr, body)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
apiAttachment := new(api.Attachment)
|
||||
DecodeJSON(t, resp, &apiAttachment)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
|
||||
// the execution of the API call supposedly lasted less than one minute
|
||||
updatedSince := time.Since(apiAttachment.Created)
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.Index})
|
||||
updatedSince = time.Since(issueAfter.UpdatedUnix.AsTime())
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
})
|
||||
|
||||
t.Run("WithUpdateDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
urlStr += fmt.Sprintf("&updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
|
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
_, err = io.Copy(part, &buff)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", urlStr, body)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
apiAttachment := new(api.Attachment)
|
||||
DecodeJSON(t, resp, &apiAttachment)
|
||||
|
||||
// dates will be converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), apiAttachment.Created.In(utcTZ))
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIEditIssueAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
|
@ -15,6 +16,7 @@ import (
|
|||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -111,6 +113,49 @@ func TestAPIAddIssueLabels(t *testing.T) {
|
|||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2})
|
||||
}
|
||||
|
||||
func TestAPIAddIssueLabelsAutoDate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
|
||||
owner.Name, repo.Name, issueBefore.Index, token)
|
||||
|
||||
t.Run("WithAutoDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
|
||||
Labels: []int64{1},
|
||||
})
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
|
||||
// the execution of the API call supposedly lasted less than one minute
|
||||
updatedSince := time.Since(issueAfter.UpdatedUnix.AsTime())
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
})
|
||||
|
||||
t.Run("WithUpdatedDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
|
||||
Labels: []int64{2},
|
||||
Updated: &updatedAt,
|
||||
})
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// dates will be converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIReplaceIssueLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
|
|
|
@ -213,6 +213,157 @@ func TestAPIEditIssue(t *testing.T) {
|
|||
assert.Equal(t, title, issueAfter.Title)
|
||||
}
|
||||
|
||||
func TestAPIEditIssueAutoDate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
|
||||
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
||||
assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
|
||||
|
||||
t.Run("WithAutoDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// User2 is not owner, but can update the 'public' issue with auto date
|
||||
session := loginUser(t, "user2")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
|
||||
|
||||
body := "new content!"
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Body: &body,
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var apiIssue api.Issue
|
||||
DecodeJSON(t, resp, &apiIssue)
|
||||
|
||||
// the execution of the API call supposedly lasted less than one minute
|
||||
updatedSince := time.Since(apiIssue.Updated)
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
|
||||
updatedSince = time.Since(issueAfter.UpdatedUnix.AsTime())
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
})
|
||||
|
||||
t.Run("WithUpdateDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// User1 is admin, and so can update the issue without auto date
|
||||
session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
|
||||
|
||||
body := "new content, with updated time"
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Body: &body,
|
||||
Updated: &updatedAt,
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var apiIssue api.Issue
|
||||
DecodeJSON(t, resp, &apiIssue)
|
||||
|
||||
// dates are converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
assert.Equal(t, updatedAt.In(utcTZ), apiIssue.Updated.In(utcTZ))
|
||||
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
|
||||
t.Run("WithoutPermission", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// User2 is not owner nor admin, and so can't update the issue without auto date
|
||||
session := loginUser(t, "user2")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
|
||||
|
||||
body := "new content, with updated time"
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Body: &body,
|
||||
Updated: &updatedAt,
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusForbidden)
|
||||
var apiError api.APIError
|
||||
DecodeJSON(t, resp, &apiError)
|
||||
|
||||
assert.Equal(t, "user needs to have admin or owner right", apiError.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIEditIssueMilestoneAutoDate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
||||
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
||||
assert.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d?token=%s", owner.Name, repoBefore.Name, issueBefore.Index, token)
|
||||
|
||||
t.Run("WithAutoDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
milestone := int64(1)
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Milestone: &milestone,
|
||||
})
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// the execution of the API call supposedly lasted less than one minute
|
||||
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
||||
updatedSince := time.Since(milestoneAfter.UpdatedUnix.AsTime())
|
||||
assert.LessOrEqual(t, updatedSince, time.Minute)
|
||||
})
|
||||
|
||||
t.Run("WithPostUpdateDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Note: the updated_unix field of the test Milestones is set to NULL
|
||||
// Hence, any date is higher than the Milestone's updated date
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
milestone := int64(2)
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Milestone: &milestone,
|
||||
Updated: &updatedAt,
|
||||
})
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// the milestone date should be set to 'updatedAt'
|
||||
// dates are converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
||||
assert.Equal(t, updatedAt.In(utcTZ), milestoneAfter.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
|
||||
t.Run("WithPastUpdateDate", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Note: This Milestone's updated_unix has been set to Now() by the first subtest
|
||||
milestone := int64(1)
|
||||
milestoneBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
||||
|
||||
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Milestone: &milestone,
|
||||
Updated: &updatedAt,
|
||||
})
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// the milestone date should not change
|
||||
// dates are converted into the same tz, in order to compare them
|
||||
utcTZ, _ := time.LoadLocation("UTC")
|
||||
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
||||
assert.Equal(t, milestoneAfter.UpdatedUnix.AsTime().In(utcTZ), milestoneBefore.UpdatedUnix.AsTime().In(utcTZ))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPISearchIssues(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
|
Loading…
Reference in a new issue