sprints do be sprinting.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/sprints"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/stats"
|
||||
"github.com/gin-gonic/gin"
|
||||
"log"
|
||||
@@ -50,6 +51,12 @@ func main() {
|
||||
Items: itemsService,
|
||||
Repository: db.Projects(),
|
||||
}
|
||||
sprintsService := &sprints.Service{
|
||||
Scopes: scopesService,
|
||||
Items: itemsService,
|
||||
Projects: projectsService,
|
||||
Repository: db.Sprints(),
|
||||
}
|
||||
|
||||
server := gin.New()
|
||||
apiV1 := server.Group("/api/v1")
|
||||
@@ -65,6 +72,7 @@ func main() {
|
||||
apiV1ScopesSub.Use(httpapi.ScopeMiddleware(scopesService, authService))
|
||||
httpapi.Projects(apiV1ScopesSub.Group("/projects"), projectsService)
|
||||
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
|
||||
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
|
||||
|
||||
exitSignal := make(chan os.Signal)
|
||||
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
@@ -23,6 +23,7 @@ require (
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
)
|
||||
|
||||
@@ -48,6 +48,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package genutils
|
||||
|
||||
func Map[I any, O any](arr []I, cb func(a I) O) []O {
|
||||
res := make([]O, 0, len(arr))
|
||||
for _, value := range arr {
|
||||
res = append(res, cb(value))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
+2
-2
@@ -5,11 +5,11 @@ type betweenContract[T any] interface {
|
||||
IsZero() bool
|
||||
}
|
||||
|
||||
type Between[T betweenContract[T]] struct {
|
||||
type TimeInterval[T betweenContract[T]] struct {
|
||||
Min T `json:"min"`
|
||||
Max T `json:"max"`
|
||||
}
|
||||
|
||||
func (b *Between[T]) Valid() bool {
|
||||
func (b *TimeInterval[T]) Valid() bool {
|
||||
return !b.Min.IsZero() && !b.Max.IsZero() && b.Min.Before(b.Max)
|
||||
}
|
||||
|
||||
+10
-9
@@ -15,13 +15,14 @@ type ItemUpdate struct {
|
||||
}
|
||||
|
||||
type ItemFilter struct {
|
||||
OwnerID *string `json:"ownerId,omitempty"`
|
||||
AcquiredTime *Between[time.Time] `json:"acquiredTime,omitempty"`
|
||||
CreatedTime *Between[time.Time] `json:"createdTime,omitempty"`
|
||||
ScheduledDate *Between[Date] `json:"scheduledDate,omitempty"`
|
||||
ScopeIDs []int `json:"scopeIds,omitempty"`
|
||||
ProjectIDs []int `json:"projectIds,omitempty"`
|
||||
RequirementIDs []int `json:"requirementIds,omitempty"`
|
||||
StatIDs []int `json:"statIds,omitempty"`
|
||||
Loose bool `json:"loose,omitempty"`
|
||||
OwnerID *string `json:"ownerId,omitempty"`
|
||||
AcquiredTime *TimeInterval[time.Time] `json:"acquiredTime,omitempty"`
|
||||
CreatedTime *TimeInterval[time.Time] `json:"createdTime,omitempty"`
|
||||
ScheduledDate *TimeInterval[Date] `json:"scheduledDate,omitempty"`
|
||||
IDs []int `json:"ids"`
|
||||
ScopeIDs []int `json:"scopeIds,omitempty"`
|
||||
ProjectIDs []int `json:"projectIds,omitempty"`
|
||||
RequirementIDs []int `json:"requirementIds,omitempty"`
|
||||
StatIDs []int `json:"statIds,omitempty"`
|
||||
Loose bool `json:"loose,omitempty"`
|
||||
}
|
||||
|
||||
+5
-4
@@ -13,11 +13,12 @@ type SprintUpdate struct {
|
||||
AggregateRequired *int `json:"aggregateRequired"`
|
||||
}
|
||||
|
||||
// SprintKind decides the composition of stat bars (SB) and what objects are included in the result (R)
|
||||
type SprintKind int
|
||||
|
||||
const (
|
||||
SprintKindItems SprintKind = iota
|
||||
SprintKindRequirements
|
||||
SprintKindStats
|
||||
SprintKindScope
|
||||
SprintKindItems SprintKind = iota // SB: items' total required, R: items
|
||||
SprintKindRequirements // SB: requirements' total required, R: requirements
|
||||
SprintKindStats // SB: parts' total required, R: items(AcquiredDate!=nil), stats
|
||||
SprintKindScope // SB: no required, R: items(AcquiredDate!=nil)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
@@ -13,6 +14,46 @@ type statusError interface {
|
||||
HttpStatus() (int, string, interface{})
|
||||
}
|
||||
|
||||
func processorHandler[I any, O any](key, idKey string, callback func(ctx context.Context, id int, input I) (O, error)) gin.HandlerFunc {
|
||||
return handler(key, func(c *gin.Context) (interface{}, error) {
|
||||
input := new(I)
|
||||
err := c.BindJSON(input)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "body",
|
||||
Field: "*",
|
||||
Problem: "Bad JSON input:" + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
id := 0
|
||||
if idKey != "" {
|
||||
id, err = reqInt(c, idKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return callback(c.Request.Context(), id, *input)
|
||||
})
|
||||
}
|
||||
|
||||
func getterHandler[O any](key, idKey string, callback func(ctx context.Context, id int) (O, error)) gin.HandlerFunc {
|
||||
return handler(key, func(c *gin.Context) (interface{}, error) {
|
||||
id := 0
|
||||
if idKey != "" {
|
||||
parsed, err := reqInt(c, idKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id = parsed
|
||||
}
|
||||
|
||||
return callback(c.Request.Context(), id)
|
||||
})
|
||||
}
|
||||
|
||||
func handler(key string, callback func(c *gin.Context) (interface{}, error)) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
res, err := callback(c)
|
||||
|
||||
@@ -38,7 +38,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
||||
}
|
||||
}
|
||||
|
||||
filter.ScheduledDate = &models.Between[models.Date]{Min: min, Max: max}
|
||||
filter.ScheduledDate = &models.TimeInterval[models.Date]{Min: min, Max: max}
|
||||
}
|
||||
|
||||
queryCreatedMin := c.Query("createdMin")
|
||||
@@ -61,7 +61,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
||||
}
|
||||
}
|
||||
|
||||
filter.CreatedTime = &models.Between[time.Time]{Min: min, Max: max}
|
||||
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
}
|
||||
|
||||
queryAcquiredMin := c.Query("acquiredMin")
|
||||
@@ -84,7 +84,7 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
||||
}
|
||||
}
|
||||
|
||||
filter.AcquiredTime = &models.Between[time.Time]{Min: min, Max: max}
|
||||
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
}
|
||||
|
||||
anyDateMin := c.Query("anyDateMin")
|
||||
@@ -110,9 +110,9 @@ func Items(g *gin.RouterGroup, items *items.Service) {
|
||||
minY, minM, minD := min.Date()
|
||||
maxY, maxM, maxD := max.Date()
|
||||
|
||||
filter.AcquiredTime = &models.Between[time.Time]{Min: min, Max: max}
|
||||
filter.CreatedTime = &models.Between[time.Time]{Min: min, Max: max}
|
||||
filter.ScheduledDate = &models.Between[models.Date]{
|
||||
filter.AcquiredTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
filter.CreatedTime = &models.TimeInterval[time.Time]{Min: min, Max: max}
|
||||
filter.ScheduledDate = &models.TimeInterval[models.Date]{
|
||||
Min: models.Date{minY, int(minM), minD},
|
||||
Max: models.Date{maxY, int(maxM), maxD},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/sprints"
|
||||
"github.com/gin-gonic/gin"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
|
||||
g.GET("", handler("sprints", func(c *gin.Context) (interface{}, error) {
|
||||
from := time.Now().Add(-time.Hour * 24 * 7)
|
||||
if fromQuery := c.Query("from"); fromQuery != "" {
|
||||
parsed, err := time.Parse(time.RFC3339Nano, fromQuery)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "from",
|
||||
Problem: "Incorrect date format: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
from = parsed
|
||||
}
|
||||
|
||||
to := time.Now().Add(time.Hour * 24 * 7)
|
||||
if toQuery := c.Query("to"); toQuery != "" {
|
||||
parsed, err := time.Parse(time.RFC3339Nano, toQuery)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Query",
|
||||
Field: "to",
|
||||
Problem: "Incorrect date format: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
to = parsed
|
||||
}
|
||||
|
||||
return sprintsService.ListScopedBetween(c.Request.Context(), from, to)
|
||||
}))
|
||||
|
||||
g.GET("/:sprint_id", getterHandler("sprint", "sprint_id", func(ctx context.Context, id int) (*sprints.Result, error) {
|
||||
return sprintsService.Find(ctx, id)
|
||||
}))
|
||||
}
|
||||
+9
-1
@@ -11,6 +11,7 @@ import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/sprints"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/stats"
|
||||
"time"
|
||||
|
||||
@@ -50,6 +51,13 @@ func (db *Database) Items() items.Repository {
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) Sprints() sprints.Repository {
|
||||
return &sprintRepository{
|
||||
db: db.db,
|
||||
q: db.q,
|
||||
}
|
||||
}
|
||||
|
||||
func Connect(host string, port int, username, password, database string) (*Database, error) {
|
||||
db, err := sql.Open("mysql", fmt.Sprintf(
|
||||
"%s:%s@(%s:%d)/%s?parseTime=true", username, password, host, port, database,
|
||||
@@ -124,4 +132,4 @@ func sqlJsonPtr(ptr interface{}) sqltypes.NullRawMessage {
|
||||
} else {
|
||||
return sqltypes.NullRawMessage{Valid: false}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ func (r *itemRepository) Find(ctx context.Context, scopeID, itemID int) (*entiti
|
||||
|
||||
func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([]entities.Item, error) {
|
||||
// Blank arrays are not the same as nulls
|
||||
if filter.IDs != nil && len(filter.IDs) == 0 {
|
||||
return []entities.Item{}, nil
|
||||
}
|
||||
if filter.ScopeIDs != nil && len(filter.ScopeIDs) == 0 {
|
||||
return []entities.Item{}, nil
|
||||
}
|
||||
@@ -79,6 +82,9 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
|
||||
sq = sq.Where(dateOr)
|
||||
}
|
||||
|
||||
if len(filter.IDs) > 0 {
|
||||
sq = sq.Where(squirrel.Eq{"i.id": filter.IDs})
|
||||
}
|
||||
if len(filter.ScopeIDs) > 0 {
|
||||
sq = sq.Where(squirrel.Eq{"i.scope_id": filter.ScopeIDs})
|
||||
}
|
||||
@@ -113,6 +119,7 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := make(map[int]bool, 32)
|
||||
res := make([]entities.Item, 0, 32)
|
||||
for rows.Next() {
|
||||
item := entities.Item{}
|
||||
@@ -140,6 +147,10 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
|
||||
|
||||
return nil, err
|
||||
}
|
||||
if seen[item.ID] {
|
||||
continue
|
||||
}
|
||||
seen[item.ID] = true
|
||||
|
||||
item.ProjectRequirementID = intPtr(projectRequirementId)
|
||||
item.ProjectID = intPtr(projectId)
|
||||
|
||||
@@ -93,6 +93,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
if q.deleteSprintStmt, err = db.PrepareContext(ctx, deleteSprint); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteSprint: %w", err)
|
||||
}
|
||||
if q.deleteSprintPartStmt, err = db.PrepareContext(ctx, deleteSprintPart); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteSprintPart: %w", err)
|
||||
}
|
||||
if q.deleteStatStmt, err = db.PrepareContext(ctx, deleteStat); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query DeleteStat: %w", err)
|
||||
}
|
||||
@@ -183,6 +186,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
if q.listScopesByUserStmt, err = db.PrepareContext(ctx, listScopesByUser); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListScopesByUser: %w", err)
|
||||
}
|
||||
if q.listSprintPartsStmt, err = db.PrepareContext(ctx, listSprintParts); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListSprintParts: %w", err)
|
||||
}
|
||||
if q.listSprintsAtStmt, err = db.PrepareContext(ctx, listSprintsAt); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListSprintsAt: %w", err)
|
||||
}
|
||||
@@ -201,6 +207,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
||||
if q.replaceScopeMemberStmt, err = db.PrepareContext(ctx, replaceScopeMember); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ReplaceScopeMember: %w", err)
|
||||
}
|
||||
if q.replaceSprintPartStmt, err = db.PrepareContext(ctx, replaceSprintPart); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ReplaceSprintPart: %w", err)
|
||||
}
|
||||
if q.updateItemStmt, err = db.PrepareContext(ctx, updateItem); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query UpdateItem: %w", err)
|
||||
}
|
||||
@@ -339,6 +348,11 @@ func (q *Queries) Close() error {
|
||||
err = fmt.Errorf("error closing deleteSprintStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.deleteSprintPartStmt != nil {
|
||||
if cerr := q.deleteSprintPartStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteSprintPartStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.deleteStatStmt != nil {
|
||||
if cerr := q.deleteStatStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing deleteStatStmt: %w", cerr)
|
||||
@@ -489,6 +503,11 @@ func (q *Queries) Close() error {
|
||||
err = fmt.Errorf("error closing listScopesByUserStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listSprintPartsStmt != nil {
|
||||
if cerr := q.listSprintPartsStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listSprintPartsStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listSprintsAtStmt != nil {
|
||||
if cerr := q.listSprintsAtStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listSprintsAtStmt: %w", cerr)
|
||||
@@ -519,6 +538,11 @@ func (q *Queries) Close() error {
|
||||
err = fmt.Errorf("error closing replaceScopeMemberStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.replaceSprintPartStmt != nil {
|
||||
if cerr := q.replaceSprintPartStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing replaceSprintPartStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.updateItemStmt != nil {
|
||||
if cerr := q.updateItemStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing updateItemStmt: %w", cerr)
|
||||
@@ -611,6 +635,7 @@ type Queries struct {
|
||||
deleteScopeStmt *sql.Stmt
|
||||
deleteScopeMemberStmt *sql.Stmt
|
||||
deleteSprintStmt *sql.Stmt
|
||||
deleteSprintPartStmt *sql.Stmt
|
||||
deleteStatStmt *sql.Stmt
|
||||
getItemStmt *sql.Stmt
|
||||
getItemStatProgressBetweenStmt *sql.Stmt
|
||||
@@ -641,12 +666,14 @@ type Queries struct {
|
||||
listScopeMembersMultiStmt *sql.Stmt
|
||||
listScopesStmt *sql.Stmt
|
||||
listScopesByUserStmt *sql.Stmt
|
||||
listSprintPartsStmt *sql.Stmt
|
||||
listSprintsAtStmt *sql.Stmt
|
||||
listSprintsBetweenStmt *sql.Stmt
|
||||
listStatsStmt *sql.Stmt
|
||||
replaceItemStatProgressStmt *sql.Stmt
|
||||
replaceProjectRequirementStatStmt *sql.Stmt
|
||||
replaceScopeMemberStmt *sql.Stmt
|
||||
replaceSprintPartStmt *sql.Stmt
|
||||
updateItemStmt *sql.Stmt
|
||||
updateProjectStmt *sql.Stmt
|
||||
updateProjectRequirementStmt *sql.Stmt
|
||||
@@ -682,6 +709,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
deleteScopeStmt: q.deleteScopeStmt,
|
||||
deleteScopeMemberStmt: q.deleteScopeMemberStmt,
|
||||
deleteSprintStmt: q.deleteSprintStmt,
|
||||
deleteSprintPartStmt: q.deleteSprintPartStmt,
|
||||
deleteStatStmt: q.deleteStatStmt,
|
||||
getItemStmt: q.getItemStmt,
|
||||
getItemStatProgressBetweenStmt: q.getItemStatProgressBetweenStmt,
|
||||
@@ -712,12 +740,14 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||
listScopeMembersMultiStmt: q.listScopeMembersMultiStmt,
|
||||
listScopesStmt: q.listScopesStmt,
|
||||
listScopesByUserStmt: q.listScopesByUserStmt,
|
||||
listSprintPartsStmt: q.listSprintPartsStmt,
|
||||
listSprintsAtStmt: q.listSprintsAtStmt,
|
||||
listSprintsBetweenStmt: q.listSprintsBetweenStmt,
|
||||
listStatsStmt: q.listStatsStmt,
|
||||
replaceItemStatProgressStmt: q.replaceItemStatProgressStmt,
|
||||
replaceProjectRequirementStatStmt: q.replaceProjectRequirementStatStmt,
|
||||
replaceScopeMemberStmt: q.replaceScopeMemberStmt,
|
||||
replaceSprintPartStmt: q.replaceSprintPartStmt,
|
||||
updateItemStmt: q.updateItemStmt,
|
||||
updateProjectStmt: q.updateProjectStmt,
|
||||
updateProjectRequirementStmt: q.updateProjectRequirementStmt,
|
||||
|
||||
@@ -29,6 +29,20 @@ func (q *Queries) DeleteSprint(ctx context.Context, id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteSprintPart = `-- name: DeleteSprintPart :exec
|
||||
DELETE FROM sprint_part WHERE sprint_id = ? AND object_id = ?
|
||||
`
|
||||
|
||||
type DeleteSprintPartParams struct {
|
||||
SprintID int
|
||||
ObjectID int
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteSprintPart(ctx context.Context, arg DeleteSprintPartParams) error {
|
||||
_, err := q.exec(ctx, q.deleteSprintPartStmt, deleteSprintPart, arg.SprintID, arg.ObjectID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getSprint = `-- name: GetSprint :one
|
||||
SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint WHERE id = ? AND scope_id = ?
|
||||
`
|
||||
@@ -95,6 +109,33 @@ func (q *Queries) InsertSprint(ctx context.Context, arg InsertSprintParams) (sql
|
||||
)
|
||||
}
|
||||
|
||||
const listSprintParts = `-- name: ListSprintParts :many
|
||||
SELECT sprint_id, object_id, required FROM sprint_part WHERE sprint_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) ListSprintParts(ctx context.Context, sprintID int) ([]SprintPart, error) {
|
||||
rows, err := q.query(ctx, q.listSprintPartsStmt, listSprintParts, sprintID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SprintPart{}
|
||||
for rows.Next() {
|
||||
var i SprintPart
|
||||
if err := rows.Scan(&i.SprintID, &i.ObjectID, &i.Required); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listSprintsAt = `-- name: ListSprintsAt :many
|
||||
SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint WHERE scope_id = ? AND from_time <= ? AND to_time > ?
|
||||
`
|
||||
@@ -206,6 +247,22 @@ func (q *Queries) ListSprintsBetween(ctx context.Context, arg ListSprintsBetween
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const replaceSprintPart = `-- name: ReplaceSprintPart :exec
|
||||
REPLACE INTO sprint_part (sprint_id, object_id, required)
|
||||
VALUES (?, ?, ?)
|
||||
`
|
||||
|
||||
type ReplaceSprintPartParams struct {
|
||||
SprintID int
|
||||
ObjectID int
|
||||
Required int
|
||||
}
|
||||
|
||||
func (q *Queries) ReplaceSprintPart(ctx context.Context, arg ReplaceSprintPartParams) error {
|
||||
_, err := q.exec(ctx, q.replaceSprintPartStmt, replaceSprintPart, arg.SprintID, arg.ObjectID, arg.Required)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateSprint = `-- name: UpdateSprint :exec
|
||||
UPDATE sprint
|
||||
SET name = ?,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/mysql/mysqlcore"
|
||||
"github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
type projectRepository struct {
|
||||
@@ -133,6 +134,70 @@ func (r *projectRepository) Delete(ctx context.Context, project entities.Project
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error) {
|
||||
if len(requirementIDs) == 0 {
|
||||
return []entities.Requirement{}, []entities.RequirementStat{}, nil
|
||||
}
|
||||
|
||||
query, args, err := squirrel.Select("id, scope_id, project_id, name, status, description").
|
||||
From("project_requirement").
|
||||
Where(squirrel.Eq{"scope_id": scopeID}).
|
||||
Where(squirrel.Eq{"id": requirementIDs}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ids := make([]int, 0, 16)
|
||||
requirements := make([]entities.Requirement, 0, len(requirementIDs))
|
||||
for rows.Next() {
|
||||
requirement := entities.Requirement{}
|
||||
if err := rows.Scan(
|
||||
&requirement.ID,
|
||||
&requirement.ScopeID,
|
||||
&requirement.ProjectID,
|
||||
&requirement.Name,
|
||||
&requirement.Status,
|
||||
&requirement.Description,
|
||||
); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
requirements = append(requirements, requirement)
|
||||
ids = append(ids, requirement.ID)
|
||||
}
|
||||
|
||||
query, args, err = squirrel.Select("project_requirement_id, stat_id, required").
|
||||
From("project_requirement_stat").
|
||||
Where(squirrel.Eq{"project_requirement_id": requirementIDs}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
rows, err = r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
stats := make([]entities.RequirementStat, 0, len(requirementIDs))
|
||||
for rows.Next() {
|
||||
stat := entities.RequirementStat{}
|
||||
if err := rows.Scan(
|
||||
&stat.RequirementID,
|
||||
&stat.StatID,
|
||||
&stat.Required,
|
||||
); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
stats = append(stats, stat)
|
||||
}
|
||||
|
||||
return requirements, stats, nil
|
||||
}
|
||||
|
||||
func (r *projectRepository) ListRequirements(ctx context.Context, projectID int) ([]entities.Requirement, []entities.RequirementStat, error) {
|
||||
reqRows, err := r.q.ListProjectRequirements(ctx, projectID)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
|
||||
@@ -12,6 +12,7 @@ AND (
|
||||
OR (from_time < ? AND to_time >= ?)
|
||||
);
|
||||
|
||||
|
||||
-- name: GetSprint :one
|
||||
SELECT * FROM sprint WHERE id = ? AND scope_id = ?;
|
||||
|
||||
@@ -38,3 +39,13 @@ DELETE FROM sprint WHERE id = ?;
|
||||
|
||||
-- name: DeleteAllScopeSprints :exec
|
||||
DELETE FROM sprint WHERE scope_id = ?;
|
||||
|
||||
-- name: ListSprintParts :many
|
||||
SELECT * FROM sprint_part WHERE sprint_id = ?;
|
||||
|
||||
-- name: ReplaceSprintPart :exec
|
||||
REPLACE INTO sprint_part (sprint_id, object_id, required)
|
||||
VALUES (?, ?, ?);
|
||||
|
||||
-- name: DeleteSprintPart :exec
|
||||
DELETE FROM sprint_part WHERE sprint_id = ? AND object_id = ?;
|
||||
|
||||
+105
-6
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"git.aiterp.net/stufflog3/stufflog3/ports/mysql/mysqlcore"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -111,16 +113,113 @@ func (r *sprintRepository) ListBetween(ctx context.Context, scopeID int, from, t
|
||||
}
|
||||
|
||||
func (r *sprintRepository) Create(ctx context.Context, sprint entities.Sprint) (*entities.Sprint, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
res, err := r.q.InsertSprint(ctx, mysqlcore.InsertSprintParams{
|
||||
ScopeID: sprint.ScopeID,
|
||||
Name: sprint.Name,
|
||||
Description: sprint.Description,
|
||||
Kind: int(sprint.Kind),
|
||||
FromTime: sprint.FromTime,
|
||||
ToTime: sprint.ToTime,
|
||||
IsTimed: sprint.IsTimed,
|
||||
IsCoarse: sprint.IsCoarse,
|
||||
AggregateName: sprint.AggregateName,
|
||||
AggregateRequired: sprint.AggregateRequired,
|
||||
IsUnweighted: sprint.IsUnweighted,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sprint.ID = int(id)
|
||||
return &sprint, nil
|
||||
}
|
||||
|
||||
func (r *sprintRepository) Update(ctx context.Context, sprint entities.Sprint, update models.SprintUpdate) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
sprint.ApplyUpdate(update)
|
||||
return r.q.UpdateSprint(ctx, mysqlcore.UpdateSprintParams{
|
||||
Name: sprint.Name,
|
||||
Description: sprint.Description,
|
||||
FromTime: sprint.FromTime,
|
||||
ToTime: sprint.ToTime,
|
||||
IsTimed: sprint.IsTimed,
|
||||
IsCoarse: sprint.IsCoarse,
|
||||
AggregateName: sprint.AggregateName,
|
||||
AggregateRequired: sprint.AggregateRequired,
|
||||
|
||||
ID: sprint.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *sprintRepository) Delete(ctx context.Context, sprint entities.Sprint) error {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
return r.q.DeleteSprint(ctx, sprint.ID)
|
||||
}
|
||||
|
||||
func (r *sprintRepository) ListParts(ctx context.Context, sprints ...entities.Sprint) ([]entities.SprintPart, error) {
|
||||
if len(sprints) == 0 {
|
||||
return []entities.SprintPart{}, nil
|
||||
} else if len(sprints) == 1 {
|
||||
rows, err := r.q.ListSprintParts(ctx, sprints[0].ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return genutils.Map(rows, func(row mysqlcore.SprintPart) entities.SprintPart {
|
||||
return entities.SprintPart{
|
||||
SprintID: row.SprintID,
|
||||
PartID: row.ObjectID,
|
||||
Required: row.Required,
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
ids := make([]int, 0, len(sprints))
|
||||
for _, sprint := range sprints {
|
||||
ids = append(ids, sprint.ID)
|
||||
}
|
||||
|
||||
query, args, err := squirrel.Select("sprint_id, object_id, required").
|
||||
From("sprint_part").
|
||||
Where(squirrel.Eq{"sprint_id": ids}).
|
||||
ToSql()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]entities.SprintPart, 0, 16)
|
||||
for rows.Next() {
|
||||
part := entities.SprintPart{}
|
||||
err = rows.Scan(&part.SprintID, &part.PartID, &part.Required)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = append(res, part)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *sprintRepository) UpdatePart(ctx context.Context, part entities.SprintPart) error {
|
||||
return r.q.ReplaceSprintPart(ctx, mysqlcore.ReplaceSprintPartParams{
|
||||
SprintID: part.SprintID,
|
||||
ObjectID: part.PartID,
|
||||
Required: part.Required,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *sprintRepository) DeletePart(ctx context.Context, part entities.SprintPart) error {
|
||||
return r.q.DeleteSprintPart(ctx, mysqlcore.DeleteSprintPartParams{
|
||||
SprintID: part.SprintID,
|
||||
ObjectID: part.PartID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,16 @@ type Result struct {
|
||||
Stats []ResultStat `json:"stats"`
|
||||
}
|
||||
|
||||
func (r *Result) Stat(statID int) *ResultStat {
|
||||
for _, stat := range r.Stats {
|
||||
if stat.ID == statID {
|
||||
return &stat
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResultStat struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -12,6 +12,7 @@ type Repository interface {
|
||||
Create(ctx context.Context, project entities.Project) (*entities.Project, error)
|
||||
Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error
|
||||
Delete(ctx context.Context, project entities.Project) error
|
||||
FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error)
|
||||
ListRequirements(ctx context.Context, projectID int) ([]entities.Requirement, []entities.RequirementStat, error)
|
||||
CreateRequirement(ctx context.Context, requirement entities.Requirement) (*entities.Requirement, error)
|
||||
UpdateRequirement(ctx context.Context, requirement entities.Requirement, update models.RequirementUpdate) error
|
||||
|
||||
+59
-44
@@ -39,6 +39,16 @@ type RequirementResult struct {
|
||||
Items []items.Result `json:"items"`
|
||||
}
|
||||
|
||||
func (r *RequirementResult) Stat(id int) *RequirementResultStat {
|
||||
for _, stat := range r.Stats {
|
||||
if stat.ID == id {
|
||||
return &stat
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type RequirementResultStat struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -65,53 +75,58 @@ func generateResult(
|
||||
continue
|
||||
}
|
||||
|
||||
resReq := RequirementResult{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
Stats: make([]RequirementResultStat, 0, 8),
|
||||
Items: make([]items.Result, 0, 8),
|
||||
}
|
||||
statIndices := make(map[int]int)
|
||||
for _, reqStat := range requirementStats {
|
||||
if reqStat.RequirementID != req.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
resStat := RequirementResultStat{
|
||||
ID: reqStat.StatID,
|
||||
Required: reqStat.Required,
|
||||
}
|
||||
for _, stat := range stats {
|
||||
if stat.ID == resStat.ID {
|
||||
resStat.Name = stat.Name
|
||||
resStat.Weight = stat.Weight
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resReq.Stats = append(resReq.Stats, resStat)
|
||||
statIndices[reqStat.StatID] = len(resReq.Stats) - 1
|
||||
}
|
||||
|
||||
for _, item := range projectItems {
|
||||
if item.ProjectRequirementID == nil || *item.ProjectRequirementID != req.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, stat := range item.Stats {
|
||||
if statIndex, ok := statIndices[stat.ID]; ok {
|
||||
resReq.Stats[statIndex].Acquired += stat.Acquired
|
||||
resReq.Stats[statIndex].Planned += stat.Required
|
||||
}
|
||||
}
|
||||
|
||||
resReq.Items = append(resReq.Items, item)
|
||||
}
|
||||
resReq := generateRequirementResult(req, requirementStats, stats, projectItems)
|
||||
|
||||
res.Requirements = append(res.Requirements, resReq)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func generateRequirementResult(req entities.Requirement, requirementStats []entities.RequirementStat, stats []entities.Stat, projectItems []items.Result) RequirementResult {
|
||||
resReq := RequirementResult{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
Stats: make([]RequirementResultStat, 0, 8),
|
||||
Items: make([]items.Result, 0, 8),
|
||||
}
|
||||
statIndices := make(map[int]int)
|
||||
for _, reqStat := range requirementStats {
|
||||
if reqStat.RequirementID != req.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
resStat := RequirementResultStat{
|
||||
ID: reqStat.StatID,
|
||||
Required: reqStat.Required,
|
||||
}
|
||||
for _, stat := range stats {
|
||||
if stat.ID == resStat.ID {
|
||||
resStat.Name = stat.Name
|
||||
resStat.Weight = stat.Weight
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resReq.Stats = append(resReq.Stats, resStat)
|
||||
statIndices[reqStat.StatID] = len(resReq.Stats) - 1
|
||||
}
|
||||
|
||||
for _, item := range projectItems {
|
||||
if item.ProjectRequirementID == nil || *item.ProjectRequirementID != req.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, stat := range item.Stats {
|
||||
if statIndex, ok := statIndices[stat.ID]; ok {
|
||||
resReq.Stats[statIndex].Acquired += stat.Acquired
|
||||
resReq.Stats[statIndex].Planned += stat.Required
|
||||
}
|
||||
}
|
||||
|
||||
resReq.Items = append(resReq.Items, item)
|
||||
}
|
||||
return resReq
|
||||
}
|
||||
|
||||
@@ -65,3 +65,36 @@ func (s *Service) List(ctx context.Context) ([]Entry, error) {
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (s *Service) FetchRequirements(ctx context.Context, ids ...int) ([]RequirementResult, error) {
|
||||
sc := s.Scopes.Context(ctx)
|
||||
|
||||
requirements, requirementStats, err := s.Repository.FetchRequirements(ctx, sc.ID, ids...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
||||
RequirementIDs: ids,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats, err := sc.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]RequirementResult, 0, len(requirements))
|
||||
for _, req := range requirements {
|
||||
results = append(results, generateRequirementResult(
|
||||
req,
|
||||
requirementStats,
|
||||
stats,
|
||||
items,
|
||||
))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -14,4 +14,7 @@ type Repository interface {
|
||||
Create(ctx context.Context, sprint entities.Sprint) (*entities.Sprint, error)
|
||||
Update(ctx context.Context, sprint entities.Sprint, update models.SprintUpdate) error
|
||||
Delete(ctx context.Context, sprint entities.Sprint) error
|
||||
ListParts(ctx context.Context, sprints ...entities.Sprint) ([]entities.SprintPart, error)
|
||||
UpdatePart(ctx context.Context, part entities.SprintPart) error
|
||||
DeletePart(ctx context.Context, part entities.SprintPart) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package sprints
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
entities.Sprint
|
||||
|
||||
AggregateAcquired int `json:"aggregateAcquired"`
|
||||
AggregateTotal int `json:"aggregateTotal"`
|
||||
|
||||
Items []items.Result `json:"items,omitempty"`
|
||||
Stats []entities.Stat `json:"stats,omitempty"`
|
||||
Requirements []projects.RequirementResult `json:"requirements,omitempty"`
|
||||
Progress []ResultProgress `json:"progress,omitempty"`
|
||||
}
|
||||
|
||||
type ResultProgress struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Weight float64 `json:"weight"`
|
||||
Acquired int `json:"acquired"`
|
||||
Required *int `json:"required,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package sprints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/projects"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Scopes *scopes.Service
|
||||
Items *items.Service
|
||||
Projects *projects.Service
|
||||
|
||||
Repository Repository
|
||||
}
|
||||
|
||||
func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
|
||||
sprint, err := s.Repository.Find(ctx, s.Scopes.Context(ctx).ID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parts, err := s.Repository.ListParts(ctx, *sprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.fill(ctx, *sprint, parts)
|
||||
}
|
||||
|
||||
func (s *Service) ListScopedBetween(ctx context.Context, from, to time.Time) ([]Result, error) {
|
||||
sprints, err := s.Repository.ListBetween(ctx, s.Scopes.Context(ctx).ID, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parts, err := s.Repository.ListParts(ctx, sprints...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
results := make([]Result, len(sprints))
|
||||
for i := range sprints {
|
||||
iCopy := i
|
||||
|
||||
eg.Go(func() error {
|
||||
res, err := s.fill(ctx, sprints[iCopy], parts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
results[iCopy] = *res
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []entities.SprintPart) (*Result, error) {
|
||||
res := Result{Sprint: sprint}
|
||||
sc := s.Scopes.Context(ctx)
|
||||
partIDs := make([]int, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if part.SprintID == sprint.ID {
|
||||
partIDs = append(partIDs, part.PartID)
|
||||
}
|
||||
}
|
||||
|
||||
allStats, err := sc.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
progressMap := make(map[int]int, len(allStats))
|
||||
res.Progress = make([]ResultProgress, 0, len(allStats))
|
||||
|
||||
switch res.Kind {
|
||||
case models.SprintKindItems:
|
||||
pickedItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
||||
IDs: partIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Items = pickedItems
|
||||
|
||||
for _, stat := range allStats {
|
||||
totalRequired := 0
|
||||
for _, item := range res.Items {
|
||||
if itemStat := item.Stat(stat.ID); itemStat != nil {
|
||||
totalRequired += itemStat.Required
|
||||
}
|
||||
}
|
||||
if totalRequired == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
progressMap[stat.ID] = len(res.Progress)
|
||||
res.Progress = append(res.Progress, ResultProgress{
|
||||
ID: stat.ID,
|
||||
Name: stat.Name,
|
||||
Weight: stat.Weight,
|
||||
Required: &totalRequired,
|
||||
})
|
||||
}
|
||||
case models.SprintKindRequirements:
|
||||
includedRequirements, err := s.Projects.FetchRequirements(ctx, partIDs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, req := range includedRequirements {
|
||||
res.Items = append(res.Items, req.Items...)
|
||||
}
|
||||
|
||||
for _, stat := range allStats {
|
||||
totalRequired := 0
|
||||
for _, req := range res.Requirements {
|
||||
if reqStat := req.Stat(stat.ID); reqStat != nil {
|
||||
totalRequired += reqStat.Required
|
||||
}
|
||||
}
|
||||
if totalRequired == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
progressMap[stat.ID] = len(res.Progress)
|
||||
res.Progress = append(res.Progress, ResultProgress{
|
||||
ID: stat.ID,
|
||||
Name: stat.Name,
|
||||
Weight: stat.Weight,
|
||||
Required: &totalRequired,
|
||||
})
|
||||
}
|
||||
|
||||
res.Requirements = includedRequirements
|
||||
case models.SprintKindStats:
|
||||
includedStats := make([]entities.Stat, 0, len(partIDs))
|
||||
for _, stat := range allStats {
|
||||
for _, id := range partIDs {
|
||||
if stat.ID == id {
|
||||
includedStats = append(includedStats, stat)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progressMap = make(map[int]int, len(includedStats))
|
||||
res.Progress = make([]ResultProgress, 0, len(includedStats))
|
||||
for _, stat := range includedStats {
|
||||
required := 0
|
||||
for _, part := range parts {
|
||||
if part.PartID == stat.ID {
|
||||
required = part.Required
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
progressMap[stat.ID] = len(res.Progress)
|
||||
res.Progress = append(res.Progress, ResultProgress{
|
||||
ID: stat.ID,
|
||||
Name: stat.Name,
|
||||
Weight: stat.Weight,
|
||||
Required: &required,
|
||||
})
|
||||
}
|
||||
|
||||
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
||||
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
|
||||
StatIDs: partIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Items = acquiredItems
|
||||
|
||||
case models.SprintKindScope:
|
||||
acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
||||
AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Items = acquiredItems
|
||||
|
||||
for _, stat := range allStats {
|
||||
progressMap[stat.ID] = len(res.Progress)
|
||||
res.Progress = append(res.Progress, ResultProgress{
|
||||
ID: stat.ID,
|
||||
Name: stat.Name,
|
||||
Weight: stat.Weight,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Measure progress
|
||||
for _, item := range res.Items {
|
||||
if item.AcquiredTime == nil || item.AcquiredTime.Before(sprint.FromTime) || !item.AcquiredTime.Before(sprint.ToTime) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, stat := range item.Stats {
|
||||
if pi, ok := progressMap[stat.ID]; ok {
|
||||
res.Progress[pi].Acquired += stat.Acquired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For requirement sprints, remove items array after enumeration
|
||||
if res.Kind == models.SprintKindRequirements {
|
||||
res.Items = nil
|
||||
}
|
||||
|
||||
// Calculate aggregate values
|
||||
aggregateAcquired := 0.0
|
||||
aggregateTotal := 0.0
|
||||
for _, progress := range res.Progress {
|
||||
if res.IsUnweighted {
|
||||
aggregateAcquired += float64(progress.Acquired)
|
||||
if progress.Required != nil {
|
||||
aggregateTotal += float64(*progress.Required)
|
||||
}
|
||||
} else {
|
||||
aggregateAcquired += float64(progress.Acquired) * progress.Weight
|
||||
if progress.Required != nil {
|
||||
aggregateTotal += float64(*progress.Required) * progress.Weight
|
||||
}
|
||||
}
|
||||
}
|
||||
res.AggregateAcquired = int(math.Round(aggregateAcquired))
|
||||
res.AggregateTotal = int(math.Round(aggregateTotal))
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
Reference in New Issue
Block a user