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