code.gitea.io/gitea@v1.22.3/models/issues/milestone_list.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package issues
     5  
     6  import (
     7  	"context"
     8  	"strings"
     9  
    10  	"code.gitea.io/gitea/models/db"
    11  	"code.gitea.io/gitea/modules/optional"
    12  
    13  	"xorm.io/builder"
    14  )
    15  
    16  // MilestoneList is a list of milestones offering additional functionality
    17  type MilestoneList []*Milestone
    18  
    19  func (milestones MilestoneList) getMilestoneIDs() []int64 {
    20  	ids := make([]int64, 0, len(milestones))
    21  	for _, ms := range milestones {
    22  		ids = append(ids, ms.ID)
    23  	}
    24  	return ids
    25  }
    26  
    27  // FindMilestoneOptions contain options to get milestones
    28  type FindMilestoneOptions struct {
    29  	db.ListOptions
    30  	RepoID   int64
    31  	IsClosed optional.Option[bool]
    32  	Name     string
    33  	SortType string
    34  	RepoCond builder.Cond
    35  	RepoIDs  []int64
    36  }
    37  
    38  func (opts FindMilestoneOptions) ToConds() builder.Cond {
    39  	cond := builder.NewCond()
    40  	if opts.RepoID != 0 {
    41  		cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
    42  	}
    43  	if opts.IsClosed.Has() {
    44  		cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()})
    45  	}
    46  	if opts.RepoCond != nil && opts.RepoCond.IsValid() {
    47  		cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
    48  	}
    49  	if len(opts.RepoIDs) > 0 {
    50  		cond = cond.And(builder.In("repo_id", opts.RepoIDs))
    51  	}
    52  	if len(opts.Name) != 0 {
    53  		cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
    54  	}
    55  
    56  	return cond
    57  }
    58  
    59  func (opts FindMilestoneOptions) ToOrders() string {
    60  	switch opts.SortType {
    61  	case "furthestduedate":
    62  		return "deadline_unix DESC"
    63  	case "leastcomplete":
    64  		return "completeness ASC"
    65  	case "mostcomplete":
    66  		return "completeness DESC"
    67  	case "leastissues":
    68  		return "num_issues ASC"
    69  	case "mostissues":
    70  		return "num_issues DESC"
    71  	case "id":
    72  		return "id ASC"
    73  	default:
    74  		return "deadline_unix ASC, id ASC"
    75  	}
    76  }
    77  
    78  // GetMilestoneIDsByNames returns a list of milestone ids by given names.
    79  // It doesn't filter them by repo, so it could return milestones belonging to different repos.
    80  // It's used for filtering issues via indexer, otherwise it would be useless.
    81  // Since it could return milestones with the same name, so the length of returned ids could be more than the length of names.
    82  func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) {
    83  	var ids []int64
    84  	return ids, db.GetEngine(ctx).Table("milestone").
    85  		Where(db.BuildCaseInsensitiveIn("name", names)).
    86  		Cols("id").
    87  		Find(&ids)
    88  }
    89  
    90  // LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
    91  func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
    92  	type totalTimesByMilestone struct {
    93  		MilestoneID int64
    94  		Time        int64
    95  	}
    96  	if len(milestones) == 0 {
    97  		return nil
    98  	}
    99  	trackedTimes := make(map[int64]int64, len(milestones))
   100  
   101  	// Get total tracked time by milestone_id
   102  	rows, err := db.GetEngine(ctx).Table("issue").
   103  		Join("INNER", "milestone", "issue.milestone_id = milestone.id").
   104  		Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
   105  		Where("tracked_time.deleted = ?", false).
   106  		Select("milestone_id, sum(time) as time").
   107  		In("milestone_id", milestones.getMilestoneIDs()).
   108  		GroupBy("milestone_id").
   109  		Rows(new(totalTimesByMilestone))
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	defer rows.Close()
   115  
   116  	for rows.Next() {
   117  		var totalTime totalTimesByMilestone
   118  		err = rows.Scan(&totalTime)
   119  		if err != nil {
   120  			return err
   121  		}
   122  		trackedTimes[totalTime.MilestoneID] = totalTime.Time
   123  	}
   124  
   125  	for _, milestone := range milestones {
   126  		milestone.TotalTrackedTime = trackedTimes[milestone.ID]
   127  	}
   128  	return nil
   129  }
   130  
   131  // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
   132  func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) {
   133  	sess := db.GetEngine(ctx).Where(opts.ToConds())
   134  
   135  	countsSlice := make([]*struct {
   136  		RepoID int64
   137  		Count  int64
   138  	}, 0, 10)
   139  	if err := sess.GroupBy("repo_id").
   140  		Select("repo_id AS repo_id, COUNT(*) AS count").
   141  		Table("milestone").
   142  		Find(&countsSlice); err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	countMap := make(map[int64]int64, len(countsSlice))
   147  	for _, c := range countsSlice {
   148  		countMap[c.RepoID] = c.Count
   149  	}
   150  	return countMap, nil
   151  }
   152  
   153  // MilestonesStats represents milestone statistic information.
   154  type MilestonesStats struct {
   155  	OpenCount, ClosedCount int64
   156  }
   157  
   158  // Total returns the total counts of milestones
   159  func (m MilestonesStats) Total() int64 {
   160  	return m.OpenCount + m.ClosedCount
   161  }
   162  
   163  // GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword.
   164  func GetMilestonesStatsByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string) (*MilestonesStats, error) {
   165  	var err error
   166  	stats := &MilestonesStats{}
   167  
   168  	sess := db.GetEngine(ctx).Where("is_closed = ?", false)
   169  	if len(keyword) > 0 {
   170  		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
   171  	}
   172  	if repoCond.IsValid() {
   173  		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
   174  	}
   175  	stats.OpenCount, err = sess.Count(new(Milestone))
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  
   180  	sess = db.GetEngine(ctx).Where("is_closed = ?", true)
   181  	if len(keyword) > 0 {
   182  		sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
   183  	}
   184  	if repoCond.IsValid() {
   185  		sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond)))
   186  	}
   187  	stats.ClosedCount, err = sess.Count(new(Milestone))
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	return stats, nil
   193  }