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