You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
396 lines
9.1 KiB
396 lines
9.1 KiB
package projects
|
|
|
|
import (
|
|
"context"
|
|
"git.aiterp.net/stufflog3/stufflog3/entities"
|
|
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
|
|
"git.aiterp.net/stufflog3/stufflog3/internal/validate"
|
|
"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"
|
|
)
|
|
|
|
type Service struct {
|
|
Scopes *scopes.Service
|
|
Stats *stats.Service
|
|
Items *items.Service
|
|
Auth *auth.Service
|
|
|
|
Repository Repository
|
|
}
|
|
|
|
func (s *Service) Find(ctx context.Context, id int) (*Result, error) {
|
|
sc := s.Scopes.Context(ctx)
|
|
|
|
project, err := s.Repository.Find(ctx, sc.ID, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
requirements, requirementStats, err := s.Repository.ListRequirements(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
items, err := s.Items.ListScoped(ctx, models.ItemFilter{
|
|
ProjectIDs: []int{id},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return generateResult(
|
|
*project,
|
|
sc.Scope,
|
|
requirements,
|
|
requirementStats,
|
|
items,
|
|
), nil
|
|
}
|
|
|
|
func (s *Service) List(ctx context.Context) ([]Entry, error) {
|
|
projects, err := s.Repository.List(ctx, s.Scopes.Context(ctx).ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries := make([]Entry, 0, len(projects))
|
|
for _, project := range projects {
|
|
entries = append(entries, generateEntry(project, s.Scopes.Context(ctx).Scope))
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
func (s *Service) ListByTags(ctx context.Context, tags []string) (interface{}, error) {
|
|
projects, err := s.Repository.ListByTags(ctx, s.Scopes.Context(ctx).ID, tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entries := make([]Entry, 0, len(projects))
|
|
for _, project := range projects {
|
|
entries = append(entries, generateEntry(project, s.Scopes.Context(ctx).Scope))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
results := make([]RequirementResult, 0, len(requirements))
|
|
for _, req := range requirements {
|
|
results = append(results, generateRequirementResult(
|
|
req,
|
|
sc.Scope,
|
|
requirementStats,
|
|
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",
|
|
}
|
|
}
|
|
|
|
err := validate.Tags(nil, project.Tags, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Allow importing and scripts to mess with the created time, so only set it to now if it's not set.
|
|
if project.CreatedTime.IsZero() {
|
|
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
|
|
}
|
|
|
|
func (s *Service) Update(ctx context.Context, id int, update models.ProjectUpdate) (*Result, error) {
|
|
project, err := s.Find(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = s.Repository.Update(ctx, project.Project, update)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
project.Update(update)
|
|
|
|
return project, nil
|
|
}
|
|
|
|
func (s *Service) Delete(ctx context.Context, id int) (*Result, error) {
|
|
project, err := s.Find(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = s.Repository.Delete(ctx, project.Project)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 == "" {
|
|
return nil, models.BadInputError{
|
|
Object: "ProjectInput",
|
|
Field: "name",
|
|
Problem: "Empty name provided",
|
|
}
|
|
}
|
|
if !requirement.Status.Valid() {
|
|
return nil, models.BadInputError{
|
|
Object: "ProjectInput",
|
|
Field: "status",
|
|
Problem: "Non-existent status ID",
|
|
}
|
|
}
|
|
|
|
err := validate.Tags(nil, requirement.Tags, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
project, err := s.Find(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
requirement.ScopeID = project.ScopeID
|
|
requirement.ProjectID = project.ID
|
|
|
|
scope := s.Scopes.Context(ctx).Scope
|
|
for _, stat := range stats {
|
|
if !scope.HasStat(stat.StatID) {
|
|
return nil, models.BadInputError{
|
|
Object: "ProjectInput",
|
|
Field: "stats",
|
|
Problem: "Non-existent stat ID",
|
|
}
|
|
}
|
|
}
|
|
|
|
newRequirement, err := s.Repository.CreateRequirement(ctx, requirement)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
goodStats := make([]entities.RequirementStat, 0, len(stats))
|
|
for _, stat := range stats {
|
|
stat.RequirementID = newRequirement.ID
|
|
err = s.Repository.UpsertRequirementStat(ctx, stat)
|
|
if err == nil {
|
|
goodStats = append(goodStats, stat)
|
|
}
|
|
}
|
|
|
|
result := generateRequirementResult(
|
|
*newRequirement,
|
|
scope,
|
|
goodStats,
|
|
[]items.Result{},
|
|
)
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (s *Service) UpdateRequirement(ctx context.Context, projectID, requirementID int, update models.RequirementUpdate, stats []entities.RequirementStat) (*RequirementResult, error) {
|
|
project, err := s.Find(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req := project.Requirement(requirementID)
|
|
if req == nil {
|
|
return nil, models.NotFoundError("Requirement")
|
|
}
|
|
|
|
err = validate.Tags(req.Tags, update.AddTags, update.RemoveTags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = s.Repository.UpdateRequirement(ctx, req.Requirement, update)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Requirement.Update(update)
|
|
req.refresh(s.Scopes.Context(ctx).Scope)
|
|
|
|
needsRefresh := false
|
|
for _, stat := range stats {
|
|
stat.RequirementID = req.ID
|
|
|
|
if stat.Required >= 0 {
|
|
err = s.Repository.UpsertRequirementStat(ctx, stat)
|
|
} else {
|
|
err = s.Repository.DeleteRequirementStat(ctx, stat)
|
|
}
|
|
|
|
if err == nil {
|
|
if !req.updateStat(stat) {
|
|
needsRefresh = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if needsRefresh {
|
|
return s.FindRequirement(ctx, requirementID)
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (s *Service) DeleteRequirement(ctx context.Context, projectID, requirementID int) (*RequirementResult, error) {
|
|
project, err := s.Find(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req := project.Requirement(requirementID)
|
|
if req == nil {
|
|
return nil, models.NotFoundError("Requirement")
|
|
}
|
|
|
|
err = s.Repository.DeleteRequirement(ctx, req.Requirement)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (s *Service) UpsertRequirementStat(ctx context.Context, projectID, requirementID int, input entities.RequirementStat) (*RequirementResult, error) {
|
|
project, err := s.Find(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req := project.Requirement(requirementID)
|
|
if req == nil {
|
|
return nil, models.NotFoundError("Requirement")
|
|
}
|
|
input.RequirementID = requirementID
|
|
|
|
scope := s.Scopes.Context(ctx).Scope
|
|
if !scope.HasStat(input.StatID) {
|
|
return nil, models.NotFoundError("Stat")
|
|
}
|
|
|
|
err = s.Repository.UpsertRequirementStat(ctx, input)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
project, err = s.Find(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req = project.Requirement(requirementID)
|
|
if req == nil {
|
|
return nil, models.NotFoundError("Requirement")
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func (s *Service) DeleteRequirementStat(ctx context.Context, projectID, requirementID, statID int) (*RequirementResult, error) {
|
|
project, err := s.Find(ctx, projectID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req := project.Requirement(requirementID)
|
|
if req == nil {
|
|
return nil, models.NotFoundError("Requirement")
|
|
}
|
|
stat := req.Stat(statID)
|
|
if stat == nil {
|
|
return nil, models.NotFoundError("Stat")
|
|
}
|
|
|
|
err = s.Repository.DeleteRequirementStat(ctx, entities.RequirementStat{
|
|
RequirementID: req.ID,
|
|
StatID: statID,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range req.Stats {
|
|
if req.Stats[i].ID == statID {
|
|
req.Stats = append(req.Stats[:i], req.Stats[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
return req, nil
|
|
}
|