You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue.go 28 kB

11 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repo
  5. import (
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "github.com/Unknwon/com"
  13. "github.com/Unknwon/paginater"
  14. "github.com/gogits/gogs/models"
  15. "github.com/gogits/gogs/modules/auth"
  16. "github.com/gogits/gogs/modules/base"
  17. "github.com/gogits/gogs/modules/log"
  18. "github.com/gogits/gogs/modules/mailer"
  19. "github.com/gogits/gogs/modules/middleware"
  20. "github.com/gogits/gogs/modules/setting"
  21. )
  22. const (
  23. ISSUES base.TplName = "repo/issue/list"
  24. ISSUE_NEW base.TplName = "repo/issue/new"
  25. ISSUE_VIEW base.TplName = "repo/issue/view"
  26. LABELS base.TplName = "repo/issue/labels"
  27. MILESTONE base.TplName = "repo/issue/milestones"
  28. MILESTONE_NEW base.TplName = "repo/issue/milestone_new"
  29. MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit"
  30. )
  31. var (
  32. ErrFileTypeForbidden = errors.New("File type is not allowed")
  33. ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded")
  34. )
  35. func RetrieveLabels(ctx *middleware.Context) {
  36. labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID)
  37. if err != nil {
  38. ctx.Handle(500, "RetrieveLabels.GetLabels: %v", err)
  39. return
  40. }
  41. for _, l := range labels {
  42. l.CalOpenIssues()
  43. }
  44. ctx.Data["Labels"] = labels
  45. ctx.Data["NumLabels"] = len(labels)
  46. }
  47. func Issues(ctx *middleware.Context) {
  48. isPullList := ctx.Params(":type") == "pulls"
  49. if isPullList {
  50. ctx.Data["Title"] = ctx.Tr("repo.pulls")
  51. ctx.Data["PageIsPullList"] = true
  52. } else {
  53. ctx.Data["Title"] = ctx.Tr("repo.issues")
  54. ctx.Data["PageIsIssueList"] = true
  55. }
  56. viewType := ctx.Query("type")
  57. sortType := ctx.Query("sort")
  58. types := []string{"assigned", "created_by", "mentioned"}
  59. if !com.IsSliceContainsStr(types, viewType) {
  60. viewType = "all"
  61. }
  62. // Must sign in to see issues about you.
  63. if viewType != "all" && !ctx.IsSigned {
  64. ctx.SetCookie("redirect_to", "/"+url.QueryEscape(setting.AppSubUrl+ctx.Req.RequestURI), 0, setting.AppSubUrl)
  65. ctx.Redirect(setting.AppSubUrl + "/user/login")
  66. return
  67. }
  68. var (
  69. assigneeID = ctx.QueryInt64("assignee")
  70. posterID int64
  71. )
  72. filterMode := models.FM_ALL
  73. switch viewType {
  74. case "assigned":
  75. filterMode = models.FM_ASSIGN
  76. assigneeID = ctx.User.Id
  77. case "created_by":
  78. filterMode = models.FM_CREATE
  79. posterID = ctx.User.Id
  80. case "mentioned":
  81. filterMode = models.FM_MENTION
  82. }
  83. var uid int64 = -1
  84. if ctx.IsSigned {
  85. uid = ctx.User.Id
  86. }
  87. repo := ctx.Repo.Repository
  88. selectLabels := ctx.Query("labels")
  89. milestoneID := ctx.QueryInt64("milestone")
  90. isShowClosed := ctx.Query("state") == "closed"
  91. issueStats := models.GetIssueStats(&models.IssueStatsOptions{
  92. RepoID: repo.ID,
  93. UserID: uid,
  94. LabelID: com.StrTo(selectLabels).MustInt64(),
  95. MilestoneID: milestoneID,
  96. AssigneeID: assigneeID,
  97. FilterMode: filterMode,
  98. IsPull: isPullList,
  99. })
  100. page := ctx.QueryInt("page")
  101. if page <= 1 {
  102. page = 1
  103. }
  104. var total int
  105. if !isShowClosed {
  106. total = int(issueStats.OpenCount)
  107. } else {
  108. total = int(issueStats.ClosedCount)
  109. }
  110. ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5)
  111. // Get issues.
  112. issues, err := models.Issues(&models.IssuesOptions{
  113. UserID: uid,
  114. AssigneeID: assigneeID,
  115. RepoID: repo.ID,
  116. PosterID: posterID,
  117. MilestoneID: milestoneID,
  118. Page: page,
  119. IsClosed: isShowClosed,
  120. IsMention: filterMode == models.FM_MENTION,
  121. IsPull: isPullList,
  122. Labels: selectLabels,
  123. SortType: sortType,
  124. })
  125. if err != nil {
  126. ctx.Handle(500, "Issues: %v", err)
  127. return
  128. }
  129. // Get issue-user relations.
  130. pairs, err := models.GetIssueUsers(repo.ID, posterID, isShowClosed)
  131. if err != nil {
  132. ctx.Handle(500, "GetIssueUsers: %v", err)
  133. return
  134. }
  135. // Get posters.
  136. for i := range issues {
  137. if err = issues[i].GetPoster(); err != nil {
  138. ctx.Handle(500, "GetPoster", fmt.Errorf("[#%d]%v", issues[i].ID, err))
  139. return
  140. }
  141. if err = issues[i].GetLabels(); err != nil {
  142. ctx.Handle(500, "GetLabels", fmt.Errorf("[#%d]%v", issues[i].ID, err))
  143. return
  144. }
  145. if !ctx.IsSigned {
  146. issues[i].IsRead = true
  147. continue
  148. }
  149. // Check read status.
  150. idx := models.PairsContains(pairs, issues[i].ID, ctx.User.Id)
  151. if idx > -1 {
  152. issues[i].IsRead = pairs[idx].IsRead
  153. } else {
  154. issues[i].IsRead = true
  155. }
  156. }
  157. ctx.Data["Issues"] = issues
  158. // Get milestones.
  159. ctx.Data["Milestones"], err = models.GetAllRepoMilestones(repo.ID)
  160. if err != nil {
  161. ctx.Handle(500, "GetAllRepoMilestones: %v", err)
  162. return
  163. }
  164. // Get assignees.
  165. ctx.Data["Assignees"], err = repo.GetAssignees()
  166. if err != nil {
  167. ctx.Handle(500, "GetAssignees: %v", err)
  168. return
  169. }
  170. ctx.Data["IssueStats"] = issueStats
  171. ctx.Data["SelectLabels"] = com.StrTo(selectLabels).MustInt64()
  172. ctx.Data["ViewType"] = viewType
  173. ctx.Data["SortType"] = sortType
  174. ctx.Data["MilestoneID"] = milestoneID
  175. ctx.Data["AssigneeID"] = assigneeID
  176. ctx.Data["IsShowClosed"] = isShowClosed
  177. if isShowClosed {
  178. ctx.Data["State"] = "closed"
  179. } else {
  180. ctx.Data["State"] = "open"
  181. }
  182. ctx.HTML(200, ISSUES)
  183. }
  184. func renderAttachmentSettings(ctx *middleware.Context) {
  185. ctx.Data["RequireDropzone"] = true
  186. ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
  187. ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
  188. ctx.Data["AttachmentMaxSize"] = setting.AttachmentMaxSize
  189. ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles
  190. }
  191. func RetrieveRepoMilestonesAndAssignees(ctx *middleware.Context, repo *models.Repository) {
  192. var err error
  193. ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
  194. if err != nil {
  195. ctx.Handle(500, "GetMilestones: %v", err)
  196. return
  197. }
  198. ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
  199. if err != nil {
  200. ctx.Handle(500, "GetMilestones: %v", err)
  201. return
  202. }
  203. ctx.Data["Assignees"], err = repo.GetAssignees()
  204. if err != nil {
  205. ctx.Handle(500, "GetAssignees: %v", err)
  206. return
  207. }
  208. }
  209. func RetrieveRepoMetas(ctx *middleware.Context, repo *models.Repository) []*models.Label {
  210. if !ctx.Repo.IsAdmin() {
  211. return nil
  212. }
  213. labels, err := models.GetLabelsByRepoID(repo.ID)
  214. if err != nil {
  215. ctx.Handle(500, "GetLabelsByRepoID: %v", err)
  216. return nil
  217. }
  218. ctx.Data["Labels"] = labels
  219. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  220. if ctx.Written() {
  221. return nil
  222. }
  223. return labels
  224. }
  225. func NewIssue(ctx *middleware.Context) {
  226. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  227. ctx.Data["PageIsIssueList"] = true
  228. renderAttachmentSettings(ctx)
  229. RetrieveRepoMetas(ctx, ctx.Repo.Repository)
  230. if ctx.Written() {
  231. return
  232. }
  233. ctx.HTML(200, ISSUE_NEW)
  234. }
  235. func ValidateRepoMetas(ctx *middleware.Context, form auth.CreateIssueForm) ([]int64, int64, int64) {
  236. var (
  237. repo = ctx.Repo.Repository
  238. err error
  239. )
  240. labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository)
  241. if ctx.Written() {
  242. return nil, 0, 0
  243. }
  244. if !ctx.Repo.IsAdmin() {
  245. return nil, 0, 0
  246. }
  247. // Check labels.
  248. labelIDs := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
  249. labelIDMark := base.Int64sToMap(labelIDs)
  250. hasSelected := false
  251. for i := range labels {
  252. if labelIDMark[labels[i].ID] {
  253. labels[i].IsChecked = true
  254. hasSelected = true
  255. }
  256. }
  257. ctx.Data["HasSelectedLabel"] = hasSelected
  258. ctx.Data["label_ids"] = form.LabelIDs
  259. ctx.Data["Labels"] = labels
  260. // Check milestone.
  261. milestoneID := form.MilestoneID
  262. if milestoneID > 0 {
  263. ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
  264. if err != nil {
  265. ctx.Handle(500, "GetMilestoneByID: %v", err)
  266. return nil, 0, 0
  267. }
  268. ctx.Data["milestone_id"] = milestoneID
  269. }
  270. // Check assignee.
  271. assigneeID := form.AssigneeID
  272. if assigneeID > 0 {
  273. ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID)
  274. if err != nil {
  275. ctx.Handle(500, "GetAssigneeByID: %v", err)
  276. return nil, 0, 0
  277. }
  278. ctx.Data["assignee_id"] = assigneeID
  279. }
  280. return labelIDs, milestoneID, assigneeID
  281. }
  282. func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
  283. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  284. ctx.Data["PageIsIssueList"] = true
  285. renderAttachmentSettings(ctx)
  286. var (
  287. repo = ctx.Repo.Repository
  288. attachments []string
  289. )
  290. labelIDs, milestoneID, assigneeID := ValidateRepoMetas(ctx, form)
  291. if ctx.Written() {
  292. return
  293. }
  294. if setting.AttachmentEnabled {
  295. attachments = form.Attachments
  296. }
  297. if ctx.HasError() {
  298. ctx.HTML(200, ISSUE_NEW)
  299. return
  300. }
  301. issue := &models.Issue{
  302. RepoID: ctx.Repo.Repository.ID,
  303. Index: int64(repo.NumIssues) + int64(repo.NumPulls) + 1,
  304. Name: form.Title,
  305. PosterID: ctx.User.Id,
  306. Poster: ctx.User,
  307. MilestoneID: milestoneID,
  308. AssigneeID: assigneeID,
  309. Content: form.Content,
  310. }
  311. if err := models.NewIssue(repo, issue, labelIDs, attachments); err != nil {
  312. ctx.Handle(500, "NewIssue", err)
  313. return
  314. }
  315. // Update mentions.
  316. mentions := base.MentionPattern.FindAllString(issue.Content, -1)
  317. if len(mentions) > 0 {
  318. for i := range mentions {
  319. mentions[i] = mentions[i][1:]
  320. }
  321. if err := models.UpdateMentions(mentions, issue.ID); err != nil {
  322. ctx.Handle(500, "UpdateMentions", err)
  323. return
  324. }
  325. }
  326. // Mail watchers and mentions.
  327. if setting.Service.EnableNotifyMail {
  328. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, repo, issue)
  329. if err != nil {
  330. ctx.Handle(500, "SendIssueNotifyMail", err)
  331. return
  332. }
  333. tos = append(tos, ctx.User.LowerName)
  334. newTos := make([]string, 0, len(mentions))
  335. for _, m := range mentions {
  336. if com.IsSliceContainsStr(tos, m) {
  337. continue
  338. }
  339. newTos = append(newTos, m)
  340. }
  341. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  342. repo, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  343. ctx.Handle(500, "SendIssueMentionMail", err)
  344. return
  345. }
  346. }
  347. log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
  348. ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
  349. }
  350. func UploadIssueAttachment(ctx *middleware.Context) {
  351. if !setting.AttachmentEnabled {
  352. ctx.Error(404, "attachment is not enabled")
  353. return
  354. }
  355. allowedTypes := strings.Split(setting.AttachmentAllowedTypes, ",")
  356. file, header, err := ctx.Req.FormFile("file")
  357. if err != nil {
  358. ctx.Error(500, fmt.Sprintf("FormFile: %v", err))
  359. return
  360. }
  361. defer file.Close()
  362. buf := make([]byte, 1024)
  363. n, _ := file.Read(buf)
  364. if n > 0 {
  365. buf = buf[:n]
  366. }
  367. fileType := http.DetectContentType(buf)
  368. allowed := false
  369. for _, t := range allowedTypes {
  370. t := strings.Trim(t, " ")
  371. if t == "*/*" || t == fileType {
  372. allowed = true
  373. break
  374. }
  375. }
  376. if !allowed {
  377. ctx.Error(400, ErrFileTypeForbidden.Error())
  378. return
  379. }
  380. attach, err := models.NewAttachment(header.Filename, buf, file)
  381. if err != nil {
  382. ctx.Error(500, fmt.Sprintf("NewAttachment: %v", err))
  383. return
  384. }
  385. log.Trace("New attachment uploaded: %s", attach.UUID)
  386. ctx.JSON(200, map[string]string{
  387. "uuid": attach.UUID,
  388. })
  389. }
  390. func ViewIssue(ctx *middleware.Context) {
  391. ctx.Data["RequireDropzone"] = true
  392. renderAttachmentSettings(ctx)
  393. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  394. if err != nil {
  395. if models.IsErrIssueNotExist(err) {
  396. ctx.Handle(404, "GetIssueByIndex", err)
  397. } else {
  398. ctx.Handle(500, "GetIssueByIndex", err)
  399. }
  400. return
  401. }
  402. ctx.Data["Title"] = issue.Name
  403. // Make sure type and URL matches.
  404. if ctx.Params(":type") == "issues" && issue.IsPull {
  405. ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
  406. return
  407. } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
  408. ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
  409. return
  410. }
  411. if issue.IsPull {
  412. ctx.Data["PageIsPullList"] = true
  413. ctx.Data["PageIsPullConversation"] = true
  414. } else {
  415. ctx.Data["PageIsIssueList"] = true
  416. }
  417. if err = issue.GetPoster(); err != nil {
  418. ctx.Handle(500, "GetPoster", err)
  419. return
  420. }
  421. issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink))
  422. repo := ctx.Repo.Repository
  423. // Get more information if it's a pull request.
  424. if issue.IsPull {
  425. if issue.HasMerged {
  426. ctx.Data["DisableStatusChange"] = issue.HasMerged
  427. PrepareMergedViewPullInfo(ctx, issue)
  428. } else {
  429. PrepareViewPullInfo(ctx, issue)
  430. }
  431. if ctx.Written() {
  432. return
  433. }
  434. }
  435. // Metas.
  436. // Check labels.
  437. if err = issue.GetLabels(); err != nil {
  438. ctx.Handle(500, "GetLabels", err)
  439. return
  440. }
  441. labelIDMark := make(map[int64]bool)
  442. for i := range issue.Labels {
  443. labelIDMark[issue.Labels[i].ID] = true
  444. }
  445. labels, err := models.GetLabelsByRepoID(repo.ID)
  446. if err != nil {
  447. ctx.Handle(500, "GetLabelsByRepoID: %v", err)
  448. return
  449. }
  450. hasSelected := false
  451. for i := range labels {
  452. if labelIDMark[labels[i].ID] {
  453. labels[i].IsChecked = true
  454. hasSelected = true
  455. }
  456. }
  457. ctx.Data["HasSelectedLabel"] = hasSelected
  458. ctx.Data["Labels"] = labels
  459. // Check milestone and assignee.
  460. if ctx.Repo.IsAdmin() {
  461. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  462. if ctx.Written() {
  463. return
  464. }
  465. }
  466. if ctx.IsSigned {
  467. // Update issue-user.
  468. if err = issue.ReadBy(ctx.User.Id); err != nil {
  469. ctx.Handle(500, "ReadBy", err)
  470. return
  471. }
  472. }
  473. var (
  474. tag models.CommentTag
  475. ok bool
  476. marked = make(map[int64]models.CommentTag)
  477. comment *models.Comment
  478. )
  479. // Render comments.
  480. for _, comment = range issue.Comments {
  481. if comment.Type == models.COMMENT_TYPE_COMMENT {
  482. comment.RenderedContent = string(base.RenderMarkdown([]byte(comment.Content), ctx.Repo.RepoLink))
  483. // Check tag.
  484. tag, ok = marked[comment.PosterID]
  485. if ok {
  486. comment.ShowTag = tag
  487. continue
  488. }
  489. if repo.IsOwnedBy(comment.PosterID) ||
  490. (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) {
  491. comment.ShowTag = models.COMMENT_TAG_OWNER
  492. } else if comment.Poster.IsAdminOfRepo(repo) {
  493. comment.ShowTag = models.COMMENT_TAG_ADMIN
  494. } else if comment.PosterID == issue.PosterID {
  495. comment.ShowTag = models.COMMENT_TAG_POSTER
  496. }
  497. marked[comment.PosterID] = comment.ShowTag
  498. }
  499. }
  500. ctx.Data["Issue"] = issue
  501. ctx.Data["IsIssueOwner"] = ctx.Repo.IsAdmin() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))
  502. ctx.Data["SignInLink"] = setting.AppSubUrl + "/user/login"
  503. ctx.HTML(200, ISSUE_VIEW)
  504. }
  505. func getActionIssue(ctx *middleware.Context) *models.Issue {
  506. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  507. if err != nil {
  508. if models.IsErrIssueNotExist(err) {
  509. ctx.Error(404, "GetIssueByIndex")
  510. } else {
  511. ctx.Handle(500, "GetIssueByIndex", err)
  512. }
  513. return nil
  514. }
  515. return issue
  516. }
  517. func UpdateIssueTitle(ctx *middleware.Context) {
  518. issue := getActionIssue(ctx)
  519. if ctx.Written() {
  520. return
  521. }
  522. if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) {
  523. ctx.Error(403)
  524. return
  525. }
  526. issue.Name = ctx.Query("title")
  527. if len(issue.Name) == 0 {
  528. ctx.Error(204)
  529. return
  530. }
  531. if err := models.UpdateIssue(issue); err != nil {
  532. ctx.Handle(500, "UpdateIssue", err)
  533. return
  534. }
  535. ctx.JSON(200, map[string]interface{}{
  536. "title": issue.Name,
  537. })
  538. }
  539. func UpdateIssueContent(ctx *middleware.Context) {
  540. issue := getActionIssue(ctx)
  541. if ctx.Written() {
  542. return
  543. }
  544. if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) {
  545. ctx.Error(403)
  546. return
  547. }
  548. issue.Content = ctx.Query("content")
  549. if err := models.UpdateIssue(issue); err != nil {
  550. ctx.Handle(500, "UpdateIssue", err)
  551. return
  552. }
  553. ctx.JSON(200, map[string]interface{}{
  554. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Query("context"))),
  555. })
  556. }
  557. func UpdateIssueLabel(ctx *middleware.Context) {
  558. issue := getActionIssue(ctx)
  559. if ctx.Written() {
  560. return
  561. }
  562. if ctx.Query("action") == "clear" {
  563. if err := issue.ClearLabels(); err != nil {
  564. ctx.Handle(500, "ClearLabels", err)
  565. return
  566. }
  567. } else {
  568. isAttach := ctx.Query("action") == "attach"
  569. label, err := models.GetLabelByID(ctx.QueryInt64("id"))
  570. if err != nil {
  571. if models.IsErrLabelNotExist(err) {
  572. ctx.Error(404, "GetLabelByID")
  573. } else {
  574. ctx.Handle(500, "GetLabelByID", err)
  575. }
  576. return
  577. }
  578. if isAttach && !issue.HasLabel(label.ID) {
  579. if err = issue.AddLabel(label); err != nil {
  580. ctx.Handle(500, "AddLabel", err)
  581. return
  582. }
  583. } else if !isAttach && issue.HasLabel(label.ID) {
  584. if err = issue.RemoveLabel(label); err != nil {
  585. ctx.Handle(500, "RemoveLabel", err)
  586. return
  587. }
  588. }
  589. }
  590. ctx.JSON(200, map[string]interface{}{
  591. "ok": true,
  592. })
  593. }
  594. func UpdateIssueMilestone(ctx *middleware.Context) {
  595. issue := getActionIssue(ctx)
  596. if ctx.Written() {
  597. return
  598. }
  599. oldMid := issue.MilestoneID
  600. mid := ctx.QueryInt64("id")
  601. if oldMid == mid {
  602. ctx.JSON(200, map[string]interface{}{
  603. "ok": true,
  604. })
  605. return
  606. }
  607. // Not check for invalid milestone id and give responsibility to owners.
  608. issue.MilestoneID = mid
  609. if err := models.ChangeMilestoneAssign(oldMid, issue); err != nil {
  610. ctx.Handle(500, "ChangeMilestoneAssign", err)
  611. return
  612. }
  613. ctx.JSON(200, map[string]interface{}{
  614. "ok": true,
  615. })
  616. }
  617. func UpdateIssueAssignee(ctx *middleware.Context) {
  618. issue := getActionIssue(ctx)
  619. if ctx.Written() {
  620. return
  621. }
  622. aid := ctx.QueryInt64("id")
  623. if issue.AssigneeID == aid {
  624. ctx.JSON(200, map[string]interface{}{
  625. "ok": true,
  626. })
  627. return
  628. }
  629. // Not check for invalid assignee id and give responsibility to owners.
  630. issue.AssigneeID = aid
  631. if err := models.UpdateIssueUserByAssignee(issue); err != nil {
  632. ctx.Handle(500, "UpdateIssueUserByAssignee: %v", err)
  633. return
  634. }
  635. ctx.JSON(200, map[string]interface{}{
  636. "ok": true,
  637. })
  638. }
  639. func NewComment(ctx *middleware.Context, form auth.CreateCommentForm) {
  640. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  641. if err != nil {
  642. if models.IsErrIssueNotExist(err) {
  643. ctx.Handle(404, "GetIssueByIndex", err)
  644. } else {
  645. ctx.Handle(500, "GetIssueByIndex", err)
  646. }
  647. return
  648. }
  649. var attachments []string
  650. if setting.AttachmentEnabled {
  651. attachments = form.Attachments
  652. }
  653. if ctx.HasError() {
  654. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  655. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  656. return
  657. }
  658. // Check if issue owner/poster changes the status of issue.
  659. if (ctx.Repo.IsOwner() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))) &&
  660. (form.Status == "reopen" || form.Status == "close") &&
  661. !(issue.IsPull && issue.HasMerged) {
  662. issue.Repo = ctx.Repo.Repository
  663. if err = issue.ChangeStatus(ctx.User, form.Status == "close"); err != nil {
  664. ctx.Handle(500, "ChangeStatus", err)
  665. return
  666. }
  667. log.Trace("%s Issue[%d] status changed: %v", ctx.Req.RequestURI, issue.ID, !issue.IsClosed)
  668. }
  669. // Fix #321: Allow empty comments, as long as we have attachments.
  670. if len(form.Content) == 0 && len(attachments) == 0 {
  671. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  672. return
  673. }
  674. comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
  675. if err != nil {
  676. ctx.Handle(500, "CreateIssueComment", err)
  677. return
  678. }
  679. // Update mentions.
  680. mentions := base.MentionPattern.FindAllString(comment.Content, -1)
  681. if len(mentions) > 0 {
  682. for i := range mentions {
  683. mentions[i] = mentions[i][1:]
  684. }
  685. if err := models.UpdateMentions(mentions, issue.ID); err != nil {
  686. ctx.Handle(500, "UpdateMentions", err)
  687. return
  688. }
  689. }
  690. // Mail watchers and mentions.
  691. if setting.Service.EnableNotifyMail {
  692. issue.Content = form.Content
  693. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  694. if err != nil {
  695. ctx.Handle(500, "SendIssueNotifyMail", err)
  696. return
  697. }
  698. tos = append(tos, ctx.User.LowerName)
  699. newTos := make([]string, 0, len(mentions))
  700. for _, m := range mentions {
  701. if com.IsSliceContainsStr(tos, m) {
  702. continue
  703. }
  704. newTos = append(newTos, m)
  705. }
  706. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  707. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  708. ctx.Handle(500, "SendIssueMentionMail", err)
  709. return
  710. }
  711. }
  712. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  713. ctx.Redirect(fmt.Sprintf("%s/issues/%d#%s", ctx.Repo.RepoLink, issue.Index, comment.HashTag()))
  714. }
  715. func UpdateCommentContent(ctx *middleware.Context) {
  716. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  717. if err != nil {
  718. if models.IsErrCommentNotExist(err) {
  719. ctx.Error(404, "GetCommentByID")
  720. } else {
  721. ctx.Handle(500, "GetCommentByID", err)
  722. }
  723. return
  724. }
  725. if !ctx.IsSigned || (ctx.User.Id != comment.PosterID && !ctx.Repo.IsAdmin()) {
  726. ctx.Error(403)
  727. return
  728. } else if comment.Type != models.COMMENT_TYPE_COMMENT {
  729. ctx.Error(204)
  730. return
  731. }
  732. comment.Content = ctx.Query("content")
  733. if len(comment.Content) == 0 {
  734. ctx.JSON(200, map[string]interface{}{
  735. "content": "",
  736. })
  737. return
  738. }
  739. if err := models.UpdateComment(comment); err != nil {
  740. ctx.Handle(500, "UpdateComment", err)
  741. return
  742. }
  743. ctx.JSON(200, map[string]interface{}{
  744. "content": string(base.RenderMarkdown([]byte(comment.Content), ctx.Query("context"))),
  745. })
  746. }
  747. func Labels(ctx *middleware.Context) {
  748. ctx.Data["Title"] = ctx.Tr("repo.labels")
  749. ctx.Data["PageIsLabels"] = true
  750. ctx.Data["RequireMinicolors"] = true
  751. ctx.HTML(200, LABELS)
  752. }
  753. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  754. ctx.Data["Title"] = ctx.Tr("repo.labels")
  755. ctx.Data["PageIsLabels"] = true
  756. if ctx.HasError() {
  757. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  758. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  759. return
  760. }
  761. l := &models.Label{
  762. RepoID: ctx.Repo.Repository.ID,
  763. Name: form.Title,
  764. Color: form.Color,
  765. }
  766. if err := models.NewLabel(l); err != nil {
  767. ctx.Handle(500, "NewLabel", err)
  768. return
  769. }
  770. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  771. }
  772. func UpdateLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  773. l, err := models.GetLabelByID(form.ID)
  774. if err != nil {
  775. switch {
  776. case models.IsErrLabelNotExist(err):
  777. ctx.Error(404)
  778. default:
  779. ctx.Handle(500, "UpdateLabel", err)
  780. }
  781. return
  782. }
  783. l.Name = form.Title
  784. l.Color = form.Color
  785. if err := models.UpdateLabel(l); err != nil {
  786. ctx.Handle(500, "UpdateLabel", err)
  787. return
  788. }
  789. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  790. }
  791. func DeleteLabel(ctx *middleware.Context) {
  792. if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
  793. ctx.Flash.Error("DeleteLabel: " + err.Error())
  794. } else {
  795. ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
  796. }
  797. ctx.JSON(200, map[string]interface{}{
  798. "redirect": ctx.Repo.RepoLink + "/labels",
  799. })
  800. return
  801. }
  802. func Milestones(ctx *middleware.Context) {
  803. ctx.Data["Title"] = ctx.Tr("repo.milestones")
  804. ctx.Data["PageIsMilestones"] = true
  805. isShowClosed := ctx.Query("state") == "closed"
  806. openCount, closedCount := models.MilestoneStats(ctx.Repo.Repository.ID)
  807. ctx.Data["OpenCount"] = openCount
  808. ctx.Data["ClosedCount"] = closedCount
  809. page := ctx.QueryInt("page")
  810. if page <= 1 {
  811. page = 1
  812. }
  813. var total int
  814. if !isShowClosed {
  815. total = int(openCount)
  816. } else {
  817. total = int(closedCount)
  818. }
  819. ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5)
  820. miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed)
  821. if err != nil {
  822. ctx.Handle(500, "GetMilestones", err)
  823. return
  824. }
  825. for _, m := range miles {
  826. m.RenderedContent = string(base.RenderMarkdown([]byte(m.Content), ctx.Repo.RepoLink))
  827. m.CalOpenIssues()
  828. }
  829. ctx.Data["Milestones"] = miles
  830. if isShowClosed {
  831. ctx.Data["State"] = "closed"
  832. } else {
  833. ctx.Data["State"] = "open"
  834. }
  835. ctx.Data["IsShowClosed"] = isShowClosed
  836. ctx.HTML(200, MILESTONE)
  837. }
  838. func NewMilestone(ctx *middleware.Context) {
  839. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  840. ctx.Data["PageIsMilestones"] = true
  841. ctx.Data["RequireDatetimepicker"] = true
  842. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  843. ctx.HTML(200, MILESTONE_NEW)
  844. }
  845. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  846. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  847. ctx.Data["PageIsMilestones"] = true
  848. ctx.Data["RequireDatetimepicker"] = true
  849. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  850. if ctx.HasError() {
  851. ctx.HTML(200, MILESTONE_NEW)
  852. return
  853. }
  854. if len(form.Deadline) == 0 {
  855. form.Deadline = "9999-12-31"
  856. }
  857. deadline, err := time.Parse("2006-01-02", form.Deadline)
  858. if err != nil {
  859. ctx.Data["Err_Deadline"] = true
  860. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  861. return
  862. }
  863. if err = models.NewMilestone(&models.Milestone{
  864. RepoID: ctx.Repo.Repository.ID,
  865. Name: form.Title,
  866. Content: form.Content,
  867. Deadline: deadline,
  868. }); err != nil {
  869. ctx.Handle(500, "NewMilestone", err)
  870. return
  871. }
  872. ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
  873. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  874. }
  875. func EditMilestone(ctx *middleware.Context) {
  876. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  877. ctx.Data["PageIsMilestones"] = true
  878. ctx.Data["PageIsEditMilestone"] = true
  879. ctx.Data["RequireDatetimepicker"] = true
  880. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  881. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  882. if err != nil {
  883. if models.IsErrMilestoneNotExist(err) {
  884. ctx.Handle(404, "GetMilestoneByID", nil)
  885. } else {
  886. ctx.Handle(500, "GetMilestoneByID", err)
  887. }
  888. return
  889. }
  890. ctx.Data["title"] = m.Name
  891. ctx.Data["content"] = m.Content
  892. if len(m.DeadlineString) > 0 {
  893. ctx.Data["deadline"] = m.DeadlineString
  894. }
  895. ctx.HTML(200, MILESTONE_NEW)
  896. }
  897. func EditMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  898. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  899. ctx.Data["PageIsMilestones"] = true
  900. ctx.Data["PageIsEditMilestone"] = true
  901. ctx.Data["RequireDatetimepicker"] = true
  902. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  903. if ctx.HasError() {
  904. ctx.HTML(200, MILESTONE_NEW)
  905. return
  906. }
  907. if len(form.Deadline) == 0 {
  908. form.Deadline = "9999-12-31"
  909. }
  910. deadline, err := time.Parse("2006-01-02", form.Deadline)
  911. if err != nil {
  912. ctx.Data["Err_Deadline"] = true
  913. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  914. return
  915. }
  916. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  917. if err != nil {
  918. if models.IsErrMilestoneNotExist(err) {
  919. ctx.Handle(404, "GetMilestoneByID", nil)
  920. } else {
  921. ctx.Handle(500, "GetMilestoneByID", err)
  922. }
  923. return
  924. }
  925. m.Name = form.Title
  926. m.Content = form.Content
  927. m.Deadline = deadline
  928. if err = models.UpdateMilestone(m); err != nil {
  929. ctx.Handle(500, "UpdateMilestone", err)
  930. return
  931. }
  932. ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
  933. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  934. }
  935. func ChangeMilestonStatus(ctx *middleware.Context) {
  936. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  937. if err != nil {
  938. if models.IsErrMilestoneNotExist(err) {
  939. ctx.Handle(404, "GetMilestoneByID", err)
  940. } else {
  941. ctx.Handle(500, "GetMilestoneByID", err)
  942. }
  943. return
  944. }
  945. switch ctx.Params(":action") {
  946. case "open":
  947. if m.IsClosed {
  948. if err = models.ChangeMilestoneStatus(m, false); err != nil {
  949. ctx.Handle(500, "ChangeMilestoneStatus", err)
  950. return
  951. }
  952. }
  953. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=open")
  954. case "close":
  955. if !m.IsClosed {
  956. m.ClosedDate = time.Now()
  957. if err = models.ChangeMilestoneStatus(m, true); err != nil {
  958. ctx.Handle(500, "ChangeMilestoneStatus", err)
  959. return
  960. }
  961. }
  962. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=closed")
  963. default:
  964. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  965. }
  966. }
  967. func DeleteMilestone(ctx *middleware.Context) {
  968. if err := models.DeleteMilestoneByID(ctx.QueryInt64("id")); err != nil {
  969. ctx.Flash.Error("DeleteMilestoneByID: " + err.Error())
  970. } else {
  971. ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
  972. }
  973. ctx.JSON(200, map[string]interface{}{
  974. "redirect": ctx.Repo.RepoLink + "/milestones",
  975. })
  976. }