Let the knowledge base list its records by kind, status, topic or a flag (issue 1064)
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
package main
|
||||
|
||||
// list (novox/hq issue 1064): one short row per record, filtered by kind, status, topic or a flag, and paged
|
||||
// newest first, number by number, like issue-tracker.list.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The flags list may be asked for.
|
||||
const (
|
||||
FlagResearchDone = "research-done"
|
||||
FlagProposedOld = "proposed-old"
|
||||
)
|
||||
|
||||
// ListFlags are the flags list takes.
|
||||
var ListFlags = []string{FlagResearchDone, FlagProposedOld}
|
||||
|
||||
// ProposedOldAfter is how long a proposal may sit at proposed before flag proposed-old names it.
|
||||
const ProposedOldAfter = 24 * time.Hour
|
||||
|
||||
// MostListResult is the most rows list may answer in one call.
|
||||
const MostListResult = 500
|
||||
|
||||
// AllStatuses are every status a record of either kind may hold.
|
||||
var AllStatuses = []string{StatusReserved, StatusProposed, StatusAccepted, StatusSuperseded, StatusWithdrawn,
|
||||
StatusActive, StatusGraduated, StatusAbandoned}
|
||||
|
||||
// ListAsk narrows a list.
|
||||
type ListAsk struct {
|
||||
Kind string
|
||||
Status string
|
||||
Topic string
|
||||
// Flag keeps only the research efforts done (research-done) or the proposals grown old (proposed-old).
|
||||
Flag string
|
||||
// Before keeps only the records numbered below it: the cursor that pages the newest-first list.
|
||||
Before int
|
||||
Limit int
|
||||
}
|
||||
|
||||
// ListRow is one record in a list: short enough to read in a chat.
|
||||
type ListRow struct {
|
||||
// Number is the record's number as the records print it: "0276" for a decision, "research 045" for
|
||||
// research.
|
||||
Number string `json:"number"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Topic string `json:"topic,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
Versions int `json:"versions"`
|
||||
// Pending says a proposal waits for the knowledge base's own review or the operator's Approve.
|
||||
Pending bool `json:"pending,omitempty"`
|
||||
}
|
||||
|
||||
// Listed is the answer to a list.
|
||||
type Listed struct {
|
||||
Rows []ListRow `json:"rows"`
|
||||
Matched int `json:"matched"`
|
||||
// Next is the before that continues the list where this page ends; 0 when nothing more matches.
|
||||
Next int `json:"next,omitempty"`
|
||||
Said string `json:"said"`
|
||||
}
|
||||
|
||||
// rowNumber is a record's number as list's row prints it.
|
||||
func rowNumber(kind string, number int) string {
|
||||
if kind == KindResearch {
|
||||
return fmt.Sprintf("research %03d", number)
|
||||
}
|
||||
return fmt.Sprintf("%04d", number)
|
||||
}
|
||||
|
||||
// List is the records that match, newest first, number by number (novox/hq issue 1064).
|
||||
func (k *KB) List(a ListAsk) (*Listed, error) {
|
||||
if a.Kind != "" && !contains(Kinds, a.Kind) {
|
||||
return nil, fmt.Errorf("kind is decision or research, not %q", a.Kind)
|
||||
}
|
||||
if a.Status != "" && !contains(AllStatuses, a.Status) {
|
||||
return nil, fmt.Errorf("status %q is not one of %s", a.Status, strings.Join(AllStatuses, ", "))
|
||||
}
|
||||
if a.Flag != "" && !contains(ListFlags, a.Flag) {
|
||||
return nil, fmt.Errorf("flag is %s, not %q", strings.Join(ListFlags, " or "), a.Flag)
|
||||
}
|
||||
limit := a.Limit
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
limit = min(limit, MostListResult)
|
||||
ctx := context.Background()
|
||||
all, err := k.Store.All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byKey := map[Key]*Entry{}
|
||||
for _, e := range all {
|
||||
byKey[Key{e.Current.Kind, e.Current.Number}] = e
|
||||
}
|
||||
keys := make([]Key, 0, len(byKey))
|
||||
for key := range byKey {
|
||||
if a.Before == 0 || key.Number < a.Before {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if keys[i].Number != keys[j].Number {
|
||||
return keys[i].Number > keys[j].Number
|
||||
}
|
||||
return keys[i].Kind < keys[j].Kind
|
||||
})
|
||||
out := &Listed{Rows: []ListRow{}}
|
||||
var lastNumber int
|
||||
for _, key := range keys {
|
||||
e := byKey[key]
|
||||
r := e.Current
|
||||
if a.Kind != "" && r.Kind != a.Kind {
|
||||
continue
|
||||
}
|
||||
if a.Status != "" && r.Status != a.Status {
|
||||
continue
|
||||
}
|
||||
if a.Topic != "" && r.Topic != a.Topic {
|
||||
continue
|
||||
}
|
||||
if a.Flag != "" {
|
||||
ok, err := k.matchesListFlag(ctx, a.Flag, key, e, byKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
out.Matched++
|
||||
if len(out.Rows) < limit {
|
||||
out.Rows = append(out.Rows, ListRow{
|
||||
Number: rowNumber(r.Kind, r.Number), Kind: r.Kind, Title: r.Title, Status: r.Status,
|
||||
Topic: r.Topic, Date: r.Date, Versions: e.Versions, Pending: e.Pending != nil || r.Ask != nil,
|
||||
})
|
||||
lastNumber = key.Number
|
||||
}
|
||||
}
|
||||
below := ""
|
||||
if a.Before != 0 {
|
||||
below = fmt.Sprintf(" below %d", a.Before)
|
||||
}
|
||||
switch {
|
||||
case out.Matched == 0:
|
||||
out.Said = "No record matches" + below + "."
|
||||
case out.Matched > len(out.Rows):
|
||||
out.Next = lastNumber
|
||||
out.Said = fmt.Sprintf("%d records match%s; the newest %d are shown. The next page is before %d.", out.Matched,
|
||||
below, len(out.Rows), out.Next)
|
||||
default:
|
||||
out.Said = fmt.Sprintf("%d records match%s, newest first.", out.Matched, below)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// matchesListFlag says whether an entry matches a list flag.
|
||||
func (k *KB) matchesListFlag(ctx context.Context, flag string, key Key, e *Entry, byKey map[Key]*Entry) (bool, error) {
|
||||
switch flag {
|
||||
case FlagResearchDone:
|
||||
return researchDone(e, byKey), nil
|
||||
case FlagProposedOld:
|
||||
return k.proposedOld(ctx, key, e)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// researchDone says whether a research effort is active, names what it became in at least one became link to a
|
||||
// decision (a became to a design's path is ignored), and every decision it names that way is accepted: a
|
||||
// candidate for graduated (novox/hq issue 1064).
|
||||
func researchDone(e *Entry, byKey map[Key]*Entry) bool {
|
||||
r := e.Current
|
||||
if r.Kind != KindResearch || r.Status != StatusActive {
|
||||
return false
|
||||
}
|
||||
any := false
|
||||
for _, l := range r.Links {
|
||||
if l.Kind != LinkBecame {
|
||||
continue
|
||||
}
|
||||
target, isRecord := l.To.Record()
|
||||
if !isRecord || target.Kind != KindDecision {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
d := byKey[target]
|
||||
if d == nil || d.Current.Status != StatusAccepted {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return any
|
||||
}
|
||||
|
||||
// proposedOld says whether a record is proposed and its current version is older than ProposedOldAfter, by the
|
||||
// KB's clock (novox/hq issue 1064).
|
||||
func (k *KB) proposedOld(ctx context.Context, key Key, e *Entry) (bool, error) {
|
||||
if e.Current.Status != StatusProposed {
|
||||
return false, nil
|
||||
}
|
||||
vs, err := k.Store.Versions(ctx, key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, v := range vs {
|
||||
if v.N == e.CurrentVersion {
|
||||
return k.now().Sub(v.At) > ProposedOldAfter, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user