mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01: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/ | .idea/ | ||||||
| *.iml | *.iml | ||||||
| public/img/avatar/ | public/img/avatar/ | ||||||
|  | files/ | ||||||
|  |  | ||||||
| # Compiled Object files, Static and Dynamic libs (Shared Objects) | # Compiled Object files, Static and Dynamic libs (Shared Objects) | ||||||
| *.o | *.o | ||||||
| @@ -34,4 +35,4 @@ _testmain.go | |||||||
| gogs | gogs | ||||||
| __pycache__ | __pycache__ | ||||||
| *.pem | *.pem | ||||||
| output* | output* | ||||||
|   | |||||||
| @@ -238,6 +238,7 @@ func runWeb(*cli.Context) { | |||||||
| 			r.Post("/:index/label", repo.UpdateIssueLabel) | 			r.Post("/:index/label", repo.UpdateIssueLabel) | ||||||
| 			r.Post("/:index/milestone", repo.UpdateIssueMilestone) | 			r.Post("/:index/milestone", repo.UpdateIssueMilestone) | ||||||
| 			r.Post("/:index/assignee", repo.UpdateAssignee) | 			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/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel) | ||||||
| 			r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel) | 			r.Post("/labels/edit", bindIgnErr(auth.CreateLabelForm{}), repo.UpdateLabel) | ||||||
| 			r.Post("/labels/delete", repo.DeleteLabel) | 			r.Post("/labels/delete", repo.DeleteLabel) | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								conf/app.ini
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								conf/app.ini
									
									
									
									
									
								
							| @@ -180,6 +180,18 @@ SESSION_ID_HASHKEY = | |||||||
| SERVICE = server | SERVICE = server | ||||||
| DISABLE_GRAVATAR = false | 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] | [log] | ||||||
| ROOT_PATH = | ROOT_PATH = | ||||||
| ; Either "console", "file", "conn", "smtp" or "database", default is "console" | ; 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) | 			url := fmt.Sprintf("/%s/%s/commit/%s", repoUserName, repoName, c.Sha1) | ||||||
| 			message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message) | 			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 | 				return err | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| @@ -142,24 +142,12 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com | |||||||
| 					return err | 					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 { | 				if err = ChangeMilestoneIssueStats(issue); err != nil { | ||||||
| 					return err | 					return err | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				// If commit happened in the referenced repository, it means the issue can be closed. | 				// 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 | 					return err | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|   | |||||||
							
								
								
									
										195
									
								
								models/issue.go
									
									
									
									
									
								
							
							
						
						
									
										195
									
								
								models/issue.go
									
									
									
									
									
								
							| @@ -8,6 +8,7 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"html/template" | 	"html/template" | ||||||
|  | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -15,14 +16,17 @@ import ( | |||||||
| 	"github.com/go-xorm/xorm" | 	"github.com/go-xorm/xorm" | ||||||
|  |  | ||||||
| 	"github.com/gogits/gogs/modules/base" | 	"github.com/gogits/gogs/modules/base" | ||||||
|  | 	"github.com/gogits/gogs/modules/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	ErrIssueNotExist      = errors.New("Issue does not exist") | 	ErrIssueNotExist       = errors.New("Issue does not exist") | ||||||
| 	ErrLabelNotExist      = errors.New("Label does not exist") | 	ErrLabelNotExist       = errors.New("Label does not exist") | ||||||
| 	ErrMilestoneNotExist  = errors.New("Milestone does not exist") | 	ErrMilestoneNotExist   = errors.New("Milestone does not exist") | ||||||
| 	ErrWrongIssueCounter  = errors.New("Invalid number of issues for this milestone") | 	ErrWrongIssueCounter   = errors.New("Invalid number of issues for this milestone") | ||||||
| 	ErrMissingIssueNumber = errors.New("No issue number specified") | 	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. | // Issue represents an issue or pull request of repository. | ||||||
| @@ -94,6 +98,19 @@ func (i *Issue) GetAssignee() (err error) { | |||||||
| 	return err | 	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. | // CreateIssue creates new issue for repository. | ||||||
| func NewIssue(issue *Issue) (err error) { | func NewIssue(issue *Issue) (err error) { | ||||||
| 	sess := x.NewSession() | 	sess := x.NewSession() | ||||||
| @@ -871,17 +888,19 @@ type Comment struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| // CreateComment creates comment of issue or commit. | // 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() | 	sess := x.NewSession() | ||||||
| 	defer sess.Close() | 	defer sess.Close() | ||||||
| 	if err := sess.Begin(); err != nil { | 	if err := sess.Begin(); err != nil { | ||||||
| 		return err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err := sess.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId, | 	comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId, | ||||||
| 		CommitId: commitId, Line: line, Content: content}); err != nil { | 		CommitId: commitId, Line: line, Content: content} | ||||||
|  |  | ||||||
|  | 	if _, err := sess.Insert(comment); err != nil { | ||||||
| 		sess.Rollback() | 		sess.Rollback() | ||||||
| 		return err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check comment type. | 	// 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 = ?" | 		rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" | ||||||
| 		if _, err := sess.Exec(rawSql, issueId); err != nil { | 		if _, err := sess.Exec(rawSql, issueId); err != nil { | ||||||
| 			sess.Rollback() | 			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: | 	case REOPEN: | ||||||
| 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" | 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" | ||||||
| 		if _, err := sess.Exec(rawSql, repoId); err != nil { | 		if _, err := sess.Exec(rawSql, repoId); err != nil { | ||||||
| 			sess.Rollback() | 			sess.Rollback() | ||||||
| 			return err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 	case CLOSE: | 	case CLOSE: | ||||||
| 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" | 		rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" | ||||||
| 		if _, err := sess.Exec(rawSql, repoId); err != nil { | 		if _, err := sess.Exec(rawSql, repoId); err != nil { | ||||||
| 			sess.Rollback() | 			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 { | 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}) | 	err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId}) | ||||||
| 	return comments, err | 	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(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow), | ||||||
| 		new(Mirror), new(Release), new(LoginSource), new(Webhook), new(IssueUser), | 		new(Mirror), new(Release), new(LoginSource), new(Webhook), new(IssueUser), | ||||||
| 		new(Milestone), new(Label), new(HookTask), new(Team), new(OrgUser), new(TeamUser), | 		new(Milestone), new(Label), new(HookTask), new(Team), new(OrgUser), new(TeamUser), | ||||||
| 		new(UpdateTask)) | 		new(UpdateTask), new(Attachment)) | ||||||
| } | } | ||||||
|  |  | ||||||
| func LoadModelsConfig() { | func LoadModelsConfig() { | ||||||
|   | |||||||
| @@ -319,7 +319,6 @@ func (f *Flash) Success(msg string) { | |||||||
| // InitContext initializes a classic context for a request. | // InitContext initializes a classic context for a request. | ||||||
| func InitContext() martini.Handler { | func InitContext() martini.Handler { | ||||||
| 	return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { | 	return func(res http.ResponseWriter, r *http.Request, c martini.Context, rd *Render) { | ||||||
|  |  | ||||||
| 		ctx := &Context{ | 		ctx := &Context{ | ||||||
| 			c: c, | 			c: c, | ||||||
| 			// p:      p, | 			// p:      p, | ||||||
| @@ -328,7 +327,6 @@ func InitContext() martini.Handler { | |||||||
| 			Cache:  setting.Cache, | 			Cache:  setting.Cache, | ||||||
| 			Render: rd, | 			Render: rd, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		ctx.Data["PageStartTime"] = time.Now() | 		ctx.Data["PageStartTime"] = time.Now() | ||||||
|  |  | ||||||
| 		// start session | 		// start session | ||||||
| @@ -370,6 +368,14 @@ func InitContext() martini.Handler { | |||||||
| 			ctx.Data["IsAdmin"] = ctx.User.IsAdmin | 			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 | 		// get or create csrf token | ||||||
| 		ctx.Data["CsrfToken"] = ctx.CsrfToken() | 		ctx.Data["CsrfToken"] = ctx.CsrfToken() | ||||||
| 		ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`) | 		ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + ctx.csrfToken + `">`) | ||||||
|   | |||||||
| @@ -71,6 +71,13 @@ var ( | |||||||
| 	LogModes    []string | 	LogModes    []string | ||||||
| 	LogConfigs  []string | 	LogConfigs  []string | ||||||
|  |  | ||||||
|  | 	// Attachment settings. | ||||||
|  | 	AttachmentPath         string | ||||||
|  | 	AttachmentAllowedTypes string | ||||||
|  | 	AttachmentMaxSize      int64 | ||||||
|  | 	AttachmentMaxFiles     int | ||||||
|  | 	AttachmentEnabled      bool | ||||||
|  |  | ||||||
| 	// Cache settings. | 	// Cache settings. | ||||||
| 	Cache        cache.Cache | 	Cache        cache.Cache | ||||||
| 	CacheAdapter string | 	CacheAdapter string | ||||||
| @@ -166,6 +173,16 @@ func NewConfigContext() { | |||||||
| 	CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") | 	CookieRememberName = Cfg.MustValue("security", "COOKIE_REMEMBER_NAME") | ||||||
| 	ReverseProxyAuthUser = Cfg.MustValue("security", "REVERSE_PROXY_AUTHENTICATION_USER", "X-WEBAUTH-USER") | 	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") | 	RunUser = Cfg.MustValue("", "RUN_USER") | ||||||
| 	curUser := os.Getenv("USER") | 	curUser := os.Getenv("USER") | ||||||
| 	if len(curUser) == 0 { | 	if len(curUser) == 0 { | ||||||
|   | |||||||
| @@ -1794,4 +1794,46 @@ body { | |||||||
|     color: #444; |     color: #444; | ||||||
|     font-weight: bold; |     font-weight: bold; | ||||||
|     line-height: 30px; |     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 |     // issue edit mode | ||||||
|     (function () { |     (function () { | ||||||
|         $("#issue-edit-btn").on("click", function () { |         $("#issue-edit-btn").on("click", function () { | ||||||
|   | |||||||
| @@ -5,7 +5,11 @@ | |||||||
| package repo | package repo | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"mime" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -32,6 +36,11 @@ const ( | |||||||
| 	MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit" | 	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) { | func Issues(ctx *middleware.Context) { | ||||||
| 	ctx.Data["Title"] = "Issues" | 	ctx.Data["Title"] = "Issues" | ||||||
| 	ctx.Data["IsRepoToolbarIssues"] = true | 	ctx.Data["IsRepoToolbarIssues"] = true | ||||||
| @@ -151,6 +160,7 @@ func CreateIssue(ctx *middleware.Context, params martini.Params) { | |||||||
| 	ctx.Data["Title"] = "Create issue" | 	ctx.Data["Title"] = "Create issue" | ||||||
| 	ctx.Data["IsRepoToolbarIssues"] = true | 	ctx.Data["IsRepoToolbarIssues"] = true | ||||||
| 	ctx.Data["IsRepoToolbarIssuesList"] = false | 	ctx.Data["IsRepoToolbarIssuesList"] = false | ||||||
|  | 	ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
| 	// Get all milestones. | 	// Get all milestones. | ||||||
| @@ -170,7 +180,10 @@ func CreateIssue(ctx *middleware.Context, params martini.Params) { | |||||||
| 		ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) | 		ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes | ||||||
| 	ctx.Data["Collaborators"] = us | 	ctx.Data["Collaborators"] = us | ||||||
|  |  | ||||||
| 	ctx.HTML(200, ISSUE_CREATE) | 	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["Title"] = "Create issue" | ||||||
| 	ctx.Data["IsRepoToolbarIssues"] = true | 	ctx.Data["IsRepoToolbarIssues"] = true | ||||||
| 	ctx.Data["IsRepoToolbarIssuesList"] = false | 	ctx.Data["IsRepoToolbarIssuesList"] = false | ||||||
|  | 	ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
| 	// Get all milestones. | 	// Get all milestones. | ||||||
| @@ -227,6 +241,10 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if setting.AttachmentEnabled { | ||||||
|  | 		uploadFiles(ctx, issue.Id, 0) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Update mentions. | 	// Update mentions. | ||||||
| 	ms := base.MentionPattern.FindAllString(issue.Content, -1) | 	ms := base.MentionPattern.FindAllString(issue.Content, -1) | ||||||
| 	if len(ms) > 0 { | 	if len(ms) > 0 { | ||||||
| @@ -299,6 +317,8 @@ func checkLabels(labels, allLabels []*models.Label) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func ViewIssue(ctx *middleware.Context, params martini.Params) { | func ViewIssue(ctx *middleware.Context, params martini.Params) { | ||||||
|  | 	ctx.Data["AttachmentsEnabled"] = setting.AttachmentEnabled | ||||||
|  |  | ||||||
| 	idx, _ := base.StrTo(params["index"]).Int64() | 	idx, _ := base.StrTo(params["index"]).Int64() | ||||||
| 	if idx == 0 { | 	if idx == 0 { | ||||||
| 		ctx.Handle(404, "issue.ViewIssue", nil) | 		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["Title"] = issue.Name | ||||||
| 	ctx.Data["Issue"] = issue | 	ctx.Data["Issue"] = issue | ||||||
| 	ctx.Data["Comments"] = comments | 	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) { | func Comment(ctx *middleware.Context, params martini.Params) { | ||||||
| 	index, err := base.StrTo(ctx.Query("issueIndex")).Int64() | 	index, err := base.StrTo(ctx.Query("issueIndex")).Int64() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -657,7 +744,7 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||||||
| 				cmtType = models.REOPEN | 				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) | 				ctx.Handle(200, "issue.Comment(create status change comment)", err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| @@ -665,12 +752,14 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	var comment *models.Comment | ||||||
|  |  | ||||||
| 	var ms []string | 	var ms []string | ||||||
| 	content := ctx.Query("content") | 	content := ctx.Query("content") | ||||||
| 	if len(content) > 0 { | 	if len(content) > 0 { | ||||||
| 		switch params["action"] { | 		switch params["action"] { | ||||||
| 		case "new": | 		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) | 				ctx.Handle(500, "issue.Comment(create comment)", err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| @@ -696,6 +785,10 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if comment != nil { | ||||||
|  | 		uploadFiles(ctx, issue.Id, comment.Id) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Notify watchers. | 	// Notify watchers. | ||||||
| 	act := &models.Action{ | 	act := &models.Action{ | ||||||
| 		ActUserId:    ctx.User.Id, | 		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") | 	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" .}} | {{template "repo/toolbar" .}} | ||||||
| <div id="body" class="container"> | <div id="body" class="container"> | ||||||
|     <div id="issue"> |     <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}} |             {{.CsrfTokenHtml}} | ||||||
|             {{template "base/alert" .}} |             {{template "base/alert" .}} | ||||||
|             <div class="col-md-1"> |             <div class="col-md-1"> | ||||||
| @@ -101,8 +101,17 @@ | |||||||
|                         <div class="tab-pane issue-preview-content" id="issue-preview">loading...</div> |                         <div class="tab-pane issue-preview-content" id="issue-preview">loading...</div> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|  |                 {{if .AttachmentsEnabled}} | ||||||
|  |                 <div id="attached"> | ||||||
|  |                     <div id="attached-list"></div> | ||||||
|  |                 </div> | ||||||
|  |                 {{end}} | ||||||
|                 <div class="text-right panel-body"> |                 <div class="text-right panel-body"> | ||||||
|                     <div class="form-group"> |                     <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"/> |                         <input type="hidden" value="id" name="repo-id"/> | ||||||
|                         <button class="btn-success btn">Create new issue</button> |                         <button class="btn-success btn">Create new issue</button> | ||||||
|                     </div> |                     </div> | ||||||
|   | |||||||
| @@ -45,8 +45,19 @@ | |||||||
|                                         <div class="tab-pane issue-preview-content" id="issue-edit-preview">Loading...</div> |                                         <div class="tab-pane issue-preview-content" id="issue-edit-preview">Loading...</div> | ||||||
|                                     </div> |                                     </div> | ||||||
|                                 </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> |                     </div> | ||||||
|                     {{range .Comments}} |                     {{range .Comments}} | ||||||
|                     {{/* 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE, 4 = COMMIT, 5 = PULL */}} |                     {{/* 0 = COMMENT, 1 = REOPEN, 2 = CLOSE, 3 = ISSUE, 4 = COMMIT, 5 = PULL */}} | ||||||
| @@ -63,6 +74,17 @@ | |||||||
|                             <div class="panel-body markdown"> |                             <div class="panel-body markdown"> | ||||||
|                                 {{str2html .Content}} |                                 {{str2html .Content}} | ||||||
|                             </div> |                             </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> | ||||||
|                     </div> |                     </div> | ||||||
|                     {{else if eq .Type 1}} |                     {{else if eq .Type 1}} | ||||||
| @@ -95,7 +117,7 @@ | |||||||
|                     <hr class="issue-line"/> |                     <hr class="issue-line"/> | ||||||
|                     {{if .SignedUser}}<div class="issue-child issue-reply"> |                     {{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> |                     <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}} |                         {{.CsrfTokenHtml}} | ||||||
|                         <div class="panel-body"> |                         <div class="panel-body"> | ||||||
|                             <div class="form-group"> |                             <div class="form-group"> | ||||||
| @@ -115,8 +137,17 @@ | |||||||
|                                     <div class="tab-pane issue-preview-content" id="issue-preview">Loading...</div> |                                     <div class="tab-pane issue-preview-content" id="issue-preview">Loading...</div> | ||||||
|                                 </div> |                                 </div> | ||||||
|                             </div> |                             </div> | ||||||
|  |                             {{if .AttachmentsEnabled}} | ||||||
|  |                             <div id="attached"> | ||||||
|  |                                 <div id="attached-list"></div> | ||||||
|  |                             </div> | ||||||
|  |                             {{end}} | ||||||
|                             <div class="text-right"> |                             <div class="text-right"> | ||||||
|                                 <div class="form-group"> |                                 <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}} |                                     {{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-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}}   |                                     <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