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