Browse Source

add tags to more stuff.

master
Gisle Aune 1 year ago
parent
commit
c565ca5ea7
  1. 22
      entities/item.go
  2. 16
      entities/project.go
  3. 2
      frontend/src/lib/components/project/ProjectMain.svelte
  4. 5
      frontend/src/lib/components/project/RequirementSection.svelte
  5. 2
      frontend/src/lib/models/project.ts
  6. 24
      internal/genutils/slice.go
  7. 49
      internal/validate/tags.go
  8. 22
      models/project.go
  9. 7
      ports/httpapi/projects.go
  10. 36
      ports/mysql/db.go
  11. 36
      ports/mysql/items.go
  12. 181
      ports/mysql/projects.go
  13. 31
      usecases/items/service.go
  14. 1
      usecases/projects/repository.go
  15. 4
      usecases/projects/result.go
  16. 30
      usecases/projects/service.go

22
entities/item.go

@ -1,6 +1,7 @@
package entities package entities
import ( import (
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/models"
"sort" "sort"
"strings" "strings"
@ -75,23 +76,10 @@ func (item *Item) ApplyUpdate(update models.ItemUpdate) {
if update.ClearAcquiredTime { if update.ClearAcquiredTime {
item.AcquiredTime = nil item.AcquiredTime = nil
} }
if update.RemoveTags != nil || update.AddTags != nil { if update.RemoveTags != nil || update.AddTags != nil {
item.Tags = append(item.Tags[:0:0], item.Tags...)
for _, tagToRemove := range update.RemoveTags {
for i, tag := range item.Tags {
if strings.EqualFold(tag, tagToRemove) {
{
item.Tags = append(item.Tags[:i], item.Tags[i+1:]...)
break
}
}
}
}
for _, tagToAdd := range update.AddTags {
item.Tags = append(item.Tags, tagToAdd)
}
if len(update.AddTags) > 0 {
sort.Strings(item.Tags)
}
item.Tags = genutils.SliceWithout(item.Tags, update.RemoveTags)
item.Tags = genutils.SliceWithUniques(item.Tags, update.AddTags)
sort.Strings(item.Tags)
} }
} }

16
entities/project.go

@ -1,7 +1,9 @@
package entities package entities
import ( import (
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/models"
"sort"
"time" "time"
) )
@ -13,6 +15,7 @@ type Project struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Status models.Status `json:"status"` Status models.Status `json:"status"`
Tags []string `json:"tags"`
} }
func (project *Project) Update(update models.ProjectUpdate) { func (project *Project) Update(update models.ProjectUpdate) {
@ -28,6 +31,12 @@ func (project *Project) Update(update models.ProjectUpdate) {
if update.Status != nil { if update.Status != nil {
project.Status = *update.Status project.Status = *update.Status
} }
if update.RemoveTags != nil || update.AddTags != nil {
project.Tags = genutils.SliceWithout(project.Tags, update.RemoveTags)
project.Tags = genutils.SliceWithUniques(project.Tags, update.AddTags)
sort.Strings(project.Tags)
}
} }
type Requirement struct { type Requirement struct {
@ -39,6 +48,7 @@ type Requirement struct {
Description string `json:"description"` Description string `json:"description"`
IsCoarse bool `json:"isCoarse"` IsCoarse bool `json:"isCoarse"`
Status models.Status `json:"status"` Status models.Status `json:"status"`
Tags []string `json:"tags"`
} }
func (requirement *Requirement) Update(update models.RequirementUpdate) { func (requirement *Requirement) Update(update models.RequirementUpdate) {
@ -57,6 +67,12 @@ func (requirement *Requirement) Update(update models.RequirementUpdate) {
if update.AggregateRequired != nil { if update.AggregateRequired != nil {
requirement.AggregateRequired = *update.AggregateRequired requirement.AggregateRequired = *update.AggregateRequired
} }
if update.RemoveTags != nil || update.AddTags != nil {
requirement.Tags = genutils.SliceWithout(requirement.Tags, update.RemoveTags)
requirement.Tags = genutils.SliceWithUniques(requirement.Tags, update.AddTags)
sort.Strings(requirement.Tags)
}
} }
type RequirementStat struct { type RequirementStat struct {

2
frontend/src/lib/components/project/ProjectMain.svelte

@ -4,6 +4,7 @@
import Main from "$lib/components/layout/Main.svelte"; import Main from "$lib/components/layout/Main.svelte";
import Option from "$lib/components/layout/Option.svelte"; import Option from "$lib/components/layout/Option.svelte";
import OptionsRow from "$lib/components/layout/OptionsRow.svelte"; import OptionsRow from "$lib/components/layout/OptionsRow.svelte";
import TagRow from "../common/TagRow.svelte";
import { getProjectContext } from "../contexts/ProjectContext.svelte"; import { getProjectContext } from "../contexts/ProjectContext.svelte";
import Icon from "../layout/Icon.svelte"; import Icon from "../layout/Icon.svelte";
import RequirementEntry from "./RequirementSection.svelte"; import RequirementEntry from "./RequirementSection.svelte";
@ -14,6 +15,7 @@
<Main big title={$project.name}> <Main big title={$project.name}>
<Progress alwaysSmooth titlePercentageOnly thin green status={$project.status} count={$project.totalAcquired} target={$project.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thin green status={$project.status} count={$project.totalAcquired} target={$project.totalRequired} />
<Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thinner gray count={$project.totalPlanned} target={$project.totalRequired} />
<TagRow names={$project.tags} />
<Markdown source={$project.description} /> <Markdown source={$project.description} />
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option open={{name: "requirement.create", project: $project}}><Icon name="plus" /></Option> <Option open={{name: "requirement.create", project: $project}}><Icon name="plus" /></Option>

5
frontend/src/lib/components/project/RequirementSection.svelte

@ -11,8 +11,8 @@
import ItemEntry from "./ItemSubSection.svelte"; import ItemEntry from "./ItemSubSection.svelte";
import Icon from "../layout/Icon.svelte"; import Icon from "../layout/Icon.svelte";
import { projectPrettyId } from "$lib/utils/prettyIds"; import { projectPrettyId } from "$lib/utils/prettyIds";
import AmountRow from "../common/AmountRow.svelte";
import AggregateAmountRow from "./AggregateAmountRow.svelte";
import AggregateAmountRow from "./AggregateAmountRow.svelte";
import TagRow from "../common/TagRow.svelte";
export let requirement: Requirement; export let requirement: Requirement;
</script> </script>
@ -21,6 +21,7 @@ import AggregateAmountRow from "./AggregateAmountRow.svelte";
<Section title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}> <Section title={requirement.name} icon={STATUS_ICONS[requirement.status]} status={requirement.status}>
<Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thin green status={requirement.status} count={requirement.totalAcquired} target={requirement.totalRequired} />
<Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} /> <Progress alwaysSmooth titlePercentageOnly thinner gray count={requirement.totalPlanned} target={requirement.totalRequired} />
<TagRow names={requirement.tags} />
<Markdown source={requirement.description} /> <Markdown source={requirement.description} />
<OptionsRow slot="right"> <OptionsRow slot="right">
<Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option> <Option open={{name: "item.create", requirement}}><Icon name="plus" /></Option>

2
frontend/src/lib/models/project.ts

@ -19,6 +19,7 @@ export interface ProjectEntry {
name: string name: string
status: number status: number
statusName: string statusName: string
tags: string[]
} }
export interface ProjectInput { export interface ProjectInput {
@ -41,6 +42,7 @@ export interface Requirement {
aggregateRequired: number aggregateRequired: number
stats: StatProgressWithPlanned[] stats: StatProgressWithPlanned[]
items: Item[] items: Item[]
tags: string[]
} }
export interface RequirementInput { export interface RequirementInput {

24
internal/genutils/slice.go

@ -0,0 +1,24 @@
package genutils
func SliceWithUniques[T comparable](slice []T, elemsToAdd []T) []T {
newSlice := slice[:len(slice):len(slice)]
addLoop:
for _, elem := range elemsToAdd {
for _, existing := range slice {
if existing == elem {
continue addLoop
}
}
newSlice = append(newSlice, elem)
}
return newSlice
}
func SliceWithout[T comparable](originals []T, elemsToRemove []T) []T {
return Retain(originals, func(v T) bool {
return !Contains(elemsToRemove, v)
})
}

49
internal/validate/tags.go

@ -0,0 +1,49 @@
package validate
import (
"fmt"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models"
)
func Tags(existingTags, addTags, removeTags []string) error {
for i, tag := range addTags {
if !Tag(tag) {
return models.BadInputError{
Object: "ItemInput",
Field: "addTags",
Problem: fmt.Sprintf("Invalid tag: %s", tag),
Element: tag,
}
}
if genutils.Contains(addTags[:i], tag) {
return models.BadInputError{
Object: "ItemUpdate",
Field: "addTags",
Problem: fmt.Sprintf("The tag %s already exists!", tag),
Element: tag,
}
}
if genutils.Contains(existingTags, tag) {
return models.BadInputError{
Object: "ItemUpdate",
Field: "addTags",
Problem: fmt.Sprintf("The tag %s already exists!", tag),
Element: tag,
}
}
}
for _, tag := range removeTags {
if !genutils.Contains(existingTags, tag) {
return models.BadInputError{
Object: "ItemUpdate",
Field: "removeTags",
Problem: fmt.Sprintf("The tag %s does not exist!", tag),
Element: tag,
}
}
}
return nil
}

22
models/project.go

@ -1,16 +1,20 @@
package models package models
type ProjectUpdate struct { type ProjectUpdate struct {
Name *string `json:"name,omitempty"`
OwnerID *string `json:"ownerId,omitempty"`
Status *Status `json:"status,omitempty"`
Description *string `json:"description,omitempty"`
Name *string `json:"name,omitempty"`
OwnerID *string `json:"ownerId,omitempty"`
Status *Status `json:"status,omitempty"`
Description *string `json:"description,omitempty"`
AddTags []string `json:"addTags"`
RemoveTags []string `json:"removeTags"`
} }
type RequirementUpdate struct { type RequirementUpdate struct {
Name *string `json:"name"`
Description *string `json:"description"`
Status *Status `json:"status"`
IsCoarse *bool `json:"isCoarse"`
AggregateRequired *int `json:"aggregateRequired"`
Name *string `json:"name"`
Description *string `json:"description"`
Status *Status `json:"status"`
IsCoarse *bool `json:"isCoarse"`
AggregateRequired *int `json:"aggregateRequired"`
AddTags []string `json:"addTags"`
RemoveTags []string `json:"removeTags"`
} }

7
ports/httpapi/projects.go

@ -5,11 +5,16 @@ import (
"git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/projects" "git.aiterp.net/stufflog3/stufflog3/usecases/projects"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"strings"
) )
func Projects(g *gin.RouterGroup, service *projects.Service) { func Projects(g *gin.RouterGroup, service *projects.Service) {
g.GET("", handler("projects", func(c *gin.Context) (interface{}, error) { g.GET("", handler("projects", func(c *gin.Context) (interface{}, error) {
return service.List(c.Request.Context())
if tagsQuery := c.Query("tags"); tagsQuery != "" {
return service.ListByTags(c.Request.Context(), strings.Split(tagsQuery, ","))
} else {
return service.List(c.Request.Context())
}
})) }))
g.GET("/:project_id", handler("project", func(c *gin.Context) (interface{}, error) { g.GET("/:project_id", handler("project", func(c *gin.Context) (interface{}, error) {

36
ports/mysql/db.go

@ -1,6 +1,7 @@
package mysql package mysql
import ( import (
"context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -12,6 +13,7 @@ import (
"git.aiterp.net/stufflog3/stufflog3/usecases/scopes" "git.aiterp.net/stufflog3/stufflog3/usecases/scopes"
"git.aiterp.net/stufflog3/stufflog3/usecases/sprints" "git.aiterp.net/stufflog3/stufflog3/usecases/sprints"
"git.aiterp.net/stufflog3/stufflog3/usecases/stats" "git.aiterp.net/stufflog3/stufflog3/usecases/stats"
"github.com/Masterminds/squirrel"
"time" "time"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
@ -135,3 +137,37 @@ const (
tagObjectKindRequirement tagObjectKindRequirement
tagObjectKindProject tagObjectKindProject
) )
func fetchTags(ctx context.Context, db *sql.DB, kind int, ids []int, cb func(id int, tag string)) error {
query, args, err := squirrel.Select("object_id, tag_name").
From("tag").
Where(squirrel.Eq{"object_id": ids, "object_kind": kind}).
OrderBy("tag_name").
ToSql()
if err != nil {
return err
}
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return err
}
for rows.Next() {
var tagStr string
var objectID int
err := rows.Scan(&objectID, &tagStr)
if err != nil {
return err
}
cb(objectID, tagStr)
}
if err := rows.Close(); err != nil {
return err
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

36
ports/mysql/items.go

@ -221,31 +221,15 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([
ids := genutils.Map(res, func(i entities.Item) int { ids := genutils.Map(res, func(i entities.Item) int {
return i.ID return i.ID
}) })
query, args, err := squirrel.Select("object_id, tag_name").
From("tag").
Where(squirrel.Eq{"object_id": ids, "object_kind": tagObjectKindItem}).
ToSql()
if err != nil {
return nil, err
}
rows, err = r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
for rows.Next() {
var tagStr string
var objectID int
err := rows.Scan(&objectID, &tagStr)
if err != nil {
return nil, err
}
err = fetchTags(ctx, r.db, tagObjectKindItem, ids, func(id int, tag string) {
for i := range res { for i := range res {
if res[i].ID == objectID {
res[i].Tags = append(res[i].Tags, tagStr)
break
if id == res[i].ID {
res[i].Tags = append(res[i].Tags, tag)
} }
} }
})
if err != nil {
return nil, err
} }
sort.Slice(res, func(i, j int) bool { sort.Slice(res, func(i, j int) bool {
@ -386,6 +370,14 @@ func (r *itemRepository) Update(ctx context.Context, item entities.Item, update
} }
func (r *itemRepository) Delete(ctx context.Context, item entities.Item) error { func (r *itemRepository) Delete(ctx context.Context, item entities.Item) error {
err := r.q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
ObjectKind: tagObjectKindItem,
ObjectID: item.ID,
})
if err != nil {
return err
}
return r.q.DeleteItem(ctx, item.ID) return r.q.DeleteItem(ctx, item.ID)
} }

181
ports/mysql/projects.go

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"git.aiterp.net/stufflog3/stufflog3/entities" "git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils"
"git.aiterp.net/stufflog3/stufflog3/models" "git.aiterp.net/stufflog3/stufflog3/models"
"git.aiterp.net/stufflog3/stufflog3/ports/mysql/mysqlcore" "git.aiterp.net/stufflog3/stufflog3/ports/mysql/mysqlcore"
"github.com/Masterminds/squirrel" "github.com/Masterminds/squirrel"
@ -24,6 +25,11 @@ func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (*
return nil, err return nil, err
} }
tags, err := r.q.ListTagsByObject(ctx, mysqlcore.ListTagsByObjectParams{
ObjectKind: tagObjectKindProject,
ObjectID: row.ID,
})
return &entities.Project{ return &entities.Project{
ID: row.ID, ID: row.ID,
ScopeID: row.ScopeID, ScopeID: row.ScopeID,
@ -32,6 +38,7 @@ func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (*
Name: row.Name, Name: row.Name,
Description: row.Description, Description: row.Description,
Status: models.Status(row.Status), Status: models.Status(row.Status),
Tags: tags,
}, nil }, nil
} }
@ -78,6 +85,8 @@ func (r *projectRepository) FetchProjects(ctx context.Context, scopeID int, ids
return nil, err return nil, err
} }
project.Tags = []string{}
projects = append(projects, project) projects = append(projects, project)
} }
if err := rows.Close(); err != nil { if err := rows.Close(); err != nil {
@ -87,6 +96,18 @@ func (r *projectRepository) FetchProjects(ctx context.Context, scopeID int, ids
return nil, err return nil, err
} }
// Fill tags
err = fetchTags(ctx, r.db, tagObjectKindProject, ids, func(id int, tag string) {
for i := range projects {
if projects[i].ID == id {
projects[i].Tags = append(projects[i].Tags, tag)
}
}
})
if err != nil {
return nil, err
}
return projects, nil return projects, nil
} }
@ -101,6 +122,7 @@ func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.P
} }
res := make([]entities.Project, 0, len(rows)) res := make([]entities.Project, 0, len(rows))
ids := make([]int, 0, len(rows))
for _, row := range rows { for _, row := range rows {
res = append(res, entities.Project{ res = append(res, entities.Project{
ID: row.ID, ID: row.ID,
@ -110,14 +132,74 @@ func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.P
Name: row.Name, Name: row.Name,
Description: row.Description, Description: row.Description,
Status: models.Status(row.Status), Status: models.Status(row.Status),
Tags: []string{},
}) })
ids = append(ids, row.ID)
}
// Fill tags
err = fetchTags(ctx, r.db, tagObjectKindProject, ids, func(id int, tag string) {
for i := range res {
if id == res[i].ID {
res[i].Tags = append(res[i].Tags, tag)
}
}
})
if err != nil {
return nil, err
} }
return res, nil return res, nil
} }
func (r *projectRepository) ListByTags(ctx context.Context, scopeID int, tags []string) ([]entities.Project, error) {
query, args, err := squirrel.Select("object_id, tag_name").
From("tag").
Where(squirrel.Eq{"tag_name": tags, "object_kind": tagObjectKindProject}).
ToSql()
if err != nil {
return nil, err
}
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
ids := make([]int, 0, 16)
matches := make(map[int]int, 64)
for rows.Next() {
var objectID int
var tagName string
err := rows.Scan(&objectID, &tagName)
if err != nil {
return nil, err
}
if matches[objectID] == 0 {
ids = append(ids, objectID)
}
matches[objectID] += 1
}
err = rows.Close()
if err != nil {
return nil, err
}
ids = genutils.RetainInPlace(ids, func(id int) bool {
return matches[id] == len(tags)
})
return r.FetchProjects(ctx, scopeID, ids...)
}
func (r *projectRepository) Insert(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{
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
q := r.q.WithTx(tx)
res, err := q.InsertProject(ctx, mysqlcore.InsertProjectParams{
ScopeID: project.ScopeID, ScopeID: project.ScopeID,
OwnerID: project.OwnerID, OwnerID: project.OwnerID,
Name: project.Name, Name: project.Name,
@ -134,6 +216,23 @@ func (r *projectRepository) Insert(ctx context.Context, project entities.Project
} }
project.ID = int(id) project.ID = int(id)
for _, tag := range project.Tags {
err := q.InsertTag(ctx, mysqlcore.InsertTagParams{
ObjectKind: tagObjectKindProject,
ObjectID: project.ID,
TagName: tag,
})
if err != nil {
return nil, err
}
}
err = tx.Commit()
if err != nil {
return nil, err
}
return &project, nil return &project, nil
} }
@ -174,6 +273,22 @@ func (r *projectRepository) Delete(ctx context.Context, project entities.Project
if err != nil { if err != nil {
return err return err
} }
err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
ObjectKind: tagObjectKindRequirement,
ObjectID: req.ID,
})
if err != nil {
return err
}
}
err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
ObjectKind: tagObjectKindProject,
ObjectID: project.ID,
})
if err != nil {
return err
} }
err = q.DeleteAllProjectRequirements(ctx, project.ID) err = q.DeleteAllProjectRequirements(ctx, project.ID)
@ -226,10 +341,25 @@ func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int,
return nil, nil, err return nil, nil, err
} }
requirement.Tags = []string{}
requirements = append(requirements, requirement) requirements = append(requirements, requirement)
ids = append(ids, requirement.ID) ids = append(ids, requirement.ID)
} }
// Fill tags
err = fetchTags(ctx, r.db, tagObjectKindRequirement, ids, func(id int, tag string) {
for i := range requirements {
if id == requirements[i].ID {
requirements[i].Tags = append(requirements[i].Tags, tag)
break
}
}
})
if err != nil {
return nil, nil, err
}
query, args, err = squirrel.Select("project_requirement_id, stat_id, required"). query, args, err = squirrel.Select("project_requirement_id, stat_id, required").
From("project_requirement_stat"). From("project_requirement_stat").
Where(squirrel.Eq{"project_requirement_id": requirementIDs}). Where(squirrel.Eq{"project_requirement_id": requirementIDs}).
@ -270,6 +400,7 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
} }
requirements := make([]entities.Requirement, 0, len(reqRows)) requirements := make([]entities.Requirement, 0, len(reqRows))
ids := make([]int, 0, len(reqRows))
for _, row := range reqRows { for _, row := range reqRows {
requirements = append(requirements, entities.Requirement{ requirements = append(requirements, entities.Requirement{
ID: row.ID, ID: row.ID,
@ -280,7 +411,22 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
IsCoarse: row.IsCoarse, IsCoarse: row.IsCoarse,
AggregateRequired: row.AggregateRequired, AggregateRequired: row.AggregateRequired,
Status: models.Status(row.Status), Status: models.Status(row.Status),
Tags: []string{},
}) })
ids = append(ids, row.ID)
}
// Fill tags
err = fetchTags(ctx, r.db, tagObjectKindRequirement, ids, func(id int, tag string) {
for i := range requirements {
if id == requirements[i].ID {
requirements[i].Tags = append(requirements[i].Tags, tag)
break
}
}
})
if err != nil {
return nil, nil, err
} }
stats := make([]entities.RequirementStat, 0, len(statsRows)) stats := make([]entities.RequirementStat, 0, len(statsRows))
@ -296,7 +442,14 @@ func (r *projectRepository) ListRequirements(ctx context.Context, projectID int)
} }
func (r *projectRepository) CreateRequirement(ctx context.Context, requirement entities.Requirement) (*entities.Requirement, error) { func (r *projectRepository) CreateRequirement(ctx context.Context, requirement entities.Requirement) (*entities.Requirement, error) {
res, err := r.q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
q := r.q.WithTx(tx)
res, err := q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{
ScopeID: requirement.ScopeID, ScopeID: requirement.ScopeID,
ProjectID: requirement.ProjectID, ProjectID: requirement.ProjectID,
Name: requirement.Name, Name: requirement.Name,
@ -314,6 +467,22 @@ func (r *projectRepository) CreateRequirement(ctx context.Context, requirement e
return nil, err return nil, err
} }
for _, tag := range requirement.Tags {
err := q.InsertTag(ctx, mysqlcore.InsertTagParams{
ObjectKind: tagObjectKindRequirement,
ObjectID: int(id),
TagName: tag,
})
if err != nil {
return nil, err
}
}
err = tx.Commit()
if err != nil {
return nil, err
}
requirement.ID = int(id) requirement.ID = int(id)
return &requirement, nil return &requirement, nil
} }
@ -358,6 +527,14 @@ func (r *projectRepository) DeleteRequirement(ctx context.Context, requirement e
return err return err
} }
err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
ObjectKind: tagObjectKindRequirement,
ObjectID: requirement.ID,
})
if err != nil {
return err
}
return tx.Commit() return tx.Commit()
} }

31
usecases/items/service.go

@ -314,34 +314,9 @@ func (s *Service) Update(ctx context.Context, id int, update models.ItemUpdate,
return nil, err return nil, err
} }
for _, tag := range update.AddTags {
if !validate.Tag(tag) {
return nil, models.BadInputError{
Object: "ItemInput",
Field: "addTags",
Problem: fmt.Sprintf("Invalid tag: %s", tag),
Element: tag,
}
}
if item.HasTag(tag) {
return nil, models.BadInputError{
Object: "ItemUpdate",
Field: "addTags",
Problem: fmt.Sprintf("The tag %s already exists!", tag),
Element: tag,
}
}
}
for _, tag := range update.RemoveTags {
if !item.HasTag(tag) {
return nil, models.BadInputError{
Object: "ItemUpdate",
Field: "removeTags",
Problem: fmt.Sprintf("The tag %s does not exist!", tag),
Element: tag,
}
}
err = validate.Tags(item.Tags, update.AddTags, update.RemoveTags)
if err != nil {
return nil, err
} }
if update.RequirementID != nil { if update.RequirementID != nil {

1
usecases/projects/repository.go

@ -9,6 +9,7 @@ import (
type Repository interface { type Repository interface {
Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error) Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error)
List(ctx context.Context, scopeID int) ([]entities.Project, error) List(ctx context.Context, scopeID int) ([]entities.Project, error)
ListByTags(ctx context.Context, scopeID int, tags []string) ([]entities.Project, error)
FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error) FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error)
Insert(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 Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error

4
usecases/projects/result.go

@ -18,6 +18,7 @@ type Entry struct {
Status models.Status `json:"status"` Status models.Status `json:"status"`
StatusName string `json:"statusName"` StatusName string `json:"statusName"`
OwnerName string `json:"ownerName,omitempty"` OwnerName string `json:"ownerName,omitempty"`
Tags []string `json:"tags"`
} }
func generateEntry(project entities.Project, scope scopes.Result) Entry { func generateEntry(project entities.Project, scope scopes.Result) Entry {
@ -27,6 +28,7 @@ func generateEntry(project entities.Project, scope scopes.Result) Entry {
CreatedTime: project.CreatedTime, CreatedTime: project.CreatedTime,
Name: project.Name, Name: project.Name,
Status: project.Status, Status: project.Status,
Tags: project.Tags,
StatusName: scope.StatusName(project.Status), StatusName: scope.StatusName(project.Status),
OwnerName: scope.MemberName(project.OwnerID), OwnerName: scope.MemberName(project.OwnerID),
} }
@ -65,6 +67,7 @@ type RequirementResult struct {
AggregateRequired int `json:"aggregateRequired"` AggregateRequired int `json:"aggregateRequired"`
Stats []RequirementResultStat `json:"stats"` Stats []RequirementResultStat `json:"stats"`
Items []items.Result `json:"items"` Items []items.Result `json:"items"`
Tags []string `json:"tags"`
Requirement entities.Requirement `json:"-"` Requirement entities.Requirement `json:"-"`
} }
@ -171,6 +174,7 @@ func generateRequirementResult(req entities.Requirement, scope scopes.Result, re
AggregateRequired: req.AggregateRequired, AggregateRequired: req.AggregateRequired,
Stats: make([]RequirementResultStat, 0, 8), Stats: make([]RequirementResultStat, 0, 8),
Items: make([]items.Result, 0, 8), Items: make([]items.Result, 0, 8),
Tags: req.Tags,
Requirement: req, Requirement: req,
} }

30
usecases/projects/service.go

@ -4,6 +4,7 @@ import (
"context" "context"
"git.aiterp.net/stufflog3/stufflog3/entities" "git.aiterp.net/stufflog3/stufflog3/entities"
"git.aiterp.net/stufflog3/stufflog3/internal/genutils" "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/models"
"git.aiterp.net/stufflog3/stufflog3/usecases/auth" "git.aiterp.net/stufflog3/stufflog3/usecases/auth"
"git.aiterp.net/stufflog3/stufflog3/usecases/items" "git.aiterp.net/stufflog3/stufflog3/usecases/items"
@ -66,6 +67,20 @@ func (s *Service) List(ctx context.Context) ([]Entry, error) {
return entries, nil 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) { func (s *Service) FetchRequirements(ctx context.Context, ids ...int) ([]RequirementResult, error) {
sc := s.Scopes.Context(ctx) sc := s.Scopes.Context(ctx)
@ -111,6 +126,11 @@ func (s *Service) Create(ctx context.Context, project entities.Project) (*Result
} }
} }
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. // 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() { if project.CreatedTime.IsZero() {
project.CreatedTime = time.Now() project.CreatedTime = time.Now()
@ -196,6 +216,11 @@ func (s *Service) CreateRequirement(ctx context.Context, id int, requirement ent
} }
} }
err := validate.Tags(nil, requirement.Tags, nil)
if err != nil {
return nil, err
}
project, err := s.Find(ctx, id) project, err := s.Find(ctx, id)
if err != nil { if err != nil {
return nil, err return nil, err
@ -250,6 +275,11 @@ func (s *Service) UpdateRequirement(ctx context.Context, projectID, requirementI
return nil, models.NotFoundError("Requirement") 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) err = s.Repository.UpdateRequirement(ctx, req.Requirement, update)
if err != nil { if err != nil {
return nil, err return nil, err

Loading…
Cancel
Save