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 37 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
10 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
10 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
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369
  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 models
  5. import (
  6. "bytes"
  7. "errors"
  8. "fmt"
  9. "html/template"
  10. "io"
  11. "mime/multipart"
  12. "os"
  13. "path"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/Unknwon/com"
  18. "github.com/go-xorm/xorm"
  19. "github.com/gogits/gogs/modules/base"
  20. "github.com/gogits/gogs/modules/log"
  21. "github.com/gogits/gogs/modules/setting"
  22. gouuid "github.com/gogits/gogs/modules/uuid"
  23. )
  24. var (
  25. ErrIssueNotExist = errors.New("Issue does not exist")
  26. ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
  27. ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
  28. ErrMissingIssueNumber = errors.New("No issue number specified")
  29. )
  30. // Issue represents an issue or pull request of repository.
  31. type Issue struct {
  32. ID int64 `xorm:"pk autoincr"`
  33. RepoID int64 `xorm:"INDEX"`
  34. Index int64 // Index in one repository.
  35. Name string
  36. Repo *Repository `xorm:"-"`
  37. PosterID int64
  38. Poster *User `xorm:"-"`
  39. Labels []*Label `xorm:"-"`
  40. MilestoneID int64
  41. Milestone *Milestone `xorm:"-"`
  42. AssigneeID int64
  43. Assignee *User `xorm:"-"`
  44. IsRead bool `xorm:"-"`
  45. IsPull bool // Indicates whether is a pull request or not.
  46. IsClosed bool
  47. Content string `xorm:"TEXT"`
  48. RenderedContent string `xorm:"-"`
  49. Priority int
  50. NumComments int
  51. Deadline time.Time
  52. Created time.Time `xorm:"CREATED"`
  53. Updated time.Time `xorm:"UPDATED"`
  54. }
  55. func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
  56. var err error
  57. switch colName {
  58. case "milestone_id":
  59. if i.MilestoneID == 0 {
  60. return
  61. }
  62. i.Milestone, err = GetMilestoneByID(i.MilestoneID)
  63. if err != nil {
  64. log.Error(3, "GetMilestoneById: %v", err)
  65. }
  66. case "assignee_id":
  67. if i.AssigneeID == 0 {
  68. return
  69. }
  70. i.Assignee, err = GetUserByID(i.AssigneeID)
  71. if err != nil {
  72. log.Error(3, "GetUserByID: %v", err)
  73. }
  74. }
  75. }
  76. func (i *Issue) GetPoster() (err error) {
  77. i.Poster, err = GetUserByID(i.PosterID)
  78. if IsErrUserNotExist(err) {
  79. i.Poster = &User{Name: "FakeUser"}
  80. return nil
  81. }
  82. return err
  83. }
  84. func (i *Issue) hasLabel(e Engine, labelID int64) bool {
  85. return hasIssueLabel(e, i.ID, labelID)
  86. }
  87. // HasLabel returns true if issue has been labeled by given ID.
  88. func (i *Issue) HasLabel(labelID int64) bool {
  89. return i.hasLabel(x, labelID)
  90. }
  91. func (i *Issue) addLabel(e Engine, labelID int64) error {
  92. return newIssueLabel(e, i.ID, labelID)
  93. }
  94. // AddLabel adds new label to issue by given ID.
  95. func (i *Issue) AddLabel(labelID int64) error {
  96. return i.addLabel(x, labelID)
  97. }
  98. func (i *Issue) getLabels(e Engine) (err error) {
  99. if len(i.Labels) > 0 {
  100. return nil
  101. }
  102. i.Labels, err = getLabelsByIssueID(e, i.ID)
  103. if err != nil {
  104. return fmt.Errorf("getLabelsByIssueID: %v", err)
  105. }
  106. return nil
  107. }
  108. // GetLabels retrieves all labels of issue and assign to corresponding field.
  109. func (i *Issue) GetLabels() error {
  110. return i.getLabels(x)
  111. }
  112. func (i *Issue) removeLabel(e Engine, labelID int64) error {
  113. return deleteIssueLabel(e, i.ID, labelID)
  114. }
  115. // RemoveLabel removes a label from issue by given ID.
  116. func (i *Issue) RemoveLabel(labelID int64) error {
  117. return i.removeLabel(x, labelID)
  118. }
  119. func (i *Issue) GetAssignee() (err error) {
  120. if i.AssigneeID == 0 || i.Assignee != nil {
  121. return nil
  122. }
  123. i.Assignee, err = GetUserByID(i.AssigneeID)
  124. if IsErrUserNotExist(err) {
  125. return nil
  126. }
  127. return err
  128. }
  129. func (i *Issue) Attachments() []*Attachment {
  130. a, _ := GetAttachmentsForIssue(i.ID)
  131. return a
  132. }
  133. func (i *Issue) AfterDelete() {
  134. _, err := DeleteAttachmentsByIssue(i.ID, true)
  135. if err != nil {
  136. log.Info("Could not delete files for issue #%d: %s", i.ID, err)
  137. }
  138. }
  139. // CreateIssue creates new issue with labels for repository.
  140. func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
  141. // Check attachments.
  142. attachments := make([]*Attachment, 0, len(uuids))
  143. for _, uuid := range uuids {
  144. attach, err := GetAttachmentByUUID(uuid)
  145. if err != nil {
  146. if IsErrAttachmentNotExist(err) {
  147. continue
  148. }
  149. return fmt.Errorf("GetAttachmentByUUID[%s]: %v", uuid, err)
  150. }
  151. attachments = append(attachments, attach)
  152. }
  153. sess := x.NewSession()
  154. defer sessionRelease(sess)
  155. if err = sess.Begin(); err != nil {
  156. return err
  157. }
  158. if _, err = sess.Insert(issue); err != nil {
  159. return err
  160. } else if _, err = sess.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID); err != nil {
  161. return err
  162. }
  163. for _, id := range labelIDs {
  164. if err = issue.addLabel(sess, id); err != nil {
  165. return fmt.Errorf("addLabel: %v", err)
  166. }
  167. }
  168. if issue.MilestoneID > 0 {
  169. if err = changeMilestoneAssign(sess, 0, issue); err != nil {
  170. return err
  171. }
  172. }
  173. if err = newIssueUsers(sess, repo, issue); err != nil {
  174. return err
  175. }
  176. for i := range attachments {
  177. attachments[i].IssueID = issue.ID
  178. // No assign value could be 0, so ignore AllCols().
  179. if _, err = sess.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  180. return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
  181. }
  182. }
  183. // Notify watchers.
  184. act := &Action{
  185. ActUserID: issue.Poster.Id,
  186. ActUserName: issue.Poster.Name,
  187. ActEmail: issue.Poster.Email,
  188. OpType: CREATE_ISSUE,
  189. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name),
  190. RepoID: repo.ID,
  191. RepoUserName: repo.Owner.Name,
  192. RepoName: repo.Name,
  193. IsPrivate: repo.IsPrivate,
  194. }
  195. if err = notifyWatchers(sess, act); err != nil {
  196. return err
  197. }
  198. return sess.Commit()
  199. }
  200. // GetIssueByRef returns an Issue specified by a GFM reference.
  201. // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
  202. func GetIssueByRef(ref string) (issue *Issue, err error) {
  203. var issueNumber int64
  204. var repo *Repository
  205. n := strings.IndexByte(ref, byte('#'))
  206. if n == -1 {
  207. return nil, ErrMissingIssueNumber
  208. }
  209. if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil {
  210. return
  211. }
  212. if repo, err = GetRepositoryByRef(ref[:n]); err != nil {
  213. return
  214. }
  215. return GetIssueByIndex(repo.ID, issueNumber)
  216. }
  217. // GetIssueByIndex returns issue by given index in repository.
  218. func GetIssueByIndex(rid, index int64) (*Issue, error) {
  219. issue := &Issue{RepoID: rid, Index: index}
  220. has, err := x.Get(issue)
  221. if err != nil {
  222. return nil, err
  223. } else if !has {
  224. return nil, ErrIssueNotExist
  225. }
  226. return issue, nil
  227. }
  228. // GetIssueById returns an issue by ID.
  229. func GetIssueById(id int64) (*Issue, error) {
  230. issue := &Issue{ID: id}
  231. has, err := x.Get(issue)
  232. if err != nil {
  233. return nil, err
  234. } else if !has {
  235. return nil, ErrIssueNotExist
  236. }
  237. return issue, nil
  238. }
  239. // Issues returns a list of issues by given conditions.
  240. func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isClosed, isMention bool, labels, sortType string) ([]*Issue, error) {
  241. sess := x.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum)
  242. if repoID > 0 {
  243. sess.Where("issue.repo_id=?", repoID).And("issue.is_closed=?", isClosed)
  244. } else {
  245. sess.Where("issue.is_closed=?", isClosed)
  246. }
  247. if assigneeID > 0 {
  248. sess.And("issue.assignee_id=?", assigneeID)
  249. } else if posterID > 0 {
  250. sess.And("issue.poster_id=?", posterID)
  251. }
  252. if milestoneID > 0 {
  253. sess.And("issue.milestone_id=?", milestoneID)
  254. }
  255. switch sortType {
  256. case "oldest":
  257. sess.Asc("created")
  258. case "recentupdate":
  259. sess.Desc("updated")
  260. case "leastupdate":
  261. sess.Asc("updated")
  262. case "mostcomment":
  263. sess.Desc("num_comments")
  264. case "leastcomment":
  265. sess.Asc("num_comments")
  266. case "priority":
  267. sess.Desc("priority")
  268. default:
  269. sess.Desc("created")
  270. }
  271. labelIDs := base.StringsToInt64s(strings.Split(labels, ","))
  272. if len(labelIDs) > 0 {
  273. validJoin := false
  274. queryStr := "issue.id=issue_label.issue_id"
  275. for _, id := range labelIDs {
  276. if id == 0 {
  277. continue
  278. }
  279. validJoin = true
  280. queryStr += " AND issue_label.label_id=" + com.ToStr(id)
  281. }
  282. if validJoin {
  283. sess.Join("INNER", "issue_label", queryStr)
  284. }
  285. }
  286. if isMention {
  287. queryStr := "issue.id=issue_user.issue_id AND issue_user.is_mentioned=1"
  288. if uid > 0 {
  289. queryStr += " AND issue_user.uid=" + com.ToStr(uid)
  290. }
  291. sess.Join("INNER", "issue_user", queryStr)
  292. }
  293. issues := make([]*Issue, 0, setting.IssuePagingNum)
  294. return issues, sess.Find(&issues)
  295. }
  296. type IssueStatus int
  297. const (
  298. IS_OPEN = iota + 1
  299. IS_CLOSE
  300. )
  301. // GetIssueCountByPoster returns number of issues of repository by poster.
  302. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
  303. count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue))
  304. return count
  305. }
  306. // .___ ____ ___
  307. // | | ______ ________ __ ____ | | \______ ___________
  308. // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
  309. // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
  310. // |___/____ >____ >____/ \___ >______//____ >\___ >__|
  311. // \/ \/ \/ \/ \/
  312. // IssueUser represents an issue-user relation.
  313. type IssueUser struct {
  314. ID int64 `xorm:"pk autoincr"`
  315. UID int64 `xorm:"uid INDEX"` // User ID.
  316. IssueID int64
  317. RepoID int64 `xorm:"INDEX"`
  318. MilestoneID int64
  319. IsRead bool
  320. IsAssigned bool
  321. IsMentioned bool
  322. IsPoster bool
  323. IsClosed bool
  324. }
  325. func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
  326. users, err := repo.GetAssignees()
  327. if err != nil {
  328. return err
  329. }
  330. iu := &IssueUser{
  331. IssueID: issue.ID,
  332. RepoID: repo.ID,
  333. }
  334. // Poster can be anyone.
  335. isNeedAddPoster := true
  336. for _, u := range users {
  337. iu.ID = 0
  338. iu.UID = u.Id
  339. iu.IsPoster = iu.UID == issue.PosterID
  340. if isNeedAddPoster && iu.IsPoster {
  341. isNeedAddPoster = false
  342. }
  343. iu.IsAssigned = iu.UID == issue.AssigneeID
  344. if _, err = e.Insert(iu); err != nil {
  345. return err
  346. }
  347. }
  348. if isNeedAddPoster {
  349. iu.ID = 0
  350. iu.UID = issue.PosterID
  351. iu.IsPoster = true
  352. if _, err = e.Insert(iu); err != nil {
  353. return err
  354. }
  355. }
  356. return nil
  357. }
  358. // NewIssueUsers adds new issue-user relations for new issue of repository.
  359. func NewIssueUsers(repo *Repository, issue *Issue) (err error) {
  360. sess := x.NewSession()
  361. defer sessionRelease(sess)
  362. if err = sess.Begin(); err != nil {
  363. return err
  364. }
  365. if err = newIssueUsers(sess, repo, issue); err != nil {
  366. return err
  367. }
  368. return sess.Commit()
  369. }
  370. // PairsContains returns true when pairs list contains given issue.
  371. func PairsContains(ius []*IssueUser, issueId, uid int64) int {
  372. for i := range ius {
  373. if ius[i].IssueID == issueId &&
  374. ius[i].UID == uid {
  375. return i
  376. }
  377. }
  378. return -1
  379. }
  380. // GetIssueUsers returns issue-user pairs by given repository and user.
  381. func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  382. ius := make([]*IssueUser, 0, 10)
  383. err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid})
  384. return ius, err
  385. }
  386. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  387. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  388. if len(rids) == 0 {
  389. return []*IssueUser{}, nil
  390. }
  391. buf := bytes.NewBufferString("")
  392. for _, rid := range rids {
  393. buf.WriteString("repo_id=")
  394. buf.WriteString(com.ToStr(rid))
  395. buf.WriteString(" OR ")
  396. }
  397. cond := strings.TrimSuffix(buf.String(), " OR ")
  398. ius := make([]*IssueUser, 0, 10)
  399. sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed)
  400. if len(cond) > 0 {
  401. sess.And(cond)
  402. }
  403. err := sess.Find(&ius)
  404. return ius, err
  405. }
  406. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  407. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  408. ius := make([]*IssueUser, 0, 10)
  409. sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed)
  410. if rid > 0 {
  411. sess.And("repo_id=?", rid)
  412. }
  413. switch filterMode {
  414. case FM_ASSIGN:
  415. sess.And("is_assigned=?", true)
  416. case FM_CREATE:
  417. sess.And("is_poster=?", true)
  418. default:
  419. return ius, nil
  420. }
  421. err := sess.Find(&ius)
  422. return ius, err
  423. }
  424. // IssueStats represents issue statistic information.
  425. type IssueStats struct {
  426. OpenCount, ClosedCount int64
  427. AllCount int64
  428. AssignCount int64
  429. CreateCount int64
  430. MentionCount int64
  431. }
  432. // Filter modes.
  433. const (
  434. FM_ALL = iota
  435. FM_ASSIGN
  436. FM_CREATE
  437. FM_MENTION
  438. )
  439. func parseCountResult(results []map[string][]byte) int64 {
  440. if len(results) == 0 {
  441. return 0
  442. }
  443. for _, result := range results[0] {
  444. return com.StrTo(string(result)).MustInt64()
  445. }
  446. return 0
  447. }
  448. // GetIssueStats returns issue statistic information by given conditions.
  449. func GetIssueStats(repoID, uid, labelID, milestoneID int64, isShowClosed bool, filterMode int) *IssueStats {
  450. stats := &IssueStats{}
  451. // issue := new(Issue)
  452. queryStr := "SELECT COUNT(*) FROM `issue` "
  453. if labelID > 0 {
  454. queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(labelID)
  455. }
  456. baseCond := " WHERE issue.repo_id=? AND issue.is_closed=?"
  457. if milestoneID > 0 {
  458. baseCond += " AND issue.milestone_id=" + com.ToStr(milestoneID)
  459. }
  460. switch filterMode {
  461. case FM_ALL:
  462. resutls, _ := x.Query(queryStr+baseCond, repoID, false)
  463. stats.OpenCount = parseCountResult(resutls)
  464. resutls, _ = x.Query(queryStr+baseCond, repoID, true)
  465. stats.ClosedCount = parseCountResult(resutls)
  466. case FM_ASSIGN:
  467. baseCond += " AND assignee_id=?"
  468. resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid)
  469. stats.OpenCount = parseCountResult(resutls)
  470. resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid)
  471. stats.ClosedCount = parseCountResult(resutls)
  472. case FM_CREATE:
  473. baseCond += " AND poster_id=?"
  474. resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid)
  475. stats.OpenCount = parseCountResult(resutls)
  476. resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid)
  477. stats.ClosedCount = parseCountResult(resutls)
  478. case FM_MENTION:
  479. queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id"
  480. baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?"
  481. resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid, true)
  482. stats.OpenCount = parseCountResult(resutls)
  483. resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid, true)
  484. stats.ClosedCount = parseCountResult(resutls)
  485. }
  486. return stats
  487. }
  488. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  489. func GetUserIssueStats(uid int64, filterMode int) *IssueStats {
  490. stats := &IssueStats{}
  491. issue := new(Issue)
  492. stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue)
  493. stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue)
  494. return stats
  495. }
  496. func updateIssue(e Engine, issue *Issue) error {
  497. _, err := e.Id(issue.ID).AllCols().Update(issue)
  498. return err
  499. }
  500. // UpdateIssue updates information of issue.
  501. func UpdateIssue(issue *Issue) error {
  502. return updateIssue(x, issue)
  503. }
  504. // UpdateIssueUserByStatus updates issue-user pairs by issue status.
  505. func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error {
  506. rawSql := "UPDATE `issue_user` SET is_closed = ? WHERE issue_id = ?"
  507. _, err := x.Exec(rawSql, isClosed, iid)
  508. return err
  509. }
  510. func updateIssueUserByAssignee(e *xorm.Session, issueID, assigneeID int64) (err error) {
  511. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE issue_id=?", false, issueID); err != nil {
  512. return err
  513. }
  514. // Assignee ID equals to 0 means clear assignee.
  515. if assigneeID == 0 {
  516. return nil
  517. }
  518. _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE uid=? AND issue_id=?", true, assigneeID, issueID)
  519. return err
  520. }
  521. // UpdateIssueUserByAssignee updates issue-user relation for assignee.
  522. func UpdateIssueUserByAssignee(issueID, assigneeID int64) (err error) {
  523. sess := x.NewSession()
  524. defer sessionRelease(sess)
  525. if err = sess.Begin(); err != nil {
  526. return err
  527. }
  528. if err = updateIssueUserByAssignee(sess, issueID, assigneeID); err != nil {
  529. return err
  530. }
  531. return sess.Commit()
  532. }
  533. // UpdateIssueUserPairByRead updates issue-user pair for reading.
  534. func UpdateIssueUserPairByRead(uid, iid int64) error {
  535. rawSql := "UPDATE `issue_user` SET is_read = ? WHERE uid = ? AND issue_id = ?"
  536. _, err := x.Exec(rawSql, true, uid, iid)
  537. return err
  538. }
  539. // UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
  540. func UpdateIssueUsersByMentions(uids []int64, iid int64) error {
  541. for _, uid := range uids {
  542. iu := &IssueUser{UID: uid, IssueID: iid}
  543. has, err := x.Get(iu)
  544. if err != nil {
  545. return err
  546. }
  547. iu.IsMentioned = true
  548. if has {
  549. _, err = x.Id(iu.ID).AllCols().Update(iu)
  550. } else {
  551. _, err = x.Insert(iu)
  552. }
  553. if err != nil {
  554. return err
  555. }
  556. }
  557. return nil
  558. }
  559. // .____ ___. .__
  560. // | | _____ \_ |__ ____ | |
  561. // | | \__ \ | __ \_/ __ \| |
  562. // | |___ / __ \| \_\ \ ___/| |__
  563. // |_______ (____ /___ /\___ >____/
  564. // \/ \/ \/ \/
  565. // Label represents a label of repository for issues.
  566. type Label struct {
  567. ID int64 `xorm:"pk autoincr"`
  568. RepoID int64 `xorm:"INDEX"`
  569. Name string
  570. Color string `xorm:"VARCHAR(7)"`
  571. NumIssues int
  572. NumClosedIssues int
  573. NumOpenIssues int `xorm:"-"`
  574. IsChecked bool `xorm:"-"`
  575. }
  576. // CalOpenIssues calculates the open issues of label.
  577. func (m *Label) CalOpenIssues() {
  578. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  579. }
  580. // NewLabel creates new label of repository.
  581. func NewLabel(l *Label) error {
  582. _, err := x.Insert(l)
  583. return err
  584. }
  585. func getLabelByID(e Engine, id int64) (*Label, error) {
  586. if id <= 0 {
  587. return nil, ErrLabelNotExist{id}
  588. }
  589. l := &Label{ID: id}
  590. has, err := x.Get(l)
  591. if err != nil {
  592. return nil, err
  593. } else if !has {
  594. return nil, ErrLabelNotExist{l.ID}
  595. }
  596. return l, nil
  597. }
  598. // GetLabelByID returns a label by given ID.
  599. func GetLabelByID(id int64) (*Label, error) {
  600. return getLabelByID(x, id)
  601. }
  602. // GetLabelsByRepoID returns all labels that belong to given repository by ID.
  603. func GetLabelsByRepoID(repoID int64) ([]*Label, error) {
  604. labels := make([]*Label, 0, 10)
  605. return labels, x.Where("repo_id=?", repoID).Find(&labels)
  606. }
  607. func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
  608. issueLabels, err := getIssueLabels(e, issueID)
  609. if err != nil {
  610. return nil, fmt.Errorf("getIssueLabels: %v", err)
  611. }
  612. var label *Label
  613. labels := make([]*Label, 0, len(issueLabels))
  614. for idx := range issueLabels {
  615. label, err = getLabelByID(e, issueLabels[idx].LabelID)
  616. if err != nil && !IsErrLabelNotExist(err) {
  617. return nil, fmt.Errorf("getLabelByID: %v", err)
  618. }
  619. labels = append(labels, label)
  620. }
  621. return labels, nil
  622. }
  623. // GetLabelsByIssueID returns all labels that belong to given issue by ID.
  624. func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
  625. return getLabelsByIssueID(x, issueID)
  626. }
  627. // UpdateLabel updates label information.
  628. func UpdateLabel(l *Label) error {
  629. _, err := x.Id(l.ID).AllCols().Update(l)
  630. return err
  631. }
  632. // DeleteLabel delete a label of given repository.
  633. func DeleteLabel(repoID, labelID int64) error {
  634. l, err := GetLabelByID(labelID)
  635. if err != nil {
  636. if IsErrLabelNotExist(err) {
  637. return nil
  638. }
  639. return err
  640. }
  641. sess := x.NewSession()
  642. defer sessionRelease(sess)
  643. if err = sess.Begin(); err != nil {
  644. return err
  645. }
  646. if _, err = x.Where("label_id=?", labelID).Delete(new(IssueLabel)); err != nil {
  647. return err
  648. } else if _, err = sess.Delete(l); err != nil {
  649. return err
  650. }
  651. return sess.Commit()
  652. }
  653. // .___ .____ ___. .__
  654. // | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
  655. // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
  656. // | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
  657. // |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
  658. // \/ \/ \/ \/ \/ \/ \/
  659. // IssueLabel represetns an issue-lable relation.
  660. type IssueLabel struct {
  661. ID int64 `xorm:"pk autoincr"`
  662. IssueID int64 `xorm:"UNIQUE(s)"`
  663. LabelID int64 `xorm:"UNIQUE(s)"`
  664. }
  665. func hasIssueLabel(e Engine, issueID, labelID int64) bool {
  666. has, _ := e.Where("issue_id=? AND label_id=?", issueID, labelID).Get(new(IssueLabel))
  667. return has
  668. }
  669. // HasIssueLabel returns true if issue has been labeled.
  670. func HasIssueLabel(issueID, labelID int64) bool {
  671. return hasIssueLabel(x, issueID, labelID)
  672. }
  673. func newIssueLabel(e Engine, issueID, labelID int64) error {
  674. if issueID == 0 || labelID == 0 {
  675. return nil
  676. }
  677. _, err := e.Insert(&IssueLabel{
  678. IssueID: issueID,
  679. LabelID: labelID,
  680. })
  681. return err
  682. }
  683. // NewIssueLabel creates a new issue-label relation.
  684. func NewIssueLabel(issueID, labelID int64) error {
  685. return newIssueLabel(x, issueID, labelID)
  686. }
  687. func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) {
  688. issueLabels := make([]*IssueLabel, 0, 10)
  689. return issueLabels, e.Where("issue_id=?", issueID).Asc("label_id").Find(&issueLabels)
  690. }
  691. // GetIssueLabels returns all issue-label relations of given issue by ID.
  692. func GetIssueLabels(issueID int64) ([]*IssueLabel, error) {
  693. return getIssueLabels(x, issueID)
  694. }
  695. func deleteIssueLabel(e Engine, issueID, labelID int64) error {
  696. _, err := e.Delete(&IssueLabel{
  697. IssueID: issueID,
  698. LabelID: labelID,
  699. })
  700. return err
  701. }
  702. // DeleteIssueLabel deletes issue-label relation.
  703. func DeleteIssueLabel(issueID, labelID int64) error {
  704. return deleteIssueLabel(x, issueID, labelID)
  705. }
  706. // _____ .__.__ __
  707. // / \ |__| | ____ _______/ |_ ____ ____ ____
  708. // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
  709. // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
  710. // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
  711. // \/ \/ \/ \/ \/
  712. // Milestone represents a milestone of repository.
  713. type Milestone struct {
  714. ID int64 `xorm:"pk autoincr"`
  715. RepoID int64 `xorm:"INDEX"`
  716. Name string
  717. Content string `xorm:"TEXT"`
  718. RenderedContent string `xorm:"-"`
  719. IsClosed bool
  720. NumIssues int
  721. NumClosedIssues int
  722. NumOpenIssues int `xorm:"-"`
  723. Completeness int // Percentage(1-100).
  724. Deadline time.Time
  725. DeadlineString string `xorm:"-"`
  726. IsOverDue bool `xorm:"-"`
  727. ClosedDate time.Time
  728. }
  729. func (m *Milestone) BeforeUpdate() {
  730. if m.NumIssues > 0 {
  731. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  732. } else {
  733. m.Completeness = 0
  734. }
  735. }
  736. func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
  737. if colName == "deadline" {
  738. if m.Deadline.Year() == 9999 {
  739. return
  740. }
  741. m.DeadlineString = m.Deadline.Format("2006-01-02")
  742. if time.Now().After(m.Deadline) {
  743. m.IsOverDue = true
  744. }
  745. }
  746. }
  747. // CalOpenIssues calculates the open issues of milestone.
  748. func (m *Milestone) CalOpenIssues() {
  749. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  750. }
  751. // NewMilestone creates new milestone of repository.
  752. func NewMilestone(m *Milestone) (err error) {
  753. sess := x.NewSession()
  754. defer sessionRelease(sess)
  755. if err = sess.Begin(); err != nil {
  756. return err
  757. }
  758. if _, err = sess.Insert(m); err != nil {
  759. return err
  760. }
  761. if _, err = sess.Exec("UPDATE `repository` SET num_milestones=num_milestones+1 WHERE id=?", m.RepoID); err != nil {
  762. return err
  763. }
  764. return sess.Commit()
  765. }
  766. func getMilestoneByID(e Engine, id int64) (*Milestone, error) {
  767. m := &Milestone{ID: id}
  768. has, err := x.Get(m)
  769. if err != nil {
  770. return nil, err
  771. } else if !has {
  772. return nil, ErrMilestoneNotExist{id, 0}
  773. }
  774. return m, nil
  775. }
  776. // GetMilestoneByID returns the milestone of given ID.
  777. func GetMilestoneByID(id int64) (*Milestone, error) {
  778. return getMilestoneByID(x, id)
  779. }
  780. // GetRepoMilestoneByID returns the milestone of given ID and repository.
  781. func GetRepoMilestoneByID(repoID, milestoneID int64) (*Milestone, error) {
  782. m := &Milestone{ID: milestoneID, RepoID: repoID}
  783. has, err := x.Get(m)
  784. if err != nil {
  785. return nil, err
  786. } else if !has {
  787. return nil, ErrMilestoneNotExist{milestoneID, repoID}
  788. }
  789. return m, nil
  790. }
  791. // GetAllRepoMilestones returns all milestones of given repository.
  792. func GetAllRepoMilestones(repoID int64) ([]*Milestone, error) {
  793. miles := make([]*Milestone, 0, 10)
  794. return miles, x.Where("repo_id=?", repoID).Find(&miles)
  795. }
  796. // GetMilestones returns a list of milestones of given repository and status.
  797. func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) {
  798. miles := make([]*Milestone, 0, setting.IssuePagingNum)
  799. sess := x.Where("repo_id=? AND is_closed=?", repoID, isClosed)
  800. if page > 0 {
  801. sess = sess.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum)
  802. }
  803. return miles, sess.Find(&miles)
  804. }
  805. func updateMilestone(e Engine, m *Milestone) error {
  806. _, err := e.Id(m.ID).AllCols().Update(m)
  807. return err
  808. }
  809. // UpdateMilestone updates information of given milestone.
  810. func UpdateMilestone(m *Milestone) error {
  811. return updateMilestone(x, m)
  812. }
  813. func countRepoMilestones(e Engine, repoID int64) int64 {
  814. count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone))
  815. return count
  816. }
  817. // CountRepoMilestones returns number of milestones in given repository.
  818. func CountRepoMilestones(repoID int64) int64 {
  819. return countRepoMilestones(x, repoID)
  820. }
  821. func countRepoClosedMilestones(e Engine, repoID int64) int64 {
  822. closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone))
  823. return closed
  824. }
  825. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  826. func CountRepoClosedMilestones(repoID int64) int64 {
  827. return countRepoClosedMilestones(x, repoID)
  828. }
  829. // MilestoneStats returns number of open and closed milestones of given repository.
  830. func MilestoneStats(repoID int64) (open int64, closed int64) {
  831. open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone))
  832. return open, CountRepoClosedMilestones(repoID)
  833. }
  834. // ChangeMilestoneStatus changes the milestone open/closed status.
  835. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  836. repo, err := GetRepositoryByID(m.RepoID)
  837. if err != nil {
  838. return err
  839. }
  840. sess := x.NewSession()
  841. defer sessionRelease(sess)
  842. if err = sess.Begin(); err != nil {
  843. return err
  844. }
  845. m.IsClosed = isClosed
  846. if err = updateMilestone(sess, m); err != nil {
  847. return err
  848. }
  849. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  850. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  851. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  852. return err
  853. }
  854. return sess.Commit()
  855. }
  856. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress
  857. // for the milestone associated witht the given issue.
  858. func ChangeMilestoneIssueStats(issue *Issue) error {
  859. if issue.MilestoneID == 0 {
  860. return nil
  861. }
  862. m, err := GetMilestoneByID(issue.MilestoneID)
  863. if err != nil {
  864. return err
  865. }
  866. if issue.IsClosed {
  867. m.NumOpenIssues--
  868. m.NumClosedIssues++
  869. } else {
  870. m.NumOpenIssues++
  871. m.NumClosedIssues--
  872. }
  873. return UpdateMilestone(m)
  874. }
  875. func changeMilestoneAssign(e *xorm.Session, oldMid int64, issue *Issue) error {
  876. if oldMid > 0 {
  877. m, err := getMilestoneByID(e, oldMid)
  878. if err != nil {
  879. return err
  880. }
  881. m.NumIssues--
  882. if issue.IsClosed {
  883. m.NumClosedIssues--
  884. }
  885. if err = updateMilestone(e, m); err != nil {
  886. return err
  887. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE issue_id=?", issue.ID); err != nil {
  888. return err
  889. }
  890. }
  891. if issue.MilestoneID > 0 {
  892. m, err := GetMilestoneByID(issue.MilestoneID)
  893. if err != nil {
  894. return err
  895. }
  896. m.NumIssues++
  897. if issue.IsClosed {
  898. m.NumClosedIssues++
  899. }
  900. if m.NumIssues == 0 {
  901. return ErrWrongIssueCounter
  902. }
  903. if err = updateMilestone(e, m); err != nil {
  904. return err
  905. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=? WHERE issue_id=?", m.ID, issue.ID); err != nil {
  906. return err
  907. }
  908. }
  909. return nil
  910. }
  911. // ChangeMilestoneAssign changes assignment of milestone for issue.
  912. func ChangeMilestoneAssign(oldMid int64, issue *Issue) (err error) {
  913. sess := x.NewSession()
  914. defer sess.Close()
  915. if err = sess.Begin(); err != nil {
  916. return err
  917. }
  918. if err = changeMilestoneAssign(sess, oldMid, issue); err != nil {
  919. return err
  920. }
  921. return sess.Commit()
  922. }
  923. // DeleteMilestoneByID deletes a milestone by given ID.
  924. func DeleteMilestoneByID(mid int64) error {
  925. m, err := GetMilestoneByID(mid)
  926. if err != nil {
  927. if IsErrMilestoneNotExist(err) {
  928. return nil
  929. }
  930. return err
  931. }
  932. repo, err := GetRepositoryByID(m.RepoID)
  933. if err != nil {
  934. return err
  935. }
  936. sess := x.NewSession()
  937. defer sessionRelease(sess)
  938. if err = sess.Begin(); err != nil {
  939. return err
  940. }
  941. if _, err = sess.Id(m.ID).Delete(m); err != nil {
  942. return err
  943. }
  944. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  945. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  946. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  947. return err
  948. }
  949. if _, err = sess.Exec("UPDATE `issue` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  950. return err
  951. } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  952. return err
  953. }
  954. return sess.Commit()
  955. }
  956. // _________ __
  957. // \_ ___ \ ____ _____ _____ ____ _____/ |_
  958. // / \ \/ / _ \ / \ / \_/ __ \ / \ __\
  959. // \ \___( <_> ) Y Y \ Y Y \ ___/| | \ |
  960. // \______ /\____/|__|_| /__|_| /\___ >___| /__|
  961. // \/ \/ \/ \/ \/
  962. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  963. type CommentType int
  964. const (
  965. // Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0)
  966. COMMENT_TYPE_COMMENT CommentType = iota
  967. COMMENT_TYPE_REOPEN
  968. COMMENT_TYPE_CLOSE
  969. // References.
  970. COMMENT_TYPE_ISSUE
  971. // Reference from some commit (not part of a pull request)
  972. COMMENT_TYPE_COMMIT
  973. // Reference from some pull request
  974. COMMENT_TYPE_PULL
  975. )
  976. // Comment represents a comment in commit and issue page.
  977. type Comment struct {
  978. Id int64
  979. Type CommentType
  980. PosterId int64
  981. Poster *User `xorm:"-"`
  982. IssueId int64
  983. CommitId int64
  984. Line int64
  985. Content string `xorm:"TEXT"`
  986. Created time.Time `xorm:"CREATED"`
  987. }
  988. // CreateComment creates comment of issue or commit.
  989. func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) {
  990. sess := x.NewSession()
  991. defer sessionRelease(sess)
  992. if err := sess.Begin(); err != nil {
  993. return nil, err
  994. }
  995. comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
  996. CommitId: commitId, Line: line, Content: content}
  997. if _, err := sess.Insert(comment); err != nil {
  998. return nil, err
  999. }
  1000. // Check comment type.
  1001. switch cmtType {
  1002. case COMMENT_TYPE_COMMENT:
  1003. rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?"
  1004. if _, err := sess.Exec(rawSql, issueId); err != nil {
  1005. return nil, err
  1006. }
  1007. if len(attachments) > 0 {
  1008. rawSql = "UPDATE `attachment` SET comment_id = ? WHERE id IN (?)"
  1009. astrs := make([]string, 0, len(attachments))
  1010. for _, a := range attachments {
  1011. astrs = append(astrs, strconv.FormatInt(a, 10))
  1012. }
  1013. if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil {
  1014. return nil, err
  1015. }
  1016. }
  1017. case COMMENT_TYPE_REOPEN:
  1018. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?"
  1019. if _, err := sess.Exec(rawSql, repoId); err != nil {
  1020. return nil, err
  1021. }
  1022. case COMMENT_TYPE_CLOSE:
  1023. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?"
  1024. if _, err := sess.Exec(rawSql, repoId); err != nil {
  1025. return nil, err
  1026. }
  1027. }
  1028. return comment, sess.Commit()
  1029. }
  1030. // GetCommentById returns the comment with the given id
  1031. func GetCommentById(commentId int64) (*Comment, error) {
  1032. c := &Comment{Id: commentId}
  1033. _, err := x.Get(c)
  1034. return c, err
  1035. }
  1036. func (c *Comment) ContentHtml() template.HTML {
  1037. return template.HTML(c.Content)
  1038. }
  1039. // GetIssueComments returns list of comment by given issue id.
  1040. func GetIssueComments(issueId int64) ([]Comment, error) {
  1041. comments := make([]Comment, 0, 10)
  1042. err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId})
  1043. return comments, err
  1044. }
  1045. // Attachments returns the attachments for this comment.
  1046. func (c *Comment) Attachments() []*Attachment {
  1047. a, _ := GetAttachmentsByComment(c.Id)
  1048. return a
  1049. }
  1050. func (c *Comment) AfterDelete() {
  1051. _, err := DeleteAttachmentsByComment(c.Id, true)
  1052. if err != nil {
  1053. log.Info("Could not delete files for comment %d on issue #%d: %s", c.Id, c.IssueId, err)
  1054. }
  1055. }
  1056. // Attachment represent a attachment of issue/comment/release.
  1057. type Attachment struct {
  1058. ID int64 `xorm:"pk autoincr"`
  1059. UUID string `xorm:"uuid UNIQUE"`
  1060. IssueID int64 `xorm:"INDEX"`
  1061. CommentID int64
  1062. ReleaseID int64 `xorm:"INDEX"`
  1063. Name string
  1064. Created time.Time `xorm:"CREATED"`
  1065. }
  1066. // AttachmentLocalPath returns where attachment is stored in local file system based on given UUID.
  1067. func AttachmentLocalPath(uuid string) string {
  1068. return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
  1069. }
  1070. // LocalPath returns where attachment is stored in local file system.
  1071. func (attach *Attachment) LocalPath() string {
  1072. return AttachmentLocalPath(attach.UUID)
  1073. }
  1074. // NewAttachment creates a new attachment object.
  1075. func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) {
  1076. attach := &Attachment{
  1077. UUID: gouuid.NewV4().String(),
  1078. Name: name,
  1079. }
  1080. if err = os.MkdirAll(path.Dir(attach.LocalPath()), os.ModePerm); err != nil {
  1081. return nil, fmt.Errorf("MkdirAll: %v", err)
  1082. }
  1083. fw, err := os.Create(attach.LocalPath())
  1084. if err != nil {
  1085. return nil, fmt.Errorf("Create: %v", err)
  1086. }
  1087. defer fw.Close()
  1088. if _, err = fw.Write(buf); err != nil {
  1089. return nil, fmt.Errorf("Write: %v", err)
  1090. } else if _, err = io.Copy(fw, file); err != nil {
  1091. return nil, fmt.Errorf("Copy: %v", err)
  1092. }
  1093. sess := x.NewSession()
  1094. defer sessionRelease(sess)
  1095. if err := sess.Begin(); err != nil {
  1096. return nil, err
  1097. }
  1098. if _, err := sess.Insert(attach); err != nil {
  1099. return nil, err
  1100. }
  1101. return attach, sess.Commit()
  1102. }
  1103. // GetAttachmentByUUID returns attachment by given UUID.
  1104. func GetAttachmentByUUID(uuid string) (*Attachment, error) {
  1105. attach := &Attachment{UUID: uuid}
  1106. has, err := x.Get(attach)
  1107. if err != nil {
  1108. return nil, err
  1109. } else if !has {
  1110. return nil, ErrAttachmentNotExist{0, uuid}
  1111. }
  1112. return attach, nil
  1113. }
  1114. func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) {
  1115. attachments := make([]*Attachment, 0, 10)
  1116. err := x.Where("issue_id = ?", issueId).And("comment_id = 0").Find(&attachments)
  1117. return attachments, err
  1118. }
  1119. // GetAttachmentsByIssue returns a list of attachments for the given issue
  1120. func GetAttachmentsByIssue(issueId int64) ([]*Attachment, error) {
  1121. attachments := make([]*Attachment, 0, 10)
  1122. err := x.Where("issue_id = ?", issueId).And("comment_id > 0").Find(&attachments)
  1123. return attachments, err
  1124. }
  1125. // GetAttachmentsByComment returns a list of attachments for the given comment
  1126. func GetAttachmentsByComment(commentId int64) ([]*Attachment, error) {
  1127. attachments := make([]*Attachment, 0, 10)
  1128. err := x.Where("comment_id = ?", commentId).Find(&attachments)
  1129. return attachments, err
  1130. }
  1131. // DeleteAttachment deletes the given attachment and optionally the associated file.
  1132. func DeleteAttachment(a *Attachment, remove bool) error {
  1133. _, err := DeleteAttachments([]*Attachment{a}, remove)
  1134. return err
  1135. }
  1136. // DeleteAttachments deletes the given attachments and optionally the associated files.
  1137. func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
  1138. for i, a := range attachments {
  1139. if remove {
  1140. if err := os.Remove(a.LocalPath()); err != nil {
  1141. return i, err
  1142. }
  1143. }
  1144. if _, err := x.Delete(a.ID); err != nil {
  1145. return i, err
  1146. }
  1147. }
  1148. return len(attachments), nil
  1149. }
  1150. // DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
  1151. func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
  1152. attachments, err := GetAttachmentsByIssue(issueId)
  1153. if err != nil {
  1154. return 0, err
  1155. }
  1156. return DeleteAttachments(attachments, remove)
  1157. }
  1158. // DeleteAttachmentsByComment deletes all attachments associated with the given comment.
  1159. func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
  1160. attachments, err := GetAttachmentsByComment(commentId)
  1161. if err != nil {
  1162. return 0, err
  1163. }
  1164. return DeleteAttachments(attachments, remove)
  1165. }