Browse Source

complete stat and sprint APIs.

master
Gisle Aune 2 years ago
parent
commit
d017095042
  1. 2
      entities/stat.go
  2. 12
      internal/genutils/ptr.go
  3. 19
      internal/genutils/set.go
  4. 17
      models/errors.go
  5. 6
      models/sprint.go
  6. 81
      ports/httpapi/sprints.go
  7. 29
      ports/httpapi/stats.go
  8. 6
      ports/mysql/items.go
  9. 10
      ports/mysql/mysqlcore/db.go
  10. 9
      ports/mysql/mysqlcore/sprint.sql.go
  11. 3
      ports/mysql/queries/sprint.sql
  12. 21
      ports/mysql/sprint.go
  13. 3
      usecases/projects/result.go
  14. 19
      usecases/projects/service.go
  15. 2
      usecases/sprints/repository.go
  16. 7
      usecases/sprints/result.go
  17. 256
      usecases/sprints/service.go
  18. 73
      usecases/stats/service.go

2
entities/stat.go

@ -21,6 +21,8 @@ func (stat *Stat) Update(update models.StatUpdate) {
if update.Description != nil {
stat.Description = *update.Description
}
stat.AllowedAmounts = append(make([]models.StatAllowedAmount, 0, 8), stat.AllowedAmounts...)
for _, amount := range update.AllowedAmounts {
found := false
for i, existing := range stat.AllowedAmounts {

12
internal/genutils/ptr.go

@ -0,0 +1,12 @@
package genutils
func UnPtr[T any](v *T) T {
return *v
}
func Ptr[T any](v T) *T {
v2 := new(T)
*v2 = v
return v2
}

19
internal/genutils/set.go

@ -20,6 +20,25 @@ func (set *Set[T]) Add(values ...T) {
}
}
func (set *Set[T]) Del(values ...T) {
if set.m == nil {
return
}
for _, value := range values {
if set.m[value] {
delete(set.m, value)
for i, value2 := range set.a {
if value2 == value {
// swap delete
set.a[i] = set.a[len(set.a)-1]
set.a = set.a[:len(set.a)-1]
}
}
}
}
}
func (set *Set[T]) Has(v T) bool {
return set.m[v]
}

17
models/errors.go

@ -2,6 +2,10 @@ package models
import "fmt"
func NotFoundIDError(obj string, id int) NotFoundError {
return NotFoundError(fmt.Sprint(obj, " ", id))
}
type NotFoundError string
func (e NotFoundError) Error() string {
@ -32,6 +36,19 @@ func (e ForbiddenError) HttpStatus() (int, string, interface{}) {
return 401, string(e), nil
}
type BadJSONError struct {
Name string
Raw error
}
func (e BadJSONError) Error() string {
return fmt.Sprintf("JSON Error (%s): %s", e.Name, e.Raw)
}
func (e BadJSONError) HttpStatus() (int, string, interface{}) {
return 401, fmt.Sprintf("Invalid JSON for %s", e.Name), e
}
type BadInputError struct {
Object string `json:"object"`
Field string `json:"field,omitempty"`

6
models/sprint.go

@ -16,9 +16,15 @@ type SprintUpdate struct {
// SprintKind decides the composition of stat bars (SB) and what objects are included in the result (R)
type SprintKind int
func (sk SprintKind) Valid() bool {
return sk >= 0 && sk < MaxSprintKind
}
const (
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)
MaxSprintKind
)

81
ports/httpapi/sprints.go

@ -2,6 +2,7 @@ package httpapi
import (
"context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/sprints"
"github.com/gin-gonic/gin"
@ -44,4 +45,84 @@ func Sprints(g *gin.RouterGroup, sprintsService *sprints.Service) {
g.GET("/:sprint_id", getterHandler("sprint", "sprint_id", func(ctx context.Context, id int) (*sprints.Result, error) {
return sprintsService.Find(ctx, id)
}))
g.POST("", handler("sprint", func(c *gin.Context) (interface{}, error) {
input := struct {
entities.Sprint
Parts []entities.SprintPart `json:"parts"`
}{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadJSONError{Name: "SprintInput", Raw: err}
}
return sprintsService.Create(c.Request.Context(), input.Sprint, input.Parts)
}))
g.PUT("/:sprint_id", handler("sprint", func(c *gin.Context) (interface{}, error) {
input := models.SprintUpdate{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadJSONError{Name: "SprintUpdate", Raw: err}
}
id, err := reqInt(c, "sprint_id")
if err != nil {
return nil, err
}
return sprintsService.Update(c.Request.Context(), id, input)
}))
g.PUT("/:sprint_id/parts/:part_id", handler("sprint", func(c *gin.Context) (interface{}, error) {
input := entities.SprintPart{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadJSONError{Name: "SprintPartInput", Raw: err}
}
sprintID, err := reqInt(c, "sprint_id")
if err != nil {
return nil, err
}
partID, err := reqInt(c, "part_id")
if err != nil {
return nil, err
}
input.SprintID = sprintID
input.PartID = partID
if input.Required > 0 {
return sprintsService.AddPart(c.Request.Context(), input)
} else {
return sprintsService.RemovePart(c.Request.Context(), input)
}
}))
g.DELETE("/:sprint_id/parts/:part_id", handler("sprint", func(c *gin.Context) (interface{}, error) {
sprintID, err := reqInt(c, "sprint_id")
if err != nil {
return nil, err
}
partID, err := reqInt(c, "part_id")
if err != nil {
return nil, err
}
return sprintsService.RemovePart(c.Request.Context(), entities.SprintPart{
SprintID: sprintID,
PartID: partID,
Required: 0,
})
}))
g.DELETE("/:sprint_id", handler("sprint", func(c *gin.Context) (interface{}, error) {
sprintID, err := reqInt(c, "sprint_id")
if err != nil {
return nil, err
}
return sprintsService.Delete(c.Request.Context(), sprintID)
}))
}

29
ports/httpapi/stats.go

@ -25,12 +25,33 @@ func Stats(g *gin.RouterGroup, stats *stats.Service) {
input := entities.Stat{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadInputError{
Object: "Scope",
Problem: "Invalid JSON: " + err.Error(),
}
return nil, models.BadJSONError{Name: "StatInput", Raw: err}
}
return stats.Create(c.Request.Context(), input)
}))
g.PUT("/:stat_id", handler("stat", func(c *gin.Context) (interface{}, error) {
input := models.StatUpdate{}
err := c.BindJSON(&input)
if err != nil {
return nil, models.BadJSONError{Name: "StatInput", Raw: err}
}
id, err := reqInt(c, "stat_id")
if err != nil {
return nil, err
}
return stats.Update(c.Request.Context(), id, input)
}))
g.DELETE("/:stat_id", handler("stat", func(c *gin.Context) (interface{}, error) {
id, err := reqInt(c, "stat_id")
if err != nil {
return nil, err
}
return stats.Delete(c.Request.Context(), id)
}))
}

6
ports/mysql/items.go

@ -57,7 +57,7 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
sq := squirrel.Select(
"i.id, i.scope_id, i.project_requirement_id, pr.project_id, i.owner_id, i.name," +
" i.description, i.created_time, i.acquired_time, i.scheduled_date",
).From("item i").RightJoin("project_requirement pr ON pr.id = i.project_requirement_id")
).From("item i").LeftJoin("project_requirement pr ON pr.id = i.project_requirement_id")
dateOr := squirrel.Or{}
if filter.CreatedTime != nil {
@ -95,7 +95,7 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
sq = sq.Where(squirrel.Eq{"pr.project_id": filter.ProjectIDs})
}
if len(filter.StatIDs) > 0 {
sq = sq.RightJoin("item_stat_progress isp ON isp.item_id = i.id")
sq = sq.LeftJoin("item_stat_progress isp ON isp.item_id = i.id")
sq = sq.Where(squirrel.Eq{"isp.stat_id": filter.StatIDs})
}
if filter.OwnerID != nil {
@ -109,7 +109,7 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
if err != nil {
return nil, err
}
rows, err := r.db.QueryContext(ctx, query, params...)
if err != nil {
if err == sql.ErrNoRows {

10
ports/mysql/mysqlcore/db.go

@ -66,6 +66,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.deleteAllScopeStatsStmt, err = db.PrepareContext(ctx, deleteAllScopeStats); err != nil {
return nil, fmt.Errorf("error preparing query DeleteAllScopeStats: %w", err)
}
if q.deleteAllSprintPartsStmt, err = db.PrepareContext(ctx, deleteAllSprintParts); err != nil {
return nil, fmt.Errorf("error preparing query DeleteAllSprintParts: %w", err)
}
if q.deleteItemStmt, err = db.PrepareContext(ctx, deleteItem); err != nil {
return nil, fmt.Errorf("error preparing query DeleteItem: %w", err)
}
@ -303,6 +306,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing deleteAllScopeStatsStmt: %w", cerr)
}
}
if q.deleteAllSprintPartsStmt != nil {
if cerr := q.deleteAllSprintPartsStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteAllSprintPartsStmt: %w", cerr)
}
}
if q.deleteItemStmt != nil {
if cerr := q.deleteItemStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteItemStmt: %w", cerr)
@ -626,6 +634,7 @@ type Queries struct {
deleteAllScopeProjectsStmt *sql.Stmt
deleteAllScopeSprintsStmt *sql.Stmt
deleteAllScopeStatsStmt *sql.Stmt
deleteAllSprintPartsStmt *sql.Stmt
deleteItemStmt *sql.Stmt
deleteItemForRequirementStmt *sql.Stmt
deleteItemStatProgressStmt *sql.Stmt
@ -700,6 +709,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
deleteAllScopeProjectsStmt: q.deleteAllScopeProjectsStmt,
deleteAllScopeSprintsStmt: q.deleteAllScopeSprintsStmt,
deleteAllScopeStatsStmt: q.deleteAllScopeStatsStmt,
deleteAllSprintPartsStmt: q.deleteAllSprintPartsStmt,
deleteItemStmt: q.deleteItemStmt,
deleteItemForRequirementStmt: q.deleteItemForRequirementStmt,
deleteItemStatProgressStmt: q.deleteItemStatProgressStmt,

9
ports/mysql/mysqlcore/sprint.sql.go

@ -20,6 +20,15 @@ func (q *Queries) DeleteAllScopeSprints(ctx context.Context, scopeID int) error
return err
}
const deleteAllSprintParts = `-- name: DeleteAllSprintParts :exec
DELETE FROM sprint_part WHERE sprint_id = ?
`
func (q *Queries) DeleteAllSprintParts(ctx context.Context, sprintID int) error {
_, err := q.exec(ctx, q.deleteAllSprintPartsStmt, deleteAllSprintParts, sprintID)
return err
}
const deleteSprint = `-- name: DeleteSprint :exec
DELETE FROM sprint WHERE id = ?
`

3
ports/mysql/queries/sprint.sql

@ -49,3 +49,6 @@ VALUES (?, ?, ?);
-- name: DeleteSprintPart :exec
DELETE FROM sprint_part WHERE sprint_id = ? AND object_id = ?;
-- name: DeleteAllSprintParts :exec
DELETE FROM sprint_part WHERE sprint_id = ?;

21
ports/mysql/sprint.go

@ -112,7 +112,7 @@ func (r *sprintRepository) ListBetween(ctx context.Context, scopeID int, from, t
return sprints, nil
}
func (r *sprintRepository) Create(ctx context.Context, sprint entities.Sprint) (*entities.Sprint, error) {
func (r *sprintRepository) Insert(ctx context.Context, sprint entities.Sprint) (*entities.Sprint, error) {
res, err := r.q.InsertSprint(ctx, mysqlcore.InsertSprintParams{
ScopeID: sprint.ScopeID,
Name: sprint.Name,
@ -156,7 +156,24 @@ func (r *sprintRepository) Update(ctx context.Context, sprint entities.Sprint, u
}
func (r *sprintRepository) Delete(ctx context.Context, sprint entities.Sprint) error {
return r.q.DeleteSprint(ctx, sprint.ID)
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
q := r.q.WithTx(tx)
err = q.DeleteAllSprintParts(ctx, sprint.ID)
if err != nil {
return err
}
err = q.DeleteSprint(ctx, sprint.ID)
if err != nil {
return err
}
return tx.Commit()
}
func (r *sprintRepository) ListParts(ctx context.Context, sprints ...entities.Sprint) ([]entities.SprintPart, error) {

3
usecases/projects/result.go

@ -148,6 +148,9 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
if item.ProjectRequirementID == nil || *item.ProjectRequirementID != req.ID {
continue
}
if item.AcquiredTime != nil {
continue
}
for _, stat := range item.Stats {
if statIndex, ok := statIndices[stat.ID]; ok {

19
usecases/projects/service.go

@ -3,11 +3,13 @@ package projects
import (
"context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/auth"
"git.aiterp.net/stufflog3/stufflog3/usecases/items"
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"git.aiterp.net/stufflog3/stufflog3/usecases/stats"
"strconv"
"strings"
"time"
)
@ -160,6 +162,23 @@ func (s *Service) Delete(ctx context.Context, id int) (*Result, error) {
return project, nil
}
func (s *Service) FindRequirement(ctx context.Context, id int) (*RequirementResult, error) {
scope := s.Scopes.Context(ctx).Scope
req, reqStats, err := s.Repository.FetchRequirements(ctx, scope.ID, id)
if err != nil {
return nil, err
}
if len(req) == 0 {
return nil, models.NotFoundError("Requirement " + strconv.Itoa(id))
}
reqItems, err := s.Items.ListScoped(ctx, models.ItemFilter{RequirementIDs: []int{id}})
if err != nil {
return nil, err
}
return genutils.Ptr(generateRequirementResult(req[0], scope, reqStats, reqItems)), nil
}
func (s *Service) CreateRequirement(ctx context.Context, id int, requirement entities.Requirement, stats []entities.RequirementStat) (*RequirementResult, error) {
requirement.Name = strings.Trim(requirement.Name, "  \t\r\n")
if requirement.Name == "" {

2
usecases/sprints/repository.go

@ -11,7 +11,7 @@ type Repository interface {
Find(ctx context.Context, scopeID, sprintID int) (*entities.Sprint, error)
ListAt(ctx context.Context, scopeID int, at time.Time) ([]entities.Sprint, error)
ListBetween(ctx context.Context, scopeID int, from, to time.Time) ([]entities.Sprint, error)
Create(ctx context.Context, sprint entities.Sprint) (*entities.Sprint, error)
Insert(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)

7
usecases/sprints/result.go

@ -10,7 +10,12 @@ type Result struct {
entities.Sprint
AggregateAcquired int `json:"aggregateAcquired"`
AggregateTotal int `json:"aggregateTotal"`
AggregatePlanned int `json:"aggregateTotal"`
ItemsAcquired *int `json:"itemsAcquired,omitempty"`
ItemsRequired *int `json:"itemsRequired,omitempty"`
PartIDs []int `json:"partIds"` // Just for simplifying code.
Items []items.Result `json:"items,omitempty"`
Stats []entities.Stat `json:"stats,omitempty"`

256
usecases/sprints/service.go

@ -3,12 +3,14 @@ package sprints
import (
"context"
"git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"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"
"strings"
"time"
)
@ -71,6 +73,239 @@ func (s *Service) ListScopedBetween(ctx context.Context, from, to time.Time) ([]
return results, nil
}
func (s *Service) Create(ctx context.Context, sprint entities.Sprint, parts []entities.SprintPart) (*Result, error) {
scope := s.Scopes.Context(ctx).Scope
sprint.Name = strings.Trim(sprint.Name, "  \t\r\n")
if sprint.Name == "" {
return nil, models.BadInputError{
Object: "SprintInput",
Field: "name",
Problem: "Empty name provided",
}
}
if !sprint.Kind.Valid() {
return nil, models.BadInputError{
Object: "SprintInput",
Field: "kind",
Problem: "Non-existent kind ID",
Min: 0,
Max: models.MaxSprintKind,
}
}
if sprint.FromTime.IsZero() {
return nil, models.BadInputError{
Object: "SprintInput",
Field: "fromTime",
Problem: "Empty from time provided",
}
}
if sprint.ToTime.IsZero() {
return nil, models.BadInputError{
Object: "SprintInput",
Field: "toTime",
Problem: "Empty from time provided",
}
}
if sprint.ToTime.Before(sprint.FromTime) {
return nil, models.BadInputError{
Object: "SprintInput",
Field: "toTime",
Problem: "Time goes only one way in this universe as fas as we know",
}
}
ids := genutils.Map(parts, func(p entities.SprintPart) int { return p.PartID })
switch sprint.Kind {
case models.SprintKindScope:
{
if len(parts) > 0 {
return nil, models.BadInputError{
Object: "SprintInput",
Field: "parts",
Problem: "Scope-sprints cannot have parts.",
}
}
}
case models.SprintKindStats:
{
for _, part := range parts {
if !scope.HasStat(part.PartID) {
return nil, models.NotFoundIDError("Stat", part.PartID)
}
}
}
case models.SprintKindItems:
{
sprintItems, err := s.Items.ListScoped(ctx, models.ItemFilter{IDs: ids})
if err != nil {
return nil, err
}
if len(sprintItems) != len(ids) {
for _, id := range ids {
if genutils.Find(sprintItems, func(i items.Result) bool { return i.ID == id }) == nil {
return nil, models.NotFoundIDError("Item", id)
}
}
}
}
case models.SprintKindRequirements:
{
sprintReqs, err := s.Projects.FetchRequirements(ctx, ids...)
if err != nil {
return nil, err
}
if len(sprintReqs) != len(ids) {
for _, id := range ids {
if genutils.Find(sprintReqs, func(i projects.RequirementResult) bool { return i.ID == id }) == nil {
return nil, models.NotFoundIDError("Requirement", id)
}
}
}
}
}
sprint.ScopeID = scope.ID
newSprint, err := s.Repository.Insert(ctx, sprint)
if err != nil {
return nil, err
}
newParts := make([]entities.SprintPart, 0, 16)
for _, part := range parts {
part.SprintID = newSprint.ID
err = s.Repository.UpdatePart(ctx, part)
if err == nil {
newParts = append(newParts, part)
}
}
return s.fill(ctx, *newSprint, newParts)
}
func (s *Service) Update(ctx context.Context, id int, update models.SprintUpdate) (*Result, error) {
scope := s.Scopes.Context(ctx).Scope
sprint, err := s.Repository.Find(ctx, scope.ID, id)
if err != nil {
return nil, err
}
if update.Name != nil && *update.Name == "" {
return nil, models.BadInputError{
Object: "SprintUpdate",
Field: "name",
Problem: "Empty name provided",
}
}
fromTime := sprint.FromTime
if update.FromTime != nil {
fromTime = *update.FromTime
}
if update.ToTime != nil && update.ToTime.Before(fromTime) {
return nil, models.BadInputError{
Object: "SprintUpdate",
Field: "toTime",
Problem: "Time goes only one way in this universe as fas as we know",
}
}
err = s.Repository.Update(ctx, *sprint, update)
if err != nil {
return nil, err
}
sprint.ApplyUpdate(update)
parts, err := s.Repository.ListParts(ctx, *sprint)
if err != nil {
return nil, err
}
return s.fill(ctx, *sprint, parts)
}
func (s *Service) AddPart(ctx context.Context, sprintPart entities.SprintPart) (*Result, error) {
scope := s.Scopes.Context(ctx).Scope
sprint, err := s.Repository.Find(ctx, scope.ID, sprintPart.SprintID)
if err != nil {
return nil, err
}
switch sprint.Kind {
case models.SprintKindItems:
{
_, err := s.Items.Find(ctx, sprintPart.PartID)
if err != nil {
return nil, err
}
}
case models.SprintKindRequirements:
{
_, err := s.Projects.FindRequirement(ctx, sprintPart.PartID)
if err != nil {
return nil, err
}
}
case models.SprintKindStats:
{
if !scope.HasStat(sprintPart.PartID) {
return nil, models.NotFoundIDError("Stat", sprintPart.PartID)
}
}
case models.SprintKindScope:
{
return nil, models.ForbiddenError("Scope-sprints cannot have parts")
}
}
err = s.Repository.UpdatePart(ctx, sprintPart)
if err != nil {
return nil, err
}
sprintParts, err := s.Repository.ListParts(ctx, *sprint)
if err != nil {
return nil, err
}
return s.fill(ctx, *sprint, sprintParts)
}
func (s *Service) RemovePart(ctx context.Context, sprintPart entities.SprintPart) (*Result, error) {
scope := s.Scopes.Context(ctx).Scope
sprint, err := s.Repository.Find(ctx, scope.ID, sprintPart.SprintID)
if err != nil {
return nil, err
}
err = s.Repository.DeletePart(ctx, sprintPart)
if err != nil {
return nil, err
}
sprintParts, err := s.Repository.ListParts(ctx, *sprint)
if err != nil {
return nil, err
}
return s.fill(ctx, *sprint, sprintParts)
}
func (s *Service) Delete(ctx context.Context, id int) (*Result, error) {
sprint, err := s.Find(ctx, id)
if err != nil {
return nil, err
}
err = s.Repository.Delete(ctx, sprint.Sprint)
if err != nil {
return nil, err
}
return sprint, 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)
@ -85,6 +320,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
progressMap := make(map[int]int, len(allStats))
res.Progress = make([]ResultProgress, 0, len(allStats))
res.PartIDs = partIDs
switch res.Kind {
case models.SprintKindItems:
@ -96,6 +332,14 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
}
res.Items = pickedItems
res.ItemsRequired = genutils.Ptr(len(pickedItems))
res.ItemsAcquired = new(int)
for _, item := range pickedItems {
if item.AcquiredTime != nil {
*res.ItemsAcquired += 1
}
}
for _, stat := range allStats {
totalRequired := 0
for _, item := range res.Items {
@ -120,8 +364,9 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
if err != nil {
return nil, err
}
res.Requirements = includedRequirements
for _, req := range includedRequirements {
for _, req := range res.Requirements {
res.Items = append(res.Items, req.Items...)
}
@ -145,7 +390,6 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
})
}
res.Requirements = includedRequirements
case models.SprintKindStats:
includedStats := make([]scopes.ResultStat, 0, len(partIDs))
for _, stat := range allStats {
@ -225,22 +469,22 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
// Calculate aggregate values
aggregateAcquired := 0.0
aggregateTotal := 0.0
aggregatePlanned := 0.0
for _, progress := range res.Progress {
if res.IsUnweighted {
aggregateAcquired += float64(progress.Acquired)
if progress.Required != nil {
aggregateTotal += float64(*progress.Required)
aggregatePlanned += float64(*progress.Required)
}
} else {
aggregateAcquired += float64(progress.Acquired) * progress.Weight
if progress.Required != nil {
aggregateTotal += float64(*progress.Required) * progress.Weight
aggregatePlanned += float64(*progress.Required) * progress.Weight
}
}
}
res.AggregateAcquired = int(math.Round(aggregateAcquired))
res.AggregateTotal = int(math.Round(aggregateTotal))
res.AggregatePlanned = int(math.Round(aggregatePlanned))
return &res, nil
}

73
usecases/stats/service.go

@ -76,3 +76,76 @@ func (s *Service) Create(ctx context.Context, stat entities.Stat) (*entities.Sta
return s.Repository.Insert(ctx, stat)
}
func (s *Service) Update(ctx context.Context, id int, update models.StatUpdate) (*entities.Stat, error) {
stat, err := s.Find(ctx, id)
if err != nil {
return nil, err
}
if update.Name != nil && *update.Name == "" {
return nil, models.BadInputError{
Object: "StatUpdate",
Field: "name",
Problem: "Empty name provided",
}
}
if update.Weight != nil && *update.Weight < 0 {
return nil, models.BadInputError{
Object: "StatUpdate",
Field: "Weight",
Problem: "Negative weight provided",
Min: 0.0,
}
}
if len(update.AllowedAmounts) > 0 {
aaValues := genutils.Set[int]{}
aaNames := genutils.Set[string]{}
for _, allowedAmount := range stat.AllowedAmounts {
aaValues.Add(allowedAmount.Value)
aaNames.Add(allowedAmount.Label)
}
for _, newAA := range update.AllowedAmounts {
if newAA.Value <= 0 {
aaValues.Del(newAA.Value)
aaNames.Del(newAA.Label)
continue
}
if aaValues.Has(newAA.Value) || aaNames.Has(newAA.Label) {
return nil, models.BadInputError{
Object: "StatInput",
Field: "allowedAmounts",
Problem: "Duplicate value or label detected",
}
}
aaValues.Add(newAA.Value)
aaNames.Add(newAA.Label)
}
}
err = s.Repository.Update(ctx, *stat, update)
if err != nil {
return nil, err
}
stat.Update(update)
return stat, nil
}
func (s *Service) Delete(ctx context.Context, id int) (*entities.Stat, error) {
stat, err := s.Find(ctx, id)
if err != nil {
return nil, err
}
err = s.Repository.Delete(ctx, *stat)
if err != nil {
return nil, err
}
return stat, nil
}
Loading…
Cancel
Save