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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  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. l, err := models.GetMilestoneById(issue.MilestoneId)
  572. if err != nil {
  573. ctx.Handle(500, "issue.Comment(GetLabelById)", err)
  574. return
  575. }
  576. if issue.IsClosed {
  577. l.NumOpenIssues = l.NumOpenIssues - 1
  578. l.NumClosedIssues = l.NumClosedIssues + 1
  579. } else {
  580. l.NumOpenIssues = l.NumOpenIssues + 1
  581. l.NumClosedIssues = l.NumClosedIssues - 1
  582. }
  583. if err = models.UpdateMilestone(l); err != nil {
  584. ctx.Handle(500, "issue.Comment(UpdateLabel)", err)
  585. return
  586. }
  587. }
  588. cmtType := models.IT_CLOSE
  589. if !issue.IsClosed {
  590. cmtType = models.IT_REOPEN
  591. }
  592. if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil {
  593. ctx.Handle(200, "issue.Comment(create status change comment)", err)
  594. return
  595. }
  596. log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed)
  597. }
  598. }
  599. var ms []string
  600. content := ctx.Query("content")
  601. if len(content) > 0 {
  602. switch params["action"] {
  603. case "new":
  604. if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil {
  605. ctx.Handle(500, "issue.Comment(create comment)", err)
  606. return
  607. }
  608. // Update mentions.
  609. ms = base.MentionPattern.FindAllString(issue.Content, -1)
  610. if len(ms) > 0 {
  611. for i := range ms {
  612. ms[i] = ms[i][1:]
  613. }
  614. ids := models.GetUserIdsByNames(ms)
  615. if err := models.UpdateIssueUserPairsByMentions(ids, issue.Id); err != nil {
  616. ctx.Handle(500, "issue.CreateIssue(UpdateIssueUserPairsByMentions)", err)
  617. return
  618. }
  619. }
  620. log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id)
  621. default:
  622. ctx.Handle(404, "issue.Comment", err)
  623. return
  624. }
  625. }
  626. // Notify watchers.
  627. act := &models.Action{
  628. ActUserId: ctx.User.Id,
  629. ActUserName: ctx.User.LowerName,
  630. ActEmail: ctx.User.Email,
  631. OpType: models.OP_COMMENT_ISSUE,
  632. Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]),
  633. RepoId: ctx.Repo.Repository.Id,
  634. RepoUserName: ctx.Repo.Owner.LowerName,
  635. RepoName: ctx.Repo.Repository.LowerName,
  636. }
  637. if err = models.NotifyWatchers(act); err != nil {
  638. ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err)
  639. return
  640. }
  641. // Mail watchers and mentions.
  642. if setting.Service.EnableNotifyMail {
  643. issue.Content = content
  644. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  645. if err != nil {
  646. ctx.Handle(500, "issue.Comment(SendIssueNotifyMail)", err)
  647. return
  648. }
  649. tos = append(tos, ctx.User.LowerName)
  650. newTos := make([]string, 0, len(ms))
  651. for _, m := range ms {
  652. if com.IsSliceContainsStr(tos, m) {
  653. continue
  654. }
  655. newTos = append(newTos, m)
  656. }
  657. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  658. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  659. ctx.Handle(500, "issue.Comment(SendIssueMentionMail)", err)
  660. return
  661. }
  662. }
  663. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, index))
  664. }
  665. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  666. if ctx.HasError() {
  667. Issues(ctx)
  668. return
  669. }
  670. l := &models.Label{
  671. RepoId: ctx.Repo.Repository.Id,
  672. Name: form.Title,
  673. Color: form.Color,
  674. }
  675. if err := models.NewLabel(l); err != nil {
  676. ctx.Handle(500, "issue.NewLabel(NewLabel)", err)
  677. return
  678. }
  679. ctx.Redirect(ctx.Repo.RepoLink + "/issues")
  680. }
  681. func UpdateLabel(ctx *middleware.Context, params martini.Params, form auth.CreateLabelForm) {
  682. id, _ := base.StrTo(ctx.Query("id")).Int64()
  683. if id == 0 {
  684. ctx.Error(404)
  685. return
  686. }
  687. l := &models.Label{
  688. Id: id,
  689. Name: form.Title,
  690. Color: form.Color,
  691. }
  692. if err := models.UpdateLabel(l); err != nil {
  693. ctx.Handle(500, "issue.UpdateLabel(UpdateLabel)", err)
  694. return
  695. }
  696. ctx.Redirect(ctx.Repo.RepoLink + "/issues")
  697. }
  698. func DeleteLabel(ctx *middleware.Context) {
  699. removes := ctx.Query("remove")
  700. if len(strings.TrimSpace(removes)) == 0 {
  701. ctx.JSON(200, map[string]interface{}{
  702. "ok": true,
  703. })
  704. return
  705. }
  706. strIds := strings.Split(removes, ",")
  707. for _, strId := range strIds {
  708. if err := models.DeleteLabel(ctx.Repo.Repository.Id, strId); err != nil {
  709. ctx.Handle(500, "issue.DeleteLabel(DeleteLabel)", err)
  710. return
  711. }
  712. }
  713. ctx.JSON(200, map[string]interface{}{
  714. "ok": true,
  715. })
  716. }
  717. func Milestones(ctx *middleware.Context) {
  718. ctx.Data["Title"] = "Milestones"
  719. ctx.Data["IsRepoToolbarIssues"] = true
  720. ctx.Data["IsRepoToolbarIssuesList"] = true
  721. isShowClosed := ctx.Query("state") == "closed"
  722. miles, err := models.GetMilestones(ctx.Repo.Repository.Id, isShowClosed)
  723. if err != nil {
  724. ctx.Handle(500, "issue.Milestones(GetMilestones)", err)
  725. return
  726. }
  727. for _, m := range miles {
  728. m.RenderedContent = string(base.RenderSpecialLink([]byte(m.Content), ctx.Repo.RepoLink))
  729. m.CalOpenIssues()
  730. }
  731. ctx.Data["Milestones"] = miles
  732. if isShowClosed {
  733. ctx.Data["State"] = "closed"
  734. } else {
  735. ctx.Data["State"] = "open"
  736. }
  737. ctx.HTML(200, MILESTONE)
  738. }
  739. func NewMilestone(ctx *middleware.Context) {
  740. ctx.Data["Title"] = "New Milestone"
  741. ctx.Data["IsRepoToolbarIssues"] = true
  742. ctx.Data["IsRepoToolbarIssuesList"] = true
  743. ctx.HTML(200, MILESTONE_NEW)
  744. }
  745. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  746. ctx.Data["Title"] = "New Milestone"
  747. ctx.Data["IsRepoToolbarIssues"] = true
  748. ctx.Data["IsRepoToolbarIssuesList"] = true
  749. if ctx.HasError() {
  750. ctx.HTML(200, MILESTONE_NEW)
  751. return
  752. }
  753. var deadline time.Time
  754. var err error
  755. if len(form.Deadline) == 0 {
  756. form.Deadline = "12/31/9999"
  757. }
  758. deadline, err = time.Parse("01/02/2006", form.Deadline)
  759. if err != nil {
  760. ctx.Handle(500, "issue.NewMilestonePost(time.Parse)", err)
  761. return
  762. }
  763. mile := &models.Milestone{
  764. RepoId: ctx.Repo.Repository.Id,
  765. Index: int64(ctx.Repo.Repository.NumMilestones) + 1,
  766. Name: form.Title,
  767. Content: form.Content,
  768. Deadline: deadline,
  769. }
  770. if err = models.NewMilestone(mile); err != nil {
  771. ctx.Handle(500, "issue.NewMilestonePost(NewMilestone)", err)
  772. return
  773. }
  774. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  775. }
  776. func UpdateMilestone(ctx *middleware.Context, params martini.Params) {
  777. ctx.Data["Title"] = "Update Milestone"
  778. ctx.Data["IsRepoToolbarIssues"] = true
  779. ctx.Data["IsRepoToolbarIssuesList"] = true
  780. idx, _ := base.StrTo(params["index"]).Int64()
  781. if idx == 0 {
  782. ctx.Handle(404, "issue.UpdateMilestone", nil)
  783. return
  784. }
  785. mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx)
  786. if err != nil {
  787. if err == models.ErrMilestoneNotExist {
  788. ctx.Handle(404, "issue.UpdateMilestone(GetMilestoneByIndex)", err)
  789. } else {
  790. ctx.Handle(500, "issue.UpdateMilestone(GetMilestoneByIndex)", err)
  791. }
  792. return
  793. }
  794. action := params["action"]
  795. if len(action) > 0 {
  796. switch action {
  797. case "open":
  798. if mile.IsClosed {
  799. if err = models.ChangeMilestoneStatus(mile, false); err != nil {
  800. ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err)
  801. return
  802. }
  803. }
  804. case "close":
  805. if !mile.IsClosed {
  806. mile.ClosedDate = time.Now()
  807. if err = models.ChangeMilestoneStatus(mile, true); err != nil {
  808. ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err)
  809. return
  810. }
  811. }
  812. case "delete":
  813. if err = models.DeleteMilestone(mile); err != nil {
  814. ctx.Handle(500, "issue.UpdateMilestone(DeleteMilestone)", err)
  815. return
  816. }
  817. }
  818. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  819. return
  820. }
  821. mile.DeadlineString = mile.Deadline.UTC().Format("01/02/2006")
  822. if mile.DeadlineString == "12/31/9999" {
  823. mile.DeadlineString = ""
  824. }
  825. ctx.Data["Milestone"] = mile
  826. ctx.HTML(200, MILESTONE_EDIT)
  827. }
  828. func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form auth.CreateMilestoneForm) {
  829. ctx.Data["Title"] = "Update Milestone"
  830. ctx.Data["IsRepoToolbarIssues"] = true
  831. ctx.Data["IsRepoToolbarIssuesList"] = true
  832. idx, _ := base.StrTo(params["index"]).Int64()
  833. if idx == 0 {
  834. ctx.Handle(404, "issue.UpdateMilestonePost", nil)
  835. return
  836. }
  837. mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx)
  838. if err != nil {
  839. if err == models.ErrMilestoneNotExist {
  840. ctx.Handle(404, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err)
  841. } else {
  842. ctx.Handle(500, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err)
  843. }
  844. return
  845. }
  846. if ctx.HasError() {
  847. ctx.HTML(200, MILESTONE_EDIT)
  848. return
  849. }
  850. var deadline time.Time
  851. if len(form.Deadline) == 0 {
  852. form.Deadline = "12/31/9999"
  853. }
  854. deadline, err = time.Parse("01/02/2006", form.Deadline)
  855. if err != nil {
  856. ctx.Handle(500, "issue.UpdateMilestonePost(time.Parse)", err)
  857. return
  858. }
  859. mile.Name = form.Title
  860. mile.Content = form.Content
  861. mile.Deadline = deadline
  862. if err = models.UpdateMilestone(mile); err != nil {
  863. ctx.Handle(500, "issue.UpdateMilestonePost(UpdateMilestone)", err)
  864. return
  865. }
  866. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  867. }