WIP: Allow attachments for comments
This commit is contained in:
		
							parent
							
								
									6e9f1c52b1
								
							
						
					
					
						commit
						4617bef895
					
				
					 8 changed files with 366 additions and 17 deletions
				
			
		|  | @ -206,7 +206,7 @@ func runWeb(*cli.Context) { | |||
| 		r.Post("/:org/teams/new", bindIgnErr(auth.CreateTeamForm{}), org.NewTeamPost) | ||||
| 		r.Get("/:org/teams/:team/edit", org.EditTeam) | ||||
| 
 | ||||
| 		r.Get("/:org/team/:team",org.SingleTeam) | ||||
| 		r.Get("/:org/team/:team", org.SingleTeam) | ||||
| 
 | ||||
| 		r.Get("/:org/settings", org.Settings) | ||||
| 		r.Post("/:org/settings", bindIgnErr(auth.OrgSettingForm{}), org.SettingsPost) | ||||
|  | @ -238,6 +238,9 @@ func runWeb(*cli.Context) { | |||
| 			r.Post("/:index/label", repo.UpdateIssueLabel) | ||||
| 			r.Post("/:index/milestone", repo.UpdateIssueMilestone) | ||||
| 			r.Post("/:index/assignee", repo.UpdateAssignee) | ||||
| 			r.Post("/:index/attachment", repo.IssuePostAttachment) | ||||
| 			r.Post("/:index/attachment/:id", repo.IssuePostAttachment) | ||||
| 			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) | ||||
|  |  | |||
|  | @ -180,6 +180,11 @@ SESSION_ID_HASHKEY = | |||
| SERVICE = server | ||||
| DISABLE_GRAVATAR = false | ||||
| 
 | ||||
| [attachment] | ||||
| PATH =  | ||||
| ; One or more allowed types, e.g. image/jpeg|image/png | ||||
| ALLOWED_TYPES =  | ||||
| 
 | ||||
| [log] | ||||
| ROOT_PATH = | ||||
| ; Either "console", "file", "conn", "smtp" or "database", default is "console" | ||||
|  |  | |||
							
								
								
									
										192
									
								
								models/issue.go
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								models/issue.go
									
									
									
									
									
								
							|  | @ -7,19 +7,24 @@ package models | |||
| import ( | ||||
| 	"bytes" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"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") | ||||
| 	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") | ||||
| ) | ||||
| 
 | ||||
| // Issue represents an issue or pull request of repository.
 | ||||
|  | @ -91,6 +96,14 @@ func (i *Issue) GetAssignee() (err error) { | |||
| 	return err | ||||
| } | ||||
| 
 | ||||
| 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() | ||||
|  | @ -795,17 +808,19 @@ type Comment struct { | |||
| } | ||||
| 
 | ||||
| // CreateComment creates comment of issue or commit.
 | ||||
| func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error { | ||||
| func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, 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.
 | ||||
|  | @ -814,22 +829,38 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, c | |||
| 		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 IT_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 IT_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() | ||||
| } | ||||
| 
 | ||||
| // GetIssueComments returns list of comment by given issue id.
 | ||||
|  | @ -838,3 +869,138 @@ 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, error) { | ||||
| 	return GetAttachmentsByComment(c.Id) | ||||
| } | ||||
| 
 | ||||
| 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 | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| // 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).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) | ||||
| } | ||||
| 
 | ||||
| // AssignAttachment assigns the given attachment to the specified comment
 | ||||
| func AssignAttachment(issueId, commentId, attachmentId int64) error { | ||||
| 	a, err := GetAttachmentById(attachmentId) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if a.IssueId != issueId { | ||||
| 		return ErrAttachmentNotLinked | ||||
| 	} | ||||
| 
 | ||||
| 	a.CommentId = commentId | ||||
| 
 | ||||
| 	_, err = x.Id(a.Id).Update(a) | ||||
| 	return err | ||||
| } | ||||
|  |  | |||
|  | @ -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() { | ||||
|  |  | |||
|  | @ -71,6 +71,10 @@ var ( | |||
| 	LogModes    []string | ||||
| 	LogConfigs  []string | ||||
| 
 | ||||
| 	// Attachment settings.
 | ||||
| 	AttachmentPath         string | ||||
| 	AttachmentAllowedTypes string | ||||
| 
 | ||||
| 	// Cache settings.
 | ||||
| 	Cache        cache.Cache | ||||
| 	CacheAdapter string | ||||
|  | @ -166,6 +170,13 @@ 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", "*/*") | ||||
| 
 | ||||
| 	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 { | ||||
|  |  | |||
|  | @ -520,6 +520,19 @@ function initIssue() { | |||
|         }); | ||||
|     }()); | ||||
| 
 | ||||
|     (function() { | ||||
|         var $attached = $("#attached"); | ||||
|         var $attachments = $("input[name=attachments]"); | ||||
|         var $addButton = $("#attachments-button"); | ||||
| 
 | ||||
|         var accepted = $addButton.attr("data-accept"); | ||||
| 
 | ||||
|         $addButton.on("click", function() { | ||||
|             // TODO: (nuss-justin): open dialog, upload file, add id to list, add file to $attached list
 | ||||
|             return false; | ||||
|         }); | ||||
|     }()); | ||||
| 
 | ||||
|     // issue edit mode
 | ||||
|     (function () { | ||||
|         $("#issue-edit-btn").on("click", function () { | ||||
|  |  | |||
|  | @ -6,6 +6,9 @@ package repo | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"mime" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | @ -396,6 +399,8 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
| 		comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink)) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes | ||||
| 
 | ||||
| 	ctx.Data["Title"] = issue.Name | ||||
| 	ctx.Data["Issue"] = issue | ||||
| 	ctx.Data["Comments"] = comments | ||||
|  | @ -670,7 +675,7 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||
| 				cmtType = models.IT_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 | ||||
| 			} | ||||
|  | @ -678,12 +683,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.IT_PLAIN, content); err != nil { | ||||
| 			if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content, nil); err != nil { | ||||
| 				ctx.Handle(500, "issue.Comment(create comment)", err) | ||||
| 				return | ||||
| 			} | ||||
|  | @ -709,6 +716,24 @@ func Comment(ctx *middleware.Context, params martini.Params) { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	attachments := strings.Split(params["attachments"], ",") | ||||
| 
 | ||||
| 	for _, a := range attachments { | ||||
| 		aId, err := base.StrTo(a).Int64() | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			ctx.Handle(400, "issue.Comment(base.StrTo.Int64)", err) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		err = models.AssignAttachment(issue.Id, comment.Id, aId) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			ctx.Handle(400, "issue.Comment(models.AssignAttachment)", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Notify watchers.
 | ||||
| 	act := &models.Action{ | ||||
| 		ActUserId:    ctx.User.Id, | ||||
|  | @ -985,3 +1010,118 @@ func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form au | |||
| 
 | ||||
| 	ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones") | ||||
| } | ||||
| 
 | ||||
| func IssuePostAttachment(ctx *middleware.Context, params martini.Params) { | ||||
| 	issueId, _ := base.StrTo(params["index"]).Int64() | ||||
| 
 | ||||
| 	if issueId == 0 { | ||||
| 		ctx.Handle(400, "issue.IssuePostAttachment", nil) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	commentId, err := base.StrTo(params["id"]).Int64() | ||||
| 
 | ||||
| 	if err != nil && len(params["id"]) > 0 { | ||||
| 		ctx.JSON(400, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": "invalid comment id", | ||||
| 		}) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	file, header, err := ctx.Req.FormFile("attachment") | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		ctx.JSON(400, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": "upload error", | ||||
| 		}) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	defer file.Close() | ||||
| 
 | ||||
| 	// check mime type, write to file, insert attachment to db
 | ||||
| 	allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|") | ||||
| 	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.JSON(400, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": "mime type not allowed", | ||||
| 		}) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_") | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		ctx.JSON(500, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": "internal server error", | ||||
| 		}) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	defer out.Close() | ||||
| 
 | ||||
| 	_, err = io.Copy(out, file) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		ctx.JSON(500, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": "internal server error", | ||||
| 		}) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	a, err := models.CreateAttachment(issueId, commentId, header.Filename, out.Name()) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		ctx.JSON(500, map[string]interface{}{ | ||||
| 			"ok":    false, | ||||
| 			"error": "internal server error", | ||||
| 		}) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(500, map[string]interface{}{ | ||||
| 		"ok": true, | ||||
| 		"id": a.Id, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| 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) | ||||
| } | ||||
|  |  | |||
|  | @ -62,6 +62,11 @@ | |||
|                             <div class="panel-body markdown"> | ||||
|                                 {{str2html .Content}} | ||||
|                             </div> | ||||
|                             <div class="attachments"> | ||||
|                                 {{range .Attachments}} | ||||
|                                 <a class="attachment" href="{{.IssueId}}/attachment/{{.Id}}">{{.Name}}</a> | ||||
|                                 {{end}} | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     {{else if eq .Type 1}} | ||||
|  | @ -103,8 +108,14 @@ | |||
|                                     <div class="tab-pane issue-preview-content" id="issue-preview">Loading...</div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                             <div> | ||||
|                                 <div id="attached"></div> | ||||
|                             </div> | ||||
|                             <div class="text-right"> | ||||
|                                 <div class="form-group"> | ||||
|                                     <input type="hidden" name="attachments" value="" /> | ||||
|                                     <button data-accept="{{AllowedTypes}}" class="btn-default btn attachment-add" id="attachments-button">Add Attachments...</button> | ||||
| 
 | ||||
|                                     {{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}}   | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue