Refactor Milestone related (#11225)
This commit is contained in:
		
							parent
							
								
									ba40263fdd
								
							
						
					
					
						commit
						7257c39ddf
					
				
					 7 changed files with 316 additions and 320 deletions
				
			
		|  | @ -69,25 +69,6 @@ func (m *Milestone) State() api.StateType { | |||
| 	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 { | ||||
| 		apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr() | ||||
| 	} | ||||
| 	if m.DeadlineUnix.Year() < 9999 { | ||||
| 		apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr() | ||||
| 	} | ||||
| 	return apiMilestone | ||||
| } | ||||
| 
 | ||||
| // NewMilestone creates new milestone of repository.
 | ||||
| func NewMilestone(m *Milestone) (err error) { | ||||
| 	sess := x.NewSession() | ||||
|  | @ -149,157 +130,6 @@ func GetMilestoneByID(id int64) (*Milestone, error) { | |||
| 	return &m, nil | ||||
| } | ||||
| 
 | ||||
| // 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"). | ||||
| 		Where("tracked_time.deleted = ?", false). | ||||
| 		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 | ||||
| } | ||||
| 
 | ||||
| func (m *Milestone) loadTotalTrackedTime(e Engine) error { | ||||
| 	type totalTimesByMilestone struct { | ||||
| 		MilestoneID int64 | ||||
| 		Time        int64 | ||||
| 	} | ||||
| 	totalTime := &totalTimesByMilestone{MilestoneID: m.ID} | ||||
| 	has, err := e.Table("issue"). | ||||
| 		Join("INNER", "milestone", "issue.milestone_id = milestone.id"). | ||||
| 		Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). | ||||
| 		Where("tracked_time.deleted = ?", false). | ||||
| 		Select("milestone_id, sum(time) as time"). | ||||
| 		Where("milestone_id = ?", m.ID). | ||||
| 		GroupBy("milestone_id"). | ||||
| 		Get(totalTime) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} else if !has { | ||||
| 		return nil | ||||
| 	} | ||||
| 	m.TotalTrackedTime = totalTime.Time | ||||
| 	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) | ||||
| } | ||||
| 
 | ||||
| // LoadTotalTrackedTime loads the tracked time for the milestone
 | ||||
| func (m *Milestone) LoadTotalTrackedTime() error { | ||||
| 	return m.loadTotalTrackedTime(x) | ||||
| } | ||||
| 
 | ||||
| func (milestones MilestoneList) getMilestoneIDs() []int64 { | ||||
| 	var ids = make([]int64, 0, len(milestones)) | ||||
| 	for _, ms := range milestones { | ||||
| 		ids = append(ids, ms.ID) | ||||
| 	} | ||||
| 	return ids | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesByRepoID returns all opened milestones of a repository.
 | ||||
| func GetMilestonesByRepoID(repoID int64, state api.StateType, listOptions ListOptions) (MilestoneList, error) { | ||||
| 	sess := x.Where("repo_id = ?", repoID) | ||||
| 
 | ||||
| 	switch state { | ||||
| 	case api.StateClosed: | ||||
| 		sess = sess.And("is_closed = ?", true) | ||||
| 
 | ||||
| 	case api.StateAll: | ||||
| 		break | ||||
| 
 | ||||
| 	case api.StateOpen: | ||||
| 		fallthrough | ||||
| 
 | ||||
| 	default: | ||||
| 		sess = sess.And("is_closed = ?", false) | ||||
| 	} | ||||
| 
 | ||||
| 	if listOptions.Page != 0 { | ||||
| 		sess = listOptions.setSessionPagination(sess) | ||||
| 	} | ||||
| 
 | ||||
| 	miles := make([]*Milestone, 0, listOptions.PageSize) | ||||
| 	return miles, sess.Asc("deadline_unix").Asc("id").Find(&miles) | ||||
| } | ||||
| 
 | ||||
| // GetMilestones returns a list of milestones of given repository and status.
 | ||||
| func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, error) { | ||||
| 	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 { | ||||
| 	m.Name = strings.TrimSpace(m.Name) | ||||
| 	_, err := e.ID(m.ID).AllCols(). | ||||
| 		SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( | ||||
| 			builder.Eq{"milestone_id": m.ID}, | ||||
| 		)). | ||||
| 		SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where( | ||||
| 			builder.Eq{ | ||||
| 				"milestone_id": m.ID, | ||||
| 				"is_closed":    true, | ||||
| 			}, | ||||
| 		)). | ||||
| 		Update(m) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // UpdateMilestone updates information of given milestone.
 | ||||
| func UpdateMilestone(m *Milestone, oldIsClosed bool) error { | ||||
| 	sess := x.NewSession() | ||||
|  | @ -330,6 +160,22 @@ func UpdateMilestone(m *Milestone, oldIsClosed bool) error { | |||
| 	return sess.Commit() | ||||
| } | ||||
| 
 | ||||
| func updateMilestone(e Engine, m *Milestone) error { | ||||
| 	m.Name = strings.TrimSpace(m.Name) | ||||
| 	_, err := e.ID(m.ID).AllCols(). | ||||
| 		SetExpr("num_issues", builder.Select("count(*)").From("issue").Where( | ||||
| 			builder.Eq{"milestone_id": m.ID}, | ||||
| 		)). | ||||
| 		SetExpr("num_closed_issues", builder.Select("count(*)").From("issue").Where( | ||||
| 			builder.Eq{ | ||||
| 				"milestone_id": m.ID, | ||||
| 				"is_closed":    true, | ||||
| 			}, | ||||
| 		)). | ||||
| 		Update(m) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func updateMilestoneCompleteness(e Engine, milestoneID int64) error { | ||||
| 	_, err := e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?", | ||||
| 		milestoneID, | ||||
|  | @ -337,35 +183,6 @@ func updateMilestoneCompleteness(e Engine, milestoneID int64) error { | |||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func countRepoMilestones(e Engine, repoID int64) (int64, error) { | ||||
| 	return e. | ||||
| 		Where("repo_id=?", repoID). | ||||
| 		Count(new(Milestone)) | ||||
| } | ||||
| 
 | ||||
| func countRepoClosedMilestones(e Engine, repoID int64) (int64, error) { | ||||
| 	return e. | ||||
| 		Where("repo_id=? AND is_closed=?", repoID, true). | ||||
| 		Count(new(Milestone)) | ||||
| } | ||||
| 
 | ||||
| // CountRepoClosedMilestones returns number of closed milestones in given repository.
 | ||||
| func CountRepoClosedMilestones(repoID int64) (int64, error) { | ||||
| 	return countRepoClosedMilestones(x, repoID) | ||||
| } | ||||
| 
 | ||||
| // MilestoneStats returns number of open and closed milestones of given repository.
 | ||||
| func MilestoneStats(repoID int64) (open int64, closed int64, err error) { | ||||
| 	open, err = x. | ||||
| 		Where("repo_id=? AND is_closed=?", repoID, false). | ||||
| 		Count(new(Milestone)) | ||||
| 	if err != nil { | ||||
| 		return 0, 0, nil | ||||
| 	} | ||||
| 	closed, err = CountRepoClosedMilestones(repoID) | ||||
| 	return open, closed, err | ||||
| } | ||||
| 
 | ||||
| // ChangeMilestoneStatus changes the milestone open/closed status.
 | ||||
| func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { | ||||
| 	sess := x.NewSession() | ||||
|  | @ -390,39 +207,6 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { | |||
| 	return sess.Commit() | ||||
| } | ||||
| 
 | ||||
| func updateRepoMilestoneNum(e Engine, repoID int64) error { | ||||
| 	_, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?", | ||||
| 		repoID, | ||||
| 		repoID, | ||||
| 		true, | ||||
| 		repoID, | ||||
| 	) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) { | ||||
| 	if _, err = e.Exec("UPDATE `milestone` SET num_issues=(SELECT count(*) FROM issue WHERE milestone_id=?) WHERE id=?", | ||||
| 		milestoneID, | ||||
| 		milestoneID, | ||||
| 	); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return updateMilestoneCompleteness(e, milestoneID) | ||||
| } | ||||
| 
 | ||||
| func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) { | ||||
| 	if _, err = e.Exec("UPDATE `milestone` SET num_closed_issues=(SELECT count(*) FROM issue WHERE milestone_id=? AND is_closed=?) WHERE id=?", | ||||
| 		milestoneID, | ||||
| 		true, | ||||
| 		milestoneID, | ||||
| 	); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return updateMilestoneCompleteness(e, milestoneID) | ||||
| } | ||||
| 
 | ||||
| func changeMilestoneAssign(e *xorm.Session, doer *User, issue *Issue, oldMilestoneID int64) error { | ||||
| 	if err := updateIssueCols(e, issue, "milestone_id"); err != nil { | ||||
| 		return err | ||||
|  | @ -535,37 +319,66 @@ func DeleteMilestoneByRepoID(repoID, id int64) error { | |||
| 	return sess.Commit() | ||||
| } | ||||
| 
 | ||||
| // CountMilestones map from repo conditions to number of milestones matching the options`
 | ||||
| func CountMilestones(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { | ||||
| 	sess := x.Where("is_closed = ?", isClosed) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| // MilestoneList is a list of milestones offering additional functionality
 | ||||
| type MilestoneList []*Milestone | ||||
| 
 | ||||
| 	countsSlice := make([]*struct { | ||||
| 		RepoID int64 | ||||
| 		Count  int64 | ||||
| 	}, 0, 10) | ||||
| 	if err := sess.GroupBy("repo_id"). | ||||
| 		Select("repo_id AS repo_id, COUNT(*) AS count"). | ||||
| 		Table("milestone"). | ||||
| 		Find(&countsSlice); err != nil { | ||||
| 		return nil, err | ||||
| func (milestones MilestoneList) getMilestoneIDs() []int64 { | ||||
| 	var ids = make([]int64, 0, len(milestones)) | ||||
| 	for _, ms := range milestones { | ||||
| 		ids = append(ids, ms.ID) | ||||
| 	} | ||||
| 
 | ||||
| 	countMap := make(map[int64]int64, len(countsSlice)) | ||||
| 	for _, c := range countsSlice { | ||||
| 		countMap[c.RepoID] = c.Count | ||||
| 	} | ||||
| 	return countMap, nil | ||||
| 	return ids | ||||
| } | ||||
| 
 | ||||
| // CountMilestonesByRepoIDs map from repoIDs to number of milestones matching the options`
 | ||||
| func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, error) { | ||||
| 	return CountMilestones( | ||||
| 		builder.In("repo_id", repoIDs), | ||||
| 		isClosed, | ||||
| 	) | ||||
| // GetMilestonesByRepoID returns all opened milestones of a repository.
 | ||||
| func GetMilestonesByRepoID(repoID int64, state api.StateType, listOptions ListOptions) (MilestoneList, error) { | ||||
| 	sess := x.Where("repo_id = ?", repoID) | ||||
| 
 | ||||
| 	switch state { | ||||
| 	case api.StateClosed: | ||||
| 		sess = sess.And("is_closed = ?", true) | ||||
| 
 | ||||
| 	case api.StateAll: | ||||
| 		break | ||||
| 
 | ||||
| 	case api.StateOpen: | ||||
| 		fallthrough | ||||
| 
 | ||||
| 	default: | ||||
| 		sess = sess.And("is_closed = ?", false) | ||||
| 	} | ||||
| 
 | ||||
| 	if listOptions.Page != 0 { | ||||
| 		sess = listOptions.setSessionPagination(sess) | ||||
| 	} | ||||
| 
 | ||||
| 	miles := make([]*Milestone, 0, listOptions.PageSize) | ||||
| 	return miles, sess.Asc("deadline_unix").Asc("id").Find(&miles) | ||||
| } | ||||
| 
 | ||||
| // GetMilestones returns a list of milestones of given repository and status.
 | ||||
| func GetMilestones(repoID int64, page int, isClosed bool, sortType string) (MilestoneList, error) { | ||||
| 	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) | ||||
| } | ||||
| 
 | ||||
| // SearchMilestones search milestones
 | ||||
|  | @ -606,6 +419,13 @@ func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType s | |||
| 	) | ||||
| } | ||||
| 
 | ||||
| //  ____  _        _
 | ||||
| // / ___|| |_ __ _| |_ ___
 | ||||
| // \___ \| __/ _` | __/ __|
 | ||||
| //  ___) | || (_| | |_\__ \
 | ||||
| // |____/ \__\__,_|\__|___/
 | ||||
| //
 | ||||
| 
 | ||||
| // MilestonesStats represents milestone statistic information.
 | ||||
| type MilestonesStats struct { | ||||
| 	OpenCount, ClosedCount int64 | ||||
|  | @ -616,8 +436,8 @@ func (m MilestonesStats) Total() int64 { | |||
| 	return m.OpenCount + m.ClosedCount | ||||
| } | ||||
| 
 | ||||
| // GetMilestonesStats returns milestone statistic information for dashboard by given conditions.
 | ||||
| func GetMilestonesStats(repoCond builder.Cond) (*MilestonesStats, error) { | ||||
| // GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions.
 | ||||
| func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) { | ||||
| 	var err error | ||||
| 	stats := &MilestonesStats{} | ||||
| 
 | ||||
|  | @ -641,3 +461,158 @@ func GetMilestonesStats(repoCond builder.Cond) (*MilestonesStats, error) { | |||
| 
 | ||||
| 	return stats, nil | ||||
| } | ||||
| 
 | ||||
| func countRepoMilestones(e Engine, repoID int64) (int64, error) { | ||||
| 	return e. | ||||
| 		Where("repo_id=?", repoID). | ||||
| 		Count(new(Milestone)) | ||||
| } | ||||
| 
 | ||||
| func countRepoClosedMilestones(e Engine, repoID int64) (int64, error) { | ||||
| 	return e. | ||||
| 		Where("repo_id=? AND is_closed=?", repoID, true). | ||||
| 		Count(new(Milestone)) | ||||
| } | ||||
| 
 | ||||
| // CountRepoClosedMilestones returns number of closed milestones in given repository.
 | ||||
| func CountRepoClosedMilestones(repoID int64) (int64, error) { | ||||
| 	return countRepoClosedMilestones(x, repoID) | ||||
| } | ||||
| 
 | ||||
| // CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
 | ||||
| func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { | ||||
| 	sess := x.Where("is_closed = ?", isClosed) | ||||
| 	if repoCond.IsValid() { | ||||
| 		sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) | ||||
| 	} | ||||
| 
 | ||||
| 	countsSlice := make([]*struct { | ||||
| 		RepoID int64 | ||||
| 		Count  int64 | ||||
| 	}, 0, 10) | ||||
| 	if err := sess.GroupBy("repo_id"). | ||||
| 		Select("repo_id AS repo_id, COUNT(*) AS count"). | ||||
| 		Table("milestone"). | ||||
| 		Find(&countsSlice); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	countMap := make(map[int64]int64, len(countsSlice)) | ||||
| 	for _, c := range countsSlice { | ||||
| 		countMap[c.RepoID] = c.Count | ||||
| 	} | ||||
| 	return countMap, nil | ||||
| } | ||||
| 
 | ||||
| func updateRepoMilestoneNum(e Engine, repoID int64) error { | ||||
| 	_, err := e.Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?", | ||||
| 		repoID, | ||||
| 		repoID, | ||||
| 		true, | ||||
| 		repoID, | ||||
| 	) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func updateMilestoneTotalNum(e Engine, milestoneID int64) (err error) { | ||||
| 	if _, err = e.Exec("UPDATE `milestone` SET num_issues=(SELECT count(*) FROM issue WHERE milestone_id=?) WHERE id=?", | ||||
| 		milestoneID, | ||||
| 		milestoneID, | ||||
| 	); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return updateMilestoneCompleteness(e, milestoneID) | ||||
| } | ||||
| 
 | ||||
| func updateMilestoneClosedNum(e Engine, milestoneID int64) (err error) { | ||||
| 	if _, err = e.Exec("UPDATE `milestone` SET num_closed_issues=(SELECT count(*) FROM issue WHERE milestone_id=? AND is_closed=?) WHERE id=?", | ||||
| 		milestoneID, | ||||
| 		true, | ||||
| 		milestoneID, | ||||
| 	); err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	return updateMilestoneCompleteness(e, milestoneID) | ||||
| } | ||||
| 
 | ||||
| //  _____               _            _ _____ _
 | ||||
| // |_   _| __ __ _  ___| | _____  __| |_   _(_)_ __ ___   ___  ___
 | ||||
| //   | || '__/ _` |/ __| |/ / _ \/ _` | | | | | '_ ` _ \ / _ \/ __|
 | ||||
| //   | || | | (_| | (__|   <  __/ (_| | | | | | | | | | |  __/\__ \
 | ||||
| //   |_||_|  \__,_|\___|_|\_\___|\__,_| |_| |_|_| |_| |_|\___||___/
 | ||||
| //
 | ||||
| 
 | ||||
| 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"). | ||||
| 		Where("tracked_time.deleted = ?", false). | ||||
| 		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 | ||||
| } | ||||
| 
 | ||||
| func (m *Milestone) loadTotalTrackedTime(e Engine) error { | ||||
| 	type totalTimesByMilestone struct { | ||||
| 		MilestoneID int64 | ||||
| 		Time        int64 | ||||
| 	} | ||||
| 	totalTime := &totalTimesByMilestone{MilestoneID: m.ID} | ||||
| 	has, err := e.Table("issue"). | ||||
| 		Join("INNER", "milestone", "issue.milestone_id = milestone.id"). | ||||
| 		Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). | ||||
| 		Where("tracked_time.deleted = ?", false). | ||||
| 		Select("milestone_id, sum(time) as time"). | ||||
| 		Where("milestone_id = ?", m.ID). | ||||
| 		GroupBy("milestone_id"). | ||||
| 		Get(totalTime) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} else if !has { | ||||
| 		return nil | ||||
| 	} | ||||
| 	m.TotalTrackedTime = totalTime.Time | ||||
| 	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) | ||||
| } | ||||
| 
 | ||||
| // LoadTotalTrackedTime loads the tracked time for the milestone
 | ||||
| func (m *Milestone) LoadTotalTrackedTime() error { | ||||
| 	return m.loadTotalTrackedTime(x) | ||||
| } | ||||
|  |  | |||
|  | @ -7,13 +7,12 @@ package models | |||
| import ( | ||||
| 	"sort" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"xorm.io/builder" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| 
 | ||||
| func TestMilestone_State(t *testing.T) { | ||||
|  | @ -21,28 +20,6 @@ func TestMilestone_State(t *testing.T) { | |||
| 	assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State()) | ||||
| } | ||||
| 
 | ||||
| func TestMilestone_APIFormat(t *testing.T) { | ||||
| 	milestone := &Milestone{ | ||||
| 		ID:              3, | ||||
| 		RepoID:          4, | ||||
| 		Name:            "milestoneName", | ||||
| 		Content:         "milestoneContent", | ||||
| 		IsClosed:        false, | ||||
| 		NumOpenIssues:   5, | ||||
| 		NumClosedIssues: 6, | ||||
| 		DeadlineUnix:    timeutil.TimeStamp(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()), | ||||
| 	} | ||||
| 	assert.Equal(t, api.Milestone{ | ||||
| 		ID:           milestone.ID, | ||||
| 		State:        api.StateOpen, | ||||
| 		Title:        milestone.Name, | ||||
| 		Description:  milestone.Content, | ||||
| 		OpenIssues:   milestone.NumOpenIssues, | ||||
| 		ClosedIssues: milestone.NumClosedIssues, | ||||
| 		Deadline:     milestone.DeadlineUnix.AsTimePtr(), | ||||
| 	}, *milestone.APIFormat()) | ||||
| } | ||||
| 
 | ||||
| func TestNewMilestone(t *testing.T) { | ||||
| 	assert.NoError(t, PrepareTestDatabase()) | ||||
| 	milestone := &Milestone{ | ||||
|  | @ -201,25 +178,6 @@ func TestCountRepoClosedMilestones(t *testing.T) { | |||
| 	assert.EqualValues(t, 0, count) | ||||
| } | ||||
| 
 | ||||
| func TestMilestoneStats(t *testing.T) { | ||||
| 	assert.NoError(t, PrepareTestDatabase()) | ||||
| 	test := func(repoID int64) { | ||||
| 		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository) | ||||
| 		open, closed, err := MilestoneStats(repoID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, open) | ||||
| 		assert.EqualValues(t, repo.NumClosedMilestones, closed) | ||||
| 	} | ||||
| 	test(1) | ||||
| 	test(2) | ||||
| 	test(3) | ||||
| 
 | ||||
| 	open, closed, err := MilestoneStats(NonexistentID) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 0, open) | ||||
| 	assert.EqualValues(t, 0, closed) | ||||
| } | ||||
| 
 | ||||
| func TestChangeMilestoneStatus(t *testing.T) { | ||||
| 	assert.NoError(t, PrepareTestDatabase()) | ||||
| 	milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone) | ||||
|  | @ -301,12 +259,12 @@ func TestCountMilestonesByRepoIDs(t *testing.T) { | |||
| 	repo1OpenCount, repo1ClosedCount := milestonesCount(1) | ||||
| 	repo2OpenCount, repo2ClosedCount := milestonesCount(2) | ||||
| 
 | ||||
| 	openCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, false) | ||||
| 	openCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, repo1OpenCount, openCounts[1]) | ||||
| 	assert.EqualValues(t, repo2OpenCount, openCounts[2]) | ||||
| 
 | ||||
| 	closedCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, true) | ||||
| 	closedCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, repo1ClosedCount, closedCounts[1]) | ||||
| 	assert.EqualValues(t, repo2ClosedCount, closedCounts[2]) | ||||
|  | @ -368,10 +326,27 @@ func TestLoadTotalTrackedTime(t *testing.T) { | |||
| 
 | ||||
| func TestGetMilestonesStats(t *testing.T) { | ||||
| 	assert.NoError(t, PrepareTestDatabase()) | ||||
| 
 | ||||
| 	test := func(repoID int64) { | ||||
| 		repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository) | ||||
| 		stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID})) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, stats.OpenCount) | ||||
| 		assert.EqualValues(t, repo.NumClosedMilestones, stats.ClosedCount) | ||||
| 	} | ||||
| 	test(1) | ||||
| 	test(2) | ||||
| 	test(3) | ||||
| 
 | ||||
| 	stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": NonexistentID})) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, 0, stats.OpenCount) | ||||
| 	assert.EqualValues(t, 0, stats.ClosedCount) | ||||
| 
 | ||||
| 	repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | ||||
| 	repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) | ||||
| 
 | ||||
| 	milestoneStats, err := GetMilestonesStats(builder.In("repo_id", []int64{repo1.ID, repo2.ID})) | ||||
| 	milestoneStats, err := GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID})) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount) | ||||
| 	assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount) | ||||
|  |  | |||
|  | @ -56,7 +56,7 @@ func ToAPIIssue(issue *models.Issue) *api.Issue { | |||
| 		return &api.Issue{} | ||||
| 	} | ||||
| 	if issue.Milestone != nil { | ||||
| 		apiIssue.Milestone = issue.Milestone.APIFormat() | ||||
| 		apiIssue.Milestone = ToAPIMilestone(issue.Milestone) | ||||
| 	} | ||||
| 
 | ||||
| 	if err := issue.LoadAssignees(); err != nil { | ||||
|  | @ -141,3 +141,22 @@ func ToLabelList(labels []*models.Label) []*api.Label { | |||
| 	} | ||||
| 	return result | ||||
| } | ||||
| 
 | ||||
| // ToAPIMilestone converts Milestone into API Format
 | ||||
| func ToAPIMilestone(m *models.Milestone) *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 { | ||||
| 		apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr() | ||||
| 	} | ||||
| 	if m.DeadlineUnix.Year() < 9999 { | ||||
| 		apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr() | ||||
| 	} | ||||
| 	return apiMilestone | ||||
| } | ||||
|  |  | |||
|  | @ -6,9 +6,11 @@ package convert | |||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | @ -22,3 +24,25 @@ func TestLabel_ToLabel(t *testing.T) { | |||
| 		Color: "abcdef", | ||||
| 	}, ToLabel(label)) | ||||
| } | ||||
| 
 | ||||
| func TestMilestone_APIFormat(t *testing.T) { | ||||
| 	milestone := &models.Milestone{ | ||||
| 		ID:              3, | ||||
| 		RepoID:          4, | ||||
| 		Name:            "milestoneName", | ||||
| 		Content:         "milestoneContent", | ||||
| 		IsClosed:        false, | ||||
| 		NumOpenIssues:   5, | ||||
| 		NumClosedIssues: 6, | ||||
| 		DeadlineUnix:    timeutil.TimeStamp(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC).Unix()), | ||||
| 	} | ||||
| 	assert.Equal(t, api.Milestone{ | ||||
| 		ID:           milestone.ID, | ||||
| 		State:        api.StateOpen, | ||||
| 		Title:        milestone.Name, | ||||
| 		Description:  milestone.Content, | ||||
| 		OpenIssues:   milestone.NumOpenIssues, | ||||
| 		ClosedIssues: milestone.NumClosedIssues, | ||||
| 		Deadline:     milestone.DeadlineUnix.AsTimePtr(), | ||||
| 	}, *ToAPIMilestone(milestone)) | ||||
| } | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import ( | |||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/routers/api/v1/utils" | ||||
|  | @ -58,7 +59,7 @@ func ListMilestones(ctx *context.APIContext) { | |||
| 
 | ||||
| 	apiMilestones := make([]*api.Milestone, len(milestones)) | ||||
| 	for i := range milestones { | ||||
| 		apiMilestones[i] = milestones[i].APIFormat() | ||||
| 		apiMilestones[i] = convert.ToAPIMilestone(milestones[i]) | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, &apiMilestones) | ||||
| } | ||||
|  | @ -100,7 +101,7 @@ func GetMilestone(ctx *context.APIContext) { | |||
| 		} | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, milestone.APIFormat()) | ||||
| 	ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone)) | ||||
| } | ||||
| 
 | ||||
| // CreateMilestone create a milestone for a repository
 | ||||
|  | @ -147,7 +148,7 @@ func CreateMilestone(ctx *context.APIContext, form api.CreateMilestoneOption) { | |||
| 		ctx.Error(http.StatusInternalServerError, "NewMilestone", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusCreated, milestone.APIFormat()) | ||||
| 	ctx.JSON(http.StatusCreated, convert.ToAPIMilestone(milestone)) | ||||
| } | ||||
| 
 | ||||
| // EditMilestone modify a milestone for a repository
 | ||||
|  | @ -213,7 +214,7 @@ func EditMilestone(ctx *context.APIContext, form api.EditMilestoneOption) { | |||
| 		ctx.ServerError("UpdateMilestone", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusOK, milestone.APIFormat()) | ||||
| 	ctx.JSON(http.StatusOK, convert.ToAPIMilestone(milestone)) | ||||
| } | ||||
| 
 | ||||
| // DeleteMilestone delete a milestone for a repository
 | ||||
|  |  | |||
|  | @ -15,6 +15,8 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 
 | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
|  | @ -30,13 +32,13 @@ func Milestones(ctx *context.Context) { | |||
| 	ctx.Data["PageIsMilestones"] = true | ||||
| 
 | ||||
| 	isShowClosed := ctx.Query("state") == "closed" | ||||
| 	openCount, closedCount, err := models.MilestoneStats(ctx.Repo.Repository.ID) | ||||
| 	stats, err := models.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"id": ctx.Repo.Repository.ID})) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("MilestoneStats", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ctx.Data["OpenCount"] = openCount | ||||
| 	ctx.Data["ClosedCount"] = closedCount | ||||
| 	ctx.Data["OpenCount"] = stats.OpenCount | ||||
| 	ctx.Data["ClosedCount"] = stats.ClosedCount | ||||
| 
 | ||||
| 	sortType := ctx.Query("sort") | ||||
| 	page := ctx.QueryInt("page") | ||||
|  | @ -46,9 +48,9 @@ func Milestones(ctx *context.Context) { | |||
| 
 | ||||
| 	var total int | ||||
| 	if !isShowClosed { | ||||
| 		total = int(openCount) | ||||
| 		total = int(stats.OpenCount) | ||||
| 	} else { | ||||
| 		total = int(closedCount) | ||||
| 		total = int(stats.ClosedCount) | ||||
| 	} | ||||
| 
 | ||||
| 	miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed, sortType) | ||||
|  |  | |||
|  | @ -224,7 +224,7 @@ func Milestones(ctx *context.Context) { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	counts, err := models.CountMilestones(userRepoCond, isShowClosed) | ||||
| 	counts, err := models.CountMilestonesByRepoCond(userRepoCond, isShowClosed) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("CountMilestonesByRepoIDs", err) | ||||
| 		return | ||||
|  | @ -267,7 +267,7 @@ func Milestones(ctx *context.Context) { | |||
| 		i++ | ||||
| 	} | ||||
| 
 | ||||
| 	milestoneStats, err := models.GetMilestonesStats(repoCond) | ||||
| 	milestoneStats, err := models.GetMilestonesStatsByRepoCond(repoCond) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetMilestoneStats", err) | ||||
| 		return | ||||
|  | @ -277,7 +277,7 @@ func Milestones(ctx *context.Context) { | |||
| 	if len(repoIDs) == 0 { | ||||
| 		totalMilestoneStats = milestoneStats | ||||
| 	} else { | ||||
| 		totalMilestoneStats, err = models.GetMilestonesStats(userRepoCond) | ||||
| 		totalMilestoneStats, err = models.GetMilestonesStatsByRepoCond(userRepoCond) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("GetMilestoneStats", err) | ||||
| 			return | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue