mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00:00 
			
		
		
		
	Merge branch 'dev' of github.com:gogits/gogs into dev
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -7,6 +7,7 @@ data/
 | 
			
		||||
.idea/
 | 
			
		||||
*.iml
 | 
			
		||||
public/img/avatar/
 | 
			
		||||
files/
 | 
			
		||||
 | 
			
		||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
 | 
			
		||||
*.o
 | 
			
		||||
@@ -34,4 +35,4 @@ _testmain.go
 | 
			
		||||
gogs
 | 
			
		||||
__pycache__
 | 
			
		||||
*.pem
 | 
			
		||||
output*
 | 
			
		||||
output*
 | 
			
		||||
 
 | 
			
		||||
@@ -238,6 +238,7 @@ func runWeb(*cli.Context) {
 | 
			
		||||
			r.Post("/:index/label", repo.UpdateIssueLabel)
 | 
			
		||||
			r.Post("/:index/milestone", repo.UpdateIssueMilestone)
 | 
			
		||||
			r.Post("/:index/assignee", repo.UpdateAssignee)
 | 
			
		||||
			r.Get("/:index/attachment/:id", repo.IssueGetAttachment)
 | 
			
		||||
			r.Post("/labels/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
 | 
			
		||||
			r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel)
 | 
			
		||||
			r.Post("/labels/delete", repo.DeleteLabel)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								conf/app.ini
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								conf/app.ini
									
									
									
									
									
								
							@@ -180,6 +180,18 @@ SESSION_ID_HASHKEY =
 | 
			
		||||
SERVICE = server
 | 
			
		||||
DISABLE_GRAVATAR = false
 | 
			
		||||
 | 
			
		||||
[attachment]
 | 
			
		||||
; Whether attachments are enabled. Defaults to `true`
 | 
			
		||||
ENABLE =
 | 
			
		||||
; Path for attachments. Defaults to files/attachments
 | 
			
		||||
PATH = 
 | 
			
		||||
; One or more allowed types, e.g. image/jpeg|image/png
 | 
			
		||||
ALLOWED_TYPES = 
 | 
			
		||||
; Max size of each file. Defaults to 32MB
 | 
			
		||||
MAX_SIZE
 | 
			
		||||
; Max number of files per upload. Defaults to 10
 | 
			
		||||
MAX_FILES =
 | 
			
		||||
 | 
			
		||||
[log]
 | 
			
		||||
ROOT_PATH =
 | 
			
		||||
; Either "console", "file", "conn", "smtp" or "database", default is "console"
 | 
			
		||||
 
 | 
			
		||||
@@ -127,7 +127,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com
 | 
			
		||||
			url := fmt.Sprintf("/%s/%s/commit/%s", repoUserName, repoName, c.Sha1)
 | 
			
		||||
			message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message)
 | 
			
		||||
 | 
			
		||||
			if err = CreateComment(userId, issue.RepoId, issue.Id, 0, 0, COMMIT, message); err != nil {
 | 
			
		||||
			if _, err = CreateComment(userId, issue.RepoId, issue.Id, 0, 0, COMMIT, message, nil); err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
@@ -142,24 +142,12 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				issue.Repo, err = GetRepositoryById(issue.RepoId)
 | 
			
		||||
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				issue.Repo.NumClosedIssues++
 | 
			
		||||
 | 
			
		||||
				if err = UpdateRepository(issue.Repo); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if err = ChangeMilestoneIssueStats(issue); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// If commit happened in the referenced repository, it means the issue can be closed.
 | 
			
		||||
				if err = CreateComment(userId, repoId, issue.Id, 0, 0, CLOSE, ""); err != nil {
 | 
			
		||||
				if _, err = CreateComment(userId, repoId, issue.Id, 0, 0, CLOSE, "", nil); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										195
									
								
								models/issue.go
									
									
									
									
									
								
							
							
						
						
									
										195
									
								
								models/issue.go
									
									
									
									
									
								
							@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -15,14 +16,17 @@ import (
 | 
			
		||||
	"github.com/go-xorm/xorm"
 | 
			
		||||
 | 
			
		||||
	"github.com/gogits/gogs/modules/base"
 | 
			
		||||
	"github.com/gogits/gogs/modules/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrIssueNotExist      = errors.New("Issue does not exist")
 | 
			
		||||
	ErrLabelNotExist      = errors.New("Label does not exist")
 | 
			
		||||
	ErrMilestoneNotExist  = errors.New("Milestone does not exist")
 | 
			
		||||
	ErrWrongIssueCounter  = errors.New("Invalid number of issues for this milestone")
 | 
			
		||||
	ErrMissingIssueNumber = errors.New("No issue number specified")
 | 
			
		||||
	ErrIssueNotExist       = errors.New("Issue does not exist")
 | 
			
		||||
	ErrLabelNotExist       = errors.New("Label does not exist")
 | 
			
		||||
	ErrMilestoneNotExist   = errors.New("Milestone does not exist")
 | 
			
		||||
	ErrWrongIssueCounter   = errors.New("Invalid number of issues for this milestone")
 | 
			
		||||
	ErrAttachmentNotExist  = errors.New("Attachment does not exist")
 | 
			
		||||
	ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
 | 
			
		||||
	ErrMissingIssueNumber  = errors.New("No issue number specified")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Issue represents an issue or pull request of repository.
 | 
			
		||||
@@ -94,6 +98,19 @@ func (i *Issue) GetAssignee() (err error) {
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (i *Issue) Attachments() []*Attachment {
 | 
			
		||||
	a, _ := GetAttachmentsForIssue(i.Id)
 | 
			
		||||
	return a
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (i *Issue) AfterDelete() {
 | 
			
		||||
	_, err := DeleteAttachmentsByIssue(i.Id, true)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Info("Could not delete files for issue #%d: %s", i.Id, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateIssue creates new issue for repository.
 | 
			
		||||
func NewIssue(issue *Issue) (err error) {
 | 
			
		||||
	sess := x.NewSession()
 | 
			
		||||
@@ -871,17 +888,19 @@ type Comment struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateComment creates comment of issue or commit.
 | 
			
		||||
func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string) error {
 | 
			
		||||
func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) {
 | 
			
		||||
	sess := x.NewSession()
 | 
			
		||||
	defer sess.Close()
 | 
			
		||||
	if err := sess.Begin(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := sess.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
 | 
			
		||||
		CommitId: commitId, Line: line, Content: content}); err != nil {
 | 
			
		||||
	comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
 | 
			
		||||
		CommitId: commitId, Line: line, Content: content}
 | 
			
		||||
 | 
			
		||||
	if _, err := sess.Insert(comment); err != nil {
 | 
			
		||||
		sess.Rollback()
 | 
			
		||||
		return err
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check comment type.
 | 
			
		||||
@@ -890,22 +909,46 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType Commen
 | 
			
		||||
		rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?"
 | 
			
		||||
		if _, err := sess.Exec(rawSql, issueId); err != nil {
 | 
			
		||||
			sess.Rollback()
 | 
			
		||||
			return err
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(attachments) > 0 {
 | 
			
		||||
			rawSql = "UPDATE `attachment` SET comment_id = ? WHERE id IN (?)"
 | 
			
		||||
 | 
			
		||||
			astrs := make([]string, 0, len(attachments))
 | 
			
		||||
 | 
			
		||||
			for _, a := range attachments {
 | 
			
		||||
				astrs = append(astrs, strconv.FormatInt(a, 10))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil {
 | 
			
		||||
				sess.Rollback()
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case REOPEN:
 | 
			
		||||
		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?"
 | 
			
		||||
		if _, err := sess.Exec(rawSql, repoId); err != nil {
 | 
			
		||||
			sess.Rollback()
 | 
			
		||||
			return err
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	case CLOSE:
 | 
			
		||||
		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?"
 | 
			
		||||
		if _, err := sess.Exec(rawSql, repoId); err != nil {
 | 
			
		||||
			sess.Rollback()
 | 
			
		||||
			return err
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return sess.Commit()
 | 
			
		||||
 | 
			
		||||
	return comment, sess.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetCommentById returns the comment with the given id
 | 
			
		||||
func GetCommentById(commentId int64) (*Comment, error) {
 | 
			
		||||
	c := &Comment{Id: commentId}
 | 
			
		||||
	_, err := x.Get(c)
 | 
			
		||||
 | 
			
		||||
	return c, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Comment) ContentHtml() template.HTML {
 | 
			
		||||
@@ -918,3 +961,127 @@ func GetIssueComments(issueId int64) ([]Comment, error) {
 | 
			
		||||
	err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId})
 | 
			
		||||
	return comments, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Attachments returns the attachments for this comment.
 | 
			
		||||
func (c *Comment) Attachments() []*Attachment {
 | 
			
		||||
	a, _ := GetAttachmentsByComment(c.Id)
 | 
			
		||||
	return a
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Comment) AfterDelete() {
 | 
			
		||||
	_, err := DeleteAttachmentsByComment(c.Id, true)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Info("Could not delete files for comment %d on issue #%d: %s", c.Id, c.IssueId, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Attachment struct {
 | 
			
		||||
	Id        int64
 | 
			
		||||
	IssueId   int64
 | 
			
		||||
	CommentId int64
 | 
			
		||||
	Name      string
 | 
			
		||||
	Path      string    `xorm:"TEXT"`
 | 
			
		||||
	Created   time.Time `xorm:"CREATED"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateAttachment creates a new attachment inside the database and
 | 
			
		||||
func CreateAttachment(issueId, commentId int64, name, path string) (*Attachment, error) {
 | 
			
		||||
	sess := x.NewSession()
 | 
			
		||||
	defer sess.Close()
 | 
			
		||||
 | 
			
		||||
	if err := sess.Begin(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	a := &Attachment{IssueId: issueId, CommentId: commentId, Name: name, Path: path}
 | 
			
		||||
 | 
			
		||||
	if _, err := sess.Insert(a); err != nil {
 | 
			
		||||
		sess.Rollback()
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return a, sess.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Attachment returns the attachment by given ID.
 | 
			
		||||
func GetAttachmentById(id int64) (*Attachment, error) {
 | 
			
		||||
	m := &Attachment{Id: id}
 | 
			
		||||
 | 
			
		||||
	has, err := x.Get(m)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !has {
 | 
			
		||||
		return nil, ErrAttachmentNotExist
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return m, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) {
 | 
			
		||||
	attachments := make([]*Attachment, 0, 10)
 | 
			
		||||
	err := x.Where("issue_id = ?", issueId).And("comment_id = 0").Find(&attachments)
 | 
			
		||||
	return attachments, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAttachmentsByIssue returns a list of attachments for the given issue
 | 
			
		||||
func GetAttachmentsByIssue(issueId int64) ([]*Attachment, error) {
 | 
			
		||||
	attachments := make([]*Attachment, 0, 10)
 | 
			
		||||
	err := x.Where("issue_id = ?", issueId).And("comment_id > 0").Find(&attachments)
 | 
			
		||||
	return attachments, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAttachmentsByComment returns a list of attachments for the given comment
 | 
			
		||||
func GetAttachmentsByComment(commentId int64) ([]*Attachment, error) {
 | 
			
		||||
	attachments := make([]*Attachment, 0, 10)
 | 
			
		||||
	err := x.Where("comment_id = ?", commentId).Find(&attachments)
 | 
			
		||||
	return attachments, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteAttachment deletes the given attachment and optionally the associated file.
 | 
			
		||||
func DeleteAttachment(a *Attachment, remove bool) error {
 | 
			
		||||
	_, err := DeleteAttachments([]*Attachment{a}, remove)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteAttachments deletes the given attachments and optionally the associated files.
 | 
			
		||||
func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
 | 
			
		||||
	for i, a := range attachments {
 | 
			
		||||
		if remove {
 | 
			
		||||
			if err := os.Remove(a.Path); err != nil {
 | 
			
		||||
				return i, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if _, err := x.Delete(a.Id); err != nil {
 | 
			
		||||
			return i, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return len(attachments), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
 | 
			
		||||
func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
 | 
			
		||||
	attachments, err := GetAttachmentsByIssue(issueId)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DeleteAttachments(attachments, remove)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteAttachmentsByComment deletes all attachments associated with the given comment.
 | 
			
		||||
func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
 | 
			
		||||
	attachments, err := GetAttachmentsByComment(commentId)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DeleteAttachments(attachments, remove)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@ func init() {
 | 
			
		||||
		new(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow),
 | 
			
		||||
		new(Mirror), new(Release), new(LoginSource), new(Webhook), new(IssueUser),
 | 
			
		||||
		new(Milestone), new(Label), new(HookTask), new(Team), new(OrgUser), new(TeamUser),
 | 
			
		||||
		new(UpdateTask))
 | 
			
		||||
		new(UpdateTask), new(Attachment))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func LoadModelsConfig() {
 | 
			
		||||
 
 | 
			
		||||
@@ -319,7 +319,6 @@ func (f *Flash) Success(msg string) {
 | 
			
		||||
// InitContext initializes a classic context for a request.
 | 
			
		||||
func InitContext() martini.Handler {
 | 
			
		||||
	return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) {
 | 
			
		||||
 | 
			
		||||
		ctx := &Context{
 | 
			
		||||
			c: c,
 | 
			
		||||
			// p:      p,
 | 
			
		||||
@@ -328,7 +327,6 @@ func InitContext() martini.Handler {
 | 
			
		||||
			Cache:  setting.Cache,
 | 
			
		||||
			Render: rd,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ctx.Data["PageStartTime"] = time.Now()
 | 
			
		||||
 | 
			
		||||
		// start session
 | 
			
		||||
@@ -370,6 +368,14 @@ func InitContext() martini.Handler {
 | 
			
		||||
			ctx.Data["IsAdmin"] = ctx.User.IsAdmin
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
 | 
			
		||||
		if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
 | 
			
		||||
			if err = ctx.Req.ParseMultipartForm(setting.AttachmentMaxSize << 20); err != nil { // 32MB max size
 | 
			
		||||
				ctx.Handle(500, "issue.Comment(ctx.Req.ParseMultipartForm)", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// get or create csrf token
 | 
			
		||||
		ctx.Data["CsrfToken"] = ctx.CsrfToken()
 | 
			
		||||
		ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`)
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,13 @@ var (
 | 
			
		||||
	LogModes    []string
 | 
			
		||||
	LogConfigs  []string
 | 
			
		||||
 | 
			
		||||
	// Attachment settings.
 | 
			
		||||
	AttachmentPath         string
 | 
			
		||||
	AttachmentAllowedTypes string
 | 
			
		||||
	AttachmentMaxSize      int64
 | 
			
		||||
	AttachmentMaxFiles     int
 | 
			
		||||
	AttachmentEnabled      bool
 | 
			
		||||
 | 
			
		||||
	// Cache settings.
 | 
			
		||||
	Cache        cache.Cache
 | 
			
		||||
	CacheAdapter string
 | 
			
		||||
@@ -166,6 +173,16 @@ func NewConfigContext() {
 | 
			
		||||
	CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME")
 | 
			
		||||
	ReverseProxyAuthUser = Cfg.MustValue("security", "REVERSE_PROXY_AUTHENTICATION_USER", "X-WEBAUTH-USER")
 | 
			
		||||
 | 
			
		||||
	AttachmentPath = Cfg.MustValue("attachment", "PATH", "files/attachments")
 | 
			
		||||
	AttachmentAllowedTypes = Cfg.MustValue("attachment", "ALLOWED_TYPES", "*/*")
 | 
			
		||||
	AttachmentMaxSize = Cfg.MustInt64("attachment", "MAX_SIZE", 32)
 | 
			
		||||
	AttachmentMaxFiles = Cfg.MustInt("attachment", "MAX_FILES", 10)
 | 
			
		||||
	AttachmentEnabled = Cfg.MustBool("attachment", "ENABLE", true)
 | 
			
		||||
 | 
			
		||||
	if err = os.MkdirAll(AttachmentPath, os.ModePerm); err != nil {
 | 
			
		||||
		log.Fatal("Could not create directory %s: %s", AttachmentPath, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	RunUser = Cfg.MustValue("", "RUN_USER")
 | 
			
		||||
	curUser := os.Getenv("USER")
 | 
			
		||||
	if len(curUser) == 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -1794,4 +1794,46 @@ body {
 | 
			
		||||
    color: #444;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    line-height: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.issue-main .attachments {
 | 
			
		||||
    margin: 0px 10px 10px 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.issue-main .attachments .attachment-label {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attachment-preview {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0px;
 | 
			
		||||
    bottom: 0px;
 | 
			
		||||
    
 | 
			
		||||
    margin: 5px;
 | 
			
		||||
    padding: 8px;
 | 
			
		||||
 | 
			
		||||
    background: #fff;
 | 
			
		||||
    border: 1px solid #d8d8d8;
 | 
			
		||||
    box-shadow: 0 0 5px 1px #d8d8d8;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attachment-preview-img {
 | 
			
		||||
    border: 1px solid #d8d8d8;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#attachments-button {
 | 
			
		||||
    float: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#attached {
 | 
			
		||||
    height: 18px;
 | 
			
		||||
    margin: 10px 10px 15px 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#attached-list .label {
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#issue-create-form #attached {
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
@@ -520,6 +520,90 @@ function initIssue() {
 | 
			
		||||
        });
 | 
			
		||||
    }());
 | 
			
		||||
 | 
			
		||||
    // Preview for images.
 | 
			
		||||
    (function() {
 | 
			
		||||
        var $hoverElement = $("<div></div>");
 | 
			
		||||
        var $hoverImage = $("<img />");
 | 
			
		||||
 | 
			
		||||
        $hoverElement.addClass("attachment-preview");
 | 
			
		||||
        $hoverElement.hide();
 | 
			
		||||
 | 
			
		||||
        $hoverImage.addClass("attachment-preview-img");
 | 
			
		||||
 | 
			
		||||
        $hoverElement.append($hoverImage);
 | 
			
		||||
        $(document.body).append($hoverElement); 
 | 
			
		||||
 | 
			
		||||
        var over = function() {
 | 
			
		||||
            var $this = $(this);
 | 
			
		||||
 | 
			
		||||
            if ($this.text().match(/\.(png|jpg|jpeg|gif)$/i) == false) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($hoverImage.attr("src") != $this.attr("href")) {
 | 
			
		||||
                $hoverImage.attr("src", $this.attr("href"));
 | 
			
		||||
                $hoverImage.load(function() {
 | 
			
		||||
                    var height = this.height;
 | 
			
		||||
                    var width = this.width;
 | 
			
		||||
 | 
			
		||||
                    if (height > 300) {
 | 
			
		||||
                        var factor = 300 / height;
 | 
			
		||||
 | 
			
		||||
                        height = factor * height;
 | 
			
		||||
                        width = factor * width;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    $hoverImage.css({"height": height, "width": width});
 | 
			
		||||
 | 
			
		||||
                    var offset = $this.offset();
 | 
			
		||||
                    var left = offset.left, top = offset.top + $this.height() + 5;
 | 
			
		||||
 | 
			
		||||
                    $hoverElement.css({"top": top + "px", "left": left + "px"});
 | 
			
		||||
                    $hoverElement.css({"height": height + 16, "width": width + 16});
 | 
			
		||||
                    $hoverElement.show();
 | 
			
		||||
                });            
 | 
			
		||||
            } else {
 | 
			
		||||
                $hoverElement.show();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        var out = function() {
 | 
			
		||||
            $hoverElement.hide();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $(".issue-main .attachments .attachment").hover(over, out);
 | 
			
		||||
    }());
 | 
			
		||||
 | 
			
		||||
    // Upload.
 | 
			
		||||
    (function() {
 | 
			
		||||
        var $attachedList = $("#attached-list");
 | 
			
		||||
        var $addButton = $("#attachments-button");
 | 
			
		||||
 | 
			
		||||
        var fileInput = $("#attachments-input")[0];
 | 
			
		||||
 | 
			
		||||
        fileInput.addEventListener("change", function(event) {
 | 
			
		||||
            $attachedList.empty();
 | 
			
		||||
            $attachedList.append("<b>Attachments:</b> ");
 | 
			
		||||
 | 
			
		||||
            for (var index = 0; index < fileInput.files.length; index++) {
 | 
			
		||||
                var file = fileInput.files[index];
 | 
			
		||||
 | 
			
		||||
                var $span = $("<span></span>");
 | 
			
		||||
 | 
			
		||||
                $span.addClass("label");
 | 
			
		||||
                $span.addClass("label-default");
 | 
			
		||||
 | 
			
		||||
                $span.append(file.name.toLowerCase());
 | 
			
		||||
                $attachedList.append($span);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $addButton.on("click", function() {
 | 
			
		||||
            fileInput.click();
 | 
			
		||||
            return false;
 | 
			
		||||
        });
 | 
			
		||||
    }());
 | 
			
		||||
 | 
			
		||||
    // issue edit mode
 | 
			
		||||
    (function () {
 | 
			
		||||
        $("#issue-edit-btn").on("click", function () {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,11 @@
 | 
			
		||||
package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -32,6 +36,11 @@ const (
 | 
			
		||||
	MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrFileTypeForbidden = errors.New("File type is not allowed")
 | 
			
		||||
	ErrTooManyFiles      = errors.New("Maximum number of files to upload exceeded")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Issues(ctx *middleware.Context) {
 | 
			
		||||
	ctx.Data["Title"] = "Issues"
 | 
			
		||||
	ctx.Data["IsRepoToolbarIssues"] = true
 | 
			
		||||
@@ -151,6 +160,7 @@ func CreateIssue(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
	ctx.Data["Title"] = "Create issue"
 | 
			
		||||
	ctx.Data["IsRepoToolbarIssues"] = true
 | 
			
		||||
	ctx.Data["IsRepoToolbarIssuesList"] = false
 | 
			
		||||
	ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	// Get all milestones.
 | 
			
		||||
@@ -170,7 +180,10 @@ func CreateIssue(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
		ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes
 | 
			
		||||
	ctx.Data["Collaborators"] = us
 | 
			
		||||
 | 
			
		||||
	ctx.HTML(200, ISSUE_CREATE)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -178,6 +191,7 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C
 | 
			
		||||
	ctx.Data["Title"] = "Create issue"
 | 
			
		||||
	ctx.Data["IsRepoToolbarIssues"] = true
 | 
			
		||||
	ctx.Data["IsRepoToolbarIssuesList"] = false
 | 
			
		||||
	ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	// Get all milestones.
 | 
			
		||||
@@ -227,6 +241,10 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if setting.AttachmentEnabled {
 | 
			
		||||
		uploadFiles(ctx, issue.Id, 0)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Update mentions.
 | 
			
		||||
	ms := base.MentionPattern.FindAllString(issue.Content, -1)
 | 
			
		||||
	if len(ms) > 0 {
 | 
			
		||||
@@ -299,6 +317,8 @@ func checkLabels(labels, allLabels []*models.Label) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ViewIssue(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
	ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled
 | 
			
		||||
 | 
			
		||||
	idx, _ := base.StrTo(params["index"]).Int64()
 | 
			
		||||
	if idx == 0 {
 | 
			
		||||
		ctx.Handle(404, "issue.ViewIssue", nil)
 | 
			
		||||
@@ -399,6 +419,8 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes
 | 
			
		||||
 | 
			
		||||
	ctx.Data["Title"] = issue.Name
 | 
			
		||||
	ctx.Data["Issue"] = issue
 | 
			
		||||
	ctx.Data["Comments"] = comments
 | 
			
		||||
@@ -611,6 +633,71 @@ func UpdateAssignee(ctx *middleware.Context) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func uploadFiles(ctx *middleware.Context, issueId, commentId int64) {
 | 
			
		||||
	if !setting.AttachmentEnabled {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|")
 | 
			
		||||
	attachments := ctx.Req.MultipartForm.File["attachments"]
 | 
			
		||||
 | 
			
		||||
	if len(attachments) > setting.AttachmentMaxFiles {
 | 
			
		||||
		ctx.Handle(400, "issue.Comment", ErrTooManyFiles)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, header := range attachments {
 | 
			
		||||
		file, err := header.Open()
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Handle(500, "issue.Comment(header.Open)", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
 | 
			
		||||
		allowed := false
 | 
			
		||||
		fileType := mime.TypeByExtension(header.Filename)
 | 
			
		||||
 | 
			
		||||
		for _, t := range allowedTypes {
 | 
			
		||||
			t := strings.Trim(t, " ")
 | 
			
		||||
 | 
			
		||||
			if t == "*/*" || t == fileType {
 | 
			
		||||
				allowed = true
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !allowed {
 | 
			
		||||
			ctx.Handle(400, "issue.Comment", ErrFileTypeForbidden)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_")
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Handle(500, "issue.Comment(ioutil.TempFile)", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		defer out.Close()
 | 
			
		||||
 | 
			
		||||
		_, err = io.Copy(out, file)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Handle(500, "issue.Comment(io.Copy)", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = models.CreateAttachment(issueId, commentId, header.Filename, out.Name())
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Handle(500, "issue.Comment(io.Copy)", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Comment(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
	index, err := base.StrTo(ctx.Query("issueIndex")).Int64()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -657,7 +744,7 @@ func Comment(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
				cmtType = models.REOPEN
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil {
 | 
			
		||||
			if _, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, "", nil); err != nil {
 | 
			
		||||
				ctx.Handle(200, "issue.Comment(create status change comment)", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
@@ -665,12 +752,14 @@ func Comment(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var comment *models.Comment
 | 
			
		||||
 | 
			
		||||
	var ms []string
 | 
			
		||||
	content := ctx.Query("content")
 | 
			
		||||
	if len(content) > 0 {
 | 
			
		||||
		switch params["action"] {
 | 
			
		||||
		case "new":
 | 
			
		||||
			if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.COMMENT, content); err != nil {
 | 
			
		||||
			if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.COMMENT, content, nil); err != nil {
 | 
			
		||||
				ctx.Handle(500, "issue.Comment(create comment)", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
@@ -696,6 +785,10 @@ func Comment(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if comment != nil {
 | 
			
		||||
		uploadFiles(ctx, issue.Id, comment.Id)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Notify watchers.
 | 
			
		||||
	act := &models.Action{
 | 
			
		||||
		ActUserId:    ctx.User.Id,
 | 
			
		||||
@@ -972,3 +1065,21 @@ func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form au
 | 
			
		||||
 | 
			
		||||
	ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IssueGetAttachment(ctx *middleware.Context, params martini.Params) {
 | 
			
		||||
	id, err := base.StrTo(params["id"]).Int64()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Handle(400, "issue.IssueGetAttachment(base.StrTo.Int64)", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	attachment, err := models.GetAttachmentById(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Handle(404, "issue.IssueGetAttachment(models.GetAttachmentById)", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.ServeFile(attachment.Path, attachment.Name)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
{{template "repo/toolbar" .}}
 | 
			
		||||
<div id="body" class="container">
 | 
			
		||||
    <div id="issue">
 | 
			
		||||
        <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form">
 | 
			
		||||
        <form class="form" action="{{.RepoLink}}/issues/new" method="post" id="issue-create-form" enctype="multipart/form-data">
 | 
			
		||||
            {{.CsrfTokenHtml}}
 | 
			
		||||
            {{template "base/alert" .}}
 | 
			
		||||
            <div class="col-md-1">
 | 
			
		||||
@@ -101,8 +101,17 @@
 | 
			
		||||
                        <div class="tab-pane issue-preview-content" id="issue-preview">loading...</div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {{if .AttachmentsEnabled}}
 | 
			
		||||
                <div id="attached">
 | 
			
		||||
                    <div id="attached-list"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {{end}}
 | 
			
		||||
                <div class="text-right panel-body">
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        {{if .AttachmentsEnabled}}
 | 
			
		||||
                        <input type="file" accept="{{.AllowedTypes}}" style="display: none;" id="attachments-input" name="attachments" multiple />
 | 
			
		||||
                        <button class="btn-default btn attachment-add" id="attachments-button">Select Attachments...</button>
 | 
			
		||||
                        {{end}}
 | 
			
		||||
                        <input type="hidden" value="id" name="repo-id"/>
 | 
			
		||||
                        <button class="btn-success btn">Create new issue</button>
 | 
			
		||||
                    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -45,8 +45,19 @@
 | 
			
		||||
                                        <div class="tab-pane issue-preview-content" id="issue-edit-preview">Loading...</div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            </div>                        
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {{with $attachments := .Issue.Attachments}}
 | 
			
		||||
                        {{if $attachments}}
 | 
			
		||||
                        <div class="attachments">
 | 
			
		||||
                            <span class="attachment-label label label-info">Attachments:</span>
 | 
			
		||||
                                
 | 
			
		||||
                            {{range $attachments}}
 | 
			
		||||
                            <a class="attachment label label-default" href="{{.IssueId}}/attachment/{{.Id}}">{{.Name}}</a>
 | 
			
		||||
                            {{end}}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {{end}}
 | 
			
		||||
                        {{end}}    
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {{range .Comments}}
 | 
			
		||||
                    {{/* 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE, 4 = COMMIT, 5 = PULL */}}
 | 
			
		||||
@@ -63,6 +74,17 @@
 | 
			
		||||
                            <div class="panel-body markdown">
 | 
			
		||||
                                {{str2html .Content}}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {{with $attachments := .Attachments}}
 | 
			
		||||
                            {{if $attachments}}
 | 
			
		||||
                            <div class="attachments">
 | 
			
		||||
                                <span class="attachment-label label label-info">Attachments:</span>
 | 
			
		||||
 | 
			
		||||
                                {{range $attachments}}
 | 
			
		||||
                                <a class="attachment label label-default" href="{{.IssueId}}/attachment/{{.Id}}">{{.Name}}</a>
 | 
			
		||||
                                {{end}}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {{end}}
 | 
			
		||||
                            {{end}}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {{else if eq .Type 1}}
 | 
			
		||||
@@ -95,7 +117,7 @@
 | 
			
		||||
                    <hr class="issue-line"/>
 | 
			
		||||
                    {{if .SignedUser}}<div class="issue-child issue-reply">
 | 
			
		||||
                    <a class="user pull-left" href="/user/{{.SignedUser.Name}}"><img class="avatar" src="{{.SignedUser.AvatarLink}}" alt=""/></a>
 | 
			
		||||
                    <form class="panel panel-default issue-content" action="{{.RepoLink}}/comment/new" method="post">
 | 
			
		||||
                    <form class="panel panel-default issue-content" action="{{.RepoLink}}/comment/new" method="post" enctype="multipart/form-data">
 | 
			
		||||
                        {{.CsrfTokenHtml}}
 | 
			
		||||
                        <div class="panel-body">
 | 
			
		||||
                            <div class="form-group">
 | 
			
		||||
@@ -115,8 +137,17 @@
 | 
			
		||||
                                    <div class="tab-pane issue-preview-content" id="issue-preview">Loading...</div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {{if .AttachmentsEnabled}}
 | 
			
		||||
                            <div id="attached">
 | 
			
		||||
                                <div id="attached-list"></div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {{end}}
 | 
			
		||||
                            <div class="text-right">
 | 
			
		||||
                                <div class="form-group">
 | 
			
		||||
                                    {{if .AttachmentsEnabled}}
 | 
			
		||||
                                    <input type="file" accept="{{.AllowedTypes}}" style="display: none;" id="attachments-input" name="attachments" multiple />
 | 
			
		||||
                                    <button class="btn-default btn attachment-add" id="attachments-button">Select Attachments...</button>
 | 
			
		||||
                                    {{end}}
 | 
			
		||||
                                    {{if .IsIssueOwner}}{{if .Issue.IsClosed}}
 | 
			
		||||
                                    <input type="submit" class="btn-default btn issue-open" id="issue-open-btn" data-origin="Reopen" data-text="Reopen & Comment" name="change_status" value="Reopen"/>{{else}}
 | 
			
		||||
                                    <input type="submit" class="btn-default btn issue-close" id="issue-close-btn" data-origin="Close" data-text="Close & Comment" name="change_status" value="Close"/>{{end}}{{end}}  
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user