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 31 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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224
  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. if comments[i].Type == models.COMMENT {
  353. comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink))
  354. }
  355. }
  356. ctx.Data["AllowedTypes"] = setting.AttachmentAllowedTypes
  357. ctx.Data["Title"] = issue.Name
  358. ctx.Data["Issue"] = issue
  359. ctx.Data["Comments"] = comments
  360. ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || (ctx.IsSigned && issue.PosterId == ctx.User.Id)
  361. ctx.Data["IsRepoToolbarIssues"] = true
  362. ctx.Data["IsRepoToolbarIssuesList"] = false
  363. ctx.HTML(200, ISSUE_VIEW)
  364. }
  365. func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
  366. idx, _ := base.StrTo(params["index"]).Int64()
  367. if idx <= 0 {
  368. ctx.Error(404)
  369. return
  370. }
  371. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx)
  372. if err != nil {
  373. if err == models.ErrIssueNotExist {
  374. ctx.Handle(404, "issue.UpdateIssue", err)
  375. } else {
  376. ctx.Handle(500, "issue.UpdateIssue(GetIssueByIndex)", err)
  377. }
  378. return
  379. }
  380. if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner {
  381. ctx.Error(403)
  382. return
  383. }
  384. issue.Name = form.IssueName
  385. issue.MilestoneId = form.MilestoneId
  386. issue.AssigneeId = form.AssigneeId
  387. issue.LabelIds = form.Labels
  388. issue.Content = form.Content
  389. // try get content from text, ignore conflict with preview ajax
  390. if form.Content == "" {
  391. issue.Content = ctx.Query("text")
  392. }
  393. if err = models.UpdateIssue(issue); err != nil {
  394. ctx.Handle(500, "issue.UpdateIssue(UpdateIssue)", err)
  395. return
  396. }
  397. ctx.JSON(200, map[string]interface{}{
  398. "ok": true,
  399. "title": issue.Name,
  400. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)),
  401. })
  402. }
  403. func UpdateIssueLabel(ctx *middleware.Context, params martini.Params) {
  404. if !ctx.Repo.IsOwner {
  405. ctx.Error(403)
  406. return
  407. }
  408. idx, _ := base.StrTo(params["index"]).Int64()
  409. if idx <= 0 {
  410. ctx.Error(404)
  411. return
  412. }
  413. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx)
  414. if err != nil {
  415. if err == models.ErrIssueNotExist {
  416. ctx.Handle(404, "issue.UpdateIssueLabel(GetIssueByIndex)", err)
  417. } else {
  418. ctx.Handle(500, "issue.UpdateIssueLabel(GetIssueByIndex)", err)
  419. }
  420. return
  421. }
  422. isAttach := ctx.Query("action") == "attach"
  423. labelStrId := ctx.Query("id")
  424. labelId, _ := base.StrTo(labelStrId).Int64()
  425. label, err := models.GetLabelById(labelId)
  426. if err != nil {
  427. if err == models.ErrLabelNotExist {
  428. ctx.Handle(404, "issue.UpdateIssueLabel(GetLabelById)", err)
  429. } else {
  430. ctx.Handle(500, "issue.UpdateIssueLabel(GetLabelById)", err)
  431. }
  432. return
  433. }
  434. isHad := strings.Contains(issue.LabelIds, "$"+labelStrId+"|")
  435. isNeedUpdate := false
  436. if isAttach {
  437. if !isHad {
  438. issue.LabelIds += "$" + labelStrId + "|"
  439. isNeedUpdate = true
  440. }
  441. } else {
  442. if isHad {
  443. issue.LabelIds = strings.Replace(issue.LabelIds, "$"+labelStrId+"|", "", -1)
  444. isNeedUpdate = true
  445. }
  446. }
  447. if isNeedUpdate {
  448. if err = models.UpdateIssue(issue); err != nil {
  449. ctx.Handle(500, "issue.UpdateIssueLabel(UpdateIssue)", err)
  450. return
  451. }
  452. if isAttach {
  453. label.NumIssues++
  454. if issue.IsClosed {
  455. label.NumClosedIssues++
  456. }
  457. } else {
  458. label.NumIssues--
  459. if issue.IsClosed {
  460. label.NumClosedIssues--
  461. }
  462. }
  463. if err = models.UpdateLabel(label); err != nil {
  464. ctx.Handle(500, "issue.UpdateIssueLabel(UpdateLabel)", err)
  465. return
  466. }
  467. }
  468. ctx.JSON(200, map[string]interface{}{
  469. "ok": true,
  470. })
  471. }
  472. func UpdateIssueMilestone(ctx *middleware.Context) {
  473. if !ctx.Repo.IsOwner {
  474. ctx.Error(403)
  475. return
  476. }
  477. issueId, err := base.StrTo(ctx.Query("issue")).Int64()
  478. if err != nil {
  479. ctx.Error(404)
  480. return
  481. }
  482. issue, err := models.GetIssueById(issueId)
  483. if err != nil {
  484. if err == models.ErrIssueNotExist {
  485. ctx.Handle(404, "issue.UpdateIssueMilestone(GetIssueById)", err)
  486. } else {
  487. ctx.Handle(500, "issue.UpdateIssueMilestone(GetIssueById)", err)
  488. }
  489. return
  490. }
  491. oldMid := issue.MilestoneId
  492. mid, _ := base.StrTo(ctx.Query("milestone")).Int64()
  493. if oldMid == mid {
  494. ctx.JSON(200, map[string]interface{}{
  495. "ok": true,
  496. })
  497. return
  498. }
  499. // Not check for invalid milestone id and give responsibility to owners.
  500. issue.MilestoneId = mid
  501. if err = models.ChangeMilestoneAssign(oldMid, mid, issue); err != nil {
  502. ctx.Handle(500, "issue.UpdateIssueMilestone(ChangeMilestoneAssign)", err)
  503. return
  504. } else if err = models.UpdateIssue(issue); err != nil {
  505. ctx.Handle(500, "issue.UpdateIssueMilestone(UpdateIssue)", err)
  506. return
  507. }
  508. ctx.JSON(200, map[string]interface{}{
  509. "ok": true,
  510. })
  511. }
  512. func UpdateAssignee(ctx *middleware.Context) {
  513. if !ctx.Repo.IsOwner {
  514. ctx.Error(403)
  515. return
  516. }
  517. issueId, err := base.StrTo(ctx.Query("issue")).Int64()
  518. if err != nil {
  519. ctx.Error(404)
  520. return
  521. }
  522. issue, err := models.GetIssueById(issueId)
  523. if err != nil {
  524. if err == models.ErrIssueNotExist {
  525. ctx.Handle(404, "issue.UpdateAssignee(GetIssueById)", err)
  526. } else {
  527. ctx.Handle(500, "issue.UpdateAssignee(GetIssueById)", err)
  528. }
  529. return
  530. }
  531. aid, _ := base.StrTo(ctx.Query("assigneeid")).Int64()
  532. // Not check for invalid assignne id and give responsibility to owners.
  533. issue.AssigneeId = aid
  534. if err = models.UpdateIssueUserPairByAssignee(aid, issue.Id); err != nil {
  535. ctx.Handle(500, "issue.UpdateAssignee(UpdateIssueUserPairByAssignee): %v", err)
  536. return
  537. } else if err = models.UpdateIssue(issue); err != nil {
  538. ctx.Handle(500, "issue.UpdateAssignee(UpdateIssue)", err)
  539. return
  540. }
  541. ctx.JSON(200, map[string]interface{}{
  542. "ok": true,
  543. })
  544. }
  545. func Comment(ctx *middleware.Context, params martini.Params) {
  546. index, err := base.StrTo(ctx.Query("issueIndex")).Int64()
  547. if err != nil {
  548. ctx.Handle(404, "issue.Comment(get index)", err)
  549. return
  550. }
  551. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index)
  552. if err != nil {
  553. if err == models.ErrIssueNotExist {
  554. ctx.Handle(404, "issue.Comment", err)
  555. } else {
  556. ctx.Handle(200, "issue.Comment(get issue)", err)
  557. }
  558. return
  559. }
  560. // Check if issue owner changes the status of issue.
  561. var newStatus string
  562. if ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id {
  563. newStatus = ctx.Query("change_status")
  564. }
  565. if len(newStatus) > 0 {
  566. if (strings.Contains(newStatus, "Reopen") && issue.IsClosed) ||
  567. (strings.Contains(newStatus, "Close") && !issue.IsClosed) {
  568. issue.IsClosed = !issue.IsClosed
  569. if err = models.UpdateIssue(issue); err != nil {
  570. ctx.Handle(500, "issue.Comment(UpdateIssue)", err)
  571. return
  572. } else if err = models.UpdateIssueUserPairsByStatus(issue.Id, issue.IsClosed); err != nil {
  573. ctx.Handle(500, "issue.Comment(UpdateIssueUserPairsByStatus)", err)
  574. return
  575. }
  576. // Change open/closed issue counter for the associated milestone
  577. if issue.MilestoneId > 0 {
  578. if err = models.ChangeMilestoneIssueStats(issue); err != nil {
  579. ctx.Handle(500, "issue.Comment(ChangeMilestoneIssueStats)", err)
  580. }
  581. }
  582. cmtType := models.CLOSE
  583. if !issue.IsClosed {
  584. cmtType = models.REOPEN
  585. }
  586. if _, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, "", nil); err != nil {
  587. ctx.Handle(200, "issue.Comment(create status change comment)", err)
  588. return
  589. }
  590. log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed)
  591. }
  592. }
  593. var comment *models.Comment
  594. var ms []string
  595. content := ctx.Query("content")
  596. if len(content) > 0 {
  597. switch params["action"] {
  598. case "new":
  599. if comment, err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.COMMENT, content, nil); err != nil {
  600. ctx.Handle(500, "issue.Comment(create comment)", err)
  601. return
  602. }
  603. // Update mentions.
  604. ms = base.MentionPattern.FindAllString(issue.Content, -1)
  605. if len(ms) > 0 {
  606. for i := range ms {
  607. ms[i] = ms[i][1:]
  608. }
  609. ids := models.GetUserIdsByNames(ms)
  610. if err := models.UpdateIssueUserPairsByMentions(ids, issue.Id); err != nil {
  611. ctx.Handle(500, "issue.CreateIssue(UpdateIssueUserPairsByMentions)", err)
  612. return
  613. }
  614. }
  615. log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id)
  616. default:
  617. ctx.Handle(404, "issue.Comment", err)
  618. return
  619. }
  620. }
  621. attachments := strings.Split(params["attachments"], ",")
  622. for _, a := range attachments {
  623. aId, err := base.StrTo(a).Int64()
  624. if err != nil {
  625. ctx.Handle(400, "issue.Comment(base.StrTo.Int64)", err)
  626. return
  627. }
  628. err = models.AssignAttachment(issue.Id, comment.Id, aId)
  629. if err != nil {
  630. ctx.Handle(400, "issue.Comment(models.AssignAttachment)", err)
  631. return
  632. }
  633. }
  634. // Notify watchers.
  635. act := &models.Action{
  636. ActUserId: ctx.User.Id,
  637. ActUserName: ctx.User.LowerName,
  638. ActEmail: ctx.User.Email,
  639. OpType: models.OP_COMMENT_ISSUE,
  640. Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]),
  641. RepoId: ctx.Repo.Repository.Id,
  642. RepoUserName: ctx.Repo.Owner.LowerName,
  643. RepoName: ctx.Repo.Repository.LowerName,
  644. }
  645. if err = models.NotifyWatchers(act); err != nil {
  646. ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err)
  647. return
  648. }
  649. // Mail watchers and mentions.
  650. if setting.Service.EnableNotifyMail {
  651. issue.Content = content
  652. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  653. if err != nil {
  654. ctx.Handle(500, "issue.Comment(SendIssueNotifyMail)", err)
  655. return
  656. }
  657. tos = append(tos, ctx.User.LowerName)
  658. newTos := make([]string, 0, len(ms))
  659. for _, m := range ms {
  660. if com.IsSliceContainsStr(tos, m) {
  661. continue
  662. }
  663. newTos = append(newTos, m)
  664. }
  665. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  666. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  667. ctx.Handle(500, "issue.Comment(SendIssueMentionMail)", err)
  668. return
  669. }
  670. }
  671. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, index))
  672. }
  673. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  674. if ctx.HasError() {
  675. Issues(ctx)
  676. return
  677. }
  678. l := &models.Label{
  679. RepoId: ctx.Repo.Repository.Id,
  680. Name: form.Title,
  681. Color: form.Color,
  682. }
  683. if err := models.NewLabel(l); err != nil {
  684. ctx.Handle(500, "issue.NewLabel(NewLabel)", err)
  685. return
  686. }
  687. ctx.Redirect(ctx.Repo.RepoLink + "/issues")
  688. }
  689. func UpdateLabel(ctx *middleware.Context, params martini.Params, form auth.CreateLabelForm) {
  690. id, _ := base.StrTo(ctx.Query("id")).Int64()
  691. if id == 0 {
  692. ctx.Error(404)
  693. return
  694. }
  695. l := &models.Label{
  696. Id: id,
  697. Name: form.Title,
  698. Color: form.Color,
  699. }
  700. if err := models.UpdateLabel(l); err != nil {
  701. ctx.Handle(500, "issue.UpdateLabel(UpdateLabel)", err)
  702. return
  703. }
  704. ctx.Redirect(ctx.Repo.RepoLink + "/issues")
  705. }
  706. func DeleteLabel(ctx *middleware.Context) {
  707. removes := ctx.Query("remove")
  708. if len(strings.TrimSpace(removes)) == 0 {
  709. ctx.JSON(200, map[string]interface{}{
  710. "ok": true,
  711. })
  712. return
  713. }
  714. strIds := strings.Split(removes, ",")
  715. for _, strId := range strIds {
  716. if err := models.DeleteLabel(ctx.Repo.Repository.Id, strId); err != nil {
  717. ctx.Handle(500, "issue.DeleteLabel(DeleteLabel)", err)
  718. return
  719. }
  720. }
  721. ctx.JSON(200, map[string]interface{}{
  722. "ok": true,
  723. })
  724. }
  725. func Milestones(ctx *middleware.Context) {
  726. ctx.Data["Title"] = "Milestones"
  727. ctx.Data["IsRepoToolbarIssues"] = true
  728. ctx.Data["IsRepoToolbarIssuesList"] = true
  729. isShowClosed := ctx.Query("state") == "closed"
  730. miles, err := models.GetMilestones(ctx.Repo.Repository.Id, isShowClosed)
  731. if err != nil {
  732. ctx.Handle(500, "issue.Milestones(GetMilestones)", err)
  733. return
  734. }
  735. for _, m := range miles {
  736. m.RenderedContent = string(base.RenderSpecialLink([]byte(m.Content), ctx.Repo.RepoLink))
  737. m.CalOpenIssues()
  738. }
  739. ctx.Data["Milestones"] = miles
  740. if isShowClosed {
  741. ctx.Data["State"] = "closed"
  742. } else {
  743. ctx.Data["State"] = "open"
  744. }
  745. ctx.HTML(200, MILESTONE)
  746. }
  747. func NewMilestone(ctx *middleware.Context) {
  748. ctx.Data["Title"] = "New Milestone"
  749. ctx.Data["IsRepoToolbarIssues"] = true
  750. ctx.Data["IsRepoToolbarIssuesList"] = true
  751. ctx.HTML(200, MILESTONE_NEW)
  752. }
  753. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  754. ctx.Data["Title"] = "New Milestone"
  755. ctx.Data["IsRepoToolbarIssues"] = true
  756. ctx.Data["IsRepoToolbarIssuesList"] = true
  757. if ctx.HasError() {
  758. ctx.HTML(200, MILESTONE_NEW)
  759. return
  760. }
  761. var deadline time.Time
  762. var err error
  763. if len(form.Deadline) == 0 {
  764. form.Deadline = "12/31/9999"
  765. }
  766. deadline, err = time.Parse("01/02/2006", form.Deadline)
  767. if err != nil {
  768. ctx.Handle(500, "issue.NewMilestonePost(time.Parse)", err)
  769. return
  770. }
  771. mile := &models.Milestone{
  772. RepoId: ctx.Repo.Repository.Id,
  773. Index: int64(ctx.Repo.Repository.NumMilestones) + 1,
  774. Name: form.Title,
  775. Content: form.Content,
  776. Deadline: deadline,
  777. }
  778. if err = models.NewMilestone(mile); err != nil {
  779. ctx.Handle(500, "issue.NewMilestonePost(NewMilestone)", err)
  780. return
  781. }
  782. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  783. }
  784. func UpdateMilestone(ctx *middleware.Context, params martini.Params) {
  785. ctx.Data["Title"] = "Update Milestone"
  786. ctx.Data["IsRepoToolbarIssues"] = true
  787. ctx.Data["IsRepoToolbarIssuesList"] = true
  788. idx, _ := base.StrTo(params["index"]).Int64()
  789. if idx == 0 {
  790. ctx.Handle(404, "issue.UpdateMilestone", nil)
  791. return
  792. }
  793. mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx)
  794. if err != nil {
  795. if err == models.ErrMilestoneNotExist {
  796. ctx.Handle(404, "issue.UpdateMilestone(GetMilestoneByIndex)", err)
  797. } else {
  798. ctx.Handle(500, "issue.UpdateMilestone(GetMilestoneByIndex)", err)
  799. }
  800. return
  801. }
  802. action := params["action"]
  803. if len(action) > 0 {
  804. switch action {
  805. case "open":
  806. if mile.IsClosed {
  807. if err = models.ChangeMilestoneStatus(mile, false); err != nil {
  808. ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err)
  809. return
  810. }
  811. }
  812. case "close":
  813. if !mile.IsClosed {
  814. mile.ClosedDate = time.Now()
  815. if err = models.ChangeMilestoneStatus(mile, true); err != nil {
  816. ctx.Handle(500, "issue.UpdateMilestone(ChangeMilestoneStatus)", err)
  817. return
  818. }
  819. }
  820. case "delete":
  821. if err = models.DeleteMilestone(mile); err != nil {
  822. ctx.Handle(500, "issue.UpdateMilestone(DeleteMilestone)", err)
  823. return
  824. }
  825. }
  826. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  827. return
  828. }
  829. mile.DeadlineString = mile.Deadline.UTC().Format("01/02/2006")
  830. if mile.DeadlineString == "12/31/9999" {
  831. mile.DeadlineString = ""
  832. }
  833. ctx.Data["Milestone"] = mile
  834. ctx.HTML(200, MILESTONE_EDIT)
  835. }
  836. func UpdateMilestonePost(ctx *middleware.Context, params martini.Params, form auth.CreateMilestoneForm) {
  837. ctx.Data["Title"] = "Update Milestone"
  838. ctx.Data["IsRepoToolbarIssues"] = true
  839. ctx.Data["IsRepoToolbarIssuesList"] = true
  840. idx, _ := base.StrTo(params["index"]).Int64()
  841. if idx == 0 {
  842. ctx.Handle(404, "issue.UpdateMilestonePost", nil)
  843. return
  844. }
  845. mile, err := models.GetMilestoneByIndex(ctx.Repo.Repository.Id, idx)
  846. if err != nil {
  847. if err == models.ErrMilestoneNotExist {
  848. ctx.Handle(404, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err)
  849. } else {
  850. ctx.Handle(500, "issue.UpdateMilestonePost(GetMilestoneByIndex)", err)
  851. }
  852. return
  853. }
  854. if ctx.HasError() {
  855. ctx.HTML(200, MILESTONE_EDIT)
  856. return
  857. }
  858. var deadline time.Time
  859. if len(form.Deadline) == 0 {
  860. form.Deadline = "12/31/9999"
  861. }
  862. deadline, err = time.Parse("01/02/2006", form.Deadline)
  863. if err != nil {
  864. ctx.Handle(500, "issue.UpdateMilestonePost(time.Parse)", err)
  865. return
  866. }
  867. mile.Name = form.Title
  868. mile.Content = form.Content
  869. mile.Deadline = deadline
  870. if err = models.UpdateMilestone(mile); err != nil {
  871. ctx.Handle(500, "issue.UpdateMilestonePost(UpdateMilestone)", err)
  872. return
  873. }
  874. ctx.Redirect(ctx.Repo.RepoLink + "/issues/milestones")
  875. }
  876. func IssuePostAttachment(ctx *middleware.Context, params martini.Params) {
  877. issueId, _ := base.StrTo(params["index"]).Int64()
  878. if issueId == 0 {
  879. ctx.JSON(400, map[string]interface{}{
  880. "ok": false,
  881. "error": "invalid issue id",
  882. })
  883. return
  884. }
  885. commentId, err := base.StrTo(params["comment"]).Int64()
  886. if err != nil && len(params["comment"]) > 0 {
  887. ctx.JSON(400, map[string]interface{}{
  888. "ok": false,
  889. "error": "invalid comment id",
  890. })
  891. return
  892. }
  893. if commentId == 0 {
  894. commentId = -1
  895. }
  896. file, header, err := ctx.Req.FormFile("attachment")
  897. if err != nil {
  898. ctx.JSON(400, map[string]interface{}{
  899. "ok": false,
  900. "error": "upload error",
  901. })
  902. return
  903. }
  904. defer file.Close()
  905. // check mime type, write to file, insert attachment to db
  906. allowedTypes := strings.Split(setting.AttachmentAllowedTypes, "|")
  907. allowed := false
  908. fileType := mime.TypeByExtension(header.Filename)
  909. for _, t := range allowedTypes {
  910. t := strings.Trim(t, " ")
  911. if t == "*/*" || t == fileType {
  912. allowed = true
  913. break
  914. }
  915. }
  916. if !allowed {
  917. ctx.JSON(400, map[string]interface{}{
  918. "ok": false,
  919. "error": "mime type not allowed",
  920. })
  921. return
  922. }
  923. out, err := ioutil.TempFile(setting.AttachmentPath, "attachment_")
  924. if err != nil {
  925. ctx.JSON(500, map[string]interface{}{
  926. "ok": false,
  927. "error": "internal server error",
  928. })
  929. return
  930. }
  931. defer out.Close()
  932. _, err = io.Copy(out, file)
  933. if err != nil {
  934. ctx.JSON(500, map[string]interface{}{
  935. "ok": false,
  936. "error": "internal server error",
  937. })
  938. return
  939. }
  940. a, err := models.CreateAttachment(issueId, commentId, header.Filename, out.Name())
  941. if err != nil {
  942. ctx.JSON(500, map[string]interface{}{
  943. "ok": false,
  944. "error": "internal server error",
  945. })
  946. return
  947. }
  948. ctx.JSON(500, map[string]interface{}{
  949. "ok": true,
  950. "id": a.Id,
  951. })
  952. }
  953. func IssueGetAttachment(ctx *middleware.Context, params martini.Params) {
  954. id, err := base.StrTo(params["id"]).Int64()
  955. if err != nil {
  956. ctx.Handle(400, "issue.IssueGetAttachment(base.StrTo.Int64)", err)
  957. return
  958. }
  959. attachment, err := models.GetAttachmentById(id)
  960. if err != nil {
  961. ctx.Handle(404, "issue.IssueGetAttachment(models.GetAttachmentById)", err)
  962. return
  963. }
  964. ctx.ServeFile(attachment.Path, attachment.Name)
  965. }
  966. func IssueDeleteAttachment(ctx *middleware.Context, params martini.Params) {
  967. issueId, _ := base.StrTo(params["index"]).Int64()
  968. if issueId == 0 {
  969. ctx.JSON(400, map[string]interface{}{
  970. "ok": false,
  971. "error": "invalid issue id",
  972. })
  973. return
  974. }
  975. commentId, err := base.StrTo(params["comment"]).Int64()
  976. if err != nil || commentId < 0 {
  977. ctx.JSON(400, map[string]interface{}{
  978. "ok": false,
  979. "error": "invalid comment id",
  980. })
  981. return
  982. }
  983. comment, err := models.GetCommentById(commentId)
  984. if err != nil {
  985. ctx.JSON(400, map[string]interface{}{
  986. "ok": false,
  987. "error": "invalid issue id",
  988. })
  989. return
  990. }
  991. if comment.PosterId != ctx.User.Id && !ctx.User.IsAdmin {
  992. ctx.JSON(400, map[string]interface{}{
  993. "ok": false,
  994. "error": "no permissions",
  995. })
  996. return
  997. }
  998. attachmentId, err := base.StrTo(params["id"]).Int64()
  999. if err != nil {
  1000. ctx.JSON(400, map[string]interface{}{
  1001. "ok": false,
  1002. "error": "invalid attachment id",
  1003. })
  1004. return
  1005. }
  1006. attachment, err := models.GetAttachmentById(attachmentId)
  1007. if err != nil {
  1008. ctx.JSON(400, map[string]interface{}{
  1009. "ok": false,
  1010. "error": "wrong attachment id",
  1011. })
  1012. return
  1013. }
  1014. if attachment.IssueId != issueId {
  1015. ctx.JSON(400, map[string]interface{}{
  1016. "ok": false,
  1017. "error": "attachment not associated with the given issue",
  1018. })
  1019. return
  1020. }
  1021. if attachment.CommentId != commentId {
  1022. ctx.JSON(400, map[string]interface{}{
  1023. "ok": false,
  1024. "error": "attachment not associated with the given comment",
  1025. })
  1026. return
  1027. }
  1028. err = models.DeleteAttachment(attachment, true)
  1029. if err != nil {
  1030. ctx.JSON(500, map[string]interface{}{
  1031. "ok": false,
  1032. "error": "could not delete attachment",
  1033. })
  1034. return
  1035. }
  1036. ctx.JSON(200, map[string]interface{}{
  1037. "ok": true,
  1038. })
  1039. }