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 26 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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016
  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.Repo.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.Repo.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 ViewIssue(ctx *middleware.Context) {
  350. ctx.Data["PageIsIssueList"] = true
  351. ctx.Data["RequireDropzone"] = true
  352. renderAttachmentSettings(ctx)
  353. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  354. if err != nil {
  355. if models.IsErrIssueNotExist(err) {
  356. ctx.Handle(404, "GetIssueByIndex", err)
  357. } else {
  358. ctx.Handle(500, "GetIssueByIndex", err)
  359. }
  360. return
  361. }
  362. ctx.Data["Title"] = issue.Name
  363. if err = issue.GetPoster(); err != nil {
  364. ctx.Handle(500, "GetPoster", err)
  365. return
  366. }
  367. issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink))
  368. repo := ctx.Repo.Repository
  369. // Metas.
  370. // Check labels.
  371. if err = issue.GetLabels(); err != nil {
  372. ctx.Handle(500, "GetLabels", err)
  373. return
  374. }
  375. labelIDMark := make(map[int64]bool)
  376. for i := range issue.Labels {
  377. labelIDMark[issue.Labels[i].ID] = true
  378. }
  379. labels, err := models.GetLabelsByRepoID(repo.ID)
  380. if err != nil {
  381. ctx.Handle(500, "GetLabelsByRepoID: %v", err)
  382. return
  383. }
  384. hasSelected := false
  385. for i := range labels {
  386. if labelIDMark[labels[i].ID] {
  387. labels[i].IsChecked = true
  388. hasSelected = true
  389. }
  390. }
  391. ctx.Data["HasSelectedLabel"] = hasSelected
  392. ctx.Data["Labels"] = labels
  393. // Check milestone and assignee.
  394. if ctx.Repo.IsAdmin() {
  395. ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
  396. if err != nil {
  397. ctx.Handle(500, "GetMilestones: %v", err)
  398. return
  399. }
  400. ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
  401. if err != nil {
  402. ctx.Handle(500, "GetMilestones: %v", err)
  403. return
  404. }
  405. ctx.Data["Assignees"], err = repo.GetAssignees()
  406. if err != nil {
  407. ctx.Handle(500, "GetAssignees: %v", err)
  408. return
  409. }
  410. }
  411. if ctx.IsSigned {
  412. // Update issue-user.
  413. if err = issue.ReadBy(ctx.User.Id); err != nil {
  414. ctx.Handle(500, "ReadBy", err)
  415. return
  416. }
  417. }
  418. var (
  419. tag models.CommentTag
  420. ok bool
  421. marked = make(map[int64]models.CommentTag)
  422. comment *models.Comment
  423. )
  424. // Render comments.
  425. for _, comment = range issue.Comments {
  426. if comment.Type == models.COMMENT_TYPE_COMMENT {
  427. comment.RenderedContent = string(base.RenderMarkdown([]byte(comment.Content), ctx.Repo.RepoLink))
  428. // Check tag.
  429. tag, ok = marked[comment.PosterID]
  430. if ok {
  431. comment.ShowTag = tag
  432. continue
  433. }
  434. if repo.IsOwnedBy(comment.PosterID) ||
  435. (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) {
  436. comment.ShowTag = models.COMMENT_TAG_OWNER
  437. } else if comment.Poster.IsAdminOfRepo(repo) {
  438. comment.ShowTag = models.COMMENT_TAG_ADMIN
  439. } else if comment.PosterID == issue.PosterID {
  440. comment.ShowTag = models.COMMENT_TAG_POSTER
  441. }
  442. marked[comment.PosterID] = comment.ShowTag
  443. }
  444. }
  445. ctx.Data["Issue"] = issue
  446. ctx.Data["IsIssueOwner"] = ctx.Repo.IsAdmin() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))
  447. ctx.HTML(200, ISSUE_VIEW)
  448. }
  449. func UpdateIssue(ctx *middleware.Context, form auth.CreateIssueForm) {
  450. idx := com.StrTo(ctx.Params(":index")).MustInt64()
  451. if idx <= 0 {
  452. ctx.Error(404)
  453. return
  454. }
  455. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, idx)
  456. if err != nil {
  457. if models.IsErrIssueNotExist(err) {
  458. ctx.Handle(404, "issue.UpdateIssue", err)
  459. } else {
  460. ctx.Handle(500, "issue.UpdateIssue(GetIssueByIndex)", err)
  461. }
  462. return
  463. }
  464. if ctx.User.Id != issue.PosterID && !ctx.Repo.IsOwner() {
  465. ctx.Error(403)
  466. return
  467. }
  468. issue.Name = form.Title
  469. //issue.MilestoneId = form.MilestoneId
  470. //issue.AssigneeId = form.AssigneeId
  471. //issue.LabelIds = form.Labels
  472. issue.Content = form.Content
  473. // try get content from text, ignore conflict with preview ajax
  474. if form.Content == "" {
  475. issue.Content = ctx.Query("text")
  476. }
  477. if err = models.UpdateIssue(issue); err != nil {
  478. ctx.Handle(500, "issue.UpdateIssue(UpdateIssue)", err)
  479. return
  480. }
  481. ctx.JSON(200, map[string]interface{}{
  482. "ok": true,
  483. "title": issue.Name,
  484. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)),
  485. })
  486. }
  487. func getActionIssue(ctx *middleware.Context) *models.Issue {
  488. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  489. if err != nil {
  490. if models.IsErrIssueNotExist(err) {
  491. ctx.Error(404, "GetIssueByIndex")
  492. } else {
  493. ctx.Handle(500, "GetIssueByIndex", err)
  494. }
  495. return nil
  496. }
  497. return issue
  498. }
  499. func UpdateIssueLabel(ctx *middleware.Context) {
  500. issue := getActionIssue(ctx)
  501. if ctx.Written() {
  502. return
  503. }
  504. if ctx.Query("action") == "clear" {
  505. if err := issue.ClearLabels(); err != nil {
  506. ctx.Handle(500, "ClearLabels", err)
  507. return
  508. }
  509. } else {
  510. isAttach := ctx.Query("action") == "attach"
  511. label, err := models.GetLabelByID(ctx.QueryInt64("id"))
  512. if err != nil {
  513. if models.IsErrLabelNotExist(err) {
  514. ctx.Error(404, "GetLabelByID")
  515. } else {
  516. ctx.Handle(500, "GetLabelByID", err)
  517. }
  518. return
  519. }
  520. if isAttach && !issue.HasLabel(label.ID) {
  521. if err = issue.AddLabel(label); err != nil {
  522. ctx.Handle(500, "AddLabel", err)
  523. return
  524. }
  525. } else if !isAttach && issue.HasLabel(label.ID) {
  526. if err = issue.RemoveLabel(label); err != nil {
  527. ctx.Handle(500, "RemoveLabel", err)
  528. return
  529. }
  530. }
  531. }
  532. ctx.JSON(200, map[string]interface{}{
  533. "ok": true,
  534. })
  535. }
  536. func UpdateIssueMilestone(ctx *middleware.Context) {
  537. issue := getActionIssue(ctx)
  538. if ctx.Written() {
  539. return
  540. }
  541. oldMid := issue.MilestoneID
  542. mid := ctx.QueryInt64("id")
  543. if oldMid == mid {
  544. ctx.JSON(200, map[string]interface{}{
  545. "ok": true,
  546. })
  547. return
  548. }
  549. // Not check for invalid milestone id and give responsibility to owners.
  550. issue.MilestoneID = mid
  551. if err := models.ChangeMilestoneAssign(oldMid, issue); err != nil {
  552. ctx.Handle(500, "ChangeMilestoneAssign", err)
  553. return
  554. }
  555. ctx.JSON(200, map[string]interface{}{
  556. "ok": true,
  557. })
  558. }
  559. func UpdateIssueAssignee(ctx *middleware.Context) {
  560. issue := getActionIssue(ctx)
  561. if ctx.Written() {
  562. return
  563. }
  564. aid := ctx.QueryInt64("id")
  565. if issue.AssigneeID == aid {
  566. ctx.JSON(200, map[string]interface{}{
  567. "ok": true,
  568. })
  569. return
  570. }
  571. // Not check for invalid assignee id and give responsibility to owners.
  572. issue.AssigneeID = aid
  573. if err := models.UpdateIssueUserByAssignee(issue); err != nil {
  574. ctx.Handle(500, "UpdateIssueUserByAssignee: %v", err)
  575. return
  576. }
  577. ctx.JSON(200, map[string]interface{}{
  578. "ok": true,
  579. })
  580. }
  581. func NewComment(ctx *middleware.Context, form auth.CreateCommentForm) {
  582. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  583. if err != nil {
  584. if models.IsErrIssueNotExist(err) {
  585. ctx.Handle(404, "GetIssueByIndex", err)
  586. } else {
  587. ctx.Handle(500, "GetIssueByIndex", err)
  588. }
  589. return
  590. }
  591. var attachments []string
  592. if setting.AttachmentEnabled {
  593. attachments = form.Attachments
  594. }
  595. if ctx.HasError() {
  596. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  597. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  598. return
  599. }
  600. // Check if issue owner/poster changes the status of issue.
  601. if (ctx.Repo.IsOwner() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))) &&
  602. (form.Status == "reopen" || form.Status == "close") {
  603. issue.Repo = ctx.Repo.Repository
  604. if err = issue.ChangeStatus(ctx.User, form.Status == "close"); err != nil {
  605. ctx.Handle(500, "ChangeStatus", err)
  606. return
  607. }
  608. log.Trace("%s Issue[%d] status changed: %v", ctx.Req.RequestURI, issue.ID, !issue.IsClosed)
  609. }
  610. // Fix #321: Allow empty comments, as long as we have attachments.
  611. if len(form.Content) == 0 && len(attachments) == 0 {
  612. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  613. return
  614. }
  615. comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
  616. if err != nil {
  617. ctx.Handle(500, "CreateIssueComment", err)
  618. return
  619. }
  620. // Update mentions.
  621. mentions := base.MentionPattern.FindAllString(comment.Content, -1)
  622. if len(mentions) > 0 {
  623. for i := range mentions {
  624. mentions[i] = mentions[i][1:]
  625. }
  626. if err := models.UpdateMentions(mentions, issue.ID); err != nil {
  627. ctx.Handle(500, "UpdateMentions", err)
  628. return
  629. }
  630. }
  631. // Mail watchers and mentions.
  632. if setting.Service.EnableNotifyMail {
  633. issue.Content = form.Content
  634. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  635. if err != nil {
  636. ctx.Handle(500, "SendIssueNotifyMail", err)
  637. return
  638. }
  639. tos = append(tos, ctx.User.LowerName)
  640. newTos := make([]string, 0, len(mentions))
  641. for _, m := range mentions {
  642. if com.IsSliceContainsStr(tos, m) {
  643. continue
  644. }
  645. newTos = append(newTos, m)
  646. }
  647. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  648. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  649. ctx.Handle(500, "SendIssueMentionMail", err)
  650. return
  651. }
  652. }
  653. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  654. ctx.Redirect(fmt.Sprintf("%s/issues/%d#%s", ctx.Repo.RepoLink, issue.Index, comment.HashTag()))
  655. }
  656. func Labels(ctx *middleware.Context) {
  657. ctx.Data["Title"] = ctx.Tr("repo.labels")
  658. ctx.Data["PageIsLabels"] = true
  659. ctx.Data["RequireMinicolors"] = true
  660. ctx.HTML(200, LABELS)
  661. }
  662. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  663. ctx.Data["Title"] = ctx.Tr("repo.labels")
  664. ctx.Data["PageIsLabels"] = true
  665. if ctx.HasError() {
  666. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  667. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  668. return
  669. }
  670. l := &models.Label{
  671. RepoID: ctx.Repo.Repository.ID,
  672. Name: form.Title,
  673. Color: form.Color,
  674. }
  675. if err := models.NewLabel(l); err != nil {
  676. ctx.Handle(500, "NewLabel", err)
  677. return
  678. }
  679. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  680. }
  681. func UpdateLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  682. l, err := models.GetLabelByID(form.ID)
  683. if err != nil {
  684. switch {
  685. case models.IsErrLabelNotExist(err):
  686. ctx.Error(404)
  687. default:
  688. ctx.Handle(500, "UpdateLabel", err)
  689. }
  690. return
  691. }
  692. l.Name = form.Title
  693. l.Color = form.Color
  694. if err := models.UpdateLabel(l); err != nil {
  695. ctx.Handle(500, "UpdateLabel", err)
  696. return
  697. }
  698. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  699. }
  700. func DeleteLabel(ctx *middleware.Context) {
  701. if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
  702. ctx.Flash.Error("DeleteLabel: " + err.Error())
  703. } else {
  704. ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
  705. }
  706. ctx.JSON(200, map[string]interface{}{
  707. "redirect": ctx.Repo.RepoLink + "/labels",
  708. })
  709. return
  710. }
  711. func Milestones(ctx *middleware.Context) {
  712. ctx.Data["Title"] = ctx.Tr("repo.milestones")
  713. ctx.Data["PageIsMilestones"] = true
  714. isShowClosed := ctx.Query("state") == "closed"
  715. openCount, closedCount := models.MilestoneStats(ctx.Repo.Repository.ID)
  716. ctx.Data["OpenCount"] = openCount
  717. ctx.Data["ClosedCount"] = closedCount
  718. page := ctx.QueryInt("page")
  719. if page <= 1 {
  720. page = 1
  721. }
  722. var total int
  723. if !isShowClosed {
  724. total = int(openCount)
  725. } else {
  726. total = int(closedCount)
  727. }
  728. ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5)
  729. miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed)
  730. if err != nil {
  731. ctx.Handle(500, "GetMilestones", err)
  732. return
  733. }
  734. for _, m := range miles {
  735. m.RenderedContent = string(base.RenderMarkdown([]byte(m.Content), ctx.Repo.RepoLink))
  736. m.CalOpenIssues()
  737. }
  738. ctx.Data["Milestones"] = miles
  739. if isShowClosed {
  740. ctx.Data["State"] = "closed"
  741. } else {
  742. ctx.Data["State"] = "open"
  743. }
  744. ctx.Data["IsShowClosed"] = isShowClosed
  745. ctx.HTML(200, MILESTONE)
  746. }
  747. func NewMilestone(ctx *middleware.Context) {
  748. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  749. ctx.Data["PageIsMilestones"] = true
  750. ctx.Data["RequireDatetimepicker"] = true
  751. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  752. ctx.HTML(200, MILESTONE_NEW)
  753. }
  754. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  755. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  756. ctx.Data["PageIsMilestones"] = true
  757. ctx.Data["RequireDatetimepicker"] = true
  758. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  759. if ctx.HasError() {
  760. ctx.HTML(200, MILESTONE_NEW)
  761. return
  762. }
  763. if len(form.Deadline) == 0 {
  764. form.Deadline = "9999-12-31"
  765. }
  766. deadline, err := time.Parse("2006-01-02", form.Deadline)
  767. if err != nil {
  768. ctx.Data["Err_Deadline"] = true
  769. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  770. return
  771. }
  772. if err = models.NewMilestone(&models.Milestone{
  773. RepoID: ctx.Repo.Repository.ID,
  774. Name: form.Title,
  775. Content: form.Content,
  776. Deadline: deadline,
  777. }); err != nil {
  778. ctx.Handle(500, "NewMilestone", err)
  779. return
  780. }
  781. ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
  782. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  783. }
  784. func EditMilestone(ctx *middleware.Context) {
  785. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  786. ctx.Data["PageIsMilestones"] = true
  787. ctx.Data["PageIsEditMilestone"] = true
  788. ctx.Data["RequireDatetimepicker"] = true
  789. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  790. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  791. if err != nil {
  792. if models.IsErrMilestoneNotExist(err) {
  793. ctx.Handle(404, "GetMilestoneByID", nil)
  794. } else {
  795. ctx.Handle(500, "GetMilestoneByID", err)
  796. }
  797. return
  798. }
  799. ctx.Data["title"] = m.Name
  800. ctx.Data["content"] = m.Content
  801. if len(m.DeadlineString) > 0 {
  802. ctx.Data["deadline"] = m.DeadlineString
  803. }
  804. ctx.HTML(200, MILESTONE_NEW)
  805. }
  806. func EditMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  807. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  808. ctx.Data["PageIsMilestones"] = true
  809. ctx.Data["PageIsEditMilestone"] = true
  810. ctx.Data["RequireDatetimepicker"] = true
  811. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  812. if ctx.HasError() {
  813. ctx.HTML(200, MILESTONE_NEW)
  814. return
  815. }
  816. if len(form.Deadline) == 0 {
  817. form.Deadline = "9999-12-31"
  818. }
  819. deadline, err := time.Parse("2006-01-02", form.Deadline)
  820. if err != nil {
  821. ctx.Data["Err_Deadline"] = true
  822. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  823. return
  824. }
  825. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  826. if err != nil {
  827. if models.IsErrMilestoneNotExist(err) {
  828. ctx.Handle(404, "GetMilestoneByID", nil)
  829. } else {
  830. ctx.Handle(500, "GetMilestoneByID", err)
  831. }
  832. return
  833. }
  834. m.Name = form.Title
  835. m.Content = form.Content
  836. m.Deadline = deadline
  837. if err = models.UpdateMilestone(m); err != nil {
  838. ctx.Handle(500, "UpdateMilestone", err)
  839. return
  840. }
  841. ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
  842. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  843. }
  844. func ChangeMilestonStatus(ctx *middleware.Context) {
  845. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  846. if err != nil {
  847. if models.IsErrMilestoneNotExist(err) {
  848. ctx.Handle(404, "GetMilestoneByID", err)
  849. } else {
  850. ctx.Handle(500, "GetMilestoneByID", err)
  851. }
  852. return
  853. }
  854. switch ctx.Params(":action") {
  855. case "open":
  856. if m.IsClosed {
  857. if err = models.ChangeMilestoneStatus(m, false); err != nil {
  858. ctx.Handle(500, "ChangeMilestoneStatus", err)
  859. return
  860. }
  861. }
  862. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=open")
  863. case "close":
  864. if !m.IsClosed {
  865. m.ClosedDate = time.Now()
  866. if err = models.ChangeMilestoneStatus(m, true); err != nil {
  867. ctx.Handle(500, "ChangeMilestoneStatus", err)
  868. return
  869. }
  870. }
  871. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=closed")
  872. default:
  873. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  874. }
  875. }
  876. func DeleteMilestone(ctx *middleware.Context) {
  877. if err := models.DeleteMilestoneByID(ctx.QueryInt64("id")); err != nil {
  878. ctx.Flash.Error("DeleteMilestone: " + err.Error())
  879. } else {
  880. ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
  881. }
  882. ctx.JSON(200, map[string]interface{}{
  883. "redirect": ctx.Repo.RepoLink + "/milestones",
  884. })
  885. }
  886. func PullRequest2(ctx *middleware.Context) {
  887. ctx.HTML(200, "repo/pr2/list")
  888. }