217 lines
6.2 KiB
Go
217 lines
6.2 KiB
Go
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
|
|
}
|