2023-12-27 03:06:11 +01:00
|
|
|
package main
|
|
|
|
|
2023-12-27 03:46:20 +01:00
|
|
|
/*
|
|
|
|
This file is part of taler-dashboard
|
|
|
|
Copyright (C) 2023 Özgür Kesim
|
|
|
|
|
|
|
|
taler-dashboard is free software; you can redistribute it and/or modify it
|
|
|
|
under the terms of the GNU Affero General Public License as published by the
|
|
|
|
Free Software Foundation; either version 3, or (at your option) any later
|
|
|
|
version.
|
|
|
|
|
|
|
|
taler-dashboard is distributed in the hope that it will be useful, but
|
|
|
|
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
|
|
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
|
|
License for more details.
|
|
|
|
|
|
|
|
You can receive a copy of the GNU Affero General Public License from
|
|
|
|
<http://www.gnu.org/licenses/>
|
|
|
|
|
|
|
|
@author Özgür Kesim <oec-taler@kesim.org>
|
|
|
|
*/
|
|
|
|
|
2023-12-27 03:06:11 +01:00
|
|
|
import (
|
2023-12-30 17:42:58 +01:00
|
|
|
"log"
|
2023-12-27 03:06:11 +01:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Issues []Issue
|
|
|
|
|
|
|
|
type Issue struct {
|
|
|
|
Id uint32
|
|
|
|
Summary string
|
|
|
|
Description string
|
|
|
|
Project KeyVal
|
|
|
|
Category KeyVal
|
|
|
|
TargetVersion KeyVal `json:"target_version"`
|
|
|
|
Reporter KeyVal
|
2023-12-28 17:03:33 +01:00
|
|
|
Handler KeyVal
|
2023-12-27 03:06:11 +01:00
|
|
|
Status KeyVal
|
|
|
|
Resolution KeyVal
|
|
|
|
ViewState KeyVal `json:"view_state"`
|
|
|
|
Priority KeyVal
|
|
|
|
Severity KeyVal
|
|
|
|
Reproducibility KeyVal
|
|
|
|
Sticky bool
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
Notes []Note
|
|
|
|
Relationships []Relationship
|
|
|
|
Tags []KeyVal
|
|
|
|
History []Change
|
|
|
|
}
|
|
|
|
|
|
|
|
type KeyVal struct {
|
|
|
|
Id uint32
|
|
|
|
Name string
|
|
|
|
Label string `json:",omitempty"`
|
|
|
|
Color string `json:",omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Note struct {
|
|
|
|
Id uint32
|
|
|
|
Text string
|
|
|
|
Reporter KeyVal
|
|
|
|
ViewState KeyVal `json:"view_state"`
|
|
|
|
Attachments []Attachment
|
|
|
|
Typ string `json:"type"`
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
UpdatedAt time.Time `json:"created_at"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Attachment struct {
|
|
|
|
Name string
|
|
|
|
Content []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
type Relationship struct {
|
|
|
|
Id uint32
|
|
|
|
Typ KeyVal `json:"type"`
|
|
|
|
Issue struct {
|
|
|
|
Id uint32
|
|
|
|
Summary string
|
|
|
|
Status KeyVal
|
|
|
|
Resolution KeyVal
|
|
|
|
Handler KeyVal
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type Change struct {
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
Message string
|
|
|
|
User KeyVal
|
|
|
|
Typ KeyVal `json:"type"`
|
|
|
|
Note struct{ id int32 }
|
|
|
|
}
|
|
|
|
|
|
|
|
type ByCategory []Issue
|
|
|
|
|
|
|
|
func (b ByCategory) Len() int { return len(b) }
|
|
|
|
func (b ByCategory) Less(i, j int) bool {
|
|
|
|
return strings.Compare(b[i].Category.Name, b[j].Category.Name) < 0
|
|
|
|
}
|
|
|
|
func (b ByCategory) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
|
|
|
2023-12-30 17:42:58 +01:00
|
|
|
type ByAssignment []Issue
|
2023-12-28 17:03:33 +01:00
|
|
|
|
|
|
|
func (b ByAssignment) Len() int { return len(b) }
|
|
|
|
func (b ByAssignment) Less(i, j int) bool {
|
|
|
|
return strings.Compare(b[i].Handler.Name, b[j].Handler.Name) < 0
|
|
|
|
}
|
|
|
|
func (b ByAssignment) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
|
|
|
2023-12-27 03:06:11 +01:00
|
|
|
type ById []Issue
|
|
|
|
|
|
|
|
func (b ById) Len() int { return len(b) }
|
|
|
|
func (b ById) Less(i, j int) bool { return b[i].Id < b[j].Id }
|
|
|
|
func (b ById) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
|
|
|
|
|
|
type ByTarget []Issue
|
|
|
|
|
|
|
|
func (b ByTarget) Len() int { return len(b) }
|
|
|
|
func (b ByTarget) Less(i, j int) bool {
|
|
|
|
return strings.Compare(b[i].TargetVersion.Name, b[j].TargetVersion.Name) < 0
|
|
|
|
}
|
|
|
|
func (b ByTarget) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
|
|
|
2023-12-28 17:03:33 +01:00
|
|
|
type ByHandlerAndId []Issue
|
|
|
|
|
|
|
|
func (b ByHandlerAndId) Len() int { return len(b) }
|
|
|
|
func (b ByHandlerAndId) Less(i, j int) bool {
|
|
|
|
if (b[i].Handler.Name == "") == (b[j].Handler.Name == "") {
|
|
|
|
return b[i].Id < b[j].Id
|
|
|
|
} else if b[i].Handler.Name == "" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
func (b ByHandlerAndId) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
|
|
|
|
2023-12-27 03:06:11 +01:00
|
|
|
func (i Issues) Tags() (tags []string) {
|
|
|
|
var m = map[string]bool{}
|
|
|
|
for _, issue := range i {
|
|
|
|
for _, tag := range issue.Tags {
|
|
|
|
m[tag.Name] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for tag := range m {
|
|
|
|
tags = append(tags, tag)
|
|
|
|
}
|
|
|
|
sort.Strings(tags)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-28 17:03:33 +01:00
|
|
|
func (is Issues) Assigned(assigned bool) (n int) {
|
|
|
|
for _, i := range is {
|
|
|
|
if assigned == (i.Handler.Name != "") {
|
|
|
|
n += 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *Issue) IsHandled() bool {
|
|
|
|
return i.Handler.Name != ""
|
|
|
|
}
|
|
|
|
|
2023-12-27 03:06:11 +01:00
|
|
|
func (i Issues) TargetVersions() (targets []string) {
|
|
|
|
var m = map[string]bool{}
|
|
|
|
for _, issue := range i {
|
|
|
|
m[issue.TargetVersion.Name] = true
|
|
|
|
}
|
|
|
|
for t := range m {
|
|
|
|
targets = append(targets, t)
|
|
|
|
}
|
|
|
|
sort.Strings(targets)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i Issues) Categories() []string {
|
|
|
|
var m = map[string]bool{}
|
|
|
|
|
|
|
|
for _, issue := range i {
|
|
|
|
m[issue.Category.Name] = true
|
|
|
|
}
|
|
|
|
|
|
|
|
var r = []string{}
|
|
|
|
for c := range m {
|
|
|
|
r = append(r, c)
|
|
|
|
}
|
|
|
|
sort.Strings(r)
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i Issues) ByCategory(cat string) (issues Issues) {
|
|
|
|
for _, issue := range i {
|
|
|
|
if issue.Category.Name == cat {
|
|
|
|
issues = append(issues, issue)
|
|
|
|
}
|
|
|
|
}
|
2023-12-30 17:42:58 +01:00
|
|
|
sort.Sort(sort.Reverse(ByHandlerAndId(issues)))
|
2023-12-27 03:06:11 +01:00
|
|
|
return issues
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i Issues) ByCategoryAndTarget(cat, tar string) (issues Issues) {
|
|
|
|
for _, is := range i {
|
|
|
|
if is.TargetVersion.Name == tar &&
|
|
|
|
is.Category.Name == cat {
|
|
|
|
issues = append(issues, is)
|
|
|
|
}
|
|
|
|
}
|
2023-12-28 17:03:33 +01:00
|
|
|
sort.Sort(sort.Reverse(ByHandlerAndId(issues)))
|
2023-12-27 03:06:11 +01:00
|
|
|
return
|
|
|
|
}
|
2023-12-30 17:42:58 +01:00
|
|
|
|
|
|
|
// Follow the example for multiSort in https://pkg.go.dev/sort
|
|
|
|
|
|
|
|
type lessFunc func(i1, i2 *Issue) bool
|
|
|
|
|
|
|
|
type multiSorter struct {
|
|
|
|
issues Issues
|
|
|
|
less []lessFunc
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ms *multiSorter) Len() int {
|
|
|
|
return len(ms.issues)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ms *multiSorter) Sort(issues Issues) Issues {
|
|
|
|
ms.issues = issues
|
|
|
|
sort.Sort(ms)
|
|
|
|
return ms.issues
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ms *multiSorter) Swap(i, j int) {
|
|
|
|
ms.issues[i], ms.issues[j] = ms.issues[j], ms.issues[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ms *multiSorter) Less(i, j int) bool {
|
|
|
|
p, q := &ms.issues[i], &ms.issues[j]
|
|
|
|
var k int
|
|
|
|
for k = 0; k < len(ms.less)-1; k++ {
|
|
|
|
less := ms.less[k]
|
|
|
|
switch {
|
|
|
|
case less(p, q):
|
|
|
|
return true
|
|
|
|
case less(q, p):
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ms.less[k](p, q)
|
|
|
|
}
|
|
|
|
|
|
|
|
var severityOrder = map[string]int{
|
|
|
|
"block": 0,
|
|
|
|
"crash": 1,
|
|
|
|
"major": 2,
|
|
|
|
"minor": 3,
|
|
|
|
"text": 4,
|
|
|
|
"trivial": 5,
|
|
|
|
"tweak": 6}
|
|
|
|
|
|
|
|
var lessFuncs = map[string]lessFunc{
|
|
|
|
"Category": func(i1, i2 *Issue) bool { return strings.Compare(i1.Category.Name, i2.Category.Name) < 0 },
|
|
|
|
"Assignment": func(i1, i2 *Issue) bool { return strings.Compare(i1.Handler.Name, i2.Handler.Name) < 0 },
|
|
|
|
"Handler": func(i1, i2 *Issue) bool { return strings.Compare(i1.Handler.Name, i2.Handler.Name) < 0 },
|
|
|
|
"Target": func(i1, i2 *Issue) bool { return strings.Compare(i1.TargetVersion.Name, i2.TargetVersion.Name) < 0 },
|
|
|
|
"Id": func(i1, i2 *Issue) bool { return i1.Id < i2.Id },
|
|
|
|
"Severity": func(i1, i2 *Issue) bool {
|
|
|
|
s1, ok := severityOrder[i1.Severity.Name]
|
|
|
|
if !ok {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
s2, ok := severityOrder[i2.Severity.Name]
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return s1 < s2
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
func OrderedBy(fields ...string) *multiSorter {
|
|
|
|
m := &multiSorter{}
|
|
|
|
for _, field := range fields {
|
|
|
|
fn, ok := lessFuncs[field]
|
|
|
|
if !ok {
|
|
|
|
log.Printf("unknown field to order by: %s\n", field)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
m.less = append(m.less, fn)
|
|
|
|
}
|
|
|
|
return m
|
|
|
|
}
|