1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

feat: comprehensive summary-level filtering (resolve #262)

This commit is contained in:
Ferdinand Mütsch
2021-12-26 17:02:14 +01:00
parent 8a3e6f0179
commit a279548c89
25 changed files with 333 additions and 183 deletions

View File

@ -1,5 +1,11 @@
package models
// AliasResolver returns the alias of an entity, given its original name. I.e., it returns Alias.Key, given an Alias.Value
type AliasResolver func(t uint8, k string) string
// AliasReverseResolver returns all original names, which have the given alias as mapping target. I.e., it returns a list of Alias.Value, given an Alias.Key
type AliasReverseResolver func(t uint8, k string) []string
type Alias struct {
ID uint `gorm:"primary_key"`
Type uint8 `gorm:"not null; index:idx_alias_type_key"`

View File

@ -3,7 +3,6 @@ package v1
import (
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"time"
)
// https://shields.io/endpoint
@ -20,18 +19,11 @@ type BadgeData struct {
Color string `json:"color"`
}
func NewBadgeDataFrom(summary *models.Summary, filters *models.Filters) *BadgeData {
var total time.Duration
if hasFilter, _, _ := filters.One(); hasFilter {
total = summary.TotalTimeByFilters(filters)
} else {
total = summary.TotalTime()
}
func NewBadgeDataFrom(summary *models.Summary) *BadgeData {
return &BadgeData{
SchemaVersion: 1,
Label: defaultLabel,
Message: utils.FmtWakatimeDuration(total),
Message: utils.FmtWakatimeDuration(summary.TotalTime()),
Color: defaultColor,
}
}

View File

@ -3,7 +3,6 @@ package v1
import (
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"time"
)
// https://wakatime.com/developers#all_time_since_today
@ -27,14 +26,8 @@ type AllTimeRange struct {
Timezone string `json:"timezone"`
}
func NewAllTimeFrom(summary *models.Summary, filters *models.Filters) *AllTimeViewModel {
var total time.Duration
if key := filters.Project; key != "" {
total = summary.TotalTimeByFilters(filters)
} else {
total = summary.TotalTime()
}
func NewAllTimeFrom(summary *models.Summary) *AllTimeViewModel {
total := summary.TotalTime()
return &AllTimeViewModel{
Data: &AllTimeData{
TotalSeconds: float32(total.Seconds()),

View File

@ -57,8 +57,7 @@ type SummariesRange struct {
Timezone string `json:"timezone"`
}
func NewSummariesFrom(summaries []*models.Summary, filters *models.Filters) *SummariesViewModel {
// TODO: implement filtering (https://github.com/muety/wakapi/issues/58)
func NewSummariesFrom(summaries []*models.Summary) *SummariesViewModel {
data := make([]*SummariesData, len(summaries))
minDate, maxDate := time.Now().Add(1*time.Second), time.Time{}

View File

@ -1,50 +1,158 @@
package models
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/mitchellh/hashstructure/v2"
)
type Filters struct {
Project string
OS string
Language string
Editor string
Machine string
Label string
Project OrFilter
OS OrFilter
Language OrFilter
Editor OrFilter
Machine OrFilter
Label OrFilter
}
type OrFilter []string
func (f OrFilter) Exists() bool {
return len(f) > 0 && f[0] != ""
}
func (f OrFilter) MatchAny(search string) bool {
for _, s := range f {
if s == search {
return true
}
}
return false
}
type FilterElement struct {
Type uint8
Key string
entity uint8
filter OrFilter
}
func NewFiltersWith(entity uint8, key string) *Filters {
switch entity {
case SummaryProject:
return &Filters{Project: key}
case SummaryOS:
return &Filters{OS: key}
case SummaryLanguage:
return &Filters{Language: key}
case SummaryEditor:
return &Filters{Editor: key}
case SummaryMachine:
return &Filters{Machine: key}
case SummaryLabel:
return &Filters{Label: key}
}
return &Filters{}
return NewFilterWithMultiple(entity, []string{key})
}
func (f *Filters) One() (bool, uint8, string) {
if f.Project != "" {
func NewFilterWithMultiple(entity uint8, keys []string) *Filters {
filters := &Filters{}
return filters.WithMultiple(entity, keys)
}
func (f *Filters) With(entity uint8, key string) *Filters {
return f.WithMultiple(entity, []string{key})
}
func (f *Filters) WithMultiple(entity uint8, keys []string) *Filters {
switch entity {
case SummaryProject:
f.Project = append(f.Project, keys...)
case SummaryOS:
f.OS = append(f.OS, keys...)
case SummaryLanguage:
f.Language = append(f.Language, keys...)
case SummaryEditor:
f.Editor = append(f.Editor, keys...)
case SummaryMachine:
f.Machine = append(f.Machine, keys...)
case SummaryLabel:
f.Label = append(f.Label, keys...)
}
return f
}
func (f *Filters) One() (bool, uint8, OrFilter) {
if f.Project != nil && f.Project.Exists() {
return true, SummaryProject, f.Project
} else if f.OS != "" {
} else if f.OS != nil && f.OS.Exists() {
return true, SummaryOS, f.OS
} else if f.Language != "" {
} else if f.Language != nil && f.Language.Exists() {
return true, SummaryLanguage, f.Language
} else if f.Editor != "" {
} else if f.Editor != nil && f.Editor.Exists() {
return true, SummaryEditor, f.Editor
} else if f.Machine != "" {
} else if f.Machine != nil && f.Machine.Exists() {
return true, SummaryMachine, f.Machine
} else if f.Label != "" {
} else if f.Label != nil && f.Label.Exists() {
return true, SummaryLabel, f.Label
}
return false, 0, ""
return false, 0, OrFilter{}
}
func (f *Filters) OneOrEmpty() FilterElement {
if ok, t, of := f.One(); ok {
return FilterElement{entity: t, filter: of}
}
return FilterElement{}
}
func (f *Filters) IsEmpty() bool {
nonEmpty, _, _ := f.One()
return !nonEmpty
}
func (f *Filters) Hash() string {
hash, err := hashstructure.Hash(f, hashstructure.FormatV2, nil)
if err != nil {
logbuch.Error("CRITICAL ERROR: failed to hash struct %v", err)
}
return fmt.Sprintf("%x", hash) // "uint64 values with high bit set are not supported"
}
func (f *Filters) Match(h *Heartbeat) bool {
// TODO: labels?
return (f.Project == nil || f.Project.MatchAny(h.Project)) &&
(f.OS == nil || f.OS.MatchAny(h.OperatingSystem)) &&
(f.Language == nil || f.Language.MatchAny(h.Language)) &&
(f.Editor == nil || f.Editor.MatchAny(h.Editor)) &&
(f.Machine == nil || f.Machine.MatchAny(h.Machine))
}
// WithAliases adds OR-conditions for every alias of a filter key as additional filter keys
func (f *Filters) WithAliases(resolver AliasReverseResolver) *Filters {
if f.Project != nil {
updated := OrFilter(make([]string, 0, len(f.Project)))
for _, e := range f.Project {
updated = append(updated, e)
updated = append(updated, resolver(SummaryProject, e)...)
}
f.Project = updated
}
if f.OS != nil {
updated := OrFilter(make([]string, 0, len(f.OS)))
for _, e := range f.OS {
updated = append(updated, e)
updated = append(updated, resolver(SummaryOS, e)...)
}
f.OS = updated
}
if f.Language != nil {
updated := OrFilter(make([]string, 0, len(f.Language)))
for _, e := range f.Language {
updated = append(updated, e)
updated = append(updated, resolver(SummaryLanguage, e)...)
}
f.Language = updated
}
if f.Editor != nil {
updated := OrFilter(make([]string, 0, len(f.Editor)))
for _, e := range f.Editor {
updated = append(updated, e)
updated = append(updated, resolver(SummaryEditor, e)...)
}
f.Editor = updated
}
if f.Machine != nil {
updated := OrFilter(make([]string, 0, len(f.Machine)))
for _, e := range f.Machine {
updated = append(updated, e)
updated = append(updated, resolver(SummaryMachine, e)...)
}
f.Machine = updated
}
return f
}

View File

@ -57,8 +57,6 @@ type SummaryParams struct {
Recompute bool
}
type AliasResolver func(t uint8, k string) string
func SummaryTypes() []uint8 {
return []uint8{SummaryProject, SummaryLanguage, SummaryEditor, SummaryOS, SummaryMachine, SummaryLabel}
}
@ -204,12 +202,12 @@ func (s *Summary) TotalTimeByKey(entityType uint8, key string) (timeSum time.Dur
return timeSum
}
func (s *Summary) TotalTimeByFilters(filters *Filters) time.Duration {
do, typeId, key := filters.One()
if do {
return s.TotalTimeByKey(typeId, key)
func (s *Summary) TotalTimeByFilter(filter FilterElement) time.Duration {
var total time.Duration
for _, f := range filter.filter {
total += s.TotalTimeByKey(filter.entity, f)
}
return 0
return total
}
func (s *Summary) MaxBy(entityType uint8) *SummaryItem {

View File

@ -98,20 +98,13 @@ func TestSummary_TotalTimeByFilters(t *testing.T) {
},
}
// Specifying filters about multiple entites is not supported at the moment
// as the current, very rudimentary, time calculation logic wouldn't make sense then.
// Evaluating a filter like (project="wakapi", language="go") can only be realized
// before computing the summary in the first place, because afterwards we can't know
// what time coded in "Go" was in the "Wakapi" project
// See https://github.com/muety/wakapi/issues/108
filters1 := NewFiltersWith(SummaryProject, "wakapi").OneOrEmpty()
filters2 := NewFiltersWith(SummaryLanguage, "Go").OneOrEmpty()
filters3 := FilterElement{}
filters1 := &Filters{Project: "wakapi"}
filters2 := &Filters{Language: "Go"}
filters3 := &Filters{}
assert.Equal(t, testDuration1, sut.TotalTimeByFilters(filters1))
assert.Equal(t, testDuration3, sut.TotalTimeByFilters(filters2))
assert.Zero(t, sut.TotalTimeByFilters(filters3))
assert.Equal(t, testDuration1, sut.TotalTimeByFilter(filters1))
assert.Equal(t, testDuration3, sut.TotalTimeByFilter(filters2))
assert.Zero(t, sut.TotalTimeByFilter(filters3))
}
func TestSummary_WithResolvedAliases(t *testing.T) {