Implement "conversation lock" for issue comments (#5073)
This commit is contained in:
parent
64ce159a6e
commit
44114b38e6
19 changed files with 435 additions and 4 deletions
|
@ -69,6 +69,10 @@ MAX_FILES = 5
|
||||||
; List of prefixes used in Pull Request title to mark them as Work In Progress
|
; List of prefixes used in Pull Request title to mark them as Work In Progress
|
||||||
WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
|
WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
|
||||||
|
|
||||||
|
[repository.issue]
|
||||||
|
; List of reasons why a Pull Request or Issue can be locked
|
||||||
|
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam
|
||||||
|
|
||||||
[ui]
|
[ui]
|
||||||
; Number of repositories that are displayed on one explore page
|
; Number of repositories that are displayed on one explore page
|
||||||
EXPLORE_PAGING_NUM = 20
|
EXPLORE_PAGING_NUM = 20
|
||||||
|
|
|
@ -71,6 +71,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request
|
- `WORK_IN_PROGRESS_PREFIXES`: **WIP:,\[WIP\]**: List of prefixes used in Pull Request
|
||||||
title to mark them as Work In Progress
|
title to mark them as Work In Progress
|
||||||
|
|
||||||
|
### Repository - Issue (`repository.issue`)
|
||||||
|
- `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked
|
||||||
|
|
||||||
## UI (`ui`)
|
## UI (`ui`)
|
||||||
|
|
||||||
- `EXPLORE_PAGING_NUM`: **20**: Number of repositories that are shown in one explore page.
|
- `EXPLORE_PAGING_NUM`: **20**: Number of repositories that are shown in one explore page.
|
||||||
|
|
|
@ -81,7 +81,7 @@ _Symbols used in table:_
|
||||||
| Related issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ |
|
| Related issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ |
|
||||||
| Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
| Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Lock Discussion | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Lock Discussion | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Issue Boards | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
| Issue Boards | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
||||||
| Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
| Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ |
|
||||||
|
|
|
@ -57,6 +57,10 @@ type Issue struct {
|
||||||
Reactions ReactionList `xorm:"-"`
|
Reactions ReactionList `xorm:"-"`
|
||||||
TotalTrackedTime int64 `xorm:"-"`
|
TotalTrackedTime int64 `xorm:"-"`
|
||||||
Assignees []*User `xorm:"-"`
|
Assignees []*User `xorm:"-"`
|
||||||
|
|
||||||
|
// IsLocked limits commenting abilities to users on an issue
|
||||||
|
// with write access
|
||||||
|
IsLocked bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -80,6 +80,10 @@ const (
|
||||||
CommentTypeCode
|
CommentTypeCode
|
||||||
// Reviews a pull request by giving general feedback
|
// Reviews a pull request by giving general feedback
|
||||||
CommentTypeReview
|
CommentTypeReview
|
||||||
|
// Lock an issue, giving only collaborators access
|
||||||
|
CommentTypeLock
|
||||||
|
// Unlocks a previously locked issue
|
||||||
|
CommentTypeUnlock
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentTag defines comment tag type
|
// CommentTag defines comment tag type
|
||||||
|
|
51
models/issue_lock.go
Normal file
51
models/issue_lock.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package models
|
||||||
|
|
||||||
|
// IssueLockOptions defines options for locking and/or unlocking an issue/PR
|
||||||
|
type IssueLockOptions struct {
|
||||||
|
Doer *User
|
||||||
|
Issue *Issue
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockIssue locks an issue. This would limit commenting abilities to
|
||||||
|
// users with write access to the repo
|
||||||
|
func LockIssue(opts *IssueLockOptions) error {
|
||||||
|
return updateIssueLock(opts, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockIssue unlocks a previously locked issue.
|
||||||
|
func UnlockIssue(opts *IssueLockOptions) error {
|
||||||
|
return updateIssueLock(opts, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIssueLock(opts *IssueLockOptions, lock bool) error {
|
||||||
|
if opts.Issue.IsLocked == lock {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Issue.IsLocked = lock
|
||||||
|
|
||||||
|
var commentType CommentType
|
||||||
|
if opts.Issue.IsLocked {
|
||||||
|
commentType = CommentTypeLock
|
||||||
|
} else {
|
||||||
|
commentType = CommentTypeUnlock
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := UpdateIssueCols(opts.Issue, "is_locked"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := CreateComment(&CreateCommentOptions{
|
||||||
|
Doer: opts.Doer,
|
||||||
|
Issue: opts.Issue,
|
||||||
|
Repo: opts.Issue.Repo,
|
||||||
|
Type: commentType,
|
||||||
|
Content: opts.Reason,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
|
@ -213,6 +213,8 @@ var migrations = []Migration{
|
||||||
NewMigration("rename repo is_bare to repo is_empty", renameRepoIsBareToIsEmpty),
|
NewMigration("rename repo is_bare to repo is_empty", renameRepoIsBareToIsEmpty),
|
||||||
// v79 -> v80
|
// v79 -> v80
|
||||||
NewMigration("add can close issues via commit in any branch", addCanCloseIssuesViaCommitInAnyBranch),
|
NewMigration("add can close issues via commit in any branch", addCanCloseIssuesViaCommitInAnyBranch),
|
||||||
|
// v80 -> v81
|
||||||
|
NewMigration("add is locked to issues", addIsLockedToIssues),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate database to current version
|
// Migrate database to current version
|
||||||
|
|
18
models/migrations/v80.go
Normal file
18
models/migrations/v80.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import "github.com/go-xorm/xorm"
|
||||||
|
|
||||||
|
func addIsLockedToIssues(x *xorm.Engine) error {
|
||||||
|
// Issue see models/issue.go
|
||||||
|
type Issue struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
IsLocked bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync2(new(Issue))
|
||||||
|
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/routers/utils"
|
"code.gitea.io/gitea/routers/utils"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
|
@ -308,6 +309,32 @@ func (f *ReactionForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi
|
||||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IssueLockForm form for locking an issue
|
||||||
|
type IssueLockForm struct {
|
||||||
|
Reason string `binding:"Required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the fields
|
||||||
|
func (i *IssueLockForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||||
|
return validate(errs, ctx.Data, i, ctx.Locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasValidReason checks to make sure that the reason submitted in
|
||||||
|
// the form matches any of the values in the config
|
||||||
|
func (i IssueLockForm) HasValidReason() bool {
|
||||||
|
if strings.TrimSpace(i.Reason) == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range setting.Repository.Issue.LockReasons {
|
||||||
|
if v == i.Reason {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// _____ .__.__ __
|
// _____ .__.__ __
|
||||||
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
||||||
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
||||||
|
|
|
@ -7,6 +7,7 @@ package auth
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,3 +40,27 @@ func TestSubmitReviewForm_IsEmpty(t *testing.T) {
|
||||||
assert.Equal(t, v.expected, v.form.HasEmptyContent())
|
assert.Equal(t, v.expected, v.form.HasEmptyContent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIssueLock_HasValidReason(t *testing.T) {
|
||||||
|
|
||||||
|
// Init settings
|
||||||
|
_ = setting.Repository
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
form IssueLockForm
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{IssueLockForm{""}, true}, // an empty reason is accepted
|
||||||
|
{IssueLockForm{"Off-topic"}, true},
|
||||||
|
{IssueLockForm{"Too heated"}, true},
|
||||||
|
{IssueLockForm{"Spam"}, true},
|
||||||
|
{IssueLockForm{"Resolved"}, true},
|
||||||
|
|
||||||
|
{IssueLockForm{"ZZZZ"}, false},
|
||||||
|
{IssueLockForm{"I want to lock this issue"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range cases {
|
||||||
|
assert.Equal(t, v.expected, v.form.HasValidReason())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -227,6 +227,11 @@ var (
|
||||||
PullRequest struct {
|
PullRequest struct {
|
||||||
WorkInProgressPrefixes []string
|
WorkInProgressPrefixes []string
|
||||||
} `ini:"repository.pull-request"`
|
} `ini:"repository.pull-request"`
|
||||||
|
|
||||||
|
// Issue Setting
|
||||||
|
Issue struct {
|
||||||
|
LockReasons []string
|
||||||
|
} `ini:"repository.issue"`
|
||||||
}{
|
}{
|
||||||
AnsiCharset: "",
|
AnsiCharset: "",
|
||||||
ForcePrivate: false,
|
ForcePrivate: false,
|
||||||
|
@ -279,6 +284,13 @@ var (
|
||||||
}{
|
}{
|
||||||
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Issue settings
|
||||||
|
Issue: struct {
|
||||||
|
LockReasons []string
|
||||||
|
}{
|
||||||
|
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
RepoRootPath string
|
RepoRootPath string
|
||||||
ScriptType = "bash"
|
ScriptType = "bash"
|
||||||
|
|
|
@ -780,6 +780,25 @@ issues.attachment.open_tab = `Click to see "%s" in a new tab`
|
||||||
issues.attachment.download = `Click to download "%s"`
|
issues.attachment.download = `Click to download "%s"`
|
||||||
issues.subscribe = Subscribe
|
issues.subscribe = Subscribe
|
||||||
issues.unsubscribe = Unsubscribe
|
issues.unsubscribe = Unsubscribe
|
||||||
|
issues.lock = Lock conversation
|
||||||
|
issues.unlock = Unlock conversation
|
||||||
|
issues.lock.unknown_reason = Cannot lock an issue with an unknown reason.
|
||||||
|
issues.lock_duplicate = An issue cannot be locked twice.
|
||||||
|
issues.unlock_error = Cannot unlock an issue that is not locked.
|
||||||
|
issues.lock_with_reason = "locked as <strong>%s</strong> and limited conversation to collaborators %s"
|
||||||
|
issues.lock_no_reason = "locked and limited conversation to collaborators %s"
|
||||||
|
issues.unlock_comment = "unlocked this conversation %s"
|
||||||
|
issues.lock_confirm = Lock
|
||||||
|
issues.unlock_confirm = Unlock
|
||||||
|
issues.lock.notice_1 = - Other users can’t add new comments to this issue.
|
||||||
|
issues.lock.notice_2 = - You and other collaborators with access to this repository can still leave comments that others can see.
|
||||||
|
issues.lock.notice_3 = - You can always unlock this issue again in the future.
|
||||||
|
issues.unlock.notice_1 = - Everyone would be able to comment on this issue once more.
|
||||||
|
issues.unlock.notice_2 = - You can always lock this issue again in the future.
|
||||||
|
issues.lock.reason = Reason for locking
|
||||||
|
issues.lock.title = Lock conversation on this issue.
|
||||||
|
issues.unlock.title = Unlock conversation on this issue.
|
||||||
|
issues.comment_on_locked = You cannot comment on a locked issue.
|
||||||
issues.tracker = Time Tracker
|
issues.tracker = Time Tracker
|
||||||
issues.start_tracking_short = Start
|
issues.start_tracking_short = Start
|
||||||
issues.start_tracking = Start Time Tracking
|
issues.start_tracking = Start Time Tracking
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
@ -169,6 +170,11 @@ func CreateIssueComment(ctx *context.APIContext, form api.CreateIssueCommentOpti
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
|
||||||
|
ctx.Error(403, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Body, nil)
|
comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Body, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(500, "CreateIssueComment", err)
|
ctx.Error(500, "CreateIssueComment", err)
|
||||||
|
|
|
@ -57,6 +57,23 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MustAllowUserComment checks to make sure if an issue is locked.
|
||||||
|
// If locked and user has permissions to write to the repository,
|
||||||
|
// then the comment is allowed, else it is blocked
|
||||||
|
func MustAllowUserComment(ctx *context.Context) {
|
||||||
|
|
||||||
|
issue := GetActionIssue(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
|
||||||
|
ctx.Redirect(issue.HTMLURL())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MustEnableIssues check if repository enable internal issues
|
// MustEnableIssues check if repository enable internal issues
|
||||||
func MustEnableIssues(ctx *context.Context) {
|
func MustEnableIssues(ctx *context.Context) {
|
||||||
if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
|
if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
|
||||||
|
@ -898,6 +915,9 @@ func ViewIssue(ctx *context.Context) {
|
||||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
|
||||||
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
|
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
|
||||||
ctx.Data["IsIssueWriter"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
|
ctx.Data["IsIssueWriter"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
|
||||||
|
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.User.IsAdmin)
|
||||||
|
ctx.Data["IsRepoIssuesWriter"] = ctx.IsSigned && (ctx.Repo.CanWrite(models.UnitTypeIssues) || ctx.User.IsAdmin)
|
||||||
|
ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
|
||||||
ctx.HTML(200, tplIssueView)
|
ctx.HTML(200, tplIssueView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1118,6 +1138,11 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
|
if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
|
||||||
ctx.Error(403)
|
ctx.Error(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.IsLocked && !ctx.Repo.CanWrite(models.UnitTypeIssues) && !ctx.User.IsAdmin {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
|
||||||
|
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
71
routers/repo/issue_lock.go
Normal file
71
routers/repo/issue_lock.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/auth"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LockIssue locks an issue. This would limit commenting abilities to
|
||||||
|
// users with write access to the repo.
|
||||||
|
func LockIssue(ctx *context.Context, form auth.IssueLockForm) {
|
||||||
|
|
||||||
|
issue := GetActionIssue(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.IsLocked {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate"))
|
||||||
|
ctx.Redirect(issue.HTMLURL())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !form.HasValidReason() {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason"))
|
||||||
|
ctx.Redirect(issue.HTMLURL())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.LockIssue(&models.IssueLockOptions{
|
||||||
|
Doer: ctx.User,
|
||||||
|
Issue: issue,
|
||||||
|
Reason: form.Reason,
|
||||||
|
}); err != nil {
|
||||||
|
ctx.ServerError("LockIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnlockIssue unlocks a previously locked issue.
|
||||||
|
func UnlockIssue(ctx *context.Context) {
|
||||||
|
|
||||||
|
issue := GetActionIssue(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !issue.IsLocked {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error"))
|
||||||
|
ctx.Redirect(issue.HTMLURL())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.UnlockIssue(&models.IssueLockOptions{
|
||||||
|
Doer: ctx.User,
|
||||||
|
Issue: issue,
|
||||||
|
}); err != nil {
|
||||||
|
ctx.ServerError("UnlockIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(issue.HTMLURL(), http.StatusSeeOther)
|
||||||
|
}
|
|
@ -432,6 +432,13 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests)
|
reqRepoIssuesOrPullsWriter := context.RequireRepoWriterOr(models.UnitTypeIssues, models.UnitTypePullRequests)
|
||||||
reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests)
|
reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests)
|
||||||
|
|
||||||
|
reqRepoIssueWriter := func(ctx *context.Context) {
|
||||||
|
if !ctx.Repo.CanWrite(models.UnitTypeIssues) {
|
||||||
|
ctx.Error(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ***** START: Organization *****
|
// ***** START: Organization *****
|
||||||
m.Group("/org", func() {
|
m.Group("/org", func() {
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
|
@ -574,7 +581,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Post("/add", repo.AddDependency)
|
m.Post("/add", repo.AddDependency)
|
||||||
m.Post("/delete", repo.RemoveDependency)
|
m.Post("/delete", repo.RemoveDependency)
|
||||||
})
|
})
|
||||||
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
|
m.Combo("/comments").Post(repo.MustAllowUserComment, bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
|
||||||
m.Group("/times", func() {
|
m.Group("/times", func() {
|
||||||
m.Post("/add", bindIgnErr(auth.AddTimeManuallyForm{}), repo.AddTimeManually)
|
m.Post("/add", bindIgnErr(auth.AddTimeManuallyForm{}), repo.AddTimeManually)
|
||||||
m.Group("/stopwatch", func() {
|
m.Group("/stopwatch", func() {
|
||||||
|
@ -583,6 +590,8 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction)
|
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction)
|
||||||
|
m.Post("/lock", reqRepoIssueWriter, bindIgnErr(auth.IssueLockForm{}), repo.LockIssue)
|
||||||
|
m.Post("/unlock", reqRepoIssueWriter, repo.UnlockIssue)
|
||||||
}, context.RepoMustNotBeArchived())
|
}, context.RepoMustNotBeArchived())
|
||||||
|
|
||||||
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
|
m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel)
|
||||||
|
|
|
@ -69,7 +69,38 @@
|
||||||
{{if and .Issue.IsPull (not $.Repository.IsArchived)}}
|
{{if and .Issue.IsPull (not $.Repository.IsArchived)}}
|
||||||
{{ template "repo/issue/view_content/pull". }}
|
{{ template "repo/issue/view_content/pull". }}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .IsSigned}}
|
||||||
|
{{ if or .IsRepoAdmin .IsRepoIssuesWriter (or (not .Issue.IsLocked)) }}
|
||||||
|
<div class="comment form">
|
||||||
|
<a class="avatar" href="{{.SignedUser.HomeLink}}">
|
||||||
|
<img src="{{.SignedUser.RelAvatarLink}}">
|
||||||
|
</a>
|
||||||
|
<div class="content">
|
||||||
|
<form class="ui segment form" id="comment-form" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/comments" method="post">
|
||||||
|
{{template "repo/issue/comment_tab" .}}
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<input id="status" name="status" type="hidden">
|
||||||
|
<div class="text right">
|
||||||
|
{{if and (or .IsIssueWriter .IsIssuePoster) (not .DisableStatusChange)}}
|
||||||
|
{{if .Issue.IsClosed}}
|
||||||
|
<div id="status-button" class="ui green basic button" tabindex="6" data-status="{{.i18n.Tr "repo.issues.reopen_issue"}}" data-status-and-comment="{{.i18n.Tr "repo.issues.reopen_comment_issue"}}" data-status-val="reopen">
|
||||||
|
{{.i18n.Tr "repo.issues.reopen_issue"}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div id="status-button" class="ui red basic button" tabindex="6" data-status="{{.i18n.Tr "repo.issues.close_issue"}}" data-status-and-comment="{{.i18n.Tr "repo.issues.close_comment_issue"}}" data-status-val="close">
|
||||||
|
{{.i18n.Tr "repo.issues.close_issue"}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
<button class="ui green button" tabindex="5">
|
||||||
|
{{.i18n.Tr "repo.issues.create_comment"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{else}}
|
||||||
{{if .Repository.IsArchived}}
|
{{if .Repository.IsArchived}}
|
||||||
<div class="ui warning message">
|
<div class="ui warning message">
|
||||||
{{if .Issue.IsPull}}
|
{{if .Issue.IsPull}}
|
||||||
|
@ -114,6 +145,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{end}}
|
||||||
</ui>
|
</ui>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
{{range .Issue.Comments}}
|
{{range .Issue.Comments}}
|
||||||
{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }}
|
{{ $createdStr:= TimeSinceUnix .CreatedUnix $.Lang }}
|
||||||
|
|
||||||
<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF, 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE, 22 = REVIEW -->
|
<!-- 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE_REF, 4 = COMMIT_REF,
|
||||||
|
5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING,
|
||||||
|
13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE,
|
||||||
|
18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE,
|
||||||
|
22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED -->
|
||||||
{{if eq .Type 0}}
|
{{if eq .Type 0}}
|
||||||
<div class="comment" id="{{.HashTag}}">
|
<div class="comment" id="{{.HashTag}}">
|
||||||
<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
|
<a class="avatar" {{if gt .Poster.ID 0}}href="{{.Poster.HomeLink}}"{{end}}>
|
||||||
|
@ -355,5 +359,35 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{else if eq .Type 23}}
|
||||||
|
<div class="event">
|
||||||
|
<span class="octicon octicon-lock"
|
||||||
|
style="font-size:20px;margin-left:-28.5px; margin-right: -1px"></span>
|
||||||
|
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
||||||
|
<img src="{{.Poster.RelAvatarLink}}">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{ if .Content }}
|
||||||
|
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
|
||||||
|
{{$.i18n.Tr "repo.issues.lock_with_reason" .Content $createdStr | Safe}}
|
||||||
|
</span>
|
||||||
|
{{ else }}
|
||||||
|
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
|
||||||
|
{{$.i18n.Tr "repo.issues.lock_no_reason" $createdStr | Safe}}
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{else if eq .Type 24}}
|
||||||
|
<div class="event">
|
||||||
|
<span class="octicon octicon-key"
|
||||||
|
style="font-size:20px;margin-left:-28.5px; margin-right: -1px"></span>
|
||||||
|
<a class="ui avatar image" href="{{.Poster.HomeLink}}">
|
||||||
|
<img src="{{.Poster.RelAvatarLink}}">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a>
|
||||||
|
{{$.i18n.Tr "repo.issues.unlock_comment" $createdStr | Safe}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -335,6 +335,91 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ if .IsRepoAdmin }}
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui watching">
|
||||||
|
<div>
|
||||||
|
<button class="fluid ui show-modal button {{if .Issue.IsLocked }} negative {{ end }}" data-modal="#lock">
|
||||||
|
{{if .Issue.IsLocked}}
|
||||||
|
<i class="octicon octicon-key"></i>
|
||||||
|
{{.i18n.Tr "repo.issues.unlock"}}
|
||||||
|
{{else}}
|
||||||
|
<i class="octicon octicon-lock"></i>
|
||||||
|
{{.i18n.Tr "repo.issues.lock"}}
|
||||||
|
{{end}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="ui tiny modal" id="lock">
|
||||||
|
<div class="header">
|
||||||
|
{{ if .Issue.IsLocked }}
|
||||||
|
{{.i18n.Tr "repo.issues.unlock.title"}}
|
||||||
|
{{ else }}
|
||||||
|
{{.i18n.Tr "repo.issues.lock.title"}}
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="ui warning message text left">
|
||||||
|
{{ if .Issue.IsLocked }}
|
||||||
|
{{.i18n.Tr "repo.issues.unlock.notice_1"}}<br>
|
||||||
|
{{.i18n.Tr "repo.issues.unlock.notice_2"}}<br>
|
||||||
|
{{ else }}
|
||||||
|
{{.i18n.Tr "repo.issues.lock.notice_1"}}<br>
|
||||||
|
{{.i18n.Tr "repo.issues.lock.notice_2"}}<br>
|
||||||
|
{{.i18n.Tr "repo.issues.lock.notice_3"}}<br>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="ui form" action="{{$.RepoLink}}/issues/{{.Issue.Index}}{{ if .Issue.IsLocked }}/unlock{{ else }}/lock{{ end }}"
|
||||||
|
method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
|
||||||
|
{{ if not .Issue.IsLocked }}
|
||||||
|
<div class="field">
|
||||||
|
<strong> {{ .i18n.Tr "repo.issues.lock.reason" }} </strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui fluid dropdown selection" tabindex="0">
|
||||||
|
|
||||||
|
<select name="reason">
|
||||||
|
<option value=""> </option>
|
||||||
|
{{range .LockReasons}}
|
||||||
|
<option value="{{.}}">{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
|
||||||
|
<div class="default text"> </div>
|
||||||
|
|
||||||
|
<div class="menu transition hidden" tabindex="-1" style="display: block !important;">
|
||||||
|
{{range .LockReasons}}
|
||||||
|
<div class="item" data-value="{{.}}">{{.}}</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="text right actions">
|
||||||
|
<div class="ui cancel button">{{.i18n.Tr "settings.cancel"}}</div>
|
||||||
|
<button class="ui red button">
|
||||||
|
{{ if .Issue.IsLocked }}
|
||||||
|
{{.i18n.Tr "repo.issues.unlock_confirm"}}
|
||||||
|
{{ else }}
|
||||||
|
{{.i18n.Tr "repo.issues.lock_confirm"}}
|
||||||
|
{{ end }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
|
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
|
||||||
|
|
Reference in a new issue