added more endpoints
This commit is contained in:
@@ -62,6 +62,7 @@ func main() {
|
||||
Repository: db.Items(),
|
||||
}
|
||||
projectsService := &projects.Service{
|
||||
Auth: authService,
|
||||
Scopes: scopesService,
|
||||
Stats: statsService,
|
||||
Items: itemsService,
|
||||
@@ -90,6 +91,7 @@ func main() {
|
||||
httpapi.Projects(apiV1ScopesSub.Group("/projects"), projectsService)
|
||||
httpapi.Items(apiV1ScopesSub.Group("/items"), itemsService)
|
||||
httpapi.Sprints(apiV1ScopesSub.Group("/sprints"), sprintsService)
|
||||
httpapi.Stats(apiV1ScopesSub.Group("/stats"), statsService)
|
||||
|
||||
exitSignal := make(chan os.Signal)
|
||||
signal.Notify(exitSignal, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
)
|
||||
|
||||
@@ -11,6 +12,14 @@ type Scope struct {
|
||||
CustomLabels map[string]string `json:"customLabels"`
|
||||
}
|
||||
|
||||
func (scope *Scope) StatusName(status models.Status) string {
|
||||
if cl, ok := scope.CustomLabels[fmt.Sprintf("status%d", status)]; ok {
|
||||
return cl
|
||||
}
|
||||
|
||||
return status.Name()
|
||||
}
|
||||
|
||||
func (scope *Scope) ApplyUpdate(update models.ScopeUpdate) {
|
||||
if update.Name != nil {
|
||||
scope.Name = *update.Name
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package genutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
func ParseJSON[T any](b []byte) (T, error) {
|
||||
var res T
|
||||
err := json.Unmarshal(b, &res)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func ParseJSONArray[T any](b []byte) []T {
|
||||
res := make([]T, 0, 16)
|
||||
|
||||
if b != nil && !bytes.Equal(b, []byte{'n', 'u', 'l', 'l'}) {
|
||||
_ = json.Unmarshal(b, &res)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package genutils
|
||||
|
||||
type Set[T comparable] struct {
|
||||
m map[T]bool
|
||||
a []T
|
||||
}
|
||||
|
||||
func (set *Set[T]) Add(values ...T) {
|
||||
if set.m == nil {
|
||||
set.m = make(map[T]bool, len(values)*4)
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
if set.m[value] {
|
||||
continue
|
||||
}
|
||||
|
||||
set.m[value] = true
|
||||
set.a = append(set.a, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (set *Set[T]) Has(v T) bool {
|
||||
return set.m[v]
|
||||
}
|
||||
|
||||
func (set *Set[T]) Len() int {
|
||||
return len(set.a)
|
||||
}
|
||||
|
||||
func (set *Set[T]) Values() []T {
|
||||
return set.a[:len(set.a):len(set.a)]
|
||||
}
|
||||
+5
-5
@@ -33,11 +33,11 @@ func (e ForbiddenError) HttpStatus() (int, string, interface{}) {
|
||||
}
|
||||
|
||||
type BadInputError struct {
|
||||
Object string
|
||||
Field string
|
||||
Problem string
|
||||
Min interface{}
|
||||
Max interface{}
|
||||
Object string `json:"object"`
|
||||
Field string `json:"field,omitempty"`
|
||||
Problem string `json:"problem"`
|
||||
Min interface{} `json:"min,omitempty"`
|
||||
Max interface{} `json:"max,omitempty"`
|
||||
}
|
||||
|
||||
func (e BadInputError) Error() string {
|
||||
|
||||
@@ -6,6 +6,14 @@ func (s Status) Valid() bool {
|
||||
return s >= 0 && s < maxStatus
|
||||
}
|
||||
|
||||
func (s Status) Name() string {
|
||||
if s < 0 || s >= maxStatus {
|
||||
return "(invalid status)"
|
||||
}
|
||||
|
||||
return StatusLabels[s]
|
||||
}
|
||||
|
||||
const (
|
||||
Blocked Status = iota
|
||||
Available
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"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/projects"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Projects(g *gin.RouterGroup, projects *projects.Service) {
|
||||
func Projects(g *gin.RouterGroup, service *projects.Service) {
|
||||
g.GET("", handler("projects", func(c *gin.Context) (interface{}, error) {
|
||||
return projects.List(c.Request.Context())
|
||||
return service.List(c.Request.Context())
|
||||
}))
|
||||
|
||||
g.GET("/:project_id", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||
@@ -16,6 +19,58 @@ func Projects(g *gin.RouterGroup, projects *projects.Service) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return projects.Find(c.Request.Context(), id)
|
||||
return service.Find(c.Request.Context(), id)
|
||||
}))
|
||||
|
||||
g.POST("", handler("project", func(c *gin.Context) (interface{}, error) {
|
||||
input := entities.Project{}
|
||||
err := c.BindJSON(&input)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Project",
|
||||
Problem: "Invalid JSON: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return service.Create(c.Request.Context(), input)
|
||||
}))
|
||||
|
||||
g.GET("/:project_id/requirements", handler("requirements", func(c *gin.Context) (interface{}, error) {
|
||||
id, err := reqInt(c, "project_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := service.Find(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return project.Requirements, nil
|
||||
}))
|
||||
|
||||
g.GET("/:project_id/requirements/:requirement_id", handler("requirements", func(c *gin.Context) (interface{}, error) {
|
||||
id, err := reqInt(c, "project_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqID, err := reqInt(c, "requirement_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err := service.Find(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := genutils.Find(project.Requirements, func(r projects.RequirementResult) bool {
|
||||
return r.ID == reqID
|
||||
})
|
||||
if err == nil {
|
||||
return nil, models.NotFoundError("Requirement")
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ func ScopeMiddleware(scopes *scopes.Service, auth *auth.Service) gin.HandlerFunc
|
||||
}
|
||||
|
||||
func Scopes(g *gin.RouterGroup, scopes *scopes.Service) {
|
||||
type scopeInput struct {
|
||||
entities.Scope
|
||||
OwnerName string `json:"ownerName"`
|
||||
}
|
||||
|
||||
g.GET("", handler("scopes", func(c *gin.Context) (interface{}, error) {
|
||||
return scopes.List(c.Request.Context())
|
||||
}))
|
||||
@@ -78,4 +83,17 @@ func Scopes(g *gin.RouterGroup, scopes *scopes.Service) {
|
||||
|
||||
return scopes.Find(c.Request.Context(), id)
|
||||
}))
|
||||
|
||||
g.POST("", handler("scope", func(c *gin.Context) (interface{}, error) {
|
||||
input := scopeInput{}
|
||||
err := c.BindJSON(&input)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Scope",
|
||||
Problem: "Invalid JSON: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return scopes.Create(c.Request.Context(), input.Scope, input.OwnerName)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/stats"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Stats(g *gin.RouterGroup, stats *stats.Service) {
|
||||
g.GET("", handler("stats", func(c *gin.Context) (interface{}, error) {
|
||||
return stats.ListScoped(c.Request.Context())
|
||||
}))
|
||||
|
||||
g.GET("/:stat_id", handler("stat", func(c *gin.Context) (interface{}, error) {
|
||||
id, err := reqInt(c, "stat_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats.Find(c.Request.Context(), id)
|
||||
}))
|
||||
|
||||
g.POST("", handler("stat", func(c *gin.Context) (interface{}, error) {
|
||||
input := entities.Stat{}
|
||||
err := c.BindJSON(&input)
|
||||
if err != nil {
|
||||
return nil, models.BadInputError{
|
||||
Object: "Scope",
|
||||
Problem: "Invalid JSON: " + err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
return stats.Create(c.Request.Context(), input)
|
||||
}))
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.P
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *projectRepository) Create(ctx context.Context, project entities.Project) (*entities.Project, error) {
|
||||
func (r *projectRepository) Insert(ctx context.Context, project entities.Project) (*entities.Project, error) {
|
||||
res, err := r.q.InsertProject(ctx, mysqlcore.InsertProjectParams{
|
||||
ScopeID: project.ScopeID,
|
||||
OwnerID: project.OwnerID,
|
||||
|
||||
+11
-1
@@ -93,7 +93,7 @@ func (r *scopeRepository) ListUser(ctx context.Context, userID string) ([]entiti
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *scopeRepository) Create(ctx context.Context, scope entities.Scope, ownerID string) (*entities.Scope, error) {
|
||||
func (r *scopeRepository) Create(ctx context.Context, scope entities.Scope, owner entities.ScopeMember) (*entities.Scope, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,6 +119,16 @@ func (r *scopeRepository) Create(ctx context.Context, scope entities.Scope, owne
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = q.ReplaceScopeMember(ctx, mysqlcore.ReplaceScopeMemberParams{
|
||||
ScopeID: int(id),
|
||||
UserID: owner.UserID,
|
||||
Name: owner.Name,
|
||||
Owner: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+68
-17
@@ -5,8 +5,10 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
type statsRepository struct {
|
||||
@@ -43,38 +45,87 @@ func (r *statsRepository) Find(ctx context.Context, scopeID, statID int) (*entit
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *statsRepository) List(ctx context.Context, scopeID int) ([]entities.Stat, error) {
|
||||
rows, err := r.q.ListStats(ctx, scopeID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, models.NotFoundError("Stat")
|
||||
func (r *statsRepository) List(ctx context.Context, scopeIDs ...int) ([]entities.Stat, error) {
|
||||
if len(scopeIDs) == 0 {
|
||||
return []entities.Stat{}, nil
|
||||
} else if len(scopeIDs) == 1 {
|
||||
rows, err := r.q.ListStats(ctx, scopeIDs[0])
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, models.NotFoundError("Stat")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]entities.Stat, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
var allowedAmounts []models.StatAllowedAmount
|
||||
if row.AllowedAmounts.Valid {
|
||||
allowedAmounts = make([]models.StatAllowedAmount, 0, 8)
|
||||
_ = json.Unmarshal(row.AllowedAmounts.RawMessage, &allowedAmounts)
|
||||
if len(allowedAmounts) == 0 {
|
||||
allowedAmounts = nil
|
||||
}
|
||||
}
|
||||
|
||||
res = append(res, entities.Stat{
|
||||
ID: row.ID,
|
||||
ScopeID: row.ScopeID,
|
||||
Name: row.Name,
|
||||
Weight: row.Weight,
|
||||
Description: row.Description,
|
||||
AllowedAmounts: allowedAmounts,
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
rows, err := squirrel.
|
||||
Select("id, scope_id, name, description, weight, allowed_amounts").
|
||||
From("stat").
|
||||
Where(squirrel.Eq{"scope_id": scopeIDs}).
|
||||
RunWith(r.db).
|
||||
Query()
|
||||
if err == sql.ErrNoRows {
|
||||
return []entities.Stat{}, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]entities.Stat, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
var allowedAmounts []models.StatAllowedAmount
|
||||
if row.AllowedAmounts.Valid {
|
||||
allowedAmounts = make([]models.StatAllowedAmount, 0, 8)
|
||||
_ = json.Unmarshal(row.AllowedAmounts.RawMessage, &allowedAmounts)
|
||||
if len(allowedAmounts) == 0 {
|
||||
allowedAmounts = nil
|
||||
}
|
||||
stats := make([]entities.Stat, 0, 16)
|
||||
for rows.Next() {
|
||||
var row mysqlcore.Stat
|
||||
if err := rows.Scan(
|
||||
&row.ID,
|
||||
&row.ScopeID,
|
||||
&row.Name,
|
||||
&row.Description,
|
||||
&row.Weight,
|
||||
&row.AllowedAmounts,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = append(res, entities.Stat{
|
||||
stats = append(stats, entities.Stat{
|
||||
ID: row.ID,
|
||||
ScopeID: row.ScopeID,
|
||||
Name: row.Name,
|
||||
Weight: row.Weight,
|
||||
Description: row.Description,
|
||||
AllowedAmounts: allowedAmounts,
|
||||
AllowedAmounts: genutils.ParseJSONArray[models.StatAllowedAmount](row.AllowedAmounts.RawMessage),
|
||||
})
|
||||
}
|
||||
|
||||
return res, nil
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (r *statsRepository) Insert(ctx context.Context, stat entities.Stat) (*entities.Stat, error) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package items
|
||||
|
||||
import "git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
entities.Item
|
||||
@@ -26,25 +29,25 @@ type ResultStat struct {
|
||||
Required int `json:"required"`
|
||||
}
|
||||
|
||||
func generateResult(item entities.Item, progresses []entities.ItemProgress, stats []entities.Stat) Result {
|
||||
func generateResult(item entities.Item, scope scopes.Result, progresses []entities.ItemProgress) Result {
|
||||
res := Result{
|
||||
Item: item,
|
||||
Stats: make([]ResultStat, 0, 8),
|
||||
}
|
||||
|
||||
for _, prog := range progresses {
|
||||
if prog.ItemID != item.ID {
|
||||
for _, progress := range progresses {
|
||||
if progress.ItemID != item.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, stat := range stats {
|
||||
if stat.ID == prog.StatID {
|
||||
for _, stat := range scope.Stats {
|
||||
if stat.ID == progress.StatID {
|
||||
res.Stats = append(res.Stats, ResultStat{
|
||||
ID: stat.ID,
|
||||
Name: stat.Name,
|
||||
Weight: stat.Weight,
|
||||
Acquired: prog.Acquired,
|
||||
Required: prog.Required,
|
||||
Acquired: progress.Acquired,
|
||||
Required: progress.Required,
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
@@ -29,12 +29,7 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopeStats, err := sc.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := generateResult(*item, progresses, scopeStats)
|
||||
result := generateResult(*item, sc.Scope, progresses)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -77,14 +72,9 @@ func (s *Service) ListScoped(ctx context.Context, filter models.ItemFilter) ([]R
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scopeStats, err := sc.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make([]Result, 0, len(items))
|
||||
for _, item := range items {
|
||||
res = append(res, generateResult(item, progresses, scopeStats))
|
||||
res = append(res, generateResult(item, sc.Scope, progresses))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
type Repository interface {
|
||||
Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error)
|
||||
List(ctx context.Context, scopeID int) ([]entities.Project, error)
|
||||
Create(ctx context.Context, project entities.Project) (*entities.Project, error)
|
||||
Insert(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)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"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/scopes"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -13,6 +14,7 @@ type Entry struct {
|
||||
CreatedTime time.Time `json:"createdTime"`
|
||||
Name string `json:"name"`
|
||||
Status models.Status `json:"status"`
|
||||
StatusName string `json:"statusName"`
|
||||
}
|
||||
|
||||
func generateEntry(project entities.Project) Entry {
|
||||
@@ -27,6 +29,8 @@ func generateEntry(project entities.Project) Entry {
|
||||
|
||||
type Result struct {
|
||||
entities.Project
|
||||
OwnerName string `json:"ownerName"`
|
||||
StatusName string `json:"statusName"`
|
||||
Requirements []RequirementResult `json:"requirements"`
|
||||
}
|
||||
|
||||
@@ -35,6 +39,7 @@ type RequirementResult struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Status models.Status `json:"status"`
|
||||
StatusName string `json:"statusName"`
|
||||
Stats []RequirementResultStat `json:"stats"`
|
||||
Items []items.Result `json:"items"`
|
||||
}
|
||||
@@ -60,13 +65,15 @@ type RequirementResultStat struct {
|
||||
|
||||
func generateResult(
|
||||
project entities.Project,
|
||||
scope scopes.Result,
|
||||
requirement []entities.Requirement,
|
||||
requirementStats []entities.RequirementStat,
|
||||
projectItems []items.Result,
|
||||
stats []entities.Stat,
|
||||
) Result {
|
||||
) *Result {
|
||||
res := Result{
|
||||
Project: project,
|
||||
OwnerName: scope.MemberName(project.OwnerID),
|
||||
StatusName: scope.StatusName(project.Status),
|
||||
Requirements: make([]RequirementResult, 0, 8),
|
||||
}
|
||||
|
||||
@@ -75,20 +82,21 @@ func generateResult(
|
||||
continue
|
||||
}
|
||||
|
||||
resReq := generateRequirementResult(req, requirementStats, stats, projectItems)
|
||||
resReq := generateRequirementResult(req, scope, requirementStats, projectItems)
|
||||
|
||||
res.Requirements = append(res.Requirements, resReq)
|
||||
}
|
||||
|
||||
return res
|
||||
return &res
|
||||
}
|
||||
|
||||
func generateRequirementResult(req entities.Requirement, requirementStats []entities.RequirementStat, stats []entities.Stat, projectItems []items.Result) RequirementResult {
|
||||
func generateRequirementResult(req entities.Requirement, scope scopes.Result, requirementStats []entities.RequirementStat, projectItems []items.Result) RequirementResult {
|
||||
resReq := RequirementResult{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Status: req.Status,
|
||||
StatusName: scope.StatusName(req.Status),
|
||||
Stats: make([]RequirementResultStat, 0, 8),
|
||||
Items: make([]items.Result, 0, 8),
|
||||
}
|
||||
@@ -102,7 +110,7 @@ func generateRequirementResult(req entities.Requirement, requirementStats []enti
|
||||
ID: reqStat.StatID,
|
||||
Required: reqStat.Required,
|
||||
}
|
||||
for _, stat := range stats {
|
||||
for _, stat := range scope.Stats {
|
||||
if stat.ID == resStat.ID {
|
||||
resStat.Name = stat.Name
|
||||
resStat.Weight = stat.Weight
|
||||
|
||||
@@ -2,16 +2,21 @@ package projects
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"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"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Scopes *scopes.Service
|
||||
Stats *stats.Service
|
||||
Items *items.Service
|
||||
Auth *auth.Service
|
||||
|
||||
Repository Repository
|
||||
}
|
||||
@@ -36,20 +41,13 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats, err := sc.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := generateResult(
|
||||
return generateResult(
|
||||
*project,
|
||||
sc.Scope,
|
||||
requirements,
|
||||
requirementStats,
|
||||
items,
|
||||
stats,
|
||||
)
|
||||
|
||||
return &result, nil
|
||||
), nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context) ([]Entry, error) {
|
||||
@@ -81,20 +79,54 @@ func (s *Service) FetchRequirements(ctx context.Context, ids ...int) ([]Requirem
|
||||
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,
|
||||
sc.Scope,
|
||||
requirementStats,
|
||||
stats,
|
||||
items,
|
||||
))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, project entities.Project) (*Result, error) {
|
||||
project.Name = strings.Trim(project.Name, " \t\r\n")
|
||||
if project.Name == "" {
|
||||
return nil, models.BadInputError{
|
||||
Object: "ProjectInput",
|
||||
Field: "name",
|
||||
Problem: "Empty name provided",
|
||||
}
|
||||
}
|
||||
if !project.Status.Valid() {
|
||||
return nil, models.BadInputError{
|
||||
Object: "ProjectInput",
|
||||
Field: "status",
|
||||
Problem: "Non-existent status ID",
|
||||
}
|
||||
}
|
||||
|
||||
// This is stufflog 3, so allow importing and scripts to mess with the created time.
|
||||
if project.CreatedTime.Year() < 2000 {
|
||||
project.CreatedTime = time.Now()
|
||||
}
|
||||
|
||||
project.OwnerID = s.Auth.GetUser(ctx).ID
|
||||
project.ScopeID = s.Scopes.Context(ctx).ID
|
||||
|
||||
newProject, err := s.Repository.Insert(ctx, project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return generateResult(
|
||||
*newProject,
|
||||
s.Scopes.Context(ctx).Scope,
|
||||
[]entities.Requirement{},
|
||||
[]entities.RequirementStat{},
|
||||
[]items.Result{},
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -23,15 +23,6 @@ type Context struct {
|
||||
scopesError error
|
||||
}
|
||||
|
||||
// Stats lazy-loads the stats in the context of the first caller.
|
||||
func (c *Context) Stats(ctx context.Context) ([]entities.Stat, error) {
|
||||
c.statsOnce.Do(func() {
|
||||
c.stats, c.statsError = c.statsRepo.List(ctx, c.ID)
|
||||
})
|
||||
|
||||
return c.stats, c.statsError
|
||||
}
|
||||
|
||||
// Scopes lazy-loads the scopes in the context of the first caller.
|
||||
func (c *Context) Scopes(ctx context.Context) ([]entities.Scope, error) {
|
||||
c.statsOnce.Do(func() {
|
||||
|
||||
@@ -10,7 +10,7 @@ type Repository interface {
|
||||
Find(ctx context.Context, id int) (*entities.Scope, error)
|
||||
List(ctx context.Context) ([]entities.Scope, error)
|
||||
ListUser(ctx context.Context, userID string) ([]entities.Scope, error)
|
||||
Create(ctx context.Context, scope entities.Scope, ownerID string) (*entities.Scope, error)
|
||||
Create(ctx context.Context, scope entities.Scope, owner entities.ScopeMember) (*entities.Scope, error)
|
||||
Update(ctx context.Context, scope entities.Scope, update models.ScopeUpdate) error
|
||||
Delete(ctx context.Context, scope entities.Scope) error
|
||||
|
||||
@@ -20,5 +20,5 @@ type Repository interface {
|
||||
}
|
||||
|
||||
type StatsLister interface {
|
||||
List(ctx context.Context, scopeID int) ([]entities.Stat, error)
|
||||
List(ctx context.Context, scopeIDs ...int) ([]entities.Stat, error)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
package scopes
|
||||
|
||||
import "git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/entities"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
entities.Scope
|
||||
|
||||
Members map[string]ResultMember `json:"members"`
|
||||
Stats []ResultStat `json:"stats"`
|
||||
}
|
||||
|
||||
func (r *Result) MemberName(id string) string {
|
||||
if member, ok := r.Members[id]; ok {
|
||||
return member.Name
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
type ResultMember struct {
|
||||
@@ -14,7 +26,15 @@ type ResultMember struct {
|
||||
Owner bool `json:"owner"`
|
||||
}
|
||||
|
||||
func generateResult(scope entities.Scope, members []entities.ScopeMember) *Result {
|
||||
type ResultStat struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Weight float64 `json:"weight"`
|
||||
Description string `json:"description"`
|
||||
AllowedAmounts []models.StatAllowedAmount `json:"allowedAmounts,omitempty"`
|
||||
}
|
||||
|
||||
func generateResult(scope entities.Scope, members []entities.ScopeMember, stats []entities.Stat) *Result {
|
||||
res := Result{Scope: scope, Members: make(map[string]ResultMember, len(members))}
|
||||
for _, member := range members {
|
||||
if member.ScopeID != scope.ID {
|
||||
@@ -27,6 +47,22 @@ func generateResult(scope entities.Scope, members []entities.ScopeMember) *Resul
|
||||
Owner: member.Owner,
|
||||
}
|
||||
}
|
||||
if stats != nil {
|
||||
res.Stats = make([]ResultStat, 0, len(stats))
|
||||
for _, stat := range stats {
|
||||
if stat.ScopeID != scope.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
res.Stats = append(res.Stats, ResultStat{
|
||||
ID: stat.ID,
|
||||
Name: stat.Name,
|
||||
Weight: stat.Weight,
|
||||
Description: stat.Description,
|
||||
AllowedAmounts: stat.AllowedAmounts,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
|
||||
"git.aiterp.net/stufflog3/stufflog3/models"
|
||||
"git.aiterp.net/stufflog3/stufflog3/usecases/auth"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -51,6 +52,10 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats, err := s.StatsLister.List(ctx, scope.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, member := range members {
|
||||
@@ -63,7 +68,7 @@ func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
|
||||
return nil, models.NotFoundError("Scope")
|
||||
}
|
||||
|
||||
return generateResult(*scope, members), nil
|
||||
return generateResult(*scope, members, stats), nil
|
||||
}
|
||||
|
||||
// List lists a scope and their members, and returns it if the logged-in user is part of this list.
|
||||
@@ -86,8 +91,68 @@ func (s *Service) List(ctx context.Context) ([]Result, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats, err := s.StatsLister.List(ctx, ids...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return genutils.Map(scopes, func(scope entities.Scope) Result {
|
||||
return *generateResult(scope, members)
|
||||
return *generateResult(scope, members, stats)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, scope entities.Scope, ownerName string) (*Result, error) {
|
||||
user := s.Auth.GetUser(ctx)
|
||||
if user == nil {
|
||||
return nil, models.PermissionDeniedError{}
|
||||
}
|
||||
|
||||
scope.Name = strings.Trim(scope.Name, " \t\r\n")
|
||||
if scope.Name == "" {
|
||||
return nil, models.BadInputError{
|
||||
Object: "ScopeInput",
|
||||
Field: "name",
|
||||
Problem: "Empty name provided",
|
||||
}
|
||||
}
|
||||
|
||||
scope.Abbreviation = strings.ToUpper(strings.Trim(scope.Abbreviation, " \t\r\n"))
|
||||
if scope.Abbreviation == "" {
|
||||
return nil, models.BadInputError{
|
||||
Object: "ScopeInput",
|
||||
Field: "abbreviation",
|
||||
Problem: "Empty abbreviation provided",
|
||||
}
|
||||
}
|
||||
if strings.ContainsAny(scope.Abbreviation, "\t \n\r\t") {
|
||||
return nil, models.BadInputError{
|
||||
Object: "ScopeInput",
|
||||
Field: "abbreviation",
|
||||
Problem: "Invalid abbreviation provided",
|
||||
}
|
||||
}
|
||||
|
||||
ownerName = strings.Trim(ownerName, " \t\r\n")
|
||||
if ownerName == "" {
|
||||
return nil, models.BadInputError{
|
||||
Object: "ScopeInput",
|
||||
Field: "ownerName",
|
||||
Problem: "Empty owner name provided",
|
||||
}
|
||||
}
|
||||
|
||||
owner := entities.ScopeMember{
|
||||
ScopeID: -1,
|
||||
UserID: user.ID,
|
||||
Name: ownerName,
|
||||
Owner: true,
|
||||
}
|
||||
|
||||
newScope, err := s.Repository.Create(ctx, scope, owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner.ScopeID = newScope.ID
|
||||
|
||||
return generateResult(*newScope, []entities.ScopeMember{owner}, []entities.Stat{}), nil
|
||||
}
|
||||
|
||||
@@ -81,10 +81,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
||||
}
|
||||
}
|
||||
|
||||
allStats, err := sc.Stats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allStats := sc.Scope.Stats
|
||||
|
||||
progressMap := make(map[int]int, len(allStats))
|
||||
res.Progress = make([]ResultProgress, 0, len(allStats))
|
||||
@@ -150,7 +147,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti
|
||||
|
||||
res.Requirements = includedRequirements
|
||||
case models.SprintKindStats:
|
||||
includedStats := make([]entities.Stat, 0, len(partIDs))
|
||||
includedStats := make([]scopes.ResultStat, 0, len(partIDs))
|
||||
for _, stat := range allStats {
|
||||
for _, id := range partIDs {
|
||||
if stat.ID == id {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type Repository interface {
|
||||
Find(ctx context.Context, scopeID, statID int) (*entities.Stat, error)
|
||||
List(ctx context.Context, scopeID int) ([]entities.Stat, error)
|
||||
List(ctx context.Context, scopeIDs ...int) ([]entities.Stat, error)
|
||||
Insert(ctx context.Context, stat entities.Stat) (*entities.Stat, error)
|
||||
Update(ctx context.Context, stat entities.Stat, update models.StatUpdate) error
|
||||
Delete(ctx context.Context, stat entities.Stat) error
|
||||
|
||||
@@ -3,7 +3,10 @@ package stats
|
||||
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/scopes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -12,6 +15,64 @@ type Service struct {
|
||||
Repository Repository
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context) ([]entities.Stat, error) {
|
||||
func (s *Service) ListScoped(ctx context.Context) ([]entities.Stat, error) {
|
||||
return s.Repository.List(ctx, s.Scopes.Context(ctx).ID)
|
||||
}
|
||||
|
||||
func (s *Service) Find(ctx context.Context, id int) (*entities.Stat, error) {
|
||||
return s.Repository.Find(ctx, s.Scopes.Context(ctx).ID, id)
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, stat entities.Stat) (*entities.Stat, error) {
|
||||
stat.Name = strings.Trim(stat.Name, " \t\r\n")
|
||||
if stat.Name == "" {
|
||||
return nil, models.BadInputError{
|
||||
Object: "StatInput",
|
||||
Field: "name",
|
||||
Problem: "Empty name provided",
|
||||
}
|
||||
}
|
||||
if stat.Weight < 0 {
|
||||
return nil, models.BadInputError{
|
||||
Object: "StatInput",
|
||||
Field: "Weight",
|
||||
Problem: "Negative weight provided",
|
||||
Min: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
aaValues := genutils.Set[int]{}
|
||||
aaNames := genutils.Set[string]{}
|
||||
for _, allowedAmount := range stat.AllowedAmounts {
|
||||
if aaValues.Has(allowedAmount.Value) {
|
||||
return nil, models.BadInputError{
|
||||
Object: "StatInput",
|
||||
Field: "allowedAmounts",
|
||||
Problem: "Duplicate value detected",
|
||||
}
|
||||
}
|
||||
if allowedAmount.Value < 0 {
|
||||
if allowedAmount.Label == "" {
|
||||
return nil, models.BadInputError{
|
||||
Object: "StatInput",
|
||||
Field: "allowedAmounts",
|
||||
Problem: "Negative valuename for allowed amount label provided",
|
||||
}
|
||||
}
|
||||
}
|
||||
if allowedAmount.Label == "" {
|
||||
return nil, models.BadInputError{
|
||||
Object: "StatInput",
|
||||
Field: "allowedAmounts",
|
||||
Problem: "Empty name for allowed amount label provided",
|
||||
}
|
||||
}
|
||||
|
||||
aaValues.Add(allowedAmount.Value)
|
||||
aaNames.Add(allowedAmount.Label)
|
||||
}
|
||||
|
||||
stat.ScopeID = s.Scopes.Context(ctx).ID
|
||||
|
||||
return s.Repository.Insert(ctx, stat)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user