From 995504d969eaac2ada37f80eba332349ff4ec058 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sun, 20 Nov 2022 16:20:46 +0100 Subject: [PATCH] add tags to sprints. --- entities/sprint.go | 9 +++ .../lib/components/scope/SprintBody.svelte | 3 +- .../lib/modals/SprintCreateUpdateModal.svelte | 5 ++ frontend/src/lib/models/sprint.ts | 3 + models/sprint.go | 1 + ports/mysql/db.go | 19 ++++- ports/mysql/items.go | 4 ++ ports/mysql/mysqlcore/models.go | 1 + ports/mysql/mysqlcore/sprint.sql.go | 69 +++++++++++++------ ports/mysql/queries/sprint.sql | 62 +++++++++++------ ports/mysql/sprint.go | 5 ++ .../20221120155110_sprint_tags.sql | 9 +++ usecases/sprints/service.go | 23 ++++++- 13 files changed, 167 insertions(+), 46 deletions(-) create mode 100644 scripts/goose-mysql/20221120155110_sprint_tags.sql diff --git a/entities/sprint.go b/entities/sprint.go index b151b31..60357d3 100644 --- a/entities/sprint.go +++ b/entities/sprint.go @@ -12,6 +12,8 @@ type Sprint struct { Description string `json:"description"` Kind models.SprintKind `json:"kind"` + Tags []string `json:"tags"` + FromTime time.Time `json:"fromTime"` ToTime time.Time `json:"toTime"` @@ -58,4 +60,11 @@ func (sprint *Sprint) ApplyUpdate(update models.SprintUpdate) { if update.AggregateRequired != nil { sprint.AggregateRequired = *update.AggregateRequired } + if update.Tags != nil { + if len(update.Tags) == 0 { + sprint.Tags = nil + } else { + sprint.Tags = update.Tags + } + } } diff --git a/frontend/src/lib/components/scope/SprintBody.svelte b/frontend/src/lib/components/scope/SprintBody.svelte index dd892f6..7d011c3 100644 --- a/frontend/src/lib/components/scope/SprintBody.svelte +++ b/frontend/src/lib/components/scope/SprintBody.svelte @@ -3,7 +3,7 @@ import { SprintKind } from "$lib/models/sprint"; import Amount from "../common/Amount.svelte"; import AmountRow from "../common/AmountRow.svelte"; - import BurndownChart from "../common/BurndownChart.svelte"; + import BurndownChart from "../common/BurndownChart.svelte"; import LabeledProgress from "../common/LabeledProgress.svelte"; import LabeledProgressRow from "../common/LabeledProgressRow.svelte"; import Markdown from "../common/Markdown.svelte"; @@ -20,7 +20,6 @@ timeRange = void(0); } - {#if sprint.kind === SprintKind.Items} diff --git a/frontend/src/lib/modals/SprintCreateUpdateModal.svelte b/frontend/src/lib/modals/SprintCreateUpdateModal.svelte index 6be6b98..07c8590 100644 --- a/frontend/src/lib/modals/SprintCreateUpdateModal.svelte +++ b/frontend/src/lib/modals/SprintCreateUpdateModal.svelte @@ -9,6 +9,7 @@ import { getSprintListContext } from "$lib/components/contexts/SprintListContext.svelte"; import PartInput from "$lib/components/controls/PartInput.svelte"; import SprintKindSelect from "$lib/components/controls/SprintKindSelect.svelte"; + import TagInput from "$lib/components/controls/TagInput.svelte"; import TimeRangeInput from "$lib/components/controls/TimeRangeInput.svelte"; import Checkbox from "$lib/components/layout/Checkbox.svelte"; import type Scope from "$lib/models/scope"; @@ -61,6 +62,7 @@ isTimed: false, isUnweighted: false, parts: [], + tags: [], } intervalName = "this_month"; scopeId = scope.id; @@ -82,6 +84,7 @@ isCoarse: current.isCoarse, isTimed: current.isTimed, isUnweighted: current.isUnweighted, + tags: [...(current.tags||[])], }; sprintId = current.id; scopeId = current.scopeId; @@ -154,6 +157,8 @@ + + {#if sprint.kind != SprintKind.Items} diff --git a/frontend/src/lib/models/sprint.ts b/frontend/src/lib/models/sprint.ts index 8d58137..6cba3aa 100644 --- a/frontend/src/lib/models/sprint.ts +++ b/frontend/src/lib/models/sprint.ts @@ -8,6 +8,8 @@ export default interface Sprint { description: string kind: SprintKind + tags?: string[] + fromTime: string toTime: string @@ -56,6 +58,7 @@ export interface SprintInput { aggregateName?: string aggregateRequired?: number parts?: SprintInputPart[] + tags?: string[] } export interface SprintInputPart { diff --git a/models/sprint.go b/models/sprint.go index 1233f17..f346f88 100644 --- a/models/sprint.go +++ b/models/sprint.go @@ -12,6 +12,7 @@ type SprintUpdate struct { IsUnweighted *bool `json:"isUnweighted"` AggregateName *string `json:"aggregateName"` AggregateRequired *int `json:"aggregateRequired"` + Tags []string `json:"tags"` } // SprintKind decides the composition of stat bars (SB) and what objects are included in the result (R) diff --git a/ports/mysql/db.go b/ports/mysql/db.go index 8567fbf..00aa107 100644 --- a/ports/mysql/db.go +++ b/ports/mysql/db.go @@ -14,6 +14,7 @@ import ( "git.aiterp.net/stufflog3/stufflog3/usecases/sprints" "git.aiterp.net/stufflog3/stufflog3/usecases/stats" "github.com/Masterminds/squirrel" + "strings" "time" _ "github.com/go-sql-driver/mysql" @@ -98,6 +99,14 @@ func intPtr(nullInt32 sql.NullInt32) *int { } } +func csvPtr(nullString sql.NullString) []string { + if !nullString.Valid { + return nil + } + + return strings.Split(nullString.String, ",") +} + func sqlTimePtr(ptr *time.Time) sql.NullTime { if ptr != nil { return sql.NullTime{Time: *ptr, Valid: true} @@ -132,6 +141,14 @@ func sqlJsonPtr(ptr interface{}) sqltypes.NullRawMessage { } } +func sqlCsvPtr(a []string) sql.NullString { + if len(a) == 0 { + return sql.NullString{Valid: false} + } + + return sql.NullString{Valid: true, String: strings.Join(a, ",")} +} + const ( tagObjectKindItem = iota tagObjectKindRequirement @@ -161,7 +178,7 @@ func fetchTags(ctx context.Context, db *sql.DB, kind int, ids []int, cb func(id cb(objectID, tagStr) } - + if err := rows.Close(); err != nil { return err } diff --git a/ports/mysql/items.go b/ports/mysql/items.go index c806135..39735a1 100644 --- a/ports/mysql/items.go +++ b/ports/mysql/items.go @@ -104,6 +104,10 @@ func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([ filter.IDs = genutils.RetainInPlace(filter.IDs, func(id int) bool { return matches[id] == len(filter.Tags) }) + + if len(filter.IDs) == 0 { + return []entities.Item{}, nil + } } sq := squirrel.Select( diff --git a/ports/mysql/mysqlcore/models.go b/ports/mysql/mysqlcore/models.go index 3df7587..2db8ec9 100644 --- a/ports/mysql/mysqlcore/models.go +++ b/ports/mysql/mysqlcore/models.go @@ -84,6 +84,7 @@ type Sprint struct { Kind int AggregateName string AggregateRequired int + TagsCsv sql.NullString } type SprintPart struct { diff --git a/ports/mysql/mysqlcore/sprint.sql.go b/ports/mysql/mysqlcore/sprint.sql.go index 51a979b..07ea7b5 100644 --- a/ports/mysql/mysqlcore/sprint.sql.go +++ b/ports/mysql/mysqlcore/sprint.sql.go @@ -12,7 +12,9 @@ import ( ) const deleteAllScopeSprints = `-- name: DeleteAllScopeSprints :exec -DELETE FROM sprint WHERE scope_id = ? +DELETE +FROM sprint +WHERE scope_id = ? ` func (q *Queries) DeleteAllScopeSprints(ctx context.Context, scopeID int) error { @@ -21,7 +23,9 @@ func (q *Queries) DeleteAllScopeSprints(ctx context.Context, scopeID int) error } const deleteAllSprintParts = `-- name: DeleteAllSprintParts :exec -DELETE FROM sprint_part WHERE sprint_id = ? +DELETE +FROM sprint_part +WHERE sprint_id = ? ` func (q *Queries) DeleteAllSprintParts(ctx context.Context, sprintID int) error { @@ -30,7 +34,9 @@ func (q *Queries) DeleteAllSprintParts(ctx context.Context, sprintID int) error } const deleteSprint = `-- name: DeleteSprint :exec -DELETE FROM sprint WHERE id = ? +DELETE +FROM sprint +WHERE id = ? ` func (q *Queries) DeleteSprint(ctx context.Context, id int) error { @@ -39,7 +45,10 @@ func (q *Queries) DeleteSprint(ctx context.Context, id int) error { } const deleteSprintPart = `-- name: DeleteSprintPart :exec -DELETE FROM sprint_part WHERE sprint_id = ? AND object_id = ? +DELETE +FROM sprint_part +WHERE sprint_id = ? + AND object_id = ? ` type DeleteSprintPartParams struct { @@ -53,7 +62,10 @@ func (q *Queries) DeleteSprintPart(ctx context.Context, arg DeleteSprintPartPara } const getSprint = `-- name: GetSprint :one -SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint WHERE id = ? AND scope_id = ? +SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required, tags_csv +FROM sprint +WHERE id = ? + AND scope_id = ? ` type GetSprintParams struct { @@ -77,15 +89,16 @@ func (q *Queries) GetSprint(ctx context.Context, arg GetSprintParams) (Sprint, e &i.Kind, &i.AggregateName, &i.AggregateRequired, + &i.TagsCsv, ) return i, err } const insertSprint = `-- name: InsertSprint :execresult -INSERT INTO sprint ( - scope_id, name, description, kind, from_time, to_time, - is_timed, is_coarse, aggregate_name, aggregate_required, is_unweighted -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +INSERT INTO sprint (scope_id, name, description, kind, from_time, to_time, + is_timed, is_coarse, aggregate_name, aggregate_required, + is_unweighted, tags_csv) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` type InsertSprintParams struct { @@ -100,6 +113,7 @@ type InsertSprintParams struct { AggregateName string AggregateRequired int IsUnweighted bool + TagsCsv sql.NullString } func (q *Queries) InsertSprint(ctx context.Context, arg InsertSprintParams) (sql.Result, error) { @@ -115,11 +129,14 @@ func (q *Queries) InsertSprint(ctx context.Context, arg InsertSprintParams) (sql arg.AggregateName, arg.AggregateRequired, arg.IsUnweighted, + arg.TagsCsv, ) } const listSprintParts = `-- name: ListSprintParts :many -SELECT sprint_id, object_id, required FROM sprint_part WHERE sprint_id = ? +SELECT sprint_id, object_id, required +FROM sprint_part +WHERE sprint_id = ? ` func (q *Queries) ListSprintParts(ctx context.Context, sprintID int) ([]SprintPart, error) { @@ -146,7 +163,11 @@ func (q *Queries) ListSprintParts(ctx context.Context, sprintID int) ([]SprintPa } const listSprintsAt = `-- name: ListSprintsAt :many -SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint WHERE scope_id = ? AND from_time <= ? AND to_time > ? +SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required, tags_csv +FROM sprint +WHERE scope_id = ? + AND from_time <= ? + AND to_time > ? ` type ListSprintsAtParams struct { @@ -176,6 +197,7 @@ func (q *Queries) ListSprintsAt(ctx context.Context, arg ListSprintsAtParams) ([ &i.Kind, &i.AggregateName, &i.AggregateRequired, + &i.TagsCsv, ); err != nil { return nil, err } @@ -191,7 +213,8 @@ func (q *Queries) ListSprintsAt(ctx context.Context, arg ListSprintsAtParams) ([ } const listSprintsBetween = `-- name: ListSprintsBetween :many -SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required FROM sprint +SELECT id, scope_id, name, description, from_time, to_time, is_timed, is_coarse, is_unweighted, kind, aggregate_name, aggregate_required, tags_csv +FROM sprint WHERE scope_id = ? AND from_time < ? AND to_time >= ? @@ -226,6 +249,7 @@ func (q *Queries) ListSprintsBetween(ctx context.Context, arg ListSprintsBetween &i.Kind, &i.AggregateName, &i.AggregateRequired, + &i.TagsCsv, ); err != nil { return nil, err } @@ -258,15 +282,16 @@ func (q *Queries) ReplaceSprintPart(ctx context.Context, arg ReplaceSprintPartPa const updateSprint = `-- name: UpdateSprint :exec UPDATE sprint -SET name = ?, - description = ?, - from_time = ?, - to_time = ?, - is_timed = ?, - is_coarse = ?, - is_unweighted = ?, - aggregate_name = ?, - aggregate_required = ? +SET name = ?, + description = ?, + from_time = ?, + to_time = ?, + is_timed = ?, + is_coarse = ?, + is_unweighted = ?, + aggregate_name = ?, + aggregate_required = ?, + tags_csv = ? WHERE id = ? ` @@ -280,6 +305,7 @@ type UpdateSprintParams struct { IsUnweighted bool AggregateName string AggregateRequired int + TagsCsv sql.NullString ID int } @@ -294,6 +320,7 @@ func (q *Queries) UpdateSprint(ctx context.Context, arg UpdateSprintParams) erro arg.IsUnweighted, arg.AggregateName, arg.AggregateRequired, + arg.TagsCsv, arg.ID, ) return err diff --git a/ports/mysql/queries/sprint.sql b/ports/mysql/queries/sprint.sql index 5fb7a7b..5aa6ee1 100644 --- a/ports/mysql/queries/sprint.sql +++ b/ports/mysql/queries/sprint.sql @@ -1,50 +1,70 @@ -- name: ListSprintsAt :many -SELECT * FROM sprint WHERE scope_id = ? AND from_time <= sqlc.arg(time) AND to_time > sqlc.arg(time); +SELECT * +FROM sprint +WHERE scope_id = ? + AND from_time <= sqlc.arg(time) + AND to_time > sqlc.arg(time); -- name: ListSprintsBetween :many -SELECT * FROM sprint +SELECT * +FROM sprint WHERE scope_id = sqlc.arg(scope_id) AND from_time < sqlc.arg(to_time) AND to_time >= sqlc.arg(from_time) ORDER BY from_time, name; -- name: GetSprint :one -SELECT * FROM sprint WHERE id = ? AND scope_id = ?; +SELECT * +FROM sprint +WHERE id = ? + AND scope_id = ?; -- name: InsertSprint :execresult -INSERT INTO sprint ( - scope_id, name, description, kind, from_time, to_time, - is_timed, is_coarse, aggregate_name, aggregate_required, is_unweighted -) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); +INSERT INTO sprint (scope_id, name, description, kind, from_time, to_time, + is_timed, is_coarse, aggregate_name, aggregate_required, + is_unweighted, tags_csv) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); -- name: UpdateSprint :exec UPDATE sprint -SET name = ?, - description = ?, - from_time = ?, - to_time = ?, - is_timed = ?, - is_coarse = ?, - is_unweighted = ?, - aggregate_name = ?, - aggregate_required = ? +SET name = ?, + description = ?, + from_time = ?, + to_time = ?, + is_timed = ?, + is_coarse = ?, + is_unweighted = ?, + aggregate_name = ?, + aggregate_required = ?, + tags_csv = ? WHERE id = ?; -- name: DeleteSprint :exec -DELETE FROM sprint WHERE id = ?; +DELETE +FROM sprint +WHERE id = ?; -- name: DeleteAllScopeSprints :exec -DELETE FROM sprint WHERE scope_id = ?; +DELETE +FROM sprint +WHERE scope_id = ?; -- name: ListSprintParts :many -SELECT * FROM sprint_part WHERE sprint_id = ?; +SELECT * +FROM sprint_part +WHERE sprint_id = ?; -- name: ReplaceSprintPart :exec REPLACE INTO sprint_part (sprint_id, object_id, required) VALUES (?, ?, ?); -- name: DeleteSprintPart :exec -DELETE FROM sprint_part WHERE sprint_id = ? AND object_id = ?; +DELETE +FROM sprint_part +WHERE sprint_id = ? + AND object_id = ?; -- name: DeleteAllSprintParts :exec -DELETE FROM sprint_part WHERE sprint_id = ?; +DELETE +FROM sprint_part +WHERE sprint_id = ?; diff --git a/ports/mysql/sprint.go b/ports/mysql/sprint.go index 3f4502f..6f43913 100644 --- a/ports/mysql/sprint.go +++ b/ports/mysql/sprint.go @@ -39,6 +39,7 @@ func (r *sprintRepository) Find(ctx context.Context, scopeID, sprintID int) (*en IsUnweighted: row.IsUnweighted, AggregateName: row.AggregateName, AggregateRequired: row.AggregateRequired, + Tags: csvPtr(row.TagsCsv), }, nil } @@ -67,6 +68,7 @@ func (r *sprintRepository) ListAt(ctx context.Context, scopeID int, at time.Time IsUnweighted: row.IsUnweighted, AggregateName: row.AggregateName, AggregateRequired: row.AggregateRequired, + Tags: csvPtr(row.TagsCsv), }) } @@ -102,6 +104,7 @@ func (r *sprintRepository) ListBetween(ctx context.Context, scopeID int, from, t IsUnweighted: row.IsUnweighted, AggregateName: row.AggregateName, AggregateRequired: row.AggregateRequired, + Tags: csvPtr(row.TagsCsv), }) } @@ -121,6 +124,7 @@ func (r *sprintRepository) Insert(ctx context.Context, sprint entities.Sprint) ( AggregateName: sprint.AggregateName, AggregateRequired: sprint.AggregateRequired, IsUnweighted: sprint.IsUnweighted, + TagsCsv: sqlCsvPtr(sprint.Tags), }) if err != nil { return nil, err @@ -147,6 +151,7 @@ func (r *sprintRepository) Update(ctx context.Context, sprint entities.Sprint, u IsUnweighted: sprint.IsUnweighted, AggregateName: sprint.AggregateName, AggregateRequired: sprint.AggregateRequired, + TagsCsv: sqlCsvPtr(sprint.Tags), ID: sprint.ID, }) diff --git a/scripts/goose-mysql/20221120155110_sprint_tags.sql b/scripts/goose-mysql/20221120155110_sprint_tags.sql new file mode 100644 index 0000000..6efc6d0 --- /dev/null +++ b/scripts/goose-mysql/20221120155110_sprint_tags.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE sprint ADD COLUMN tags_csv VARCHAR(1024) NULL; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE sprint DROP COLUMN tags_csv; +-- +goose StatementEnd diff --git a/usecases/sprints/service.go b/usecases/sprints/service.go index 07db561..64fa111 100644 --- a/usecases/sprints/service.go +++ b/usecases/sprints/service.go @@ -214,6 +214,14 @@ func (s *Service) Create(ctx context.Context, sprint entities.Sprint, parts []en } } } + + if len(sprint.Tags) == 0 { + return nil, models.BadInputError{ + Object: "SprintInput", + Field: "tags", + Problem: "Item sprints cannot have tags.", + } + } } case models.SprintKindRequirements: { @@ -455,7 +463,18 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti res.Requirements = includedRequirements for _, req := range res.Requirements { - res.Items = append(res.Items, req.Items...) + for _, item := range req.Items { + disqualified := false + for _, tag := range sprint.Tags { + if !item.HasTag(tag) { + disqualified = true + break + } + } + if !disqualified { + res.Items = append(res.Items, item) + } + } } for _, stat := range allStats { @@ -513,6 +532,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime}, StatIDs: partIDs, ScopeIDs: []int{sprint.ScopeID}, + Tags: sprint.Tags, }) if err != nil { return nil, err @@ -523,6 +543,7 @@ func (s *Service) fill(ctx context.Context, sprint entities.Sprint, parts []enti acquiredItems, err := s.Items.ListScoped(ctx, models.ItemFilter{ AcquiredTime: &models.TimeInterval[time.Time]{Min: sprint.FromTime, Max: sprint.ToTime}, ScopeIDs: []int{sprint.ScopeID}, + Tags: sprint.Tags, }) if err != nil { return nil, err