2017-02-09 09:39:26 +03:00
// Copyright 2017 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"code.gitea.io/gitea/modules/setting"
2017-12-11 07:37:04 +03:00
"code.gitea.io/gitea/modules/util"
2017-02-09 09:39:26 +03:00
api "code.gitea.io/sdk/gitea"
"github.com/go-xorm/xorm"
)
// Milestone represents a milestone of repository.
type Milestone struct {
ID int64 ` xorm:"pk autoincr" `
RepoID int64 ` xorm:"INDEX" `
Name string
Content string ` xorm:"TEXT" `
RenderedContent string ` xorm:"-" `
IsClosed bool
NumIssues int
NumClosedIssues int
NumOpenIssues int ` xorm:"-" `
Completeness int // Percentage(1-100).
IsOverDue bool ` xorm:"-" `
2017-12-11 07:37:04 +03:00
DeadlineString string ` xorm:"-" `
DeadlineUnix util . TimeStamp
ClosedDateUnix util . TimeStamp
2018-04-29 08:58:47 +03:00
TotalTrackedTime int64 ` xorm:"-" `
2017-02-09 09:39:26 +03:00
}
// BeforeUpdate is invoked from XORM before updating this object.
func ( m * Milestone ) BeforeUpdate ( ) {
if m . NumIssues > 0 {
m . Completeness = m . NumClosedIssues * 100 / m . NumIssues
} else {
m . Completeness = 0
}
}
2017-10-01 19:52:35 +03:00
// AfterLoad is invoked from XORM after setting the value of a field of
2017-02-09 09:39:26 +03:00
// this object.
2017-10-01 19:52:35 +03:00
func ( m * Milestone ) AfterLoad ( ) {
m . NumOpenIssues = m . NumIssues - m . NumClosedIssues
2017-12-11 07:37:04 +03:00
if m . DeadlineUnix . Year ( ) == 9999 {
2017-10-01 19:52:35 +03:00
return
}
2017-02-09 09:39:26 +03:00
2017-12-11 07:37:04 +03:00
m . DeadlineString = m . DeadlineUnix . Format ( "2006-01-02" )
if util . TimeStampNow ( ) >= m . DeadlineUnix {
2017-10-01 19:52:35 +03:00
m . IsOverDue = true
2017-02-09 09:39:26 +03:00
}
}
// State returns string representation of milestone status.
func ( m * Milestone ) State ( ) api . StateType {
if m . IsClosed {
return api . StateClosed
}
return api . StateOpen
}
// APIFormat returns this Milestone in API format.
func ( m * Milestone ) APIFormat ( ) * api . Milestone {
apiMilestone := & api . Milestone {
ID : m . ID ,
State : m . State ( ) ,
Title : m . Name ,
Description : m . Content ,
OpenIssues : m . NumOpenIssues ,
ClosedIssues : m . NumClosedIssues ,
}
if m . IsClosed {
2017-12-11 07:37:04 +03:00
apiMilestone . Closed = m . ClosedDateUnix . AsTimePtr ( )
2017-02-09 09:39:26 +03:00
}
2017-12-11 07:37:04 +03:00
if m . DeadlineUnix . Year ( ) < 9999 {
apiMilestone . Deadline = m . DeadlineUnix . AsTimePtr ( )
2017-02-09 09:39:26 +03:00
}
return apiMilestone
}
// NewMilestone creates new milestone of repository.
func NewMilestone ( m * Milestone ) ( err error ) {
sess := x . NewSession ( )
2017-06-21 03:57:05 +03:00
defer sess . Close ( )
2017-02-09 09:39:26 +03:00
if err = sess . Begin ( ) ; err != nil {
return err
}
if _ , err = sess . Insert ( m ) ; err != nil {
return err
}
if _ , err = sess . Exec ( "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?" , m . RepoID ) ; err != nil {
return err
}
return sess . Commit ( )
}
func getMilestoneByRepoID ( e Engine , repoID , id int64 ) ( * Milestone , error ) {
m := & Milestone {
ID : id ,
RepoID : repoID ,
}
has , err := e . Get ( m )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrMilestoneNotExist { id , repoID }
}
return m , nil
}
// GetMilestoneByRepoID returns the milestone in a repository.
func GetMilestoneByRepoID ( repoID , id int64 ) ( * Milestone , error ) {
return getMilestoneByRepoID ( x , repoID , id )
}
2018-04-29 08:58:47 +03:00
// MilestoneList is a list of milestones offering additional functionality
type MilestoneList [ ] * Milestone
func ( milestones MilestoneList ) loadTotalTrackedTimes ( e Engine ) error {
type totalTimesByMilestone struct {
MilestoneID int64
Time int64
}
if len ( milestones ) == 0 {
return nil
}
var trackedTimes = make ( map [ int64 ] int64 , len ( milestones ) )
// Get total tracked time by milestone_id
rows , err := e . Table ( "issue" ) .
Join ( "INNER" , "milestone" , "issue.milestone_id = milestone.id" ) .
Join ( "LEFT" , "tracked_time" , "tracked_time.issue_id = issue.id" ) .
Select ( "milestone_id, sum(time) as time" ) .
In ( "milestone_id" , milestones . getMilestoneIDs ( ) ) .
GroupBy ( "milestone_id" ) .
Rows ( new ( totalTimesByMilestone ) )
if err != nil {
return err
}
defer rows . Close ( )
for rows . Next ( ) {
var totalTime totalTimesByMilestone
err = rows . Scan ( & totalTime )
if err != nil {
return err
}
trackedTimes [ totalTime . MilestoneID ] = totalTime . Time
}
for _ , milestone := range milestones {
milestone . TotalTrackedTime = trackedTimes [ milestone . ID ]
}
return nil
}
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
func ( milestones MilestoneList ) LoadTotalTrackedTimes ( ) error {
return milestones . loadTotalTrackedTimes ( x )
}
func ( milestones MilestoneList ) getMilestoneIDs ( ) [ ] int64 {
var ids = make ( [ ] int64 , 0 , len ( milestones ) )
for _ , ms := range milestones {
ids = append ( ids , ms . ID )
}
return ids
}
2017-02-09 09:39:26 +03:00
// GetMilestonesByRepoID returns all milestones of a repository.
2018-04-29 08:58:47 +03:00
func GetMilestonesByRepoID ( repoID int64 ) ( MilestoneList , error ) {
2017-02-09 09:39:26 +03:00
miles := make ( [ ] * Milestone , 0 , 10 )
return miles , x . Where ( "repo_id = ?" , repoID ) . Find ( & miles )
}
// GetMilestones returns a list of milestones of given repository and status.
2018-04-29 08:58:47 +03:00
func GetMilestones ( repoID int64 , page int , isClosed bool , sortType string ) ( MilestoneList , error ) {
2017-02-09 09:39:26 +03:00
miles := make ( [ ] * Milestone , 0 , setting . UI . IssuePagingNum )
sess := x . Where ( "repo_id = ? AND is_closed = ?" , repoID , isClosed )
if page > 0 {
sess = sess . Limit ( setting . UI . IssuePagingNum , ( page - 1 ) * setting . UI . IssuePagingNum )
}
switch sortType {
case "furthestduedate" :
sess . Desc ( "deadline_unix" )
case "leastcomplete" :
sess . Asc ( "completeness" )
case "mostcomplete" :
sess . Desc ( "completeness" )
case "leastissues" :
sess . Asc ( "num_issues" )
case "mostissues" :
sess . Desc ( "num_issues" )
default :
sess . Asc ( "deadline_unix" )
}
return miles , sess . Find ( & miles )
}
func updateMilestone ( e Engine , m * Milestone ) error {
2017-10-05 07:43:04 +03:00
_ , err := e . ID ( m . ID ) . AllCols ( ) . Update ( m )
2017-02-09 09:39:26 +03:00
return err
}
// UpdateMilestone updates information of given milestone.
func UpdateMilestone ( m * Milestone ) error {
return updateMilestone ( x , m )
}
2017-12-18 17:06:51 +03:00
func countRepoMilestones ( e Engine , repoID int64 ) ( int64 , error ) {
return e .
2017-02-09 09:39:26 +03:00
Where ( "repo_id=?" , repoID ) .
Count ( new ( Milestone ) )
}
2017-12-18 17:06:51 +03:00
func countRepoClosedMilestones ( e Engine , repoID int64 ) ( int64 , error ) {
return e .
2017-02-09 09:39:26 +03:00
Where ( "repo_id=? AND is_closed=?" , repoID , true ) .
Count ( new ( Milestone ) )
}
// CountRepoClosedMilestones returns number of closed milestones in given repository.
2017-12-18 17:06:51 +03:00
func CountRepoClosedMilestones ( repoID int64 ) ( int64 , error ) {
2017-02-09 09:39:26 +03:00
return countRepoClosedMilestones ( x , repoID )
}
// MilestoneStats returns number of open and closed milestones of given repository.
2017-12-18 17:06:51 +03:00
func MilestoneStats ( repoID int64 ) ( open int64 , closed int64 , err error ) {
open , err = x .
2017-02-09 09:39:26 +03:00
Where ( "repo_id=? AND is_closed=?" , repoID , false ) .
Count ( new ( Milestone ) )
2017-12-18 17:06:51 +03:00
if err != nil {
return 0 , 0 , nil
}
closed , err = CountRepoClosedMilestones ( repoID )
return open , closed , err
2017-02-09 09:39:26 +03:00
}
// ChangeMilestoneStatus changes the milestone open/closed status.
func ChangeMilestoneStatus ( m * Milestone , isClosed bool ) ( err error ) {
repo , err := GetRepositoryByID ( m . RepoID )
if err != nil {
return err
}
sess := x . NewSession ( )
2017-06-21 03:57:05 +03:00
defer sess . Close ( )
2017-02-09 09:39:26 +03:00
if err = sess . Begin ( ) ; err != nil {
return err
}
m . IsClosed = isClosed
if err = updateMilestone ( sess , m ) ; err != nil {
return err
}
2017-12-18 17:06:51 +03:00
numMilestones , err := countRepoMilestones ( sess , repo . ID )
if err != nil {
return err
}
numClosedMilestones , err := countRepoClosedMilestones ( sess , repo . ID )
if err != nil {
return err
}
repo . NumMilestones = int ( numMilestones )
repo . NumClosedMilestones = int ( numClosedMilestones )
2017-10-05 07:43:04 +03:00
if _ , err = sess . ID ( repo . ID ) . Cols ( "num_milestones, num_closed_milestones" ) . Update ( repo ) ; err != nil {
2017-02-09 09:39:26 +03:00
return err
}
return sess . Commit ( )
}
func changeMilestoneIssueStats ( e * xorm . Session , issue * Issue ) error {
if issue . MilestoneID == 0 {
return nil
}
m , err := getMilestoneByRepoID ( e , issue . RepoID , issue . MilestoneID )
if err != nil {
return err
}
if issue . IsClosed {
m . NumOpenIssues --
m . NumClosedIssues ++
} else {
m . NumOpenIssues ++
m . NumClosedIssues --
}
return updateMilestone ( e , m )
}
func changeMilestoneAssign ( e * xorm . Session , doer * User , issue * Issue , oldMilestoneID int64 ) error {
if oldMilestoneID > 0 {
m , err := getMilestoneByRepoID ( e , issue . RepoID , oldMilestoneID )
if err != nil {
return err
}
m . NumIssues --
if issue . IsClosed {
m . NumClosedIssues --
}
if err = updateMilestone ( e , m ) ; err != nil {
return err
}
}
if issue . MilestoneID > 0 {
m , err := getMilestoneByRepoID ( e , issue . RepoID , issue . MilestoneID )
if err != nil {
return err
}
m . NumIssues ++
if issue . IsClosed {
m . NumClosedIssues ++
}
if err = updateMilestone ( e , m ) ; err != nil {
return err
}
}
if err := issue . loadRepo ( e ) ; err != nil {
return err
}
if oldMilestoneID > 0 || issue . MilestoneID > 0 {
if _ , err := createMilestoneComment ( e , doer , issue . Repo , issue , oldMilestoneID , issue . MilestoneID ) ; err != nil {
return err
}
}
2017-12-17 14:53:02 +03:00
return updateIssueCols ( e , issue , "milestone_id" )
2017-02-09 09:39:26 +03:00
}
// ChangeMilestoneAssign changes assignment of milestone for issue.
func ChangeMilestoneAssign ( issue * Issue , doer * User , oldMilestoneID int64 ) ( err error ) {
sess := x . NewSession ( )
defer sess . Close ( )
if err = sess . Begin ( ) ; err != nil {
return err
}
if err = changeMilestoneAssign ( sess , doer , issue , oldMilestoneID ) ; err != nil {
return err
}
return sess . Commit ( )
}
// DeleteMilestoneByRepoID deletes a milestone from a repository.
func DeleteMilestoneByRepoID ( repoID , id int64 ) error {
m , err := GetMilestoneByRepoID ( repoID , id )
if err != nil {
if IsErrMilestoneNotExist ( err ) {
return nil
}
return err
}
repo , err := GetRepositoryByID ( m . RepoID )
if err != nil {
return err
}
sess := x . NewSession ( )
2017-06-21 03:57:05 +03:00
defer sess . Close ( )
2017-02-09 09:39:26 +03:00
if err = sess . Begin ( ) ; err != nil {
return err
}
2017-10-05 07:43:04 +03:00
if _ , err = sess . ID ( m . ID ) . Delete ( new ( Milestone ) ) ; err != nil {
2017-02-09 09:39:26 +03:00
return err
}
2017-12-18 17:06:51 +03:00
numMilestones , err := countRepoMilestones ( sess , repo . ID )
if err != nil {
return err
}
numClosedMilestones , err := countRepoClosedMilestones ( sess , repo . ID )
if err != nil {
return err
}
repo . NumMilestones = int ( numMilestones )
repo . NumClosedMilestones = int ( numClosedMilestones )
2017-10-05 07:43:04 +03:00
if _ , err = sess . ID ( repo . ID ) . Cols ( "num_milestones, num_closed_milestones" ) . Update ( repo ) ; err != nil {
2017-02-09 09:39:26 +03:00
return err
}
if _ , err = sess . Exec ( "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?" , m . ID ) ; err != nil {
return err
}
return sess . Commit ( )
}