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 26 kB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  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. "fmt"
  7. "net/url"
  8. "strings"
  9. "time"
  10. "github.com/Unknwon/com"
  11. "github.com/go-martini/martini"
  12. "github.com/gogits/gogs/models"
  13. "github.com/gogits/gogs/modules/auth"
  14. "github.com/gogits/gogs/modules/base"
  15. "github.com/gogits/gogs/modules/log"
  16. "github.com/gogits/gogs/modules/mailer"
  17. "github.com/gogits/gogs/modules/middleware"
  18. "github.com/gogits/gogs/modules/setting"
  19. )
  20. const (
  21. ISSUES base.TplName = "repo/issue/list"
  22. ISSUE_CREATE base.TplName = "repo/issue/create"
  23. ISSUE_VIEW base.TplName = "repo/issue/view"
  24. MILESTONE base.TplName = "repo/issue/milestone"
  25. MILESTONE_NEW base.TplName = "repo/issue/milestone_new"
  26. MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit"
  27. )
  28. func Issues(ctx *middleware.Context) {
  29. ctx.Data["Title"] = "Issues"
  30. ctx.Data["IsRepoToolbarIssues"] = true
  31. ctx.Data["IsRepoToolbarIssuesList"] = true
  32. viewType := ctx.Query("type")
  33. types := []string{"assigned", "created_by", "mentioned"}
  34. if !com.IsSliceContainsStr(types, viewType) {
  35. viewType = "all"
  36. }
  37. isShowClosed := ctx.Query("state") == "closed"
  38. if viewType != "all" && !ctx.IsSigned {
  39. ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI))
  40. ctx.Redirect("/user/login")
  41. return
  42. }
  43. var assigneeId, posterId int64
  44. var filterMode int
  45. switch viewType {
  46. case "assigned":
  47. assigneeId = ctx.User.Id
  48. filterMode = models.FM_ASSIGN
  49. case "created_by":
  50. posterId = ctx.User.Id
  51. filterMode = models.FM_CREATE
  52. case "mentioned":
  53. filterMode = models.FM_MENTION
  54. }
  55. var mid int64
  56. midx, _ := base.StrTo(ctx.Query("milestone")).Int64()
  57. if midx > 0 {
  58. mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, midx)
  59. if err != nil {
  60. ctx.Handle(500, "issue.Issues(GetMilestoneByIndex): %v", err)
  61. return
  62. }
  63. mid = mile.Id
  64. }
  65. selectLabels := ctx.Query("labels")
  66. labels, err := models.GetLabels(ctx.Repo.Repository.Id)
  67. if err != nil {
  68. ctx.Handle(500, "issue.Issues(GetLabels): %v", err)
  69. return
  70. }
  71. for _, l := range labels {
  72. l.CalOpenIssues()
  73. }
  74. ctx.Data["Labels"] = labels
  75. page, _ := base.StrTo(ctx.Query("page")).Int()
  76. // Get issues.
  77. issues, err := models.GetIssues(assigneeId, ctx.Repo.Repository.Id, posterId, mid, page,
  78. isShowClosed, selectLabels, ctx.Query("sortType"))
  79. if err != nil {
  80. ctx.Handle(500, "issue.Issues(GetIssues): %v", err)
  81. return
  82. }
  83. // Get issue-user pairs.
  84. pairs, err := models.GetIssueUserPairs(ctx.Repo.Repository.Id, posterId, isShowClosed)
  85. if err != nil {
  86. ctx.Handle(500, "issue.Issues(GetIssueUserPairs): %v", err)
  87. return
  88. }
  89. // Get posters.
  90. for i := range issues {
  91. if err = issues[i].GetLabels(); err != nil {
  92. ctx.Handle(500, "issue.Issues(GetLabels)", fmt.Errorf("[#%d]%v", issues[i].Id, err))
  93. return
  94. }
  95. idx := models.PairsContains(pairs, issues[i].Id)
  96. if filterMode == models.FM_MENTION && (idx == -1 || !pairs[idx].IsMentioned) {
  97. continue
  98. }
  99. if idx > -1 {
  100. issues[i].IsRead = pairs[idx].IsRead
  101. } else {
  102. issues[i].IsRead = true
  103. }
  104. if err = issues[i].GetPoster(); err != nil {
  105. ctx.Handle(500, "issue.Issues(GetPoster)", fmt.Errorf("[#%d]%v", issues[i].Id, err))
  106. return
  107. }
  108. }
  109. var uid int64 = -1
  110. if ctx.User != nil {
  111. uid = ctx.User.Id
  112. }
  113. issueStats := models.GetIssueStats(ctx.Repo.Repository.Id, uid, isShowClosed, filterMode)
  114. ctx.Data["IssueStats"] = issueStats
  115. ctx.Data["SelectLabels"], _ = base.StrTo(selectLabels).Int64()
  116. ctx.Data["ViewType"] = viewType
  117. ctx.Data["Issues"] = issues
  118. ctx.Data["IsShowClosed"] = isShowClosed
  119. if isShowClosed {
  120. ctx.Data["State"] = "closed"
  121. ctx.Data["ShowCount"] = issueStats.ClosedCount
  122. } else {
  123. ctx.Data["ShowCount"] = issueStats.OpenCount
  124. }
  125. ctx.HTML(200, ISSUES)
  126. }
  127. func CreateIssue(ctx *middleware.Context, params martini.Params) {
  128. ctx.Data["Title"] = "Create issue"
  129. ctx.Data["IsRepoToolbarIssues"] = true
  130. ctx.Data["IsRepoToolbarIssuesList"] = false
  131. var err error
  132. // Get all milestones.
  133. ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, false)
  134. if err != nil {
  135. ctx.Handle(500, "issue.ViewIssue(GetMilestones.1): %v", err)
  136. return
  137. }
  138. ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, true)
  139. if err != nil {
  140. ctx.Handle(500, "issue.ViewIssue(GetMilestones.2): %v", err)
  141. return
  142. }
  143. us, err := models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/"))
  144. if err != nil {
  145. ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err)
  146. return
  147. }
  148. ctx.Data["Collaborators"] = us
  149. ctx.HTML(200, ISSUE_CREATE)
  150. }
  151. func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
  152. ctx.Data["Title"] = "Create issue"
  153. ctx.Data["IsRepoToolbarIssues"] = true
  154. ctx.Data["IsRepoToolbarIssuesList"] = false
  155. var err error
  156. // Get all milestones.
  157. ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, false)
  158. if err != nil {
  159. ctx.Handle(500, "issue.ViewIssue(GetMilestones.1): %v", err)
  160. return
  161. }
  162. ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, true)
  163. if err != nil {
  164. ctx.Handle(500, "issue.ViewIssue(GetMilestones.2): %v", err)
  165. return
  166. }
  167. us, err := models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/"))
  168. if err != nil {
  169. ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err)
  170. return
  171. }
  172. ctx.Data["Collaborators"] = us
  173. if ctx.HasError() {
  174. ctx.HTML(200, ISSUE_CREATE)
  175. return
  176. }
  177. // Only collaborators can assign.
  178. if !ctx.Repo.IsOwner {
  179. form.AssigneeId = 0
  180. }
  181. issue := &models.Issue{
  182. RepoId: ctx.Repo.Repository.Id,
  183. Index: int64(ctx.Repo.Repository.NumIssues) + 1,
  184. Name: form.IssueName,
  185. PosterId: ctx.User.Id,
  186. MilestoneId: form.MilestoneId,
  187. AssigneeId: form.AssigneeId,
  188. LabelIds: form.Labels,
  189. Content: form.Content,
  190. }
  191. if err := models.NewIssue(issue); err != nil {
  192. ctx.Handle(500, "issue.CreateIssue(NewIssue)", err)
  193. return
  194. } else if err := models.NewIssueUserPairs(issue.RepoId, issue.Id, ctx.Repo.Owner.Id,
  195. ctx.User.Id, form.AssigneeId, ctx.Repo.Repository.Name); err != nil {
  196. ctx.Handle(500, "issue.CreateIssue(NewIssueUserPairs)", err)
  197. return
  198. }
  199. // Update mentions.
  200. ms := base.MentionPattern.FindAllString(issue.Content, -1)
  201. if len(ms) > 0 {
  202. for i := range ms {
  203. ms[i] = ms[i][1:]
  204. }
  205. ids := models.GetUserIdsByNames(ms)
  206. if err := models.UpdateIssueUserPairsByMentions(ids, issue.Id); err != nil {
  207. ctx.Handle(500, "issue.CreateIssue(UpdateIssueUserPairsByMentions)", err)
  208. return
  209. }
  210. }
  211. act := &models.Action{
  212. ActUserId: ctx.User.Id,
  213. ActUserName: ctx.User.Name,
  214. ActEmail: ctx.User.Email,
  215. OpType: models.OP_CREATE_ISSUE,
  216. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name),
  217. RepoId: ctx.Repo.Repository.Id,
  218. RepoUserName: ctx.Repo.Owner.Name,
  219. RepoName: ctx.Repo.Repository.Name,
  220. RefName: ctx.Repo.BranchName,
  221. IsPrivate: ctx.Repo.Repository.IsPrivate,
  222. }
  223. // Notify watchers.
  224. if err := models.NotifyWatchers(act); err != nil {
  225. ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err)
  226. return
  227. }
  228. // Mail watchers and mentions.
  229. if setting.Service.EnableNotifyMail {
  230. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  231. if err != nil {
  232. ctx.Handle(500, "issue.CreateIssue(SendIssueNotifyMail)", err)
  233. return
  234. }
  235. tos = append(tos, ctx.User.LowerName)
  236. newTos := make([]string, 0, len(ms))
  237. for _, m := range ms {
  238. if com.IsSliceContainsStr(tos, m) {
  239. continue
  240. }
  241. newTos = append(newTos, m)
  242. }
  243. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  244. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  245. ctx.Handle(500, "issue.CreateIssue(SendIssueMentionMail)", err)
  246. return
  247. }
  248. }
  249. log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id)
  250. ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index))
  251. }
  252. func checkLabels(labels, allLabels []*models.Label) {
  253. for _, l := range labels {
  254. for _, l2 := range allLabels {
  255. if l.Id == l2.Id {
  256. l2.IsChecked = true
  257. break
  258. }
  259. }
  260. }
  261. }
  262. func ViewIssue(ctx *middleware.Context, params martini.Params) {
  263. idx, _ := base.StrTo(params["index"]).Int64()
  264. if idx == 0 {
  265. ctx.Handle(404, "issue.ViewIssue", nil)
  266. return
  267. }
  268. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx)
  269. if err != nil {
  270. if err == models.ErrIssueNotExist {
  271. ctx.Handle(404, "issue.ViewIssue(GetIssueByIndex)", err)
  272. } else {
  273. ctx.Handle(500, "issue.ViewIssue(GetIssueByIndex)", err)
  274. }
  275. return
  276. }
  277. // Get labels.
  278. if err = issue.GetLabels(); err != nil {
  279. ctx.Handle(500, "issue.ViewIssue(GetLabels)", err)
  280. return
  281. }
  282. labels, err := models.GetLabels(ctx.Repo.Repository.Id)
  283. if err != nil {
  284. ctx.Handle(500, "issue.ViewIssue(GetLabels.2)", err)
  285. return
  286. }
  287. checkLabels(issue.Labels, labels)
  288. ctx.Data["Labels"] = labels
  289. // Get assigned milestone.
  290. if issue.MilestoneId > 0 {
  291. ctx.Data["Milestone"], err = models.GetMilestoneById(issue.MilestoneId)
  292. if err != nil {
  293. if err == models.ErrMilestoneNotExist {
  294. log.Warn("issue.ViewIssue(GetMilestoneById): %v", err)
  295. } else {
  296. ctx.Handle(500, "issue.ViewIssue(GetMilestoneById)", err)
  297. return
  298. }
  299. }
  300. }
  301. // Get all milestones.
  302. ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, false)
  303. if err != nil {
  304. ctx.Handle(500, "issue.ViewIssue(GetMilestones.1): %v", err)
  305. return
  306. }
  307. ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.Id, true)
  308. if err != nil {
  309. ctx.Handle(500, "issue.ViewIssue(GetMilestones.2): %v", err)
  310. return
  311. }
  312. // Get all collaborators.
  313. ctx.Data["Collaborators"], err = models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/"))
  314. if err != nil {
  315. ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err)
  316. return
  317. }
  318. if ctx.IsSigned {
  319. // Update issue-user.
  320. if err = models.UpdateIssueUserPairByRead(ctx.User.Id, issue.Id); err != nil {
  321. ctx.Handle(500, "issue.ViewIssue(UpdateIssueUserPairByRead): %v", err)
  322. return
  323. }
  324. }
  325. // Get poster and Assignee.
  326. if err = issue.GetPoster(); err != nil {
  327. ctx.Handle(500, "issue.ViewIssue(GetPoster): %v", err)
  328. return
  329. } else if err = issue.GetAssignee(); err != nil {
  330. ctx.Handle(500, "issue.ViewIssue(GetAssignee): %v", err)
  331. return
  332. }
  333. issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink))
  334. // Get comments.
  335. comments, err := models.GetIssueComments(issue.Id)
  336. if err != nil {
  337. ctx.Handle(500, "issue.ViewIssue(GetIssueComments): %v", err)
  338. return
  339. }
  340. // Get posters.
  341. for i := range comments {
  342. u, err := models.GetUserById(comments[i].PosterId)
  343. if err != nil {
  344. ctx.Handle(500, "issue.ViewIssue(GetUserById.2): %v", err)
  345. return
  346. }
  347. comments[i].Poster = u
  348. comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink))
  349. }
  350. ctx.Data["Title"] = issue.Name
  351. ctx.Data["Issue"] = issue
  352. ctx.Data["Comments"] = comments
  353. ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || (ctx.IsSigned && issue.PosterId == ctx.User.Id)
  354. ctx.Data["IsRepoToolbarIssues"] = true
  355. ctx.Data["IsRepoToolbarIssuesList"] = false
  356. ctx.HTML(200, ISSUE_VIEW)
  357. }
  358. func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
  359. idx, _ := base.StrTo(params["index"]).Int64()
  360. if idx <= 0 {
  361. ctx.Error(404)
  362. return
  363. }
  364. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx)
  365. if err != nil {
  366. if err == models.ErrIssueNotExist {
  367. ctx.Handle(404, "issue.UpdateIssue", err)
  368. } else {
  369. ctx.Handle(500, "issue.UpdateIssue(GetIssueByIndex)", err)
  370. }
  371. return
  372. }
  373. if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner {
  374. ctx.Error(403)
  375. return
  376. }
  377. issue.Name = form.IssueName
  378. issue.MilestoneId = form.MilestoneId
  379. issue.AssigneeId = form.AssigneeId
  380. issue.LabelIds = form.Labels
  381. issue.Content = form.Content
  382. // try get content from text, ignore conflict with preview ajax
  383. if form.Content == "" {
  384. issue.Content = ctx.Query("text")
  385. }
  386. if err = models.UpdateIssue(issue); err != nil {
  387. ctx.Handle(500, "issue.UpdateIssue(UpdateIssue)", err)
  388. return
  389. }
  390. ctx.JSON(200, map[string]interface{}{
  391. "ok": true,
  392. "title": issue.Name,
  393. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)),
  394. })
  395. }
  396. func UpdateIssueLabel(ctx *middleware.Context, params martini.Params) {
  397. if !ctx.Repo.IsOwner {
  398. ctx.Error(403)
  399. return
  400. }
  401. idx, _ := base.StrTo(params["index"]).Int64()
  402. if idx <= 0 {
  403. ctx.Error(404)
  404. return
  405. }
  406. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx)
  407. if err != nil {
  408. if err == models.ErrIssueNotExist {
  409. ctx.Handle(404, "issue.UpdateIssueLabel(GetIssueByIndex)", err)
  410. } else {
  411. ctx.Handle(500, "issue.UpdateIssueLabel(GetIssueByIndex)", err)
  412. }
  413. return
  414. }
  415. isAttach := ctx.Query("action") == "attach"
  416. labelStrId := ctx.Query("id")
  417. labelId, _ := base.StrTo(labelStrId).Int64()
  418. label, err := models.GetLabelById(labelId)
  419. if err != nil {
  420. if err == models.ErrLabelNotExist {
  421. ctx.Handle(404, "issue.UpdateIssueLabel(GetLabelById)", err)
  422. } else {
  423. ctx.Handle(500, "issue.UpdateIssueLabel(GetLabelById)", err)
  424. }
  425. return
  426. }
  427. isHad := strings.Contains(issue.LabelIds, "$"+labelStrId+"|")
  428. isNeedUpdate := false
  429. if isAttach {
  430. if !isHad {
  431. issue.LabelIds += "$" + labelStrId + "|"
  432. isNeedUpdate = true
  433. }
  434. } else {
  435. if isHad {
  436. issue.LabelIds = strings.Replace(issue.LabelIds, "$"+labelStrId+"|", "", -1)
  437. isNeedUpdate = true
  438. }
  439. }
  440. if isNeedUpdate {
  441. if err = models.UpdateIssue(issue); err != nil {
  442. ctx.Handle(500, "issue.UpdateIssueLabel(UpdateIssue)", err)
  443. return
  444. }
  445. if isAttach {
  446. label.NumIssues++
  447. if issue.IsClosed {
  448. label.NumClosedIssues++
  449. }
  450. } else {
  451. label.NumIssues--
  452. if issue.IsClosed {
  453. label.NumClosedIssues--
  454. }
  455. }
  456. if err = models.UpdateLabel(label); err != nil {
  457. ctx.Handle(500, "issue.UpdateIssueLabel(UpdateLabel)", err)
  458. return
  459. }
  460. }
  461. ctx.JSON(200, map[string]interface{}{
  462. "ok": true,
  463. })
  464. }
  465. func UpdateIssueMilestone(ctx *middleware.Context) {
  466. if !ctx.Repo.IsOwner {
  467. ctx.Error(403)
  468. return
  469. }
  470. issueId, err := base.StrTo(ctx.Query("issue")).Int64()
  471. if err != nil {
  472. ctx.Error(404)
  473. return
  474. }
  475. issue, err := models.GetIssueById(issueId)
  476. if err != nil {
  477. if err == models.ErrIssueNotExist {
  478. ctx.Handle(404, "issue.UpdateIssueMilestone(GetIssueById)", err)
  479. } else {
  480. ctx.Handle(500, "issue.UpdateIssueMilestone(GetIssueById)", err)
  481. }
  482. return
  483. }
  484. oldMid := issue.MilestoneId
  485. mid, _ := base.StrTo(ctx.Query("milestone")).Int64()
  486. if oldMid == mid {
  487. ctx.JSON(200, map[string]interface{}{
  488. "ok": true,
  489. })
  490. return
  491. }
  492. // Not check for invalid milestone id and give responsibility to owners.
  493. issue.MilestoneId = mid
  494. if err = models.ChangeMilestoneAssign(oldMid, mid, issue); err != nil {
  495. ctx.Handle(500, "issue.UpdateIssueMilestone(ChangeMilestoneAssign)", err)
  496. return
  497. } else if err = models.UpdateIssue(issue); err != nil {
  498. ctx.Handle(500, "issue.UpdateIssueMilestone(UpdateIssue)", err)
  499. return
  500. }
  501. ctx.JSON(200, map[string]interface{}{
  502. "ok": true,
  503. })
  504. }
  505. func UpdateAssignee(ctx *middleware.Context) {
  506. if !ctx.Repo.IsOwner {
  507. ctx.Error(403)
  508. return
  509. }
  510. issueId, err := base.StrTo(ctx.Query("issue")).Int64()
  511. if err != nil {
  512. ctx.Error(404)
  513. return
  514. }
  515. issue, err := models.GetIssueById(issueId)
  516. if err != nil {
  517. if err == models.ErrIssueNotExist {
  518. ctx.Handle(404, "issue.UpdateAssignee(GetIssueById)", err)
  519. } else {
  520. ctx.Handle(500, "issue.UpdateAssignee(GetIssueById)", err)
  521. }
  522. return
  523. }
  524. aid, _ := base.StrTo(ctx.Query("assigneeid")).Int64()
  525. // Not check for invalid assignne id and give responsibility to owners.
  526. issue.AssigneeId = aid
  527. if err = models.UpdateIssueUserPairByAssignee(aid, issue.Id); err != nil {
  528. ctx.Handle(500, "issue.UpdateAssignee(UpdateIssueUserPairByAssignee): %v", err)
  529. return
  530. } else if err = models.UpdateIssue(issue); err != nil {
  531. ctx.Handle(500, "issue.UpdateAssignee(UpdateIssue)", err)
  532. return
  533. }
  534. ctx.JSON(200, map[string]interface{}{
  535. "ok": true,
  536. })
  537. }
  538. func Comment(ctx *middleware.Context, params martini.Params) {
  539. index, err := base.StrTo(ctx.Query("issueIndex")).Int64()
  540. if err != nil {
  541. ctx.Handle(404, "issue.Comment(get index)", err)
  542. return
  543. }
  544. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index)
  545. if err != nil {
  546. if err == models.ErrIssueNotExist {
  547. ctx.Handle(404, "issue.Comment", err)
  548. } else {
  549. ctx.Handle(200, "issue.Comment(get issue)", err)
  550. }
  551. return
  552. }
  553. // Check if issue owner changes the status of issue.
  554. var newStatus string
  555. if ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id {
  556. newStatus = ctx.Query("change_status")
  557. }
  558. if len(newStatus) > 0 {
  559. if (strings.Contains(newStatus, "Reopen") && issue.IsClosed) ||
  560. (strings.Contains(newStatus, "Close") && !issue.IsClosed) {
  561. issue.IsClosed = !issue.IsClosed
  562. if err = models.UpdateIssue(issue); err != nil {
  563. ctx.Handle(500, "issue.Comment(UpdateIssue)", err)
  564. return
  565. } else if err = models.UpdateIssueUserPairsByStatus(issue.Id, issue.IsClosed); err != nil {
  566. ctx.Handle(500, "issue.Comment(UpdateIssueUserPairsByStatus)", err)
  567. return
  568. }
  569. // Change open/closed issue counter for the associated milestone
  570. if issue.MilestoneId > 0 {
  571. if err = models.ChangeMilestoneIssueStats(issue); err != nil {
  572. ctx.Handle(500, "issue.Comment(ChangeMilestoneIssueStats)", err)
  573. }
  574. }
  575. cmtType := models.IT_CLOSE
  576. if !issue.IsClosed {
  577. cmtType = models.IT_REOPEN
  578. }
  579. if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil {
  580. ctx.Handle(200, "issue.Comment(create status change comment)", err)
  581. return
  582. }
  583. log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed)
  584. }
  585. }
  586. var ms []string
  587. content := ctx.Query("content")
  588. if len(content) > 0 {
  589. switch params["action"] {
  590. case "new":
  591. if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil {
  592. ctx.Handle(500, "issue.Comment(create comment)", err)
  593. return
  594. }
  595. // Update mentions.
  596. ms = base.MentionPattern.FindAllString(issue.Content, -1)
  597. if len(ms) > 0 {
  598. for i := range ms {
  599. ms[i] = ms[i][1:]
  600. }
  601. ids := models.GetUserIdsByNames(ms)
  602. if err := models.UpdateIssueUserPairsByMentions(ids, issue.Id); err != nil {
  603. ctx.Handle(500, "issue.CreateIssue(UpdateIssueUserPairsByMentions)", err)
  604. return
  605. }
  606. }
  607. log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id)
  608. default:
  609. ctx.Handle(404, "issue.Comment", err)
  610. return
  611. }
  612. }
  613. // Notify watchers.
  614. act := &models.Action{
  615. ActUserId: ctx.User.Id,
  616. ActUserName: ctx.User.LowerName,
  617. ActEmail: ctx.User.Email,
  618. OpType: models.OP_COMMENT_ISSUE,
  619. Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]),
  620. RepoId: ctx.Repo.Repository.Id,
  621. RepoUserName: ctx.Repo.Owner.LowerName,
  622. RepoName: ctx.Repo.Repository.LowerName,
  623. }
  624. if err = models.NotifyWatchers(act); err != nil {
  625. ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err)
  626. return
  627. }
  628. // Mail watchers and mentions.
  629. if setting.Service.EnableNotifyMail {
  630. issue.Content = content
  631. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  632. if err != nil {
  633. ctx.Handle(500, "issue.Comment(SendIssueNotifyMail)", err)
  634. return
  635. }
  636. tos = append(tos, ctx.User.LowerName)
  637. newTos := make([]string, 0, len(ms))
  638. for _, m := range ms {
  639. if com.IsSliceContainsStr(tos, m) {
  640. continue
  641. }
  642. newTos = append(newTos, m)
  643. }
  644. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  645. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  646. ctx.Handle(500, "issue.Comment(SendIssueMentionMail)", err)
  647. return
  648. }
  649. }
  650. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, index))
  651. }
  652. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  653. if ctx.HasError() {
  654. Issues(ctx)
  655. return
  656. }
  657. l := &models.Label{
  658. RepoId: ctx.Repo.Repository.Id,
  659. Name: form.Title,
  660. Color: form.Color,
  661. }
  662. if err := models.NewLabel(l); err != nil {
  663. ctx.Handle(500, "issue.NewLabel(NewLabel)", err)
  664. return
  665. }
  666. ctx.Redirect(ctx.Repo.RepoLink + "/issues")
  667. }
  668. func UpdateLabel(ctx *middleware.Context, params martini.Params, form auth.CreateLabelForm) {
  669. id, _ := base.StrTo(ctx.Query("id")).Int64()
  670. if id == 0 {
  671. ctx.Error(404)
  672. return
  673. }
  674. l := &models.Label{
  675. Id: id,
  676. Name: form.Title,
  677. Color: form.Color,
  678. }
  679. if err := models.UpdateLabel(l); err != nil {
  680. ctx.Handle(500, "issue.UpdateLabel(UpdateLabel)", err)
  681. return
  682. }
  683. ctx.Redirect(ctx.Repo.RepoLink + "/issues")
  684. }
  685. func DeleteLabel(ctx *middleware.Context) {
  686. removes := ctx.Query("remove")
  687. if len(strings.TrimSpace(removes)) == 0 {
  688. ctx.JSON(200, map[string]interface{}{
  689. "ok": true,
  690. })
  691. return
  692. }
  693. strIds := strings.Split(removes, ",")
  694. for _, strId := range strIds {
  695. if err := models.DeleteLabel(ctx.Repo.Repository.Id, strId); err != nil {
  696. ctx.Handle(500, "issue.DeleteLabel(DeleteLabel)", err)
  697. return
  698. }
  699. }
  700. ctx.JSON(200, map[string]interface{}{
  701. "ok": true,
  702. })
  703. }
  704. func Milestones(ctx *middleware.Context) {
  705. ctx.Data["Title"] = "Milestones"
  706. ctx.Data["IsRepoToolbarIssues"] = true
  707. ctx.Data["IsRepoToolbarIssuesList"] = true
  708. isShowClosed := ctx.Query("state") == "closed"
  709. miles, err := models.GetMilestones(ctx.Repo.Repository.Id, isShowClosed)
  710. if err != nil {
  711. ctx.Handle(500, "issue.Milestones(GetMilestones)", err)
  712. return
  713. }
  714. for _, m := range miles {
  715. m.RenderedContent = string(base.RenderSpecialLink([]byte(m.Content), ctx.Repo.RepoLink))
  716. m.CalOpenIssues()
  717. }
  718. ctx.Data["Milestones"] = miles
  719. if isShowClosed {
  720. ctx.Data["State"] = "closed"
  721. } else {
  722. ctx.Data["State"] = "open"
  723. }
  724. ctx.HTML(200, MILESTONE)
  725. }
  726. func NewMilestone(ctx *middleware.Context) {
  727. ctx.Data["Title"] = "New Milestone"
  728. ctx.Data["IsRepoToolbarIssues"] = true
  729. ctx.Data["IsRepoToolbarIssuesList"] = true
  730. ctx.HTML(200, MILESTONE_NEW)
  731. }
  732. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  733. ctx.Data["Title"] = "New Milestone"
  734. ctx.Data["IsRepoToolbarIssues"] = true
  735. ctx.Data["IsRepoToolbarIssuesList"] = true
  736. if ctx.HasError() {
  737. ctx.HTML(200, MILESTONE_NEW)
  738. return
  739. }
  740. var deadline time.Time
  741. var err error
  742. if len(form.Deadline) == 0 {
  743. form.Deadline = "12/31/9999"
  744. }
  745. deadline, err = time.Parse("01/02/2006", form.Deadline)
  746. if err != nil {
  747. ctx.Handle(500, "issue.NewMilestonePost(time.Parse)", err)
  748. return
  749. }
  750. mile := &models.Milestone{
  751. RepoId: ctx.Repo.Repository.Id,
  752. Index: int64(ctx.Repo.Repository.NumMilestones) + 1,
  753. Name: form.Title,
  754. Content: form.Content,
  755. Deadline: deadline,
  756. }
  757. if err = models.NewMilestone(mile); err != nil {
  758. ctx.Handle(500, "issue.NewMilestonePost(NewMilestone)", err)
  759. return
  760. }
  761. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  762. }
  763. func UpdateMilestone(ctx *middleware.Context, params martini.Params) {
  764. ctx.Data["Title"] = "Update Milestone"
  765. ctx.Data["IsRepoToolbarIssues"] = true
  766. ctx.Data["IsRepoToolbarIssuesList"] = true
  767. idx, _ := base.StrTo(params["index"]).Int64()
  768. if idx == 0 {
  769. ctx.Handle(404, "issue.UpdateMilestone", nil)
  770. return
  771. }
  772. mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx)
  773. if err != nil {
  774. if err == models.ErrMilestoneNotExist {
  775. ctx.Handle(404, "issue.UpdateMilestone(GetMilestoneByIndex)", err)
  776. } else {
  777. ctx.Handle(500, "issue.UpdateMilestone(GetMilestoneByIndex)", err)
  778. }
  779. return
  780. }
  781. action := params["action"]
  782. if len(action) > 0 {
  783. switch action {
  784. case "open":
  785. if mile.IsClosed {
  786. if err = models.ChangeMilestoneStatus(mile, false); err != nil {
  787. ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err)
  788. return
  789. }
  790. }
  791. case "close":
  792. if !mile.IsClosed {
  793. mile.ClosedDate = time.Now()
  794. if err = models.ChangeMilestoneStatus(mile, true); err != nil {
  795. ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err)
  796. return
  797. }
  798. }
  799. case "delete":
  800. if err = models.DeleteMilestone(mile); err != nil {
  801. ctx.Handle(500, "issue.UpdateMilestone(DeleteMilestone)", err)
  802. return
  803. }
  804. }
  805. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  806. return
  807. }
  808. mile.DeadlineString = mile.Deadline.UTC().Format("01/02/2006")
  809. if mile.DeadlineString == "12/31/9999" {
  810. mile.DeadlineString = ""
  811. }
  812. ctx.Data["Milestone"] = mile
  813. ctx.HTML(200, MILESTONE_EDIT)
  814. }
  815. func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form auth.CreateMilestoneForm) {
  816. ctx.Data["Title"] = "Update Milestone"
  817. ctx.Data["IsRepoToolbarIssues"] = true
  818. ctx.Data["IsRepoToolbarIssuesList"] = true
  819. idx, _ := base.StrTo(params["index"]).Int64()
  820. if idx == 0 {
  821. ctx.Handle(404, "issue.UpdateMilestonePost", nil)
  822. return
  823. }
  824. mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx)
  825. if err != nil {
  826. if err == models.ErrMilestoneNotExist {
  827. ctx.Handle(404, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err)
  828. } else {
  829. ctx.Handle(500, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err)
  830. }
  831. return
  832. }
  833. if ctx.HasError() {
  834. ctx.HTML(200, MILESTONE_EDIT)
  835. return
  836. }
  837. var deadline time.Time
  838. if len(form.Deadline) == 0 {
  839. form.Deadline = "12/31/9999"
  840. }
  841. deadline, err = time.Parse("01/02/2006", form.Deadline)
  842. if err != nil {
  843. ctx.Handle(500, "issue.UpdateMilestonePost(time.Parse)", err)
  844. return
  845. }
  846. mile.Name = form.Title
  847. mile.Content = form.Content
  848. mile.Deadline = deadline
  849. if err = models.UpdateMilestone(mile); err != nil {
  850. ctx.Handle(500, "issue.UpdateMilestonePost(UpdateMilestone)", err)
  851. return
  852. }
  853. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  854. }