issue.go 26 KB

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