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 27 kB

11 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050
  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. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "github.com/Unknwon/com"
  13. "github.com/Unknwon/paginater"
  14. "github.com/gogits/gogs/models"
  15. "github.com/gogits/gogs/modules/auth"
  16. "github.com/gogits/gogs/modules/base"
  17. "github.com/gogits/gogs/modules/log"
  18. "github.com/gogits/gogs/modules/mailer"
  19. "github.com/gogits/gogs/modules/middleware"
  20. "github.com/gogits/gogs/modules/setting"
  21. )
  22. const (
  23. ISSUES base.TplName = "repo/issue/list"
  24. ISSUE_NEW base.TplName = "repo/issue/new"
  25. ISSUE_VIEW base.TplName = "repo/issue/view"
  26. LABELS base.TplName = "repo/issue/labels"
  27. MILESTONE base.TplName = "repo/issue/milestones"
  28. MILESTONE_NEW base.TplName = "repo/issue/milestone_new"
  29. MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit"
  30. )
  31. var (
  32. ErrFileTypeForbidden = errors.New("File type is not allowed")
  33. ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded")
  34. )
  35. func RetrieveLabels(ctx *middleware.Context) {
  36. labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID)
  37. if err != nil {
  38. ctx.Handle(500, "RetrieveLabels.GetLabels: %v", err)
  39. return
  40. }
  41. for _, l := range labels {
  42. l.CalOpenIssues()
  43. }
  44. ctx.Data["Labels"] = labels
  45. ctx.Data["NumLabels"] = len(labels)
  46. }
  47. func Issues(ctx *middleware.Context) {
  48. ctx.Data["Title"] = ctx.Tr("repo.issues")
  49. ctx.Data["PageIsIssueList"] = true
  50. viewType := ctx.Query("type")
  51. types := []string{"assigned", "created_by", "mentioned"}
  52. if !com.IsSliceContainsStr(types, viewType) {
  53. viewType = "all"
  54. }
  55. // Must sign in to see issues about you.
  56. if viewType != "all" && !ctx.IsSigned {
  57. ctx.SetCookie("redirect_to", "/"+url.QueryEscape(setting.AppSubUrl+ctx.Req.RequestURI), 0, setting.AppSubUrl)
  58. ctx.Redirect(setting.AppSubUrl + "/user/login")
  59. return
  60. }
  61. var assigneeID, posterID int64
  62. filterMode := models.FM_ALL
  63. switch viewType {
  64. case "assigned":
  65. assigneeID = ctx.User.Id
  66. filterMode = models.FM_ASSIGN
  67. case "created_by":
  68. posterID = ctx.User.Id
  69. filterMode = models.FM_CREATE
  70. case "mentioned":
  71. filterMode = models.FM_MENTION
  72. }
  73. var uid int64 = -1
  74. if ctx.IsSigned {
  75. uid = ctx.User.Id
  76. }
  77. repo := ctx.Repo.Repository
  78. selectLabels := ctx.Query("labels")
  79. milestoneID := ctx.QueryInt64("milestone")
  80. isShowClosed := ctx.Query("state") == "closed"
  81. issueStats := models.GetIssueStats(repo.ID, uid, com.StrTo(selectLabels).MustInt64(), milestoneID, isShowClosed, filterMode)
  82. page := ctx.QueryInt("page")
  83. if page <= 1 {
  84. page = 1
  85. }
  86. var total int
  87. if !isShowClosed {
  88. total = int(issueStats.OpenCount)
  89. } else {
  90. total = int(issueStats.ClosedCount)
  91. }
  92. ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5)
  93. // Get issues.
  94. issues, err := models.Issues(uid, assigneeID, repo.ID, posterID, milestoneID,
  95. page, isShowClosed, filterMode == models.FM_MENTION, selectLabels, ctx.Query("sortType"))
  96. if err != nil {
  97. ctx.Handle(500, "Issues: %v", err)
  98. return
  99. }
  100. // Get issue-user relations.
  101. pairs, err := models.GetIssueUsers(repo.ID, posterID, isShowClosed)
  102. if err != nil {
  103. ctx.Handle(500, "GetIssueUsers: %v", err)
  104. return
  105. }
  106. // Get posters.
  107. for i := range issues {
  108. if err = issues[i].GetPoster(); err != nil {
  109. ctx.Handle(500, "GetPoster", fmt.Errorf("[#%d]%v", issues[i].ID, err))
  110. return
  111. }
  112. if err = issues[i].GetLabels(); err != nil {
  113. ctx.Handle(500, "GetLabels", fmt.Errorf("[#%d]%v", issues[i].ID, err))
  114. return
  115. }
  116. if !ctx.IsSigned {
  117. issues[i].IsRead = true
  118. continue
  119. }
  120. // Check read status.
  121. idx := models.PairsContains(pairs, issues[i].ID, ctx.User.Id)
  122. if idx > -1 {
  123. issues[i].IsRead = pairs[idx].IsRead
  124. } else {
  125. issues[i].IsRead = true
  126. }
  127. }
  128. ctx.Data["Issues"] = issues
  129. // Get milestones.
  130. miles, err := models.GetAllRepoMilestones(repo.ID)
  131. if err != nil {
  132. ctx.Handle(500, "GetAllRepoMilestones: %v", err)
  133. return
  134. }
  135. ctx.Data["Milestones"] = miles
  136. ctx.Data["IssueStats"] = issueStats
  137. ctx.Data["SelectLabels"] = com.StrTo(selectLabels).MustInt64()
  138. ctx.Data["ViewType"] = viewType
  139. ctx.Data["MilestoneID"] = milestoneID
  140. ctx.Data["IsShowClosed"] = isShowClosed
  141. if isShowClosed {
  142. ctx.Data["State"] = "closed"
  143. } else {
  144. ctx.Data["State"] = "open"
  145. }
  146. ctx.HTML(200, ISSUES)
  147. }
  148. func renderAttachmentSettings(ctx *middleware.Context) {
  149. ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
  150. ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
  151. ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles
  152. }
  153. func NewIssue(ctx *middleware.Context) {
  154. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  155. ctx.Data["PageIsIssueList"] = true
  156. ctx.Data["RequireDropzone"] = true
  157. renderAttachmentSettings(ctx)
  158. if ctx.User.IsAdmin {
  159. var (
  160. repo = ctx.Repo.Repository
  161. err error
  162. )
  163. ctx.Data["Labels"], err = models.GetLabelsByRepoID(repo.ID)
  164. if err != nil {
  165. ctx.Handle(500, "GetLabelsByRepoID: %v", err)
  166. return
  167. }
  168. ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
  169. if err != nil {
  170. ctx.Handle(500, "GetMilestones: %v", err)
  171. return
  172. }
  173. ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
  174. if err != nil {
  175. ctx.Handle(500, "GetMilestones: %v", err)
  176. return
  177. }
  178. ctx.Data["Assignees"], err = repo.GetAssignees()
  179. if err != nil {
  180. ctx.Handle(500, "GetAssignees: %v", err)
  181. return
  182. }
  183. }
  184. ctx.HTML(200, ISSUE_NEW)
  185. }
  186. func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
  187. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  188. ctx.Data["PageIsIssueList"] = true
  189. ctx.Data["RequireDropzone"] = true
  190. renderAttachmentSettings(ctx)
  191. var (
  192. repo = ctx.Repo.Repository
  193. labelIDs []int64
  194. milestoneID int64
  195. assigneeID int64
  196. attachments []string
  197. )
  198. if ctx.User.IsAdmin {
  199. // Check labels.
  200. labelIDs = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
  201. labelIDMark := base.Int64sToMap(labelIDs)
  202. labels, err := models.GetLabelsByRepoID(repo.ID)
  203. if err != nil {
  204. ctx.Handle(500, "GetLabelsByRepoID: %v", err)
  205. return
  206. }
  207. hasSelected := false
  208. for i := range labels {
  209. if labelIDMark[labels[i].ID] {
  210. labels[i].IsChecked = true
  211. hasSelected = true
  212. }
  213. }
  214. ctx.Data["HasSelectedLabel"] = hasSelected
  215. ctx.Data["label_ids"] = form.LabelIDs
  216. ctx.Data["Labels"] = labels
  217. // Check milestone.
  218. milestoneID = form.MilestoneID
  219. if milestoneID > 0 {
  220. ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
  221. if err != nil {
  222. ctx.Handle(500, "GetMilestones: %v", err)
  223. return
  224. }
  225. ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
  226. if err != nil {
  227. ctx.Handle(500, "GetMilestones: %v", err)
  228. return
  229. }
  230. ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
  231. if err != nil {
  232. ctx.Handle(500, "GetMilestoneByID: %v", err)
  233. return
  234. }
  235. ctx.Data["milestone_id"] = milestoneID
  236. }
  237. // Check assignee.
  238. assigneeID = form.AssigneeID
  239. if assigneeID > 0 {
  240. ctx.Data["Assignees"], err = repo.GetAssignees()
  241. if err != nil {
  242. ctx.Handle(500, "GetAssignees: %v", err)
  243. return
  244. }
  245. ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID)
  246. if err != nil {
  247. ctx.Handle(500, "GetAssigneeByID: %v", err)
  248. return
  249. }
  250. ctx.Data["assignee_id"] = assigneeID
  251. }
  252. }
  253. if setting.AttachmentEnabled {
  254. attachments = form.Attachments
  255. }
  256. if ctx.HasError() {
  257. ctx.HTML(200, ISSUE_NEW)
  258. return
  259. }
  260. issue := &models.Issue{
  261. RepoID: ctx.Repo.Repository.ID,
  262. Index: int64(repo.NumIssues) + 1,
  263. Name: form.Title,
  264. PosterID: ctx.User.Id,
  265. Poster: ctx.User,
  266. MilestoneID: milestoneID,
  267. AssigneeID: assigneeID,
  268. Content: form.Content,
  269. }
  270. if err := models.NewIssue(repo, issue, labelIDs, attachments); err != nil {
  271. ctx.Handle(500, "NewIssue", err)
  272. return
  273. }
  274. // Update mentions.
  275. mentions := base.MentionPattern.FindAllString(issue.Content, -1)
  276. if len(mentions) > 0 {
  277. for i := range mentions {
  278. mentions[i] = mentions[i][1:]
  279. }
  280. if err := models.UpdateMentions(mentions, issue.ID); err != nil {
  281. ctx.Handle(500, "UpdateMentions", err)
  282. return
  283. }
  284. }
  285. // Mail watchers and mentions.
  286. if setting.Service.EnableNotifyMail {
  287. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  288. if err != nil {
  289. ctx.Handle(500, "SendIssueNotifyMail", err)
  290. return
  291. }
  292. tos = append(tos, ctx.User.LowerName)
  293. newTos := make([]string, 0, len(mentions))
  294. for _, m := range mentions {
  295. if com.IsSliceContainsStr(tos, m) {
  296. continue
  297. }
  298. newTos = append(newTos, m)
  299. }
  300. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  301. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  302. ctx.Handle(500, "SendIssueMentionMail", err)
  303. return
  304. }
  305. }
  306. log.Trace("Issue created: %d/%d", ctx.Repo.Repository.ID, issue.ID)
  307. ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
  308. }
  309. func UploadIssueAttachment(ctx *middleware.Context) {
  310. if !setting.AttachmentEnabled {
  311. ctx.Error(404, "attachment is not enabled")
  312. return
  313. }
  314. allowedTypes := strings.Split(setting.AttachmentAllowedTypes, ",")
  315. file, header, err := ctx.Req.FormFile("file")
  316. if err != nil {
  317. ctx.Error(500, fmt.Sprintf("FormFile: %v", err))
  318. return
  319. }
  320. defer file.Close()
  321. buf := make([]byte, 1024)
  322. n, _ := file.Read(buf)
  323. if n > 0 {
  324. buf = buf[:n]
  325. }
  326. fileType := http.DetectContentType(buf)
  327. allowed := false
  328. for _, t := range allowedTypes {
  329. t := strings.Trim(t, " ")
  330. if t == "*/*" || t == fileType {
  331. allowed = true
  332. break
  333. }
  334. }
  335. if !allowed {
  336. ctx.Error(400, ErrFileTypeForbidden.Error())
  337. return
  338. }
  339. attach, err := models.NewAttachment(header.Filename, buf, file)
  340. if err != nil {
  341. ctx.Error(500, fmt.Sprintf("NewAttachment: %v", err))
  342. return
  343. }
  344. log.Trace("New attachment uploaded: %s", attach.UUID)
  345. ctx.JSON(200, map[string]string{
  346. "uuid": attach.UUID,
  347. })
  348. }
  349. func checkLabels(labels, allLabels []*models.Label) {
  350. for _, l := range labels {
  351. for _, l2 := range allLabels {
  352. if l.ID == l2.ID {
  353. l2.IsChecked = true
  354. break
  355. }
  356. }
  357. }
  358. }
  359. func ViewIssue(ctx *middleware.Context) {
  360. ctx.Data["PageIsIssueList"] = true
  361. ctx.Data["RequireDropzone"] = true
  362. renderAttachmentSettings(ctx)
  363. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  364. if err != nil {
  365. if models.IsErrIssueNotExist(err) {
  366. ctx.Handle(404, "GetIssueByIndex", err)
  367. } else {
  368. ctx.Handle(500, "GetIssueByIndex", err)
  369. }
  370. return
  371. }
  372. ctx.Data["Title"] = issue.Name
  373. if err = issue.GetPoster(); err != nil {
  374. ctx.Handle(500, "GetPoster: %v", err)
  375. return
  376. }
  377. issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink))
  378. // Metas.
  379. if err = issue.GetLabels(); err != nil {
  380. ctx.Handle(500, "GetLabels", err)
  381. return
  382. }
  383. if ctx.IsSigned {
  384. // Update issue-user.
  385. if err = issue.ReadBy(ctx.User.Id); err != nil {
  386. ctx.Handle(500, "ReadBy: %v", err)
  387. return
  388. }
  389. if ctx.User.IsAdmin {
  390. // labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID)
  391. // if err != nil {
  392. // ctx.Handle(500, "GetLabels.2", err)
  393. // return
  394. // }
  395. // checkLabels(issue.Labels, labels)
  396. // ctx.Data["Labels"] = labels
  397. // // Get all milestones.
  398. // ctx.Data["OpenMilestones"], err = models.GetMilestones(ctx.Repo.Repository.ID, -1, false)
  399. // if err != nil {
  400. // ctx.Handle(500, "GetMilestones.1: %v", err)
  401. // return
  402. // }
  403. // ctx.Data["ClosedMilestones"], err = models.GetMilestones(ctx.Repo.Repository.ID, -1, true)
  404. // if err != nil {
  405. // ctx.Handle(500, "GetMilestones.2: %v", err)
  406. // return
  407. // }
  408. // // Get all collaborators.
  409. // ctx.Data["Collaborators"], err = ctx.Repo.Repository.GetCollaborators()
  410. // if err != nil {
  411. // ctx.Handle(500, "GetCollaborators", err)
  412. // return
  413. // }
  414. }
  415. }
  416. // Render comments.
  417. for i := range issue.Comments {
  418. if issue.Comments[i].Type == models.COMMENT_TYPE_COMMENT {
  419. issue.Comments[i].RenderedContent = string(base.RenderMarkdown([]byte(issue.Comments[i].Content), ctx.Repo.RepoLink))
  420. }
  421. }
  422. ctx.Data["Issue"] = issue
  423. // ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner() || (ctx.IsSigned && issue.PosterID == ctx.User.Id)
  424. ctx.HTML(200, ISSUE_VIEW)
  425. }
  426. func UpdateIssue(ctx *middleware.Context, form auth.CreateIssueForm) {
  427. idx := com.StrTo(ctx.Params(":index")).MustInt64()
  428. if idx <= 0 {
  429. ctx.Error(404)
  430. return
  431. }
  432. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, idx)
  433. if err != nil {
  434. if models.IsErrIssueNotExist(err) {
  435. ctx.Handle(404, "issue.UpdateIssue", err)
  436. } else {
  437. ctx.Handle(500, "issue.UpdateIssue(GetIssueByIndex)", err)
  438. }
  439. return
  440. }
  441. if ctx.User.Id != issue.PosterID && !ctx.Repo.IsOwner() {
  442. ctx.Error(403)
  443. return
  444. }
  445. issue.Name = form.Title
  446. //issue.MilestoneId = form.MilestoneId
  447. //issue.AssigneeId = form.AssigneeId
  448. //issue.LabelIds = form.Labels
  449. issue.Content = form.Content
  450. // try get content from text, ignore conflict with preview ajax
  451. if form.Content == "" {
  452. issue.Content = ctx.Query("text")
  453. }
  454. if err = models.UpdateIssue(issue); err != nil {
  455. ctx.Handle(500, "issue.UpdateIssue(UpdateIssue)", err)
  456. return
  457. }
  458. ctx.JSON(200, map[string]interface{}{
  459. "ok": true,
  460. "title": issue.Name,
  461. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)),
  462. })
  463. }
  464. func UpdateIssueLabel(ctx *middleware.Context) {
  465. if !ctx.Repo.IsOwner() {
  466. ctx.Error(403)
  467. return
  468. }
  469. idx := com.StrTo(ctx.Params(":index")).MustInt64()
  470. if idx <= 0 {
  471. ctx.Error(404)
  472. return
  473. }
  474. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, idx)
  475. if err != nil {
  476. if models.IsErrIssueNotExist(err) {
  477. ctx.Handle(404, "issue.UpdateIssueLabel(GetIssueByIndex)", err)
  478. } else {
  479. ctx.Handle(500, "issue.UpdateIssueLabel(GetIssueByIndex)", err)
  480. }
  481. return
  482. }
  483. isAttach := ctx.Query("action") == "attach"
  484. labelStrId := ctx.Query("id")
  485. labelID := com.StrTo(labelStrId).MustInt64()
  486. label, err := models.GetLabelByID(labelID)
  487. if err != nil {
  488. if models.IsErrLabelNotExist(err) {
  489. ctx.Handle(404, "issue.UpdateIssueLabel(GetLabelById)", err)
  490. } else {
  491. ctx.Handle(500, "issue.UpdateIssueLabel(GetLabelById)", err)
  492. }
  493. return
  494. }
  495. isNeedUpdate := false
  496. if isAttach {
  497. if !issue.HasLabel(labelID) {
  498. if err = issue.AddLabel(labelID); err != nil {
  499. ctx.Handle(500, "AddLabel", err)
  500. return
  501. }
  502. isNeedUpdate = true
  503. }
  504. } else {
  505. if issue.HasLabel(labelID) {
  506. if err = issue.RemoveLabel(labelID); err != nil {
  507. ctx.Handle(500, "RemoveLabel", err)
  508. return
  509. }
  510. isNeedUpdate = true
  511. }
  512. }
  513. if isNeedUpdate {
  514. if err = models.UpdateIssue(issue); err != nil {
  515. ctx.Handle(500, "issue.UpdateIssueLabel(UpdateIssue)", err)
  516. return
  517. }
  518. if isAttach {
  519. label.NumIssues++
  520. if issue.IsClosed {
  521. label.NumClosedIssues++
  522. }
  523. } else {
  524. label.NumIssues--
  525. if issue.IsClosed {
  526. label.NumClosedIssues--
  527. }
  528. }
  529. if err = models.UpdateLabel(label); err != nil {
  530. ctx.Handle(500, "issue.UpdateIssueLabel(UpdateLabel)", err)
  531. return
  532. }
  533. }
  534. ctx.JSON(200, map[string]interface{}{
  535. "ok": true,
  536. })
  537. }
  538. func UpdateIssueMilestone(ctx *middleware.Context) {
  539. if !ctx.Repo.IsOwner() {
  540. ctx.Error(403)
  541. return
  542. }
  543. issueId := com.StrTo(ctx.Query("issue")).MustInt64()
  544. if issueId == 0 {
  545. ctx.Error(404)
  546. return
  547. }
  548. issue, err := models.GetIssueByID(issueId)
  549. if err != nil {
  550. if models.IsErrIssueNotExist(err) {
  551. ctx.Handle(404, "issue.UpdateIssueMilestone(GetIssueByID)", err)
  552. } else {
  553. ctx.Handle(500, "issue.UpdateIssueMilestone(GetIssueByID)", err)
  554. }
  555. return
  556. }
  557. oldMid := issue.MilestoneID
  558. mid := com.StrTo(ctx.Query("milestoneid")).MustInt64()
  559. if oldMid == mid {
  560. ctx.JSON(200, map[string]interface{}{
  561. "ok": true,
  562. })
  563. return
  564. }
  565. // Not check for invalid milestone id and give responsibility to owners.
  566. issue.MilestoneID = mid
  567. if err = models.ChangeMilestoneAssign(oldMid, issue); err != nil {
  568. ctx.Handle(500, "issue.UpdateIssueMilestone(ChangeMilestoneAssign)", err)
  569. return
  570. } else if err = models.UpdateIssue(issue); err != nil {
  571. ctx.Handle(500, "issue.UpdateIssueMilestone(UpdateIssue)", err)
  572. return
  573. }
  574. ctx.JSON(200, map[string]interface{}{
  575. "ok": true,
  576. })
  577. }
  578. func UpdateAssignee(ctx *middleware.Context) {
  579. if !ctx.Repo.IsOwner() {
  580. ctx.Error(403)
  581. return
  582. }
  583. issueId := com.StrTo(ctx.Query("issue")).MustInt64()
  584. if issueId == 0 {
  585. ctx.Error(404)
  586. return
  587. }
  588. issue, err := models.GetIssueByID(issueId)
  589. if err != nil {
  590. if models.IsErrIssueNotExist(err) {
  591. ctx.Handle(404, "GetIssueByID", err)
  592. } else {
  593. ctx.Handle(500, "GetIssueByID", err)
  594. }
  595. return
  596. }
  597. aid := com.StrTo(ctx.Query("assigneeid")).MustInt64()
  598. // Not check for invalid assignee id and give responsibility to owners.
  599. issue.AssigneeID = aid
  600. if err = models.UpdateIssueUserByAssignee(issue.ID, aid); err != nil {
  601. ctx.Handle(500, "UpdateIssueUserPairByAssignee: %v", err)
  602. return
  603. } else if err = models.UpdateIssue(issue); err != nil {
  604. ctx.Handle(500, "UpdateIssue", err)
  605. return
  606. }
  607. ctx.JSON(200, map[string]interface{}{
  608. "ok": true,
  609. })
  610. }
  611. func NewComment(ctx *middleware.Context, form auth.CreateCommentForm) {
  612. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  613. if err != nil {
  614. if models.IsErrIssueNotExist(err) {
  615. ctx.Handle(404, "GetIssueByIndex", err)
  616. } else {
  617. ctx.Handle(500, "GetIssueByIndex", err)
  618. }
  619. return
  620. }
  621. var attachments []string
  622. if setting.AttachmentEnabled {
  623. attachments = form.Attachments
  624. }
  625. if ctx.HasError() {
  626. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  627. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  628. return
  629. }
  630. // Check if issue owner/poster changes the status of issue.
  631. if (ctx.Repo.IsOwner() || issue.IsPoster(ctx.User.Id)) &&
  632. (form.Status == "reopen" || form.Status == "close") {
  633. issue.Repo = ctx.Repo.Repository
  634. if err = issue.ChangeStatus(ctx.User, form.Status == "close"); err != nil {
  635. ctx.Handle(500, "ChangeStatus", err)
  636. return
  637. }
  638. log.Trace("%s Issue[%d] status changed: %v", ctx.Req.RequestURI, issue.ID, !issue.IsClosed)
  639. }
  640. // Fix #321: Allow empty comments, as long as we have attachments.
  641. if len(form.Content) == 0 && len(attachments) == 0 {
  642. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  643. return
  644. }
  645. comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
  646. if err != nil {
  647. ctx.Handle(500, "CreateIssueComment", err)
  648. return
  649. }
  650. // Update mentions.
  651. mentions := base.MentionPattern.FindAllString(comment.Content, -1)
  652. if len(mentions) > 0 {
  653. for i := range mentions {
  654. mentions[i] = mentions[i][1:]
  655. }
  656. if err := models.UpdateMentions(mentions, issue.ID); err != nil {
  657. ctx.Handle(500, "UpdateMentions", err)
  658. return
  659. }
  660. }
  661. // Mail watchers and mentions.
  662. if setting.Service.EnableNotifyMail {
  663. issue.Content = form.Content
  664. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  665. if err != nil {
  666. ctx.Handle(500, "SendIssueNotifyMail", err)
  667. return
  668. }
  669. tos = append(tos, ctx.User.LowerName)
  670. newTos := make([]string, 0, len(mentions))
  671. for _, m := range mentions {
  672. if com.IsSliceContainsStr(tos, m) {
  673. continue
  674. }
  675. newTos = append(newTos, m)
  676. }
  677. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  678. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  679. ctx.Handle(500, "SendIssueMentionMail", err)
  680. return
  681. }
  682. }
  683. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  684. ctx.Redirect(fmt.Sprintf("%s/issues/%d#%s", ctx.Repo.RepoLink, issue.Index, comment.HashTag()))
  685. }
  686. func Labels(ctx *middleware.Context) {
  687. ctx.Data["Title"] = ctx.Tr("repo.labels")
  688. ctx.Data["PageIsLabels"] = true
  689. ctx.Data["RequireMinicolors"] = true
  690. ctx.HTML(200, LABELS)
  691. }
  692. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  693. ctx.Data["Title"] = ctx.Tr("repo.labels")
  694. ctx.Data["PageIsLabels"] = true
  695. if ctx.HasError() {
  696. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  697. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  698. return
  699. }
  700. l := &models.Label{
  701. RepoID: ctx.Repo.Repository.ID,
  702. Name: form.Title,
  703. Color: form.Color,
  704. }
  705. if err := models.NewLabel(l); err != nil {
  706. ctx.Handle(500, "NewLabel", err)
  707. return
  708. }
  709. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  710. }
  711. func UpdateLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  712. l, err := models.GetLabelByID(form.ID)
  713. if err != nil {
  714. switch {
  715. case models.IsErrLabelNotExist(err):
  716. ctx.Error(404)
  717. default:
  718. ctx.Handle(500, "UpdateLabel", err)
  719. }
  720. return
  721. }
  722. l.Name = form.Title
  723. l.Color = form.Color
  724. if err := models.UpdateLabel(l); err != nil {
  725. ctx.Handle(500, "UpdateLabel", err)
  726. return
  727. }
  728. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  729. }
  730. func DeleteLabel(ctx *middleware.Context) {
  731. if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
  732. ctx.Flash.Error("DeleteLabel: " + err.Error())
  733. } else {
  734. ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
  735. }
  736. ctx.JSON(200, map[string]interface{}{
  737. "redirect": ctx.Repo.RepoLink + "/labels",
  738. })
  739. return
  740. }
  741. func Milestones(ctx *middleware.Context) {
  742. ctx.Data["Title"] = ctx.Tr("repo.milestones")
  743. ctx.Data["PageIsMilestones"] = true
  744. isShowClosed := ctx.Query("state") == "closed"
  745. openCount, closedCount := models.MilestoneStats(ctx.Repo.Repository.ID)
  746. ctx.Data["OpenCount"] = openCount
  747. ctx.Data["ClosedCount"] = closedCount
  748. page := ctx.QueryInt("page")
  749. if page <= 1 {
  750. page = 1
  751. }
  752. var total int
  753. if !isShowClosed {
  754. total = int(openCount)
  755. } else {
  756. total = int(closedCount)
  757. }
  758. ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5)
  759. miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed)
  760. if err != nil {
  761. ctx.Handle(500, "GetMilestones", err)
  762. return
  763. }
  764. for _, m := range miles {
  765. m.RenderedContent = string(base.RenderMarkdown([]byte(m.Content), ctx.Repo.RepoLink))
  766. m.CalOpenIssues()
  767. }
  768. ctx.Data["Milestones"] = miles
  769. if isShowClosed {
  770. ctx.Data["State"] = "closed"
  771. } else {
  772. ctx.Data["State"] = "open"
  773. }
  774. ctx.Data["IsShowClosed"] = isShowClosed
  775. ctx.HTML(200, MILESTONE)
  776. }
  777. func NewMilestone(ctx *middleware.Context) {
  778. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  779. ctx.Data["PageIsMilestones"] = true
  780. ctx.Data["RequireDatetimepicker"] = true
  781. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  782. ctx.HTML(200, MILESTONE_NEW)
  783. }
  784. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  785. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  786. ctx.Data["PageIsMilestones"] = true
  787. ctx.Data["RequireDatetimepicker"] = true
  788. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  789. if ctx.HasError() {
  790. ctx.HTML(200, MILESTONE_NEW)
  791. return
  792. }
  793. if len(form.Deadline) == 0 {
  794. form.Deadline = "9999-12-31"
  795. }
  796. deadline, err := time.Parse("2006-01-02", form.Deadline)
  797. if err != nil {
  798. ctx.Data["Err_Deadline"] = true
  799. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  800. return
  801. }
  802. if err = models.NewMilestone(&models.Milestone{
  803. RepoID: ctx.Repo.Repository.ID,
  804. Name: form.Title,
  805. Content: form.Content,
  806. Deadline: deadline,
  807. }); err != nil {
  808. ctx.Handle(500, "NewMilestone", err)
  809. return
  810. }
  811. ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
  812. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  813. }
  814. func EditMilestone(ctx *middleware.Context) {
  815. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  816. ctx.Data["PageIsMilestones"] = true
  817. ctx.Data["PageIsEditMilestone"] = true
  818. ctx.Data["RequireDatetimepicker"] = true
  819. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  820. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  821. if err != nil {
  822. if models.IsErrMilestoneNotExist(err) {
  823. ctx.Handle(404, "GetMilestoneByID", nil)
  824. } else {
  825. ctx.Handle(500, "GetMilestoneByID", err)
  826. }
  827. return
  828. }
  829. ctx.Data["title"] = m.Name
  830. ctx.Data["content"] = m.Content
  831. if len(m.DeadlineString) > 0 {
  832. ctx.Data["deadline"] = m.DeadlineString
  833. }
  834. ctx.HTML(200, MILESTONE_NEW)
  835. }
  836. func EditMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  837. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  838. ctx.Data["PageIsMilestones"] = true
  839. ctx.Data["PageIsEditMilestone"] = true
  840. ctx.Data["RequireDatetimepicker"] = true
  841. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  842. if ctx.HasError() {
  843. ctx.HTML(200, MILESTONE_NEW)
  844. return
  845. }
  846. if len(form.Deadline) == 0 {
  847. form.Deadline = "9999-12-31"
  848. }
  849. deadline, err := time.Parse("2006-01-02", form.Deadline)
  850. if err != nil {
  851. ctx.Data["Err_Deadline"] = true
  852. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  853. return
  854. }
  855. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  856. if err != nil {
  857. if models.IsErrMilestoneNotExist(err) {
  858. ctx.Handle(404, "GetMilestoneByID", nil)
  859. } else {
  860. ctx.Handle(500, "GetMilestoneByID", err)
  861. }
  862. return
  863. }
  864. m.Name = form.Title
  865. m.Content = form.Content
  866. m.Deadline = deadline
  867. if err = models.UpdateMilestone(m); err != nil {
  868. ctx.Handle(500, "UpdateMilestone", err)
  869. return
  870. }
  871. ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
  872. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  873. }
  874. func ChangeMilestonStatus(ctx *middleware.Context) {
  875. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  876. if err != nil {
  877. if models.IsErrMilestoneNotExist(err) {
  878. ctx.Handle(404, "GetMilestoneByID", err)
  879. } else {
  880. ctx.Handle(500, "GetMilestoneByID", err)
  881. }
  882. return
  883. }
  884. switch ctx.Params(":action") {
  885. case "open":
  886. if m.IsClosed {
  887. if err = models.ChangeMilestoneStatus(m, false); err != nil {
  888. ctx.Handle(500, "ChangeMilestoneStatus", err)
  889. return
  890. }
  891. }
  892. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=open")
  893. case "close":
  894. if !m.IsClosed {
  895. m.ClosedDate = time.Now()
  896. if err = models.ChangeMilestoneStatus(m, true); err != nil {
  897. ctx.Handle(500, "ChangeMilestoneStatus", err)
  898. return
  899. }
  900. }
  901. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=closed")
  902. default:
  903. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  904. }
  905. }
  906. func DeleteMilestone(ctx *middleware.Context) {
  907. if err := models.DeleteMilestoneByID(ctx.QueryInt64("id")); err != nil {
  908. ctx.Flash.Error("DeleteMilestone: " + err.Error())
  909. } else {
  910. ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
  911. }
  912. ctx.JSON(200, map[string]interface{}{
  913. "redirect": ctx.Repo.RepoLink + "/milestones",
  914. })
  915. }
  916. func PullRequest2(ctx *middleware.Context) {
  917. ctx.HTML(200, "repo/pr2/list")
  918. }