Browse Source

add tags to sprints.

master
Gisle Aune 1 year ago
parent
commit
995504d969
  1. 9
      entities/sprint.go
  2. 3
      frontend/src/lib/components/scope/SprintBody.svelte
  3. 5
      frontend/src/lib/modals/SprintCreateUpdateModal.svelte
  4. 3
      frontend/src/lib/models/sprint.ts
  5. 1
      models/sprint.go
  6. 19
      ports/mysql/db.go
  7. 4
      ports/mysql/items.go
  8. 1
      ports/mysql/mysqlcore/models.go
  9. 69
      ports/mysql/mysqlcore/sprint.sql.go
  10. 62
      ports/mysql/queries/sprint.sql
  11. 5
      ports/mysql/sprint.go
  12. 9
      scripts/goose-mysql/20221120155110_sprint_tags.sql
  13. 23
      usecases/sprints/service.go

9
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
}
}
}

3
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);
}
</script>
<Markdown source={sprint.description} />
{#if sprint.kind === SprintKind.Items}
<LabeledProgress timeRange={timeRange} green name={sprint.aggregateName || "Items"} count={sprint.itemsAcquired} target={sprint.itemsRequired} />

5
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 @@
<TimeRangeInput openDate={openedDate} bind:from={sprint.fromTime} bind:to={sprint.toTime} bind:intervalName={intervalName} />
<label for="aggregateName">Aggregate Name</label>
<input name="aggregateName" type="text" bind:value={sprint.aggregateName} />
<label for="tags">Tags</label>
<TagInput bind:value={sprint.tags} />
{#if sprint.kind != SprintKind.Items}
<label for="aggregateValue">Aggregate Goal</label>
<input name="aggregateValue" type="number" bind:value={sprint.aggregateRequired} />

3
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 {

1
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)

19
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
}

4
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(

1
ports/mysql/mysqlcore/models.go

@ -84,6 +84,7 @@ type Sprint struct {
Kind int
AggregateName string
AggregateRequired int
TagsCsv sql.NullString
}
type SprintPart struct {

69
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

62
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 = ?;

5
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,
})

9
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

23
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

Loading…
Cancel
Save