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

issue.go 28 kB

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